Files

182 lines
4.2 KiB
Go
Raw Permalink Normal View History

2026-02-21 23:01:39 +08:00
package telegram
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
"laodingbot/internal/logger"
)
type Bot struct {
token string
baseURL string
http *http.Client
pollTimeout int
log *logger.Logger
}
type IncomingMessage struct {
ChatID string
UserID string
Text string
}
func NewBot(token string, pollTimeout int, log *logger.Logger) (*Bot, error) {
if token == "" {
return nil, fmt.Errorf("empty telegram token")
}
if pollTimeout <= 0 {
pollTimeout = 30
}
return &Bot{
token: token,
baseURL: "https://api.telegram.org/bot" + token,
http: &http.Client{Timeout: 70 * time.Second},
pollTimeout: pollTimeout,
log: log,
}, nil
}
func (b *Bot) Run(ctx context.Context, handler func(context.Context, IncomingMessage) (string, error)) error {
if b.log != nil {
b.log.Infof("telegram polling started timeout=%ds", b.pollTimeout)
}
offset := 0
for {
select {
case <-ctx.Done():
if b.log != nil {
b.log.Infof("telegram polling stopped: %v", ctx.Err())
}
return ctx.Err()
default:
}
updates, err := b.getUpdates(ctx, offset)
if err != nil {
if b.log != nil {
b.log.Errorf("telegram getUpdates failed err=%v", err)
}
return err
}
if b.log != nil && len(updates) > 0 {
b.log.Debugf("telegram updates received count=%d offset=%d", len(updates), offset)
}
for _, u := range updates {
offset = u.UpdateID + 1
if u.Message.Text == "" {
continue
}
in := IncomingMessage{
ChatID: strconv.FormatInt(u.Message.Chat.ID, 10),
UserID: strconv.FormatInt(u.Message.From.ID, 10),
Text: u.Message.Text,
}
if b.log != nil {
b.log.Infof("telegram message received chat_id=%s user_id=%s text_len=%d", in.ChatID, in.UserID, len(in.Text))
}
resp, err := handler(ctx, in)
if err != nil {
if b.log != nil {
b.log.Errorf("telegram handler failed chat_id=%s err=%v", in.ChatID, err)
}
resp = "处理失败: " + err.Error()
}
if err := b.sendMessage(ctx, in.ChatID, resp); err != nil {
if b.log != nil {
b.log.Errorf("telegram sendMessage failed chat_id=%s err=%v", in.ChatID, err)
}
return err
}
if b.log != nil {
b.log.Debugf("telegram message sent chat_id=%s text_len=%d", in.ChatID, len(resp))
}
}
}
}
type updatesResponse struct {
OK bool `json:"ok"`
Result []update `json:"result"`
}
type update struct {
UpdateID int `json:"update_id"`
Message struct {
Text string `json:"text"`
Chat struct {
ID int64 `json:"id"`
} `json:"chat"`
From struct {
ID int64 `json:"id"`
} `json:"from"`
} `json:"message"`
}
func (b *Bot) getUpdates(ctx context.Context, offset int) ([]update, error) {
url := fmt.Sprintf("%s/getUpdates?timeout=%d&offset=%d", b.baseURL, b.pollTimeout, offset)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := b.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var out updatesResponse
if err := json.Unmarshal(raw, &out); err != nil {
if b.log != nil {
b.log.Errorf("telegram parse getUpdates response failed err=%v", err)
}
return nil, err
}
if !out.OK {
if b.log != nil {
b.log.Warnf("telegram getUpdates not ok")
}
return nil, fmt.Errorf("telegram getUpdates failed")
}
return out.Result, nil
}
func (b *Bot) sendMessage(ctx context.Context, chatID, text string) error {
payload := map[string]string{
"chat_id": chatID,
"text": text,
}
bts, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, b.baseURL+"/sendMessage", bytes.NewReader(bts))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := b.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
if b.log != nil {
b.log.Warnf("telegram sendMessage bad status=%d chat_id=%s", resp.StatusCode, chatID)
}
return fmt.Errorf("telegram sendMessage status: %d", resp.StatusCode)
}
return nil
}