feat(gateway): unified proxy support for Discord and Telegram with macOS auto-detection

- Add resolve_proxy_url() to base.py — shared by all platform adapters
- Check HTTPS_PROXY / HTTP_PROXY / ALL_PROXY env vars first
- Fall back to macOS system proxy via scutil --proxy (zero-config)
- Pass proxy= to discord.py commands.Bot() for gateway connectivity
- Refactor telegram_network.py to use shared resolver
- Update test fixtures to accept proxy kwarg
This commit is contained in:
Zheng Li
2026-04-09 19:37:58 +08:00
committed by Teknium
parent 88845b99d2
commit 88dbbfe982
4 changed files with 68 additions and 8 deletions

View File

@@ -10,11 +10,66 @@ import logging
import os
import random
import re
import subprocess
import sys
import uuid
from abc import ABC, abstractmethod
from urllib.parse import urlsplit
logger = logging.getLogger(__name__)
def _detect_macos_system_proxy() -> str | None:
"""Read the macOS system HTTP(S) proxy via ``scutil --proxy``.
Returns an ``http://host:port`` URL string if an HTTP or HTTPS proxy is
enabled, otherwise *None*. Falls back silently on non-macOS or on any
subprocess error.
"""
if sys.platform != "darwin":
return None
try:
out = subprocess.check_output(
["scutil", "--proxy"], timeout=3, text=True, stderr=subprocess.DEVNULL,
)
except Exception:
return None
props: dict[str, str] = {}
for line in out.splitlines():
line = line.strip()
if " : " in line:
key, _, val = line.partition(" : ")
props[key.strip()] = val.strip()
# Prefer HTTPS, fall back to HTTP
for enable_key, host_key, port_key in (
("HTTPSEnable", "HTTPSProxy", "HTTPSPort"),
("HTTPEnable", "HTTPProxy", "HTTPPort"),
):
if props.get(enable_key) == "1":
host = props.get(host_key)
port = props.get(port_key)
if host and port:
return f"http://{host}:{port}"
return None
def resolve_proxy_url() -> str | None:
"""Return an HTTP(S) proxy URL from env vars, or macOS system proxy.
Check order:
1. HTTPS_PROXY / HTTP_PROXY / ALL_PROXY (and lowercase variants)
2. macOS system proxy via ``scutil --proxy`` (auto-detect)
Returns *None* if no proxy is found.
"""
for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY",
"https_proxy", "http_proxy", "all_proxy"):
value = (os.environ.get(key) or "").strip()
if value:
return value
return _detect_macos_system_proxy()
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path

View File

@@ -529,10 +529,17 @@ class DiscordAdapter(BasePlatformAdapter):
intents.members = any(not entry.isdigit() for entry in self._allowed_user_ids)
intents.voice_states = True
# Resolve HTTP proxy (env vars first, then macOS system proxy)
from gateway.platforms.base import resolve_proxy_url
proxy_url = resolve_proxy_url()
if proxy_url:
logger.info("[%s] Using HTTP proxy: %s", self.name, proxy_url)
# Create bot
self._client = commands.Bot(
command_prefix="!", # Not really used, we handle raw messages
intents=intents,
proxy=proxy_url,
)
adapter_self = self # capture for closure

View File

@@ -45,11 +45,9 @@ _SEED_FALLBACK_IPS: list[str] = ["149.154.167.220"]
def _resolve_proxy_url() -> str | None:
for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "https_proxy", "http_proxy", "all_proxy"):
value = (os.environ.get(key) or "").strip()
if value:
return value
return None
# Delegate to shared implementation (env vars + macOS system proxy detection)
from gateway.platforms.base import resolve_proxy_url
return resolve_proxy_url()
class TelegramFallbackTransport(httpx.AsyncBaseTransport):

View File

@@ -56,7 +56,7 @@ class FakeTree:
class FakeBot:
def __init__(self, *, intents):
def __init__(self, *, intents, proxy=None):
self.intents = intents
self.user = SimpleNamespace(id=999, name="Hermes")
self._events = {}
@@ -95,7 +95,7 @@ async def test_connect_only_requests_members_intent_when_needed(monkeypatch, all
created = {}
def fake_bot_factory(*, command_prefix, intents):
def fake_bot_factory(*, command_prefix, intents, proxy=None):
created["bot"] = FakeBot(intents=intents)
return created["bot"]
@@ -124,7 +124,7 @@ async def test_connect_releases_token_lock_on_timeout(monkeypatch):
monkeypatch.setattr(
discord_platform.commands,
"Bot",
lambda **kwargs: FakeBot(intents=kwargs["intents"]),
lambda **kwargs: FakeBot(intents=kwargs["intents"], proxy=kwargs.get("proxy")),
)
async def fake_wait_for(awaitable, timeout):