feat(config): add proxy support and API timeout
Build and Push (dev) / build (push) Successful in 3m31s

This commit is contained in:
2026-08-16 05:15:24 +05:00
parent 62873c7f8c
commit 5c325bd8b9
6 changed files with 261 additions and 19 deletions
+15 -1
View File
@@ -20,8 +20,9 @@ STARS_COUNT=1
DEFAULT_CHOICE=2 DEFAULT_CHOICE=2
START_PAGE=1 START_PAGE=1
CHECK_INTERVAL=30 CHECK_INTERVAL=10
API_DELAY=5 API_DELAY=5
API_TIMEOUT=15
MAX_RETRIES=3 MAX_RETRIES=3
ENABLE_REPLY=true ENABLE_REPLY=true
@@ -39,3 +40,16 @@ CAPTCHA_ANSWER=40
# Капча будет автоматически обновляться из файла storage/captcha.txt # Капча будет автоматически обновляться из файла storage/captcha.txt
CAPTCHA_CHECK_INTERVAL=1800 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= # Можно не ставить, если для прокси авторизация не нужна
+139
View File
@@ -115,6 +115,7 @@ class Config:
api_delay = max( api_delay = max(
float(os.getenv(key="API_DELAY", default=5)), 2 float(os.getenv(key="API_DELAY", default=5)), 2
) # 30r/60s для non-GET запросов, 60/30 = минимум ) # 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)) max_retries = int(os.getenv(key="MAX_RETRIES", default=3))
enable_reply = ( enable_reply = (
@@ -145,6 +146,133 @@ class Config:
"ENABLE_CAPTCHA установлен, но CAPTCHA_ANSWER не задан" "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 { return {
# Pyrogram # Pyrogram
"api_id": api_id, "api_id": api_id,
@@ -165,6 +293,7 @@ class Config:
"skip_commented": skip_commented, "skip_commented": skip_commented,
# Misc # Misc
"api_delay": api_delay, "api_delay": api_delay,
"api_timeout": api_timeout,
"max_retries": max_retries, "max_retries": max_retries,
"default_choice": default_choice, "default_choice": default_choice,
# Bump # Bump
@@ -174,13 +303,23 @@ class Config:
"enable_captcha": enable_captcha, "enable_captcha": enable_captcha,
"captcha_answer": captcha_answer, "captcha_answer": captcha_answer,
"captcha_check_interval": captcha_check_interval, "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: except ConfigError:
raise raise
except (ValueError, TypeError) as e: except (ValueError, TypeError) as e:
logger.error(f"Invalid configuration value: {e}") logger.error(f"Invalid configuration value: {e}")
raise ConfigError(f"Invalid configuration value: {e}") from e raise ConfigError(f"Invalid configuration value: {e}") from e
except Exception as e: except Exception as e:
logger.error(f"Unexpected error loading configuration: {e}") logger.error(f"Unexpected error loading configuration: {e}")
raise ConfigError(f"Failed to load configuration: {e}") from e raise ConfigError(f"Failed to load configuration: {e}") from e
+16 -1
View File
@@ -23,8 +23,9 @@ STARS_COUNT=1
DEFAULT_CHOICE=2 DEFAULT_CHOICE=2
START_PAGE=1 START_PAGE=1
CHECK_INTERVAL=30 CHECK_INTERVAL=10
API_DELAY=5 API_DELAY=5
API_TIMEOUT=15
MAX_RETRIES=3 MAX_RETRIES=3
ENABLE_REPLY=true ENABLE_REPLY=true
@@ -42,3 +43,17 @@ CAPTCHA_ANSWER=40
# Капча будет автоматически обновляться из файла storage/captcha.txt # Капча будет автоматически обновляться из файла storage/captcha.txt
CAPTCHA_CHECK_INTERVAL=1800 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= # Можно не ставить, если для прокси авторизация не нужна
+30 -5
View File
@@ -4,6 +4,8 @@ import asyncio
from LOLZTEAM.Client import Forum from LOLZTEAM.Client import Forum
from typing import Dict, Any, Optional, Union, List from typing import Dict, Any, Optional, Union, List
from config import Config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -26,15 +28,38 @@ class LolzResponseError(LolzAPIError):
class Lolz: class Lolz:
def __init__(self, token: str): def __init__(self, config: Config):
if not token: if not config.lolz_token:
raise ValueError("LOLZ_TOKEN обязателен") raise ValueError("LOLZ_TOKEN обязателен")
try: 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() # self.client.settings.logger.enable()
except Exception as e: except Exception as e:
+52 -3
View File
@@ -1,10 +1,18 @@
import logging import logging
from aiogram import Bot from aiogram import Bot
from aiohttp import BasicAuth
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup 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 config import Config
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
@@ -17,6 +25,42 @@ class NotificationBot:
self.config = config self.config = config
try: try:
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( self.bot: Bot = Bot(
token=config.bot_token, token=config.bot_token,
default=DefaultBotProperties( default=DefaultBotProperties(
@@ -25,6 +69,7 @@ class NotificationBot:
link_preview_is_disabled=True, link_preview_is_disabled=True,
), ),
) )
logger.info( logger.info(
"Бот для уведомлений инициализирован (admin_id=%s)", config.admin_id "Бот для уведомлений инициализирован (admin_id=%s)", config.admin_id
) )
@@ -33,7 +78,9 @@ class NotificationBot:
logger.error(f"Не удалось инициализировать Бот для уведомлений: {e}") logger.error(f"Не удалось инициализировать Бот для уведомлений: {e}")
raise 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: try:
logger.debug("Отправка уведомления (reply_to=%s, link=%s)", reply_to, link) logger.debug("Отправка уведомления (reply_to=%s, link=%s)", reply_to, link)
keyboard = None keyboard = None
@@ -142,7 +189,9 @@ class NotificationBot:
logger.error(f"Неожиданная ошибка в success: {e}") logger.error(f"Неожиданная ошибка в success: {e}")
return None 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: try:
permalink = post.get("links", {}).get("permalink") permalink = post.get("links", {}).get("permalink")
logger.warning( logger.warning(
+1 -1
View File
@@ -53,7 +53,7 @@ class TelegramStarsBot:
self.config = config self.config = config
self.start_page: int = 1 self.start_page: int = 1
self.lolz_api = Lolz(config.lolz_token) self.lolz_api = Lolz(config=config)
self.processed_manager = ProcessedPostsManager() self.processed_manager = ProcessedPostsManager()
self.client: Optional[Client] = None self.client: Optional[Client] = None