diff --git a/.env.example b/.env.example index f2a061b..49ce767 100644 --- a/.env.example +++ b/.env.example @@ -20,8 +20,9 @@ STARS_COUNT=1 DEFAULT_CHOICE=2 START_PAGE=1 -CHECK_INTERVAL=30 +CHECK_INTERVAL=10 API_DELAY=5 +API_TIMEOUT=15 MAX_RETRIES=3 ENABLE_REPLY=true @@ -39,3 +40,16 @@ CAPTCHA_ANSWER=40 # Капча будет автоматически обновляться из файла storage/captcha.txt CAPTCHA_CHECK_INTERVAL=1800 +# Прокси: применяется в зависимости от выбранных опций PROXY_TELEGRAM и PROXY_LOLZ +PROXY_TELEGRAM=false +PROXY_LOLZ=false +# Прокси +# Формат protocol://username:password@host:port или protocol://host:port +PROXY= + +# Можно задать данные для прокси отдельно +PROXY_PROTOCOL=socks5 # поддерживаются socks5 и https форматы +PROXY_HOST= +PROXY_PORT= +PROXY_USERNAME= # Можно не ставить, если для прокси авторизация не нужна +PROXY_PASSWORD= # Можно не ставить, если для прокси авторизация не нужна diff --git a/config.py b/config.py index bc1f098..9bea2f6 100644 --- a/config.py +++ b/config.py @@ -115,6 +115,7 @@ class Config: api_delay = max( float(os.getenv(key="API_DELAY", default=5)), 2 ) # 30r/60s для non-GET запросов, 60/30 = минимум + api_timeout = float(os.getenv(key="API_TIMEOUT", default=15)) max_retries = int(os.getenv(key="MAX_RETRIES", default=3)) enable_reply = ( @@ -145,6 +146,133 @@ class Config: "ENABLE_CAPTCHA установлен, но CAPTCHA_ANSWER не задан" ) + proxy_telegram = ( + os.getenv(key="PROXY_TELEGRAM", default="false") + ).lower() == "true" + proxy_lolz = ( + os.getenv(key="PROXY_LOLZ", default="false") + ).lower() == "true" + + proxy_protocol = None + proxy_host = None + proxy_port = None + proxy_port_str = None + proxy_username = None + proxy_password = None + + if proxy_telegram or proxy_lolz: + proxy = os.getenv(key="PROXY", default=None) + + proxy_protocol = os.getenv(key="PROXY_PROTOCOL", default="socks5") + proxy_host = os.getenv(key="PROXY_HOST", default=None) + proxy_port_str = os.getenv(key="PROXY_PORT", default=None) + proxy_username = os.getenv(key="PROXY_USERNAME", default=None) + proxy_password = os.getenv(key="PROXY_PASSWORD", default=None) + + if proxy: + logger.info("Парсинг прокси из переменной PROXY...") + try: + parts = proxy.split("://") + if len(parts) != 2: + raise ConfigError( + "Неверный формат PROXY. Ожидается: protocol://[username:password@]host:port" + ) + + proxy_protocol = parts[0] + main = parts[1] + + # Проверяем наличие авторизации (username:password@host:port) + if "@" in main: + main_split = main.split("@") + if len(main_split) != 2: + raise ConfigError( + "Неверный формат авторизации в PROXY" + ) + + authorization = main_split[0] + server = main_split[1] + + # Парсим username:password (используем split с limit для поддержки : в пароле) + auth_parts = authorization.split(":", 1) + if len(auth_parts) != 2: + raise ConfigError( + "Неверный формат авторизации в PROXY. Ожидается: username:password" + ) + proxy_username = auth_parts[0] + proxy_password = auth_parts[1] + else: + server = main + + # Парсим host:port (используем rsplit для поддержки IPv6) + server_parts = server.rsplit(":", 1) + if len(server_parts) != 2: + raise ConfigError( + "Неверный формат сервера в PROXY. Ожидается: host:port" + ) + proxy_host = server_parts[0] + try: + proxy_port = int(server_parts[1]) + except ValueError as e: + raise ConfigError( + f"PROXY_PORT должен быть целым числом: {server_parts[1]}" + ) from e + + logger.info( + "Прокси успешно распарсен: protocol=%s, host=%s, port=%s, auth=%s", + proxy_protocol, + proxy_host, + proxy_port, + "да" if proxy_username else "нет" + ) + + except ConfigError: + raise + except Exception as e: + raise ConfigError(f"Произошла ошибка при парсе PROXY: {e}") from e + + else: + logger.info("Загрузка прокси из отдельных переменных...") + # Используем отдельные поля + if not proxy_host or not proxy_port_str: + raise ConfigError( + "PROXY не указан, но PROXY_HOST или PROXY_PORT также отсутствуют" + ) + + try: + proxy_port = int(proxy_port_str) + except (ValueError, TypeError) as e: + raise ConfigError( + f"PROXY_PORT должен быть целым числом: {proxy_port_str}" + ) from e + + logger.info( + "Прокси загружен: protocol=%s, host=%s, port=%s, auth=%s", + proxy_protocol, + proxy_host, + proxy_port, + "да" if proxy_username else "нет" + ) + + # Валидация + if proxy_protocol not in ["socks5", "https"]: + raise ConfigError( + f"Формат прокси '{proxy_protocol}' не поддерживается. Укажите socks5 или https" + ) + + if (proxy_username and not proxy_password) or (proxy_password and not proxy_username): + raise ConfigError( + "PROXY_USERNAME и PROXY_PASSWORD должны быть указаны оба или не указаны вовсе" + ) + + # Финальная проверка + if not (proxy_protocol and proxy_host and proxy_port): + raise ConfigError("Прокси не полностью настроены") + + if proxy_telegram: + logger.info("Прокси для Telegram включен") + if proxy_lolz: + logger.info("Прокси для Lolz API включен") + return { # Pyrogram "api_id": api_id, @@ -165,6 +293,7 @@ class Config: "skip_commented": skip_commented, # Misc "api_delay": api_delay, + "api_timeout": api_timeout, "max_retries": max_retries, "default_choice": default_choice, # Bump @@ -174,13 +303,23 @@ class Config: "enable_captcha": enable_captcha, "captcha_answer": captcha_answer, "captcha_check_interval": captcha_check_interval, + # Proxy + "proxy_telegram": proxy_telegram, + "proxy_lolz": proxy_lolz, + "proxy_protocol": proxy_protocol, + "proxy_host": proxy_host, + "proxy_port": proxy_port, + "proxy_username": proxy_username, + "proxy_password": proxy_password, } except ConfigError: raise + except (ValueError, TypeError) as e: logger.error(f"Invalid configuration value: {e}") raise ConfigError(f"Invalid configuration value: {e}") from e + except Exception as e: logger.error(f"Unexpected error loading configuration: {e}") raise ConfigError(f"Failed to load configuration: {e}") from e diff --git a/examples/.env.example b/examples/.env.example index 7b48f67..f2e2b2d 100644 --- a/examples/.env.example +++ b/examples/.env.example @@ -23,8 +23,9 @@ STARS_COUNT=1 DEFAULT_CHOICE=2 START_PAGE=1 -CHECK_INTERVAL=30 +CHECK_INTERVAL=10 API_DELAY=5 +API_TIMEOUT=15 MAX_RETRIES=3 ENABLE_REPLY=true @@ -42,3 +43,17 @@ CAPTCHA_ANSWER=40 # Капча будет автоматически обновляться из файла storage/captcha.txt CAPTCHA_CHECK_INTERVAL=1800 +# Прокси: применяется в зависимости от выбранных опций PROXY_TELEGRAM и PROXY_LOLZ +PROXY_TELEGRAM=false +PROXY_LOLZ=false +# Прокси +# Формат protocol://username:password@host:port или protocol://host:port +PROXY= + +# Можно задать данные для прокси отдельно +PROXY_PROTOCOL=socks5 # поддерживаются socks5 и https форматы +PROXY_HOST= +PROXY_PORT= +PROXY_USERNAME= # Можно не ставить, если для прокси авторизация не нужна +PROXY_PASSWORD= # Можно не ставить, если для прокси авторизация не нужна + diff --git a/modules/lolz.py b/modules/lolz.py index d716962..9ca7693 100644 --- a/modules/lolz.py +++ b/modules/lolz.py @@ -4,6 +4,8 @@ import asyncio from LOLZTEAM.Client import Forum from typing import Dict, Any, Optional, Union, List +from config import Config + logger = logging.getLogger(__name__) @@ -26,15 +28,38 @@ class LolzResponseError(LolzAPIError): class Lolz: - def __init__(self, token: str): - if not token: + def __init__(self, config: Config): + if not config.lolz_token: raise ValueError("LOLZ_TOKEN обязателен") try: - self.client = Forum(token=token, timeout=15) + if config.proxy_lolz: + if config.proxy_username and config.proxy_password: + proxy = f"{config.proxy_protocol}://{config.proxy_username}:{config.proxy_password}@{config.proxy_host}:{config.proxy_port}" + logger.info( + "Инициализация Lolz клиента с прокси: %s://%s:%s (с авторизацией)", + config.proxy_protocol, + config.proxy_host, + config.proxy_port + ) + else: + proxy = f"{config.proxy_protocol}://{config.proxy_host}:{config.proxy_port}" + logger.info( + "Инициализация Lolz клиента с прокси: %s://%s:%s (без авторизации)", + config.proxy_protocol, + config.proxy_host, + config.proxy_port + ) - # ! Не рекомендуется включить этот логгер, - # ! ибо логи занимают очень много места в экране при этом логгере + self.client = Forum( + token=config.lolz_token, proxy=proxy, timeout=config.api_timeout + ) + else: + logger.info("Инициализация Lolz клиента без прокси") + self.client = Forum(token=config.lolz_token, timeout=config.api_timeout) + + # ! Не рекомендуется включать этот логгер, + # ! ибо логи занимают очень много места в экране с этим логгером # self.client.settings.logger.enable() except Exception as e: diff --git a/modules/notification_bot.py b/modules/notification_bot.py index b224645..94d89a2 100644 --- a/modules/notification_bot.py +++ b/modules/notification_bot.py @@ -1,10 +1,18 @@ import logging from aiogram import Bot + +from aiohttp import BasicAuth +from aiogram.client.session.aiohttp import AiohttpSession + from aiogram.enums import ParseMode from aiogram.client.default import DefaultBotProperties from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup -from aiogram.exceptions import TelegramAPIError, TelegramBadRequest, TelegramNetworkError +from aiogram.exceptions import ( + TelegramAPIError, + TelegramBadRequest, + TelegramNetworkError, +) from config import Config from typing import Dict, Any, List, Optional @@ -17,14 +25,51 @@ class NotificationBot: self.config = config try: - self.bot: Bot = Bot( - token=config.bot_token, - default=DefaultBotProperties( - parse_mode=ParseMode.HTML, - disable_notification=True, - link_preview_is_disabled=True, - ), - ) + if config.proxy_telegram: + proxy = ( + f"{config.proxy_protocol}://{config.proxy_host}:{config.proxy_port}" + ) + + if config.proxy_username and config.proxy_password: + logger.info( + "Инициализация Telegram бота с прокси: %s://%s:%s (с авторизацией)", + config.proxy_protocol, + config.proxy_host, + config.proxy_port + ) + auth = BasicAuth( + login=config.proxy_username, password=config.proxy_password + ) + self.session = AiohttpSession(proxy=(proxy, auth)) + else: + logger.info( + "Инициализация Telegram бота с прокси: %s://%s:%s (без авторизации)", + config.proxy_protocol, + config.proxy_host, + config.proxy_port + ) + self.session = AiohttpSession(proxy=proxy) + + self.bot: Bot = Bot( + token=config.bot_token, + default=DefaultBotProperties( + parse_mode=ParseMode.HTML, + disable_notification=True, + link_preview_is_disabled=True, + ), + session=self.session, + ) + else: + logger.info("Инициализация Telegram бота без прокси") + self.bot: Bot = Bot( + token=config.bot_token, + default=DefaultBotProperties( + parse_mode=ParseMode.HTML, + disable_notification=True, + link_preview_is_disabled=True, + ), + ) + logger.info( "Бот для уведомлений инициализирован (admin_id=%s)", config.admin_id ) @@ -33,7 +78,9 @@ class NotificationBot: logger.error(f"Не удалось инициализировать Бот для уведомлений: {e}") raise - async def _notify(self, message: str, reply_to: int = None, link: str = None) -> Optional[int]: + async def _notify( + self, message: str, reply_to: int = None, link: str = None + ) -> Optional[int]: try: logger.debug("Отправка уведомления (reply_to=%s, link=%s)", reply_to, link) keyboard = None @@ -142,7 +189,9 @@ class NotificationBot: logger.error(f"Неожиданная ошибка в success: {e}") return None - async def failure(self, post: Dict[str, Any], reason: str, post_message_id: int) -> Optional[int]: + async def failure( + self, post: Dict[str, Any], reason: str, post_message_id: int + ) -> Optional[int]: try: permalink = post.get("links", {}).get("permalink") logger.warning( diff --git a/modules/stars_bot.py b/modules/stars_bot.py index 9791663..a341558 100644 --- a/modules/stars_bot.py +++ b/modules/stars_bot.py @@ -53,7 +53,7 @@ class TelegramStarsBot: self.config = config self.start_page: int = 1 - self.lolz_api = Lolz(config.lolz_token) + self.lolz_api = Lolz(config=config) self.processed_manager = ProcessedPostsManager() self.client: Optional[Client] = None