This commit is contained in:
2026-07-13 00:06:51 -03:00
parent a4798f8db6
commit 09c1f57a34
17 changed files with 3799 additions and 0 deletions
+253
View File
@@ -0,0 +1,253 @@
package main
// bot_telegram.go — minimal Telegram Bot API client built on net/http.
// No third-party dependency.
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"strconv"
"strings"
"time"
)
const tgAPIBase = "https://api.telegram.org/bot"
// ---------- Wire types (subset) ----------
type tgUpdate struct {
UpdateID int64 `json:"update_id"`
Message *tgMessage `json:"message"`
CallbackQuery *tgCallbackQuery `json:"callback_query"`
}
type tgMessage struct {
MessageID int64 `json:"message_id"`
From *tgUser `json:"from"`
Chat tgChat `json:"chat"`
Text string `json:"text"`
}
type tgCallbackQuery struct {
ID string `json:"id"`
From tgUser `json:"from"`
Message *tgMessage `json:"message"`
Data string `json:"data"`
}
type tgUser struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
Username string `json:"username"`
}
type tgChat struct {
ID int64 `json:"id"`
}
type tgInlineKeyboard struct {
InlineKeyboard [][]tgInlineButton `json:"inline_keyboard"`
}
type tgInlineButton struct {
Text string `json:"text"`
CallbackData string `json:"callback_data,omitempty"`
URL string `json:"url,omitempty"`
}
// ---------- Client ----------
type tgClient struct {
token string
hc *http.Client
}
func newTGClient(token string) *tgClient {
return &tgClient{token: token, hc: &http.Client{Timeout: 65 * time.Second}}
}
type tgResponse struct {
OK bool `json:"ok"`
Description string `json:"description"`
Result json.RawMessage `json:"result"`
}
func (c *tgClient) call(ctx context.Context, method string, payload interface{}) (json.RawMessage, error) {
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tgAPIBase+c.token+"/"+method, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
var tr tgResponse
if err := json.Unmarshal(data, &tr); err != nil {
return nil, fmt.Errorf("telegram %s: bad response: %s", method, string(data))
}
if !tr.OK {
return nil, fmt.Errorf("telegram %s: %s", method, tr.Description)
}
return tr.Result, nil
}
// getUpdates long-polls. offset is the next update_id to fetch.
func (c *tgClient) getUpdates(ctx context.Context, offset int64, timeoutSec int) ([]tgUpdate, error) {
payload := map[string]interface{}{
"offset": offset,
"timeout": timeoutSec,
"allowed_updates": []string{"message", "callback_query"},
}
raw, err := c.call(ctx, "getUpdates", payload)
if err != nil {
return nil, err
}
var ups []tgUpdate
if err := json.Unmarshal(raw, &ups); err != nil {
return nil, err
}
return ups, nil
}
func (c *tgClient) sendMessage(ctx context.Context, chatID int64, text string, kb *tgInlineKeyboard) (int64, error) {
payload := map[string]interface{}{
"chat_id": chatID,
"text": text,
"parse_mode": "HTML",
"disable_web_page_preview": true,
}
if kb != nil {
payload["reply_markup"] = kb
}
raw, err := c.call(ctx, "sendMessage", payload)
if err != nil {
return 0, err
}
var m tgMessage
_ = json.Unmarshal(raw, &m)
return m.MessageID, nil
}
func (c *tgClient) editMessageText(ctx context.Context, chatID, messageID int64, text string, kb *tgInlineKeyboard) error {
payload := map[string]interface{}{
"chat_id": chatID,
"message_id": messageID,
"text": text,
"parse_mode": "HTML",
"disable_web_page_preview": true,
}
if kb != nil {
payload["reply_markup"] = kb
}
_, err := c.call(ctx, "editMessageText", payload)
return err
}
func (c *tgClient) answerCallback(ctx context.Context, callbackID, text string) error {
payload := map[string]interface{}{"callback_query_id": callbackID}
if text != "" {
payload["text"] = text
}
_, err := c.call(ctx, "answerCallbackQuery", payload)
return err
}
// sendPhotoBytes uploads a photo (e.g. a PIX QR PNG) via multipart.
func (c *tgClient) sendPhotoBytes(ctx context.Context, chatID int64, photo []byte, filename, caption string, kb *tgInlineKeyboard) (int64, error) {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
_ = w.WriteField("chat_id", strconv.FormatInt(chatID, 10))
if caption != "" {
_ = w.WriteField("caption", caption)
_ = w.WriteField("parse_mode", "HTML")
}
if kb != nil {
kbJSON, _ := json.Marshal(kb)
_ = w.WriteField("reply_markup", string(kbJSON))
}
fw, err := w.CreateFormFile("photo", filename)
if err != nil {
return 0, err
}
if _, err := fw.Write(photo); err != nil {
return 0, err
}
_ = w.Close()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tgAPIBase+c.token+"/sendPhoto", &buf)
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", w.FormDataContentType())
resp, err := c.hc.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
var tr tgResponse
if err := json.Unmarshal(data, &tr); err != nil || !tr.OK {
return 0, fmt.Errorf("telegram sendPhoto: %s", string(data))
}
var m tgMessage
_ = json.Unmarshal(tr.Result, &m)
return m.MessageID, nil
}
func (c *tgClient) setWebhook(ctx context.Context, webhookURL, secret string) error {
payload := map[string]interface{}{
"url": webhookURL,
"allowed_updates": []string{"message", "callback_query"},
}
if secret != "" {
payload["secret_token"] = secret
}
_, err := c.call(ctx, "setWebhook", payload)
return err
}
func (c *tgClient) deleteWebhook(ctx context.Context) error {
_, err := c.call(ctx, "deleteWebhook", map[string]interface{}{"drop_pending_updates": false})
return err
}
// getMe validates the token and returns the bot username.
func (c *tgClient) getMe(ctx context.Context) (string, error) {
raw, err := c.call(ctx, "getMe", map[string]interface{}{})
if err != nil {
return "", err
}
var me tgUser
_ = json.Unmarshal(raw, &me)
return me.Username, nil
}
// ---------- helpers ----------
func htmlEscape(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
return s
}
// parseWebhookUpdate decodes a Telegram webhook POST body.
func parseWebhookUpdate(r io.Reader) (*tgUpdate, error) {
var u tgUpdate
if err := json.NewDecoder(io.LimitReader(r, 4<<20)).Decode(&u); err != nil {
return nil, err
}
return &u, nil
}