mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-01 12:06:38 +08:00
fix(telegram): honor proxy fallbacks for bot api
This commit is contained in:
@@ -31,3 +31,12 @@ ADMIN_INITIAL_PASSWORD=admin123
|
|||||||
# MEDIASTATION_TRANSCODER_THREADS=2
|
# MEDIASTATION_TRANSCODER_THREADS=2
|
||||||
# MEDIASTATION_TRANSCODER_MAX_CONCURRENT=1
|
# MEDIASTATION_TRANSCODER_MAX_CONCURRENT=1
|
||||||
# MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS=120
|
# MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS=120
|
||||||
|
|
||||||
|
# Telegram Bot / notify outbound network.
|
||||||
|
# Leave empty if direct network or v2rayA redirect already covers Docker bridge traffic.
|
||||||
|
# MEDIASTATION_TELEGRAM_API_BASE_URL=https://api.telegram.org
|
||||||
|
# MEDIASTATION_TELEGRAM_PROXY_URL=http://host.docker.internal:20171
|
||||||
|
# HTTP_PROXY=
|
||||||
|
# HTTPS_PROXY=
|
||||||
|
# ALL_PROXY=
|
||||||
|
# NO_PROXY=127.0.0.1,localhost
|
||||||
|
|||||||
+11
-2
@@ -142,11 +142,20 @@ services:
|
|||||||
MEDIASTATION_APP_CORS_ORIGINS: ${MEDIASTATION_APP_CORS_ORIGINS:-}
|
MEDIASTATION_APP_CORS_ORIGINS: ${MEDIASTATION_APP_CORS_ORIGINS:-}
|
||||||
|
|
||||||
# Telegram 通知/Bot 出站网络。通常留空即可;如 NAS 访问 Telegram 超时,
|
# Telegram 通知/Bot 出站网络。通常留空即可;如 NAS 访问 Telegram 超时,
|
||||||
# 可在 .env 中填写反代 API 或代理地址:
|
# 可在 .env 中填写反代 API 或代理地址。v2rayA 常见代理:
|
||||||
|
# HTTP http://host.docker.internal:20171 或 http://172.17.0.1:20171
|
||||||
|
# SOCKS socks5://host.docker.internal:20170 或 socks5://172.17.0.1:20170
|
||||||
# MEDIASTATION_TELEGRAM_API_BASE_URL=https://api.telegram.org
|
# MEDIASTATION_TELEGRAM_API_BASE_URL=https://api.telegram.org
|
||||||
# MEDIASTATION_TELEGRAM_PROXY_URL=http://172.17.0.1:7890
|
# MEDIASTATION_TELEGRAM_PROXY_URL=http://host.docker.internal:20171
|
||||||
MEDIASTATION_TELEGRAM_API_BASE_URL: ${MEDIASTATION_TELEGRAM_API_BASE_URL:-}
|
MEDIASTATION_TELEGRAM_API_BASE_URL: ${MEDIASTATION_TELEGRAM_API_BASE_URL:-}
|
||||||
MEDIASTATION_TELEGRAM_PROXY_URL: ${MEDIASTATION_TELEGRAM_PROXY_URL:-}
|
MEDIASTATION_TELEGRAM_PROXY_URL: ${MEDIASTATION_TELEGRAM_PROXY_URL:-}
|
||||||
|
# 可选:把宿主机 shell/.env 中的代理变量透传给容器。留空无效果。
|
||||||
|
# 如果 NAS 已使用 v2rayA redirect/透明代理且 Docker 网桥流量已被接管,
|
||||||
|
# 通常不需要填写这些变量。
|
||||||
|
HTTP_PROXY: ${HTTP_PROXY:-}
|
||||||
|
HTTPS_PROXY: ${HTTPS_PROXY:-}
|
||||||
|
ALL_PROXY: ${ALL_PROXY:-}
|
||||||
|
NO_PROXY: ${NO_PROXY:-127.0.0.1,localhost}
|
||||||
|
|
||||||
# 宿主机文件权限映射。Linux/NAS 常用 1000:1000,可用 id 命令查看。
|
# 宿主机文件权限映射。Linux/NAS 常用 1000:1000,可用 id 命令查看。
|
||||||
PUID: ${PUID:-1000}
|
PUID: ${PUID:-1000}
|
||||||
|
|||||||
@@ -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 {
|
func telegramHTTPClients(timeout time.Duration, cfg map[string]string) []*http.Client {
|
||||||
clients := []*http.Client{}
|
clients := []*http.Client{}
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for _, proxyRaw := range telegramProxyCandidates(cfg) {
|
|
||||||
proxyURL, err := normalizeProxyURL(proxyRaw, "http")
|
addProxy := func(proxyURL *url.URL) {
|
||||||
if err != nil || proxyURL == nil {
|
if proxyURL == nil {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
key := proxyURL.String()
|
key := proxyURL.String()
|
||||||
if seen[key] {
|
if seen[key] {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
transport := NewExternalTransport()
|
transport := NewExternalTransport()
|
||||||
transport.Proxy = http.ProxyURL(proxyURL)
|
transport.Proxy = http.ProxyURL(proxyURL)
|
||||||
clients = append(clients, &http.Client{Timeout: timeout, Transport: transport})
|
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 := NewExternalTransport()
|
||||||
|
transport.Proxy = nil
|
||||||
clients = append(clients, &http.Client{Timeout: timeout, Transport: transport})
|
clients = append(clients, &http.Client{Timeout: timeout, Transport: transport})
|
||||||
return clients
|
return clients
|
||||||
}
|
}
|
||||||
|
|
||||||
func telegramProxyCandidates(cfg map[string]string) []string {
|
func telegramExplicitProxyCandidates(cfg map[string]string) []string {
|
||||||
out := []string{}
|
out := []string{}
|
||||||
for _, value := range []string{
|
for _, value := range []string{
|
||||||
cfg["proxy_url"],
|
cfg["proxy_url"],
|
||||||
@@ -82,22 +101,48 @@ func telegramProxyCandidates(cfg map[string]string) []string {
|
|||||||
out = append(out, value)
|
out = append(out, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func telegramProxyCandidates(cfg map[string]string) []string {
|
||||||
|
out := telegramExplicitProxyCandidates(cfg)
|
||||||
if len(out) > 0 {
|
if len(out) > 0 {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
for _, value := range []string{
|
return telegramFallbackProxyCandidates()
|
||||||
"http://127.0.0.1:10808",
|
}
|
||||||
"http://127.0.0.1:10809",
|
|
||||||
"http://127.0.0.1:7890",
|
func telegramFallbackProxyCandidates() []string {
|
||||||
"http://127.0.0.1:7891",
|
// 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:7890",
|
||||||
"http://host.docker.internal:10808",
|
"http://host.docker.internal:10808",
|
||||||
"http://172.17.0.1:7890",
|
"http://172.17.0.1:7890",
|
||||||
"http://172.17.0.1:10808",
|
"http://172.17.0.1:10808",
|
||||||
} {
|
"http://127.0.0.1:10808",
|
||||||
out = append(out, value)
|
"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 {
|
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")
|
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 {
|
func telegramStringConfigFromAny(cfg map[string]any) map[string]string {
|
||||||
out := make(map[string]string, len(cfg))
|
out := make(map[string]string, len(cfg))
|
||||||
for key, value := range cfg {
|
for key, value := range cfg {
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTelegramMethodURLUsesCustomAPIBase(t *testing.T) {
|
func TestTelegramMethodURLUsesCustomAPIBase(t *testing.T) {
|
||||||
@@ -90,13 +92,45 @@ func TestTelegramTargetChatIDsUsesLegacyPrivateChatID(t *testing.T) {
|
|||||||
func TestTelegramProxyCandidatesDefaultLocalFallbacks(t *testing.T) {
|
func TestTelegramProxyCandidatesDefaultLocalFallbacks(t *testing.T) {
|
||||||
got := telegramProxyCandidates(map[string]string{})
|
got := telegramProxyCandidates(map[string]string{})
|
||||||
joined := strings.Join(got, ",")
|
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) {
|
if !strings.Contains(joined, want) {
|
||||||
t.Fatalf("default proxy candidates %q missing %q", 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) {
|
func TestTelegramCommandFiltering(t *testing.T) {
|
||||||
if telegramIsCommandText("今天看什么") {
|
if telegramIsCommandText("今天看什么") {
|
||||||
t.Fatal("plain chat message should not be treated as command")
|
t.Fatal("plain chat message should not be treated as command")
|
||||||
|
|||||||
@@ -1296,49 +1296,21 @@ func userNameOrFallback(user *model.User) string {
|
|||||||
|
|
||||||
// SetWebhook 注册 Telegram Bot Webhook URL。
|
// SetWebhook 注册 Telegram Bot Webhook URL。
|
||||||
func (s *TelegramBotService) SetWebhook(ctx context.Context, botToken, webhookURL string) error {
|
func (s *TelegramBotService) SetWebhook(ctx context.Context, botToken, webhookURL string) error {
|
||||||
payload, _ := json.Marshal(map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"url": webhookURL,
|
"url": webhookURL,
|
||||||
"allowed_updates": []string{"message", "callback_query"},
|
"allowed_updates": []string{"message", "callback_query"},
|
||||||
})
|
}
|
||||||
cfg := map[string]string{"bot_token": botToken}
|
cfg := map[string]string{"bot_token": botToken}
|
||||||
apiURL, err := telegramMethodURL(cfg, botToken, "setWebhook")
|
return telegramPostJSON(ctx, cfg, "setWebhook", payload, 15*time.Second)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebhookInfo 获取 Webhook 配置信息。
|
// GetWebhookInfo 获取 Webhook 配置信息。
|
||||||
func (s *TelegramBotService) GetWebhookInfo(ctx context.Context, botToken string) (map[string]interface{}, error) {
|
func (s *TelegramBotService) GetWebhookInfo(ctx context.Context, botToken string) (map[string]interface{}, error) {
|
||||||
cfg := map[string]string{"bot_token": botToken}
|
cfg := map[string]string{"bot_token": botToken}
|
||||||
apiURL, err := telegramMethodURL(cfg, botToken, "getWebhookInfo")
|
var result map[string]interface{}
|
||||||
if err != nil {
|
if err := telegramGetJSONDecode(ctx, cfg, "getWebhookInfo", 10*time.Second, &result); err != nil {
|
||||||
return nil, err
|
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
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user