add telegram proxy support (#988)

This commit is contained in:
debil746429 2026-07-06 06:59:23 +03:00 committed by GitHub
parent 15cab79a43
commit 05dae97248
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 118 additions and 6 deletions

View file

@ -208,6 +208,10 @@ WHISPER_MODEL="openai/whisper-large-v3"
# Telegram Config
TELEGRAM_BOT_TOKEN=""
ALLOWED_TELEGRAM_USER_ID=""
# Optional Telegram-only proxy.
# Supported schemes: http, https, socks4, socks5, socks5h.
# Example: "socks5://127.0.0.1:1080" or "https://user:password@host:port"
TELEGRAM_PROXY_URL=""
# Discord Config

View file

@ -279,6 +279,16 @@ _NON_PROVIDER_FIELDS: tuple[ConfigFieldSpec, ...] = (
settings_attr="allowed_telegram_user_id",
session_sensitive=True,
),
ConfigFieldSpec(
"TELEGRAM_PROXY_URL",
"Telegram Proxy URL",
"messaging",
"secret",
settings_attr="telegram_proxy_url",
secret=True,
session_sensitive=True,
description="Optional Telegram-only proxy, e.g. socks5://127.0.0.1:1080.",
),
ConfigFieldSpec(
"DISCORD_BOT_TOKEN",
"Discord Bot Token",

View file

@ -185,6 +185,7 @@ class AppRuntime:
MessagingPlatformOptions(
telegram_bot_token=self.settings.telegram_bot_token,
allowed_telegram_user_id=self.settings.allowed_telegram_user_id,
telegram_proxy_url=self.settings.telegram_proxy_url,
discord_bot_token=self.settings.discord_bot_token,
allowed_discord_channels=self.settings.allowed_discord_channels,
voice_note_enabled=self.settings.voice_note_enabled,

View file

@ -259,6 +259,7 @@ class Settings(BaseSettings):
# ==================== Bot Wrapper Config ====================
telegram_bot_token: str | None = None
allowed_telegram_user_id: str | None = None
telegram_proxy_url: str = Field(default="", validation_alias="TELEGRAM_PROXY_URL")
discord_bot_token: str | None = Field(
default=None, validation_alias="DISCORD_BOT_TOKEN"
)

View file

@ -13,6 +13,7 @@ class MessagingPlatformOptions:
telegram_bot_token: str | None = None
allowed_telegram_user_id: str | None = None
telegram_proxy_url: str = ""
discord_bot_token: str | None = None
allowed_discord_channels: str | None = None
voice_note_enabled: bool = True
@ -47,6 +48,7 @@ def create_messaging_components(
runtime = TelegramRuntime(
bot_token=bot_token,
allowed_user_id=opts.allowed_telegram_user_id,
telegram_proxy_url=opts.telegram_proxy_url,
voice_note_enabled=opts.voice_note_enabled,
whisper_model=opts.whisper_model,
whisper_device=opts.whisper_device,

View file

@ -49,6 +49,7 @@ class TelegramRuntime:
bot_token: str | None = None,
allowed_user_id: str | None = None,
*,
telegram_proxy_url: str = "",
voice_note_enabled: bool = True,
whisper_model: str = "base",
whisper_device: str = "cpu",
@ -66,6 +67,7 @@ class TelegramRuntime:
self.bot_token = bot_token
self.allowed_user_id = allowed_user_id
self.telegram_proxy_url = telegram_proxy_url.strip()
if not self.bot_token:
logger.warning("TELEGRAM_BOT_TOKEN not set")
@ -102,10 +104,30 @@ class TelegramRuntime:
if not self.bot_token:
raise ValueError("TELEGRAM_BOT_TOKEN is required")
request = HTTPXRequest(
connection_pool_size=8, connect_timeout=30.0, read_timeout=30.0
)
builder = Application.builder().token(self.bot_token).request(request)
if self.telegram_proxy_url:
request = HTTPXRequest(
connection_pool_size=8,
connect_timeout=30.0,
read_timeout=30.0,
proxy=self.telegram_proxy_url,
)
update_request = HTTPXRequest(
connection_pool_size=8,
connect_timeout=30.0,
read_timeout=30.0,
proxy=self.telegram_proxy_url,
)
builder = (
Application.builder()
.token(self.bot_token)
.request(request)
.get_updates_request(update_request)
)
else:
request = HTTPXRequest(
connection_pool_size=8, connect_timeout=30.0, read_timeout=30.0
)
builder = Application.builder().token(self.bot_token).request(request)
self._application = builder.build()
self._application.add_handler(

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "free-claude-code"
version = "3.3.1"
version = "3.4.0"
description = "Middleware between Claude Code CLI (Anthropic API) and NVIDIA NIM"
readme = "README.md"
requires-python = ">=3.14.0"

View file

@ -26,6 +26,7 @@ def _clear_process_config(monkeypatch) -> None:
"NVIDIA_NIM_API_KEY",
"OPENROUTER_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"TELEGRAM_PROXY_URL",
"FCC_ENV_FILE",
"CLOUDFLARE_API_TOKEN",
"CLOUDFLARE_ACCOUNT_ID",
@ -111,6 +112,7 @@ def test_admin_config_masks_secrets_and_exposes_manifest(monkeypatch, tmp_path):
assert "GEMINI_API_KEY" in keys
assert "GROQ_API_KEY" in keys
assert "SAMBANOVA_API_KEY" in keys
assert "TELEGRAM_PROXY_URL" in keys
assert "CEREBRAS_API_KEY" in keys
assert "ZAI_BASE_URL" not in keys
assert "CLAUDE_WORKSPACE" not in keys
@ -122,6 +124,10 @@ def test_admin_config_masks_secrets_and_exposes_manifest(monkeypatch, tmp_path):
assert auth_field["secret"] is True
assert auth_field["value"] == MASKED_SECRET
assert auth_field["source"] == "template"
telegram_proxy_field = next(
field for field in body["fields"] if field["key"] == "TELEGRAM_PROXY_URL"
)
assert telegram_proxy_field["secret"] is True
def test_admin_config_preserves_managed_env_source_contract(monkeypatch, tmp_path):
@ -141,6 +147,27 @@ def test_admin_config_preserves_managed_env_source_contract(monkeypatch, tmp_pat
assert model_field["locked"] is False
def test_admin_apply_masks_telegram_proxy_credentials(monkeypatch, tmp_path):
_set_home(monkeypatch, tmp_path)
_clear_process_config(monkeypatch)
app = create_app(lifespan_enabled=False)
proxy_url = "https://user:password@proxy.example:8443"
response = _local_client(app).post(
"/admin/api/config/apply",
json={"values": {"TELEGRAM_PROXY_URL": proxy_url}},
)
assert response.status_code == 200
body = response.json()
assert body["applied"] is True
assert "TELEGRAM_PROXY_URL=********" in body["env_preview"]
assert proxy_url not in body["env_preview"]
env_file = tmp_path / ".fcc" / ".env"
text = env_file.read_text(encoding="utf-8")
assert f"TELEGRAM_PROXY_URL={proxy_url}" in text
def test_admin_validate_rejects_bad_model_shape(monkeypatch, tmp_path):
_set_home(monkeypatch, tmp_path)
_clear_process_config(monkeypatch)

View file

@ -18,6 +18,7 @@ _RUNTIME_EXTRAS = {
"model_opus": None,
"model_sonnet": None,
"model_haiku": None,
"telegram_proxy_url": "",
"voice_note_enabled": True,
"whisper_model": "base",
"whisper_device": "cpu",

View file

@ -28,6 +28,7 @@ class TestCreateMessagingComponents:
MessagingPlatformOptions(
telegram_bot_token="test_token",
allowed_telegram_user_id="12345",
telegram_proxy_url="socks5://127.0.0.1:1080",
voice_note_enabled=False,
whisper_model="large-v3",
whisper_device="cuda",
@ -41,6 +42,7 @@ class TestCreateMessagingComponents:
runtime_cls.assert_called_once_with(
bot_token="test_token",
allowed_user_id="12345",
telegram_proxy_url="socks5://127.0.0.1:1080",
voice_note_enabled=False,
whisper_model="large-v3",
whisper_device="cuda",

View file

@ -37,6 +37,48 @@ async def test_telegram_platform_start_success(telegram_platform):
mock_app.start.assert_called_once()
@pytest.mark.asyncio
async def test_telegram_platform_start_with_proxy():
with patch("messaging.platforms.telegram.TELEGRAM_AVAILABLE", True):
platform = TelegramRuntime(
bot_token="test_token",
allowed_user_id="12345",
telegram_proxy_url="socks5://127.0.0.1:1080",
)
with (
patch("telegram.ext.Application.builder") as mock_builder,
patch("messaging.platforms.telegram.HTTPXRequest") as request_cls,
):
mock_app = MagicMock()
mock_app.initialize = AsyncMock()
mock_app.start = AsyncMock()
mock_app.updater.start_polling = AsyncMock()
builder = mock_builder.return_value
builder.token.return_value = builder
builder.request.return_value = builder
builder.get_updates_request.return_value = builder
builder.build.return_value = mock_app
request = MagicMock()
update_request = MagicMock()
request_cls.side_effect = [request, update_request]
with patch("messaging.limiter.MessagingRateLimiter.get_instance", AsyncMock()):
await platform.start()
assert request_cls.call_count == 2
request_cls.assert_any_call(
connection_pool_size=8,
connect_timeout=30.0,
read_timeout=30.0,
proxy="socks5://127.0.0.1:1080",
)
builder.request.assert_called_once_with(request)
builder.get_updates_request.assert_called_once_with(update_request)
assert platform._connected is True
@pytest.mark.asyncio
async def test_telegram_platform_send_message_success(telegram_platform):
mock_bot = AsyncMock()

2
uv.lock generated
View file

@ -561,7 +561,7 @@ wheels = [
[[package]]
name = "free-claude-code"
version = "3.3.1"
version = "3.4.0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },