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 }