feat(config): add proxy support and API timeout
Build and Push (dev) / build (push) Successful in 3m31s
Build and Push (dev) / build (push) Successful in 3m31s
This commit is contained in:
+15
-1
@@ -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= # Можно не ставить, если для прокси авторизация не нужна
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
+60
-11
@@ -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,14 +25,51 @@ class NotificationBot:
|
|||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.bot: Bot = Bot(
|
if config.proxy_telegram:
|
||||||
token=config.bot_token,
|
proxy = (
|
||||||
default=DefaultBotProperties(
|
f"{config.proxy_protocol}://{config.proxy_host}:{config.proxy_port}"
|
||||||
parse_mode=ParseMode.HTML,
|
)
|
||||||
disable_notification=True,
|
|
||||||
link_preview_is_disabled=True,
|
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(
|
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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user