buhapp-backend/internal/telegram/bot.go
ga c30faf02ef Sprint 6: security hardening
1. CORS whitelist (was '*'): only buhapp.mygoodservice.ru, t.me, web.telegram.org
2. Rate limit on /auth/login (10/min/IP) and /auth/register (5/hour/IP)
3. TGHandler: removed unused JWTSecret, added WebhookSecret, real secret check
4. Login: constant-time bcrypt on user enumeration (dummy hash)
5. TgUsername saved in users (was lost)
6. User.IsVerified=true for Telegram users
7. Register: 409 instead of 500 on duplicate email
8. Bumped version to 0.4.0
2026-08-20 20:42:22 +00:00

195 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package telegram
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
)
const apiBase = "https://api.telegram.org/bot"
type Bot struct {
Token string
BaseURL string
hc *http.Client
WebhookURL string
}
func NewBot(token string) *Bot {
return &Bot{
Token: token,
BaseURL: apiBase + token,
hc: &http.Client{Timeout: 15 * time.Second},
}
}
// SetWebhook — регистрирует webhook URL у Telegram
func (b *Bot) SetWebhook(url string, secret string) error {
type setWebhookReq struct {
URL string `json:"url"`
SecretToken string `json:"secret_token,omitempty"`
AllowedUpdates []string `json:"allowed_updates,omitempty"`
}
return b.call("setWebhook", setWebhookReq{
URL: url,
SecretToken: secret,
AllowedUpdates: []string{"message", "callback_query"},
}, nil)
}
type Update struct {
UpdateID int64 `json:"update_id"`
Message *Message `json:"message,omitempty"`
Callback *Callback `json:"callback_query,omitempty"`
}
type Message struct {
MessageID int64 `json:"message_id"`
From *User `json:"from,omitempty"`
Chat *Chat `json:"chat"`
Text string `json:"text,omitempty"`
Date int64 `json:"date"`
}
type Callback struct {
ID string `json:"id"`
From *User `json:"from,omitempty"`
Message *Message `json:"message,omitempty"`
Data string `json:"data,omitempty"`
}
type User struct {
ID int64 `json:"id"`
IsBot bool `json:"is_bot"`
FirstName string `json:"first_name"`
LastName string `json:"last_name,omitempty"`
Username string `json:"username,omitempty"`
}
// call — обёртка для вызова Telegram Bot API
func (b *Bot) call(method string, params any, result any) error {
body, err := json.Marshal(params)
if err != nil {
return err
}
r, err := b.hc.Post(b.BaseURL+"/"+method, "application/json", bytes.NewReader(body))
if err != nil {
return err
}
defer r.Body.Close()
if r.StatusCode != 200 {
buf, _ := io.ReadAll(r.Body)
return fmt.Errorf("telegram %s: HTTP %d: %s", method, r.StatusCode, string(buf))
}
var resp struct {
OK bool `json:"ok"`
Result json.RawMessage `json:"result"`
Error string `json:"description,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&resp); err != nil {
return err
}
if !resp.OK {
return fmt.Errorf("telegram error: %s", resp.Error)
}
if result != nil && len(resp.Result) > 0 {
return json.Unmarshal(resp.Result, result)
}
return nil
}
// GetUpdates — long polling (для простого бота без webhook)
func (b *Bot) GetUpdates(offset int64, timeoutSec int) ([]Update, error) {
var updates []Update
err := b.call("getUpdates", map[string]any{
"offset": offset,
"timeout": timeoutSec,
"allowed_updates": []string{"message", "callback_query"},
}, &updates)
return updates, err
}
// SendMessage — отправить текстовое сообщение
func (b *Bot) SendMessage(chatID int64, text string, opts ...func(m *MessageOptions)) error {
m := &MessageOptions{}
for _, o := range opts {
o(m)
}
params := map[string]any{
"chat_id": chatID,
"text": text,
}
if m.ParseMode != "" {
params["parse_mode"] = m.ParseMode
}
if m.ReplyMarkup != nil {
params["reply_markup"] = m.ReplyMarkup
}
return b.call("sendMessage", params, nil)
}
// AnswerCallback — ответ на callback_query
func (b *Bot) AnswerCallback(callbackID, text string) error {
return b.call("answerCallbackQuery", map[string]any{
"callback_query_id": callbackID,
"text": text,
}, nil)
}
type MessageOptions struct {
ParseMode string
ReplyMarkup any
}
// ReplyMarkupInline — кнопка-ссылка на WebApp
type ReplyMarkupInline struct {
InlineKeyboard [][]InlineButton `json:"inline_keyboard"`
}
type InlineButton struct {
Text string `json:"text"`
URL string `json:"url,omitempty"`
// WebApp URL — открывает Mini App
WebApp *WebAppInfo `json:"web_app,omitempty"`
}
type WebAppInfo struct {
URL string `json:"url"`
}
func InlineKeyboard(rows ...[]InlineButton) *ReplyMarkupInline {
return &ReplyMarkupInline{InlineKeyboard: rows}
}
// RunLoop — простой long-poll loop с обработчиком update
func (b *Bot) RunLoop(handler func(u Update)) {
var offset int64
for {
updates, err := b.GetUpdates(offset, 30)
if err != nil {
log.Printf("telegram getUpdates: %v", err)
time.Sleep(3 * time.Second)
continue
}
for _, u := range updates {
offset = u.UpdateID + 1
handler(u)
}
}
}
// RunOnce — однократная обработка (для теста)
func (b *Bot) RunOnce(handler func(u Update)) error {
updates, err := b.GetUpdates(0, 0)
if err != nil {
return err
}
for _, u := range updates {
handler(u)
}
return nil
}