import asyncio import contextlib import logging import os import re import aiohttp from aiogram import Bot from aiogram.client.session.aiohttp import AiohttpSession from dotenv import load_dotenv try: from aiohttp_socks import ProxyConnector except ImportError: # pragma: no cover ProxyConnector = None load_dotenv() logging.basicConfig(level=logging.INFO) logger = logging.getLogger("config") BOT_TOKEN = os.getenv("BOT_TOKEN", "").strip() YSK_API_KEY = os.getenv("YSK_API_KEY", "").strip() API_WEATHER = os.getenv("API_WEATHER", "").strip() ACCESS_KEY = os.getenv("ACCESS_KEY", "").strip() SECRET_KEY = os.getenv("SECRET_KEY", "").strip() BUCKET_NAME = os.getenv("BUCKET_NAME", "").strip() DB_PATH = os.getenv("DB_PATH", "bot_database.db").strip() or "bot_database.db" # Можно использовать старую переменную PROXY_URL или новую PROXY_URLS. # Примеры: # PROXY_URL=socks5://127.0.0.1:12553 # PROXY_URLS=socks5://127.0.0.1:12553,socks5://127.0.0.1:12554 _PROXY_RAW = ( os.getenv("PROXY_URLS", "").strip() or os.getenv("PROXY_URL", "").strip() ) class NetworkUnavailableError(RuntimeError): """Нет рабочего подключения ни через прокси, ни напрямую.""" def _parse_proxy_urls(raw_value: str) -> list[str]: """Разбирает список прокси из .env. Поддерживаются разделители: - запятая - точка с запятой - перенос строки Важно: AiohttpSession принимает только один proxy URL за раз, поэтому список нужно разбирать и проверять по очереди. """ if not raw_value: return [] items = [] for item in re.split(r"[,;\r\n]+", raw_value): proxy = item.strip().strip("'\"") if proxy: items.append(proxy) # Убираем дубли, но сохраняем порядок. result = [] seen = set() for proxy in items: if proxy not in seen: result.append(proxy) seen.add(proxy) return result PROXY_URLS = _parse_proxy_urls(_PROXY_RAW) # Оставляем PROXY_URL для обратной совместимости со старым кодом. # Но новый код должен брать именно get_active_proxy_url(). PROXY_URL = PROXY_URLS[0] if PROXY_URLS else None _ACTIVE_PROXY_URL: str | None = None _CONNECTION_READY = False if not BOT_TOKEN: raise RuntimeError("BOT_TOKEN is not set. Put it into .env") def _mask_proxy(proxy_url: str | None) -> str: """Прячет логин/пароль в логах.""" if not proxy_url: return "напрямую" return re.sub( r"(://)([^:/@\s]+):([^/@\s]+)@", r"\1***:***@", proxy_url, ) def _is_socks_proxy(proxy_url: str | None) -> bool: if not proxy_url: return False return proxy_url.lower().startswith(("socks4://", "socks5://", "socks5h://")) def get_active_proxy_url() -> str | None: """Возвращает прокси, который реально прошёл проверку. Если подключение выбрано напрямую, вернёт None. Это нужно для других модулей, которые используют aiohttp. """ return _ACTIVE_PROXY_URL def ensure_connection_ready() -> None: """Защита от сетевых вызовов до setup_bot_connection().""" if not _CONNECTION_READY: raise NetworkUnavailableError( "Подключение к Telegram не настроено. Сначала вызовите setup_bot_connection()." ) def create_aiohttp_session(timeout: aiohttp.ClientTimeout | None = None) -> aiohttp.ClientSession: """Создаёт aiohttp-сессию с учётом активного прокси. Почему так: - aiogram.AiohttpSession умеет SOCKS-прокси сам; - обычный aiohttp не должен получать socks5:// через параметр proxy=; - для SOCKS в aiohttp нужен aiohttp_socks.ProxyConnector. """ ensure_connection_ready() proxy_url = get_active_proxy_url() if not proxy_url: return aiohttp.ClientSession(timeout=timeout) if _is_socks_proxy(proxy_url): if ProxyConnector is None: raise RuntimeError( "Для SOCKS-прокси нужен пакет aiohttp-socks. Установите: pip install aiohttp-socks" ) connector = ProxyConnector.from_url(proxy_url) return aiohttp.ClientSession(timeout=timeout, connector=connector) # Для HTTP/HTTPS-прокси aiohttp использует параметр proxy= в конкретном запросе. return aiohttp.ClientSession(timeout=timeout) def get_aiohttp_request_kwargs() -> dict: """Аргументы для session.get/post у обычного aiohttp. SOCKS-прокси уже встроен в connector внутри create_aiohttp_session(), поэтому proxy= для socks5:// здесь не передаём. """ proxy_url = get_active_proxy_url() if proxy_url and not _is_socks_proxy(proxy_url): return {"proxy": proxy_url} return {} async def _close_session_safely(session: AiohttpSession | None) -> None: if session is None: return with contextlib.suppress(Exception): await session.close() async def _build_checked_session(proxy_url: str | None) -> AiohttpSession: """Создаёт сессию и проверяет связь с Telegram методом get_me().""" session = AiohttpSession(proxy=proxy_url) if proxy_url else AiohttpSession() test_bot = Bot(token=BOT_TOKEN, session=session) try: await test_bot.get_me() except Exception: await _close_session_safely(session) raise return session async def setup_bot_connection() -> bool: """Подбирает рабочее подключение для bot. Алгоритм: 1. Проверить все прокси из .env по очереди. 2. Если ни один не подошёл — попробовать прямое подключение. 3. Если прямое подключение тоже не работает — вернуть False. Функция не бросает исключение наружу из-за нерабочей сети/прокси. main.py сможет аккуратно завершить работу без traceback. """ global _ACTIVE_PROXY_URL, _CONNECTION_READY _CONNECTION_READY = False for proxy_url in PROXY_URLS: shown_proxy = _mask_proxy(proxy_url) logger.info("Пробую подключиться через прокси %s", shown_proxy) try: checked_session = await _build_checked_session(proxy_url) except Exception as exc: logger.warning( "Связь через прокси %s не удалась: %s. Пробую следующий вариант подключения.", shown_proxy, exc, ) continue old_session = bot.session bot.session = checked_session await _close_session_safely(old_session) _ACTIVE_PROXY_URL = proxy_url _CONNECTION_READY = True logger.info("Подключение через прокси %s успешно.", shown_proxy) return True if PROXY_URLS: logger.warning("Ни один прокси не помог. Пробую подключиться напрямую.") else: logger.info("Прокси не указаны. Пробую подключиться напрямую.") try: checked_session = await _build_checked_session(None) except Exception as exc: _ACTIVE_PROXY_URL = None _CONNECTION_READY = False logger.error("Прямое подключение не удалось: %s", exc) logger.error("Нет возможности соединиться с Telegram. Завершаю работу.") return False old_session = bot.session bot.session = checked_session await _close_session_safely(old_session) _ACTIVE_PROXY_URL = None _CONNECTION_READY = True logger.info("Прямое подключение успешно.") return True # Важно: не создаём AiohttpSession(proxy=PROXY_URL) на импорте. # Если PROXY_URL кривой, программа должна не падать при импорте, # а аккуратно проверить все варианты в setup_bot_connection(). bot = Bot(token=BOT_TOKEN, session=AiohttpSession())