From 7567b84fa021866d4bcf20b10f64675f2e2066bc Mon Sep 17 00:00:00 2001 From: Verum Date: Mon, 23 Feb 2026 14:25:10 +0700 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B2=D0=B5=D0=B1-=D0=B2=D0=B5=D0=B1=D1=85=D1=83?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D0=B1=D0=BE=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/core/webhook.py | 259 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 bot/core/webhook.py diff --git a/bot/core/webhook.py b/bot/core/webhook.py new file mode 100644 index 0000000..616ceb1 --- /dev/null +++ b/bot/core/webhook.py @@ -0,0 +1,259 @@ +""" +Управление вебхуком бота через класс-менеджер +""" +import secrets +from typing import Optional + +from aiohttp import web +from aiogram import Bot, Dispatcher +from aiogram.types import WebhookInfo +from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application + +from configs import settings +from middleware.loggers import logger + +__all__ = ('WebhookManager',) + + +class WebhookManager: + """ + Менеджер для управления webhook режимом. + + Инкапсулирует всю логику работы с webhook: + - Создание aiohttp приложения + - Регистрация handlers + - Установка/удаление webhook + - Запуск webhook сервера + + Attributes: + bot: Экземпляр бота + dp: Диспетчер + app: aiohttp приложение + secret_token: Секретный токен для webhook + """ + + def __init__(self, bot: Bot, dp: Dispatcher): + """ + Args: + bot: Экземпляр бота + dp: Диспетчер + """ + self.bot = bot + self.dp = dp + self.app = web.Application() + self._configured = False + + # Генерируем или используем существующий токен + self.secret_token = self._get_or_generate_token() + + def _get_or_generate_token(self) -> str: + """ + Получает токен из настроек или генерирует новый. + + Returns: + str: Секретный токен + """ + if hasattr(settings, 'SECRET_TOKEN') and settings.SECRET_TOKEN: + logger.debug("Используется SECRET_TOKEN из настроек", log_type='WEBHOOK') + return settings.SECRET_TOKEN + + # Генерируем случайный токен (32 символа) + token = secrets.token_urlsafe(32) + logger.info( + f"🔐 Сгенерирован новый SECRET_TOKEN: {token[:8]}...", + log_type='WEBHOOK' + ) + return token + + async def get_info(self) -> WebhookInfo: + """ + Получает информацию о текущем вебхуке. + + Returns: + WebhookInfo: Информация о вебхуке + """ + try: + info = await self.bot.get_webhook_info() + logger.debug( + f"Webhook URL: {info.url or 'не установлен'}", + log_type='WEBHOOK' + ) + return info + except Exception as e: + logger.error( + f"Ошибка получения информации о вебхуке: {e}", + log_type='WEBHOOK' + ) + raise + + async def delete(self, drop_pending_updates: bool = True) -> bool: + """ + Удаляет текущий вебхук. + + Args: + drop_pending_updates: Удалить накопленные обновления + + Returns: + bool: True если удаление успешно + """ + try: + result = await self.bot.delete_webhook( + drop_pending_updates=drop_pending_updates + ) + + if result: + logger.success("✓ Вебхук успешно удален", log_type='WEBHOOK') + else: + logger.debug("Вебхук не был установлен", log_type='WEBHOOK') + + return result + + except Exception as e: + logger.error(f"Ошибка удаления вебхука: {e}", log_type='WEBHOOK') + return False + + async def setup( + self, + webhook_url: Optional[str] = None, + secret_token: Optional[str] = None, + drop_pending_updates: bool = True + ) -> bool: + """ + Устанавливает вебхук для бота. + + Args: + webhook_url: URL вебхука (если None, берется из settings) + secret_token: Секретный токен (если None, используется self.secret_token) + drop_pending_updates: Удалить накопленные обновления + + Returns: + bool: True если установка успешна + """ + url = webhook_url or settings.WEBHOOK_URL + token = secret_token or self.secret_token + + if not url: + logger.error("WEBHOOK_URL не установлен", log_type='WEBHOOK') + return False + + try: + # Проверяем текущий webhook + current_info = await self.bot.get_webhook_info() + + # Если уже установлен правильный URL, не трогаем + if current_info.url == url: + logger.info( + f"✓ Webhook уже установлен на {url}", + log_type='WEBHOOK' + ) + return True + + # Удаляем старый webhook если есть + if current_info.url: + logger.debug(f"Удаление старого webhook: {current_info.url}", log_type='WEBHOOK') + await self.delete(drop_pending_updates=drop_pending_updates) + + # Небольшая задержка + import asyncio + await asyncio.sleep(0.5) + + # Устанавливаем новый + result = await self.bot.set_webhook( + url=url, + secret_token=token, + drop_pending_updates=drop_pending_updates + ) + + if result: + logger.success(f"✓ Вебхук установлен: {url}", log_type='WEBHOOK') + else: + logger.error("❌ Не удалось установить вебхук", log_type='WEBHOOK') + + return result + + except Exception as e: + logger.error(f"❌ Ошибка установки вебхука: {e}", log_type='WEBHOOK') + return False + + def configure( + self, + webhook_path: Optional[str] = None, + secret_token: Optional[str] = None + ) -> None: + """ + Конфигурирует webhook handler для aiohttp app. + + Args: + webhook_path: Путь для webhook (если None, извлекается из WEBHOOK_URL) + secret_token: Секретный токен (если None, используется self.secret_token) + """ + if self._configured: + logger.warning("Webhook уже сконфигурирован", log_type='WEBHOOK') + return + + # Определяем путь из WEBHOOK_URL + if webhook_path: + path = webhook_path + elif settings.WEBHOOK_URL: + from urllib.parse import urlparse + parsed = urlparse(settings.WEBHOOK_URL) + path = parsed.path if parsed.path else "/webhook" + else: + path = "/webhook" + + # Используем токен + token = secret_token or self.secret_token + + # Создаём webhook handler + webhook_handler = SimpleRequestHandler( + dispatcher=self.dp, + bot=self.bot, + secret_token=token + ) + + # Регистрируем в aiohttp app + webhook_handler.register(self.app, path=path) + setup_application(self.app, self.dp, bot=self.bot) + + self._configured = True + logger.success( + f"✓ Webhook handler настроен на путь: {path}", + log_type='WEBHOOK' + ) + + def run( + self, + host: Optional[str] = None, + port: Optional[int] = None, + access_log: Optional[bool] = None + ) -> None: + """ + Запускает webhook сервер (блокирующий вызов). + + Args: + host: Хост сервера (если None, берется из settings) + port: Порт сервера (если None, берется из settings) + access_log: Логировать запросы (если None, берется из settings) + """ + if not self._configured: + logger.error( + "Webhook не сконфигурирован! Вызовите configure() перед run()", + log_type='WEBHOOK' + ) + return + + host = host or settings.WEBAPP_HOST + port = port or settings.WEBAPP_PORT + access_log_enabled = access_log if access_log is not None else settings.ACCES_LOG + + logger.info( + f"🌐 Запуск webhook сервера: {host}:{port}", + log_type='WEBHOOK' + ) + + web.run_app( + self.app, + host=host, + port=port, + access_log=logger if access_log_enabled else None + )