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:
parent
88845b99d2
commit
88dbbfe982
@ -10,11 +10,66 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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 dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@ -529,10 +529,17 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
intents.members = any(not entry.isdigit() for entry in self._allowed_user_ids)
|
intents.members = any(not entry.isdigit() for entry in self._allowed_user_ids)
|
||||||
intents.voice_states = True
|
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
|
# Create bot
|
||||||
self._client = commands.Bot(
|
self._client = commands.Bot(
|
||||||
command_prefix="!", # Not really used, we handle raw messages
|
command_prefix="!", # Not really used, we handle raw messages
|
||||||
intents=intents,
|
intents=intents,
|
||||||
|
proxy=proxy_url,
|
||||||
)
|
)
|
||||||
adapter_self = self # capture for closure
|
adapter_self = self # capture for closure
|
||||||
|
|
||||||
|
|||||||
@ -45,11 +45,9 @@ _SEED_FALLBACK_IPS: list[str] = ["149.154.167.220"]
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_proxy_url() -> str | None:
|
def _resolve_proxy_url() -> str | None:
|
||||||
for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "https_proxy", "http_proxy", "all_proxy"):
|
# Delegate to shared implementation (env vars + macOS system proxy detection)
|
||||||
value = (os.environ.get(key) or "").strip()
|
from gateway.platforms.base import resolve_proxy_url
|
||||||
if value:
|
return resolve_proxy_url()
|
||||||
return value
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramFallbackTransport(httpx.AsyncBaseTransport):
|
class TelegramFallbackTransport(httpx.AsyncBaseTransport):
|
||||||
|
|||||||
@ -56,7 +56,7 @@ class FakeTree:
|
|||||||
|
|
||||||
|
|
||||||
class FakeBot:
|
class FakeBot:
|
||||||
def __init__(self, *, intents):
|
def __init__(self, *, intents, proxy=None):
|
||||||
self.intents = intents
|
self.intents = intents
|
||||||
self.user = SimpleNamespace(id=999, name="Hermes")
|
self.user = SimpleNamespace(id=999, name="Hermes")
|
||||||
self._events = {}
|
self._events = {}
|
||||||
@ -95,7 +95,7 @@ async def test_connect_only_requests_members_intent_when_needed(monkeypatch, all
|
|||||||
|
|
||||||
created = {}
|
created = {}
|
||||||
|
|
||||||
def fake_bot_factory(*, command_prefix, intents):
|
def fake_bot_factory(*, command_prefix, intents, proxy=None):
|
||||||
created["bot"] = FakeBot(intents=intents)
|
created["bot"] = FakeBot(intents=intents)
|
||||||
return created["bot"]
|
return created["bot"]
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ async def test_connect_releases_token_lock_on_timeout(monkeypatch):
|
|||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
discord_platform.commands,
|
discord_platform.commands,
|
||||||
"Bot",
|
"Bot",
|
||||||
lambda **kwargs: FakeBot(intents=kwargs["intents"]),
|
lambda **kwargs: FakeBot(intents=kwargs["intents"], proxy=kwargs.get("proxy")),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def fake_wait_for(awaitable, timeout):
|
async def fake_wait_for(awaitable, timeout):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user