mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-07-09 16:00:45 +00:00
add telegram proxy support (#988)
This commit is contained in:
parent
15cab79a43
commit
05dae97248
12 changed files with 118 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
2
uv.lock
generated
|
|
@ -561,7 +561,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "free-claude-code"
|
||||
version = "3.3.1"
|
||||
version = "3.4.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue