mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 19:36:36 +08:00
209 lines
6.0 KiB
Go
209 lines
6.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
)
|
|
|
|
// TelegramPollingStartResult describes what happened when local long polling
|
|
// was requested. The admin UI uses it to avoid a silent "started" toast when
|
|
// no Telegram channel can actually poll.
|
|
type TelegramPollingStartResult struct {
|
|
Message string `json:"message"`
|
|
Started int `json:"started"`
|
|
AlreadyRunning int `json:"already_running"`
|
|
Skipped int `json:"skipped"`
|
|
Errors []string `json:"errors,omitempty"`
|
|
}
|
|
|
|
// StartPolling 为所有已启用的 Telegram 通知渠道启动长轮询。
|
|
func (s *TelegramBotService) StartPolling(ctx context.Context) TelegramPollingStartResult {
|
|
result := TelegramPollingStartResult{Message: "telegram polling started"}
|
|
channels, err := s.repo.NotifyChannel.ListByType(ctx, "telegram")
|
|
if err != nil {
|
|
s.log.Error("failed to list telegram channels for polling", zap.Error(err))
|
|
result.Message = "failed to list telegram channels"
|
|
result.Errors = append(result.Errors, err.Error())
|
|
return result
|
|
}
|
|
if len(channels) == 0 {
|
|
result.Message = "no telegram channels configured"
|
|
result.Errors = append(result.Errors, "没有配置 Telegram 通知渠道")
|
|
return result
|
|
}
|
|
|
|
for _, ch := range channels {
|
|
if !ch.Enabled {
|
|
result.Skipped++
|
|
result.Errors = append(result.Errors, ch.Name+": 通知渠道未启用")
|
|
continue
|
|
}
|
|
configStr := ch.Config
|
|
if s.crypto != nil && configStr != "" {
|
|
configStr = s.crypto.Decrypt(configStr)
|
|
}
|
|
var rawCfg map[string]any
|
|
if err := json.Unmarshal([]byte(configStr), &rawCfg); err != nil {
|
|
result.Skipped++
|
|
result.Errors = append(result.Errors, ch.Name+": Telegram 配置解析失败: "+err.Error())
|
|
continue
|
|
}
|
|
cfg := telegramStringConfigFromAny(rawCfg)
|
|
botToken := cfg["bot_token"]
|
|
if botToken == "" {
|
|
result.Skipped++
|
|
result.Errors = append(result.Errors, ch.Name+": Telegram Bot Token 为空")
|
|
continue
|
|
}
|
|
s.pollingMu.Lock()
|
|
if _, running := s.pollingCancel[botToken]; running {
|
|
s.pollingMu.Unlock()
|
|
result.AlreadyRunning++
|
|
continue
|
|
}
|
|
s.pollingMu.Unlock()
|
|
|
|
if err := registerTelegramBotCommands(ctx, cfg); err != nil && s.log != nil {
|
|
s.log.Warn("telegram setMyCommands failed", zap.Error(sanitizeTelegramError(err)))
|
|
}
|
|
if err := deleteTelegramWebhook(ctx, cfg); err != nil {
|
|
result.Skipped++
|
|
result.Errors = append(result.Errors, ch.Name+": "+sanitizeTelegramError(err).Error())
|
|
continue
|
|
}
|
|
|
|
s.pollingMu.Lock()
|
|
if _, running := s.pollingCancel[botToken]; running {
|
|
s.pollingMu.Unlock()
|
|
result.AlreadyRunning++
|
|
continue
|
|
}
|
|
pollCtx, cancel := context.WithCancel(context.Background())
|
|
s.pollingCancel[botToken] = cancel
|
|
s.pollingMu.Unlock()
|
|
|
|
channel := ch
|
|
go s.pollLoop(pollCtx, cfg, &channel)
|
|
result.Started++
|
|
s.log.Info("started telegram polling", zap.String("channel", ch.Name))
|
|
}
|
|
if result.Started == 0 && result.AlreadyRunning == 0 {
|
|
result.Message = "no enabled telegram channels started"
|
|
}
|
|
return result
|
|
}
|
|
|
|
// StopPolling 停止所有 Telegram 长轮询。
|
|
func (s *TelegramBotService) StopPolling() int {
|
|
s.pollingMu.Lock()
|
|
defer s.pollingMu.Unlock()
|
|
stopped := 0
|
|
for token, cancel := range s.pollingCancel {
|
|
cancel()
|
|
delete(s.pollingCancel, token)
|
|
stopped++
|
|
}
|
|
s.log.Info("telegram polling stopped")
|
|
return stopped
|
|
}
|
|
|
|
// pollLoop 对单个 Bot Token 执行长轮询。
|
|
func (s *TelegramBotService) pollLoop(ctx context.Context, cfg map[string]string, channel *model.NotifyChannel) {
|
|
var offset int64 = 0
|
|
pollURL, err := telegramMethodURL(cfg, cfg["bot_token"], "getUpdates")
|
|
if err != nil {
|
|
s.log.Warn("telegram polling config invalid", zap.Error(err))
|
|
return
|
|
}
|
|
clients := telegramHTTPClients(45*time.Second, cfg)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
|
|
reqBody, _ := json.Marshal(map[string]interface{}{
|
|
"offset": offset,
|
|
"timeout": 30,
|
|
})
|
|
respBody, err := telegramPollingRequest(ctx, clients, pollURL, string(reqBody))
|
|
if err != nil {
|
|
s.log.Debug("telegram polling failed", zap.Error(err))
|
|
time.Sleep(5 * time.Second)
|
|
continue
|
|
}
|
|
|
|
var result struct {
|
|
OK bool `json:"ok"`
|
|
Result []TelegramUpdate `json:"result"`
|
|
}
|
|
if err := json.Unmarshal(respBody, &result); err != nil || !result.OK {
|
|
time.Sleep(3 * time.Second)
|
|
continue
|
|
}
|
|
|
|
for _, upd := range result.Result {
|
|
if upd.UpdateID >= int(offset) {
|
|
offset = int64(upd.UpdateID) + 1
|
|
}
|
|
if !telegramUpdateActionable(upd) {
|
|
continue
|
|
}
|
|
go func(u TelegramUpdate) {
|
|
handlerCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
|
defer cancel()
|
|
_ = s.handleTelegramUpdate(handlerCtx, u, channel)
|
|
}(upd)
|
|
}
|
|
}
|
|
}
|
|
|
|
// telegramUpdateActionable 判断一条 update 是否需要分发处理。
|
|
// 长轮询默认会返回 message 与 callback_query 两类更新;命令消息需有文本,
|
|
// 而内联按钮回调(callback_query)必须被分发,否则成人目录显隐开关会失效。
|
|
func telegramUpdateActionable(upd TelegramUpdate) bool {
|
|
if upd.CallbackQuery != nil {
|
|
return true
|
|
}
|
|
return upd.Message != nil && upd.Message.Text != ""
|
|
}
|
|
|
|
func telegramPollingRequest(ctx context.Context, clients []*http.Client, pollURL, body string) ([]byte, error) {
|
|
var lastErr error
|
|
for _, client := range clients {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, pollURL, strings.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
lastErr = sanitizeTelegramError(err)
|
|
continue
|
|
}
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode >= 400 {
|
|
lastErr = fmt.Errorf("telegram api error %d: %s", resp.StatusCode, sanitizeTelegramText(string(respBody)))
|
|
continue
|
|
}
|
|
return respBody, nil
|
|
}
|
|
if lastErr != nil {
|
|
return nil, lastErr
|
|
}
|
|
return nil, errors.New("telegram polling failed")
|
|
}
|