diff --git a/.env.example b/.env.example index 36bdac6b..6c290d9c 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,11 @@ SUPPORT_USERNAME=@support # Имя пользователя бота (опционально, автоопределяется) # BOT_USERNAME= +# ===== SOCKS5 ПРОКСИ ===== +# URL SOCKS5 прокси-сервера для маршрутизации трафика бота к Telegram API +# Формат: socks5://user:password@host:port или socks5://host:port +# PROXY_URL=socks5://127.0.0.1:1080 + # ===== СИСТЕМА ПОДДЕРЖКИ ===== # Включить меню поддержки в интерфейсе SUPPORT_MENU_ENABLED=true diff --git a/app/bot.py b/app/bot.py index f456798a..e4c92dbb 100644 --- a/app/bot.py +++ b/app/bot.py @@ -99,7 +99,19 @@ async def setup_bot() -> tuple[Bot, Dispatcher]: from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode - bot = Bot(token=settings.BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) + proxy_url = settings.get_proxy_url() + session = None + if proxy_url: + from urllib.parse import urlparse + + from aiogram.client.session.aiohttp import AiohttpSession + + session = AiohttpSession(proxy=proxy_url) + parsed = urlparse(proxy_url) + masked = f'{parsed.scheme}://***@{parsed.hostname}:{parsed.port}' if parsed.username else proxy_url + logger.info('Proxy configured', proxy_url=masked) + + bot = Bot(token=settings.BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML), session=session) maintenance_service.set_bot(bot) logger.info('Бот установлен в maintenance_service') diff --git a/app/config.py b/app/config.py index 14b984a1..30fd9f2b 100644 --- a/app/config.py +++ b/app/config.py @@ -802,6 +802,27 @@ class Settings(BaseSettings): BAN_SYSTEM_API_TOKEN: str | None = None BAN_SYSTEM_REQUEST_TIMEOUT: int = 30 + # SOCKS5 proxy for routing bot traffic to Telegram API + # Format: socks5://user:password@host:port or socks5://host:port + PROXY_URL: str | None = None + + @field_validator('PROXY_URL', mode='before') + @classmethod + def validate_proxy_url(cls, value: str | None) -> str | None: + if not value: + return None + from urllib.parse import urlparse + + parsed = urlparse(value) + if parsed.scheme not in ('socks5', 'socks4'): + raise ValueError( + f'PROXY_URL must use socks5:// or socks4:// scheme, got: {parsed.scheme!r}. ' + 'HTTP proxies are not supported for security reasons (bot token would be exposed).' + ) + if not parsed.hostname: + raise ValueError('PROXY_URL must contain a hostname') + return value + @field_validator('MAIN_MENU_MODE', mode='before') @classmethod def normalize_main_menu_mode(cls, value: str | None) -> str: @@ -928,6 +949,10 @@ class Settings(BaseSettings): """Проверяет, используется ли SQLite""" return 'sqlite' in self.get_database_url() + def get_proxy_url(self) -> str | None: + """Return SOCKS5 proxy URL or None.""" + return self.PROXY_URL if self.PROXY_URL else None + def is_admin(self, telegram_id: int | None = None, email: str | None = None) -> bool: """ Check if user is admin by telegram_id or email. diff --git a/pyproject.toml b/pyproject.toml index 6c98fbee..6a9d0ba1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ 'pyzipper>=0.3.6', 'structlog>=25.1.0,<26', 'rich>=14.0', + 'aiohttp-socks>=0.10.1', ] [dependency-groups] diff --git a/requirements.txt b/requirements.txt index 3065a0d2..f94b1657 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # Основные зависимости aiogram==3.22.0 aiohttp==3.12.15 +aiohttp-socks>=0.10.1 asyncpg==0.31.0 SQLAlchemy==2.0.46 alembic==1.18.4 diff --git a/uv.lock b/uv.lock index 392c7554..b04db498 100644 --- a/uv.lock +++ b/uv.lock @@ -71,6 +71,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, ] +[[package]] +name = "aiohttp-socks" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "python-socks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/cc/e5bbd54f76bd56291522251e47267b645dac76327b2657ade9545e30522c/aiohttp_socks-0.11.0.tar.gz", hash = "sha256:0afe51638527c79077e4bd6e57052c87c4824233d6e20bb061c53766421b10f0", size = 11196, upload-time = "2025-12-09T13:35:52.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/7d/4b633d709b8901d59444d2e512b93e72fe62d2b492a040097c3f7ba017bb/aiohttp_socks-0.11.0-py3-none-any.whl", hash = "sha256:9aacce57c931b8fbf8f6d333cf3cafe4c35b971b35430309e167a35a8aab9ec1", size = 10556, upload-time = "2025-12-09T13:35:50.18Z" }, +] + [[package]] name = "aiosignal" version = "1.4.0" @@ -1057,6 +1070,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, ] +[[package]] +name = "python-socks" +version = "2.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/0b/cd77011c1bc01b76404f7aba07fca18aca02a19c7626e329b40201217624/python_socks-2.8.1.tar.gz", hash = "sha256:698daa9616d46dddaffe65b87db222f2902177a2d2b2c0b9a9361df607ab3687", size = 38909, upload-time = "2026-02-16T05:24:00.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/fe/9a58cb6eec633ff6afae150ca53c16f8cc8b65862ccb3d088051efdfceb7/python_socks-2.8.1-py3-none-any.whl", hash = "sha256:28232739c4988064e725cdbcd15be194743dd23f1c910f784163365b9d7be035", size = 55087, upload-time = "2026-02-16T05:23:59.147Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1115,10 +1137,11 @@ wheels = [ [[package]] name = "remnawave-bedolaga-telegram-bot" -version = "3.34.1" +version = "3.36.0" source = { virtual = "." } dependencies = [ { name = "aiogram" }, + { name = "aiohttp-socks" }, { name = "aiosqlite" }, { name = "alembic" }, { name = "asyncpg" }, @@ -1149,6 +1172,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiogram", specifier = ">=3.25.0" }, + { name = "aiohttp-socks", specifier = ">=0.10.1" }, { name = "aiosqlite", specifier = ">=0.22.1" }, { name = "alembic", specifier = ">=1.18.4" }, { name = "asyncpg", specifier = ">=0.31.0" },