fix(telegram): honor proxy fallbacks for bot api

This commit is contained in:
ShukeBta
2026-06-07 13:58:24 +08:00
parent 1d88a09568
commit b35b36738e
5 changed files with 152 additions and 50 deletions
+92 -14
View File
@@ -53,26 +53,45 @@ func telegramHTTPClient(timeout time.Duration, cfg map[string]string) *http.Clie
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
addProxy := func(proxyURL *url.URL) {
if proxyURL == nil {
return
}
key := proxyURL.String()
if seen[key] {
continue
return
}
seen[key] = true
transport := NewExternalTransport()
transport.Proxy = http.ProxyURL(proxyURL)
clients = append(clients, &http.Client{Timeout: timeout, Transport: transport})
}
for _, proxyRaw := range telegramExplicitProxyCandidates(cfg) {
proxyURL, err := normalizeProxyURL(proxyRaw, "http")
if err != nil || proxyURL == nil {
continue
}
addProxy(proxyURL)
}
if proxyURL, err := telegramAutoProxyURL(cfg); err == nil {
addProxy(proxyURL)
}
for _, proxyRaw := range telegramFallbackProxyCandidates() {
proxyURL, err := normalizeProxyURL(proxyRaw, "http")
if err != nil || proxyURL == nil {
continue
}
addProxy(proxyURL)
}
transport := NewExternalTransport()
transport.Proxy = nil
clients = append(clients, &http.Client{Timeout: timeout, Transport: transport})
return clients
}
func telegramProxyCandidates(cfg map[string]string) []string {
func telegramExplicitProxyCandidates(cfg map[string]string) []string {
out := []string{}
for _, value := range []string{
cfg["proxy_url"],
@@ -82,22 +101,48 @@ func telegramProxyCandidates(cfg map[string]string) []string {
out = append(out, value)
}
}
return out
}
func telegramProxyCandidates(cfg map[string]string) []string {
out := telegramExplicitProxyCandidates(cfg)
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",
return telegramFallbackProxyCandidates()
}
func telegramFallbackProxyCandidates() []string {
// Common local proxy ports used by Clash / v2rayN / v2rayA. Docker on Linux
// reaches host services through host.docker.internal (when extra_hosts is
// configured) or the bridge gateway 172.17.0.1. These are only fallbacks:
// explicit channel/env proxy and environment/system proxy are tried first.
return []string{
"http://host.docker.internal:20171",
"socks5://host.docker.internal:20170",
"http://172.17.0.1:20171",
"socks5://172.17.0.1:20170",
"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)
"http://127.0.0.1:10808",
"http://127.0.0.1:10809",
"http://127.0.0.1:7890",
"http://127.0.0.1:7891",
}
return out
}
func telegramAutoProxyURL(cfg map[string]string) (*url.URL, error) {
base := telegramAPIBaseURL(cfg)
if _, err := url.ParseRequestURI(base); err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodGet, base, nil)
if err != nil {
return nil, err
}
return ProxyFromEnvironmentOrSystem(req)
}
func telegramPostForm(ctx context.Context, cfg map[string]string, method string, form url.Values, timeout time.Duration) error {
@@ -193,6 +238,39 @@ func telegramPostJSONDecode(ctx context.Context, cfg map[string]string, method s
return errors.New("telegram request failed")
}
func telegramGetJSONDecode(ctx context.Context, cfg map[string]string, method string, timeout time.Duration, out any) error {
apiURL, err := telegramMethodURL(cfg, cfg["bot_token"], method)
if err != nil {
return err
}
var lastErr error
for _, client := range telegramHTTPClients(timeout, cfg) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
if err != nil {
return err
}
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 {
+35 -1
View File
@@ -2,8 +2,10 @@ package service
import (
"errors"
"net/http"
"strings"
"testing"
"time"
)
func TestTelegramMethodURLUsesCustomAPIBase(t *testing.T) {
@@ -90,13 +92,45 @@ func TestTelegramTargetChatIDsUsesLegacyPrivateChatID(t *testing.T) {
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"} {
for _, want := range []string{"host.docker.internal:20171", "socks5://172.17.0.1:20170", "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)
}
}
}
func TestTelegramHTTPClientsPreferConfiguredProxy(t *testing.T) {
clients := telegramHTTPClients(time.Second, map[string]string{
"proxy_url": "http://proxy.example:7890",
})
if len(clients) == 0 {
t.Fatal("expected telegram clients")
}
if got := telegramClientProxyString(t, clients[0]); got != "http://proxy.example:7890" {
t.Fatalf("first client proxy = %q, want configured proxy", got)
}
}
func telegramClientProxyString(t *testing.T, client *http.Client) string {
t.Helper()
transport, ok := client.Transport.(*http.Transport)
if !ok || transport.Proxy == nil {
return ""
}
req, err := http.NewRequest(http.MethodGet, defaultTelegramAPIBaseURL, nil)
if err != nil {
t.Fatal(err)
}
proxyURL, err := transport.Proxy(req)
if err != nil {
t.Fatal(err)
}
if proxyURL == nil {
return ""
}
return proxyURL.String()
}
func TestTelegramCommandFiltering(t *testing.T) {
if telegramIsCommandText("今天看什么") {
t.Fatal("plain chat message should not be treated as command")
+5 -33
View File
@@ -1296,49 +1296,21 @@ func userNameOrFallback(user *model.User) string {
// SetWebhook 注册 Telegram Bot Webhook URL。
func (s *TelegramBotService) SetWebhook(ctx context.Context, botToken, webhookURL string) error {
payload, _ := json.Marshal(map[string]interface{}{
payload := map[string]interface{}{
"url": webhookURL,
"allowed_updates": []string{"message", "callback_query"},
})
}
cfg := map[string]string{"bot_token": botToken}
apiURL, err := telegramMethodURL(cfg, botToken, "setWebhook")
if err != nil {
return err
}
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(string(payload)))
req.Header.Set("Content-Type", "application/json")
client := telegramHTTPClient(15*time.Second, cfg)
resp, err := client.Do(req)
if err != nil {
return sanitizeTelegramError(err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("setWebhook failed: %s", sanitizeTelegramText(string(body)))
}
return nil
return telegramPostJSON(ctx, cfg, "setWebhook", payload, 15*time.Second)
}
// GetWebhookInfo 获取 Webhook 配置信息。
func (s *TelegramBotService) GetWebhookInfo(ctx context.Context, botToken string) (map[string]interface{}, error) {
cfg := map[string]string{"bot_token": botToken}
apiURL, err := telegramMethodURL(cfg, botToken, "getWebhookInfo")
if err != nil {
var result map[string]interface{}
if err := telegramGetJSONDecode(ctx, cfg, "getWebhookInfo", 10*time.Second, &result); err != nil {
return nil, err
}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
resp, err := telegramHTTPClient(10*time.Second, cfg).Do(req)
if err != nil {
return nil, sanitizeTelegramError(err)
}
defer resp.Body.Close()
var result map[string]interface{}
body, _ := io.ReadAll(resp.Body)
json.Unmarshal(body, &result)
return result, nil
}