- 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)
180 lines
4.3 KiB
Go
180 lines
4.3 KiB
Go
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
|
||
} |