diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index bd07459ac8..48fb148593 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -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 diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index 2ace06e779..2715400a4a 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -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 diff --git a/gateway/platforms/telegram_network.py b/gateway/platforms/telegram_network.py index 9f6d8bb460..2b26ab9163 100644 --- a/gateway/platforms/telegram_network.py +++ b/gateway/platforms/telegram_network.py @@ -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): diff --git a/tests/gateway/test_discord_connect.py b/tests/gateway/test_discord_connect.py index 6809c443ea..dd594cf7ed 100644 --- a/tests/gateway/test_discord_connect.py +++ b/tests/gateway/test_discord_connect.py @@ -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):