fix: stabilize telegram bot proxy and legacy permissions

This commit is contained in:
ShukeBta
2026-05-30 02:54:25 +08:00
parent 67ba1bdcce
commit f8ef35c8e1
8 changed files with 241 additions and 82 deletions
+8 -2
View File
@@ -346,11 +346,15 @@ func normalizeChannelInput(in *ChannelInput) {
in.Config = map[string]any{}
}
chatID := str(in.Config["chat_id"])
if chatID == "" || !strings.HasPrefix(chatID, "-") {
if chatID == "" {
return
}
if str(in.Config["group_chat_id"]) == "" && str(in.Config["channel_chat_id"]) == "" && str(in.Config["command_chat_id"]) == "" {
if strings.HasPrefix(chatID, "-") && str(in.Config["group_chat_id"]) == "" && str(in.Config["channel_chat_id"]) == "" && str(in.Config["command_chat_id"]) == "" {
in.Config["group_chat_id"] = chatID
return
}
if !strings.HasPrefix(chatID, "-") && str(in.Config["admin_user_ids"]) == "" {
in.Config["admin_user_ids"] = chatID
}
}
@@ -369,6 +373,8 @@ func telegramTargetChatIDs(cfg map[string]string) []string {
chatID := strings.TrimSpace(cfg["chat_id"])
if strings.HasPrefix(chatID, "-") {
targets = append(targets, chatID)
} else if chatID != "" && strings.TrimSpace(cfg["admin_user_ids"]) == "" {
targets = append(targets, chatID)
}
}
if len(targets) == 0 {
+112 -20
View File
@@ -46,17 +46,58 @@ func telegramMethodURL(cfg map[string]string, botToken, method string) (string,
}
func telegramHTTPClient(timeout time.Duration, cfg map[string]string) *http.Client {
transport := NewExternalTransport()
proxyRaw := strings.TrimSpace(cfg["proxy_url"])
if proxyRaw == "" {
proxyRaw = strings.TrimSpace(os.Getenv("MEDIASTATION_TELEGRAM_PROXY_URL"))
clients := telegramHTTPClients(timeout, cfg)
return clients[0]
}
func telegramHTTPClients(timeout time.Duration, cfg map[string]string) []*http.Client {
clients := []*http.Client{}
seen := map[string]bool{}
for _, proxyRaw := range telegramProxyCandidates(cfg) {
proxyURL, err := normalizeProxyURL(proxyRaw, "http")
if err != nil || proxyURL == nil {
continue
}
key := proxyURL.String()
if seen[key] {
continue
}
seen[key] = true
transport := NewExternalTransport()
transport.Proxy = http.ProxyURL(proxyURL)
clients = append(clients, &http.Client{Timeout: timeout, Transport: transport})
}
if proxyRaw != "" {
if proxyURL, err := normalizeProxyURL(proxyRaw, "http"); err == nil {
transport.Proxy = http.ProxyURL(proxyURL)
transport := NewExternalTransport()
clients = append(clients, &http.Client{Timeout: timeout, Transport: transport})
return clients
}
func telegramProxyCandidates(cfg map[string]string) []string {
out := []string{}
for _, value := range []string{
cfg["proxy_url"],
os.Getenv("MEDIASTATION_TELEGRAM_PROXY_URL"),
} {
if strings.TrimSpace(value) != "" {
out = append(out, value)
}
}
return &http.Client{Timeout: timeout, Transport: transport}
if len(out) > 0 {
return out
}
for _, value := range []string{
"http://127.0.0.1:10808",
"http://127.0.0.1:10809",
"http://127.0.0.1:7890",
"http://127.0.0.1:7891",
"http://host.docker.internal:7890",
"http://host.docker.internal:10808",
"http://172.17.0.1:7890",
"http://172.17.0.1:10808",
} {
out = append(out, value)
}
return out
}
func telegramPostForm(ctx context.Context, cfg map[string]string, method string, form url.Values, timeout time.Duration) error {
@@ -64,12 +105,7 @@ func telegramPostForm(ctx context.Context, cfg map[string]string, method string,
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(form.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return telegramDo(telegramHTTPClient(timeout, cfg), req)
return telegramDoWithFallback(ctx, cfg, http.MethodPost, apiURL, form.Encode(), "application/x-www-form-urlencoded", timeout)
}
func telegramPostJSON(ctx context.Context, cfg map[string]string, method string, payload any, timeout time.Duration) error {
@@ -81,12 +117,7 @@ func telegramPostJSON(ctx context.Context, cfg map[string]string, method string,
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(string(body)))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
return telegramDo(telegramHTTPClient(timeout, cfg), req)
return telegramDoWithFallback(ctx, cfg, http.MethodPost, apiURL, string(body), "application/json", timeout)
}
func telegramDo(client *http.Client, req *http.Request) error {
@@ -102,11 +133,72 @@ func telegramDo(client *http.Client, req *http.Request) error {
return nil
}
func telegramDoWithFallback(ctx context.Context, cfg map[string]string, method, apiURL, body, contentType string, timeout time.Duration) error {
var lastErr error
for _, client := range telegramHTTPClients(timeout, cfg) {
req, err := http.NewRequestWithContext(ctx, method, apiURL, strings.NewReader(body))
if err != nil {
return err
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if err := telegramDo(client, req); err != nil {
lastErr = err
continue
}
return nil
}
if lastErr != nil {
return lastErr
}
return errors.New("telegram request failed")
}
func telegramPostJSONDecode(ctx context.Context, cfg map[string]string, method string, payload any, timeout time.Duration, out any) error {
apiURL, err := telegramMethodURL(cfg, cfg["bot_token"], method)
if err != nil {
return err
}
body, err := json.Marshal(payload)
if err != nil {
return err
}
var lastErr error
for _, client := range telegramHTTPClients(timeout, cfg) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(string(body)))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
lastErr = sanitizeTelegramError(err)
continue
}
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
resp.Body.Close()
if resp.StatusCode >= 400 {
lastErr = fmt.Errorf("telegram api error %d: %s", resp.StatusCode, sanitizeTelegramText(string(respBody)))
continue
}
if out != nil {
return json.Unmarshal(respBody, out)
}
return nil
}
if lastErr != nil {
return lastErr
}
return errors.New("telegram request failed")
}
func telegramStringConfigFromAny(cfg map[string]any) map[string]string {
out := make(map[string]string, len(cfg))
for key, value := range cfg {
out[key] = str(value)
}
normalizeTelegramConfig(out)
return out
}
+27
View File
@@ -69,3 +69,30 @@ func TestNormalizeTelegramChannelMigratesLegacyChatID(t *testing.T) {
t.Fatalf("group_chat_id = %q, want -10001", got)
}
}
func TestNormalizeTelegramChannelMigratesLegacyPrivateChatIDToAdmin(t *testing.T) {
cfg := map[string]string{"chat_id": "5812333517"}
normalizeTelegramConfig(cfg)
if got := cfg["admin_user_ids"]; got != "5812333517" {
t.Fatalf("admin_user_ids = %q, want legacy chat_id", got)
}
}
func TestTelegramTargetChatIDsUsesLegacyPrivateChatID(t *testing.T) {
got := telegramTargetChatIDs(map[string]string{
"chat_id": "5812333517",
})
if len(got) != 1 || got[0] != "5812333517" {
t.Fatalf("got %#v, want legacy private chat target", got)
}
}
func TestTelegramProxyCandidatesDefaultLocalFallbacks(t *testing.T) {
got := telegramProxyCandidates(map[string]string{})
joined := strings.Join(got, ",")
for _, want := range []string{"127.0.0.1:10808", "172.17.0.1:7890"} {
if !strings.Contains(joined, want) {
t.Fatalf("default proxy candidates %q missing %q", joined, want)
}
}
}
+67 -36
View File
@@ -7,6 +7,7 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -501,7 +502,7 @@ func (s *TelegramBotService) pollLoop(ctx context.Context, cfg map[string]string
s.log.Warn("telegram polling config invalid", zap.Error(err))
return
}
client := telegramHTTPClient(45*time.Second, cfg)
clients := telegramHTTPClients(45*time.Second, cfg)
for {
select {
@@ -514,21 +515,12 @@ func (s *TelegramBotService) pollLoop(ctx context.Context, cfg map[string]string
"offset": offset,
"timeout": 30,
})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, pollURL, strings.NewReader(string(reqBody)))
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
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
s.log.Debug("telegram polling failed", zap.Error(sanitizeTelegramError(err)))
time.Sleep(5 * time.Second)
continue
}
respBody, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var result struct {
OK bool `json:"ok"`
@@ -554,6 +546,33 @@ func (s *TelegramBotService) pollLoop(ctx context.Context, cfg map[string]string
}
}
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")
}
// ── Message Sending ──
// reply 通过 Telegram Bot API 发送回复消息。
@@ -651,11 +670,14 @@ func (s *TelegramBotService) handleCallback(ctx context.Context, cb *TelegramCal
if cb == nil || cb.Message == nil {
return nil
}
channel := s.findChannelByChatID(ctx, cb.Message.Chat.ID)
msg := *cb.Message
msg.From = cb.From
channel := s.findChannelForMessage(ctx, &msg)
if channel == nil {
channel = s.findChannelByChatID(ctx, cb.Message.Chat.ID)
}
switch strings.TrimSpace(cb.Data) {
case "adult_toggle":
msg := *cb.Message
msg.From = cb.From
reply := s.cmdHideAdult(ctx, &msg, nil)
if reply.Text != "" {
return s.reply(ctx, channel, cb.Message.Chat.ID, reply)
@@ -745,29 +767,17 @@ func (s *TelegramBotService) telegramUserIsChatMember(ctx context.Context, chann
"chat_id": chatID,
"user_id": telegramUserID,
}
apiURL, err := telegramMethodURL(cfg, cfg["bot_token"], "getChatMember")
if err != nil {
return false
}
body, _ := json.Marshal(payload)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(string(body)))
if err != nil {
return false
}
req.Header.Set("Content-Type", "application/json")
resp, err := telegramHTTPClient(8*time.Second, cfg).Do(req)
if err != nil {
s.log.Warn("telegram getChatMember failed", zap.String("chat_id", chatID), zap.Int("telegram_user_id", telegramUserID), zap.Error(sanitizeTelegramError(err)))
return false
}
defer resp.Body.Close()
var result struct {
OK bool `json:"ok"`
Result struct {
Status string `json:"status"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || !result.OK {
if err := telegramPostJSONDecode(ctx, cfg, "getChatMember", payload, 8*time.Second, &result); err != nil {
s.log.Warn("telegram getChatMember failed", zap.String("chat_id", chatID), zap.Int("telegram_user_id", telegramUserID), zap.Error(sanitizeTelegramError(err)))
return false
}
if !result.OK {
return false
}
switch strings.ToLower(result.Result.Status) {
@@ -784,13 +794,14 @@ func (s *TelegramBotService) telegramUserIDConfigured(channel *model.NotifyChann
}
cfg := s.telegramChannelConfig(channel)
target := strconv.Itoa(telegramUserID)
for _, value := range strings.FieldsFunc(cfg["admin_user_ids"], func(r rune) bool {
return r == ',' || r == ';' || r == ',' || r == ' ' || r == '\n' || r == '\t'
}) {
if strings.TrimSpace(value) == target {
for _, value := range telegramConfiguredUserIDs(cfg["admin_user_ids"]) {
if value == target {
return true
}
}
if strings.TrimSpace(cfg["admin_user_ids"]) == "" && strings.TrimSpace(cfg["chat_id"]) == target {
return true
}
return false
}
@@ -810,9 +821,29 @@ func telegramConfigFromChannel(crypto *CryptoService, channel *model.NotifyChann
if err := json.Unmarshal([]byte(configStr), &cfg); err != nil || cfg == nil {
return map[string]string{}
}
normalizeTelegramConfig(cfg)
return cfg
}
func normalizeTelegramConfig(cfg map[string]string) {
if cfg == nil {
return
}
chatID := strings.TrimSpace(cfg["chat_id"])
if chatID == "" {
return
}
if strings.HasPrefix(chatID, "-") {
if strings.TrimSpace(cfg["group_chat_id"]) == "" && strings.TrimSpace(cfg["channel_chat_id"]) == "" && strings.TrimSpace(cfg["command_chat_id"]) == "" {
cfg["group_chat_id"] = chatID
}
return
}
if strings.TrimSpace(cfg["admin_user_ids"]) == "" {
cfg["admin_user_ids"] = chatID
}
}
func (s *TelegramBotService) upsertTelegramBinding(ctx context.Context, msg *TelegramMessage, userID string) error {
name := strings.TrimSpace(msg.From.FirstName)
if msg.From.Username != "" {