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 }