fix(bot): Telegram через прокси на сервере #109

Merged
Eleboba merged 1 commits from fix/tg-proxy into dev 2026-10-05 12:23:03 +03:00
8 changed files with 58 additions and 4 deletions
Showing only changes of commit 9087e2ac8d - Show all commits
+2
View File
@@ -23,6 +23,8 @@ CLINICAL_DB_URL=postgresql+asyncpg://clinical_app:clinical_app@localhost:5432/cl
# --- Telegram: prototype on synthetic data only; prod channel is MAX / Gosuslugi. Empty = bot disabled.
TG_BOT_TOKEN=
# http://user:pass@host:port or socks5://… — needed on the RF server (docker has no route to Telegram over IPv4)
TG_PROXY_URL=
DOCTOR_NOTIFY_CHAT_ID=
# --- Demo virtual clock (ADR-005)
+8 -1
View File
@@ -19,6 +19,7 @@ from typing import Protocol
import yaml
from aiogram import Bot, Dispatcher, F, Router
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.filters import CommandObject, CommandStart
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
from pydantic import BaseModel
@@ -272,6 +273,12 @@ _task: asyncio.Task[None] | None = None
_prev_outbox: Outbox | None = None
def make_bot(token: str) -> Bot:
"""aiogram `Bot`, through `TG_PROXY_URL` when it is set."""
proxy = get_settings().tg_proxy_url
return Bot(token, session=AiohttpSession(proxy=proxy)) if proxy else Bot(token)
async def start() -> None:
"""Start polling if TG_BOT_TOKEN is set; no-op otherwise."""
global _bot, _dp, _task, _prev_outbox
@@ -280,7 +287,7 @@ async def start() -> None:
if not token:
log.info("telegram disabled: TG_BOT_TOKEN is empty")
return
bot = Bot(token)
bot = make_bot(token)
channel = TelegramChannel(bot)
dp = Dispatcher()
dp.include_router(build_router(channel))
+2
View File
@@ -44,6 +44,8 @@ class Settings(BaseSettings):
# Telegram is used only for the prototype on synthetic data; prod channel is MAX / Gosuslugi.
tg_bot_token: str = ""
# The RF server reaches api.telegram.org over IPv6 only; docker has IPv4 only → go through a proxy (http/socks).
tg_proxy_url: str = ""
doctor_notify_chat_id: int | None = None
# MAX is opt-in. Its adapter is delivered in the related backend/channel changes.
+2 -2
View File
@@ -92,9 +92,9 @@ class TelegramDoctorChat:
def _get_sender(self) -> ChatSender:
if self._sender is None:
from aiogram import Bot
from app.channels.telegram import make_bot
self._bot = Bot(self._bot_token)
self._bot = make_bot(self._bot_token)
self._sender = self._bot.send_message
return self._sender
+1
View File
@@ -20,6 +20,7 @@ dependencies = [
"pandas>=2.2",
"qrcode[pil]>=7.4",
"tzdata>=2026.5",
"aiohttp-socks>=0.8",
]
[dependency-groups]
+3 -1
View File
@@ -261,7 +261,9 @@ async def test_start_is_noop_without_token(monkeypatch: pytest.MonkeyPatch) -> N
async def test_start_stop_swaps_outbox(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(telegram, "get_settings", lambda: type("S", (), {"tg_bot_token": "123:fake"})())
monkeypatch.setattr(
telegram, "get_settings", lambda: type("S", (), {"tg_bot_token": "123:fake", "tg_proxy_url": ""})()
)
async def fake_polling(self: object, *bots: object, **kwargs: object) -> None:
import asyncio
+16
View File
@@ -0,0 +1,16 @@
"""TG_PROXY_URL: the bot session goes through the proxy when it is set."""
from app.channels import telegram
from app.config import get_settings
TOKEN = "123456:" + "A" * 35
def test_make_bot_without_proxy(monkeypatch):
monkeypatch.setattr(get_settings(), "tg_proxy_url", "")
assert telegram.make_bot(TOKEN).session.proxy is None
def test_make_bot_with_proxy(monkeypatch):
monkeypatch.setattr(get_settings(), "tg_proxy_url", "http://user:pass@127.0.0.1:8888")
assert telegram.make_bot(TOKEN).session.proxy == "http://user:pass@127.0.0.1:8888"
Generated
+24
View File
@@ -122,6 +122,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" },
]
[[package]]
name = "aiohttp-socks"
version = "0.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "python-socks" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/1d/a306e0111222180e60f17131a3f5d9bc694dd999a8115959a7dd76c2238e/aiohttp_socks-0.12.0.tar.gz", hash = "sha256:3caf9f5a4164611122d412bc11b2f9114fd29c85e1ba27bb38060d3c236bdc8d", size = 12061, upload-time = "2026-08-12T04:43:15.791Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/64/ca6289632020523ea1841f01363f83ada10eeb0f21dd931fc1118dd85668/aiohttp_socks-0.12.0-py3-none-any.whl", hash = "sha256:ba6f95ec775c761d87f8578ab48f137d0457c676da104984202bf75e747d5ee6", size = 10693, upload-time = "2026-08-12T04:43:14.524Z" },
]
[[package]]
name = "aiosignal"
version = "1.4.0"
@@ -1075,6 +1088,7 @@ version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "aiogram" },
{ name = "aiohttp-socks" },
{ name = "apscheduler" },
{ name = "asyncpg" },
{ name = "fastapi" },
@@ -1103,6 +1117,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiogram", specifier = ">=3.10" },
{ name = "aiohttp-socks", specifier = ">=0.8" },
{ name = "apscheduler", specifier = ">=3.10,<4" },
{ name = "asyncpg", specifier = ">=0.29" },
{ name = "fastapi", specifier = ">=0.115" },
@@ -1340,6 +1355,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
]
[[package]]
name = "python-socks"
version = "3.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/04/ad/484ffb79532517b11a90af38647c38652224650b31a7ae1cedd5a418d8ab/python_socks-3.1.1.tar.gz", hash = "sha256:8d3e817cdbe858dc0bb8c8fdc8e79b6ce37acce110d33374c6f57a675cc9029e", size = 232781, upload-time = "2026-09-08T13:03:31.058Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/23/2c2cef1b4313c55d1713201acd4ee2043fb2cf22e546ca9d36bf4317faea/python_socks-3.1.1-py3-none-any.whl", hash = "sha256:327e0d6378702c73a7790bf732e9f01392f17b48c7348a50b5bd1f710c2df1be", size = 49566, upload-time = "2026-09-08T13:03:29.604Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"