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
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= # Можно не ставить, если для прокси авторизация не нужна
+139
View File
@@ -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
+16 -1
View File
@@ -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= # Можно не ставить, если для прокси авторизация не нужна
+30 -5
View File
@@ -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:
+60 -11
View File
@@ -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(
+1 -1
View File
@@ -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