buhapp-backend/internal/telegram/bot.go
ga c6d219c51f Telegram WebApp support + consent endpoint
- Telegram bot (long polling)
- /api/v1/auth/telegram (validate initData, auto-create user)
- /api/v1/auth/consents (record acceptances)
- users.telegram_id column, GetByTelegramID/CreateWithTelegramID
- ValidateInitData (HMAC-SHA256 with WebAppData secret)
2026-08-20 17:43:20 +00:00

180 lines
4.3 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
}
func NewBot(token string) *Bot {
return &Bot{
Token: token,
BaseURL: apiBase + token,
hc: &http.Client{Timeout: 15 * time.Second},
}
}
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
}