mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-05 19:50:23 +00:00
209 lines
7.3 KiB
Python
209 lines
7.3 KiB
Python
"""Middleware для автоматического логирования кликов по кнопкам."""
|
||
|
||
import asyncio
|
||
import logging
|
||
from typing import Callable, Dict, Any, Awaitable, Set
|
||
|
||
from aiogram import BaseMiddleware
|
||
from aiogram.types import CallbackQuery, TelegramObject
|
||
|
||
from app.config import settings
|
||
from app.database.database import AsyncSessionLocal
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Известные builtin callback_data из меню
|
||
BUILTIN_CALLBACKS: Set[str] = {
|
||
# Основные кнопки меню
|
||
"subscription_connect",
|
||
"subscription_happ_download",
|
||
"menu_subscription",
|
||
"buy_traffic",
|
||
"menu_balance",
|
||
"menu_trial",
|
||
"menu_buy",
|
||
"simple_subscription_purchase",
|
||
"return_to_saved_cart",
|
||
"menu_promocode",
|
||
"menu_referrals",
|
||
"contests_menu",
|
||
"menu_support",
|
||
"menu_info",
|
||
"menu_language",
|
||
"admin_panel",
|
||
"moderator_panel",
|
||
# Навигация
|
||
"back_to_menu",
|
||
"menu_faq",
|
||
"menu_info_promo_groups",
|
||
"menu_privacy_policy",
|
||
"menu_public_offer",
|
||
"menu_rules",
|
||
"menu_server_status",
|
||
# Баланс
|
||
"balance_history",
|
||
"balance_topup",
|
||
# Подписка
|
||
"subscription_extend",
|
||
"subscription_autopay",
|
||
"subscription_settings",
|
||
"open_subscription_link",
|
||
"subscription_add_countries",
|
||
"subscription_reset_traffic",
|
||
"subscription_switch_traffic",
|
||
"subscription_change_devices",
|
||
"subscription_manage_devices",
|
||
"subscription_upgrade",
|
||
# Устройства
|
||
"device_guide_ios",
|
||
"device_guide_android",
|
||
"device_guide_windows",
|
||
"device_guide_mac",
|
||
"device_guide_tv",
|
||
"device_guide_appletv",
|
||
# Happ
|
||
"happ_download_ios",
|
||
"happ_download_android",
|
||
"happ_download_macos",
|
||
"happ_download_windows",
|
||
# Рефералы
|
||
"referral_create_invite",
|
||
"referral_show_qr",
|
||
"referral_list",
|
||
"referral_analytics",
|
||
# Поддержка
|
||
"create_ticket",
|
||
"my_tickets",
|
||
# Триал
|
||
"trial_activate",
|
||
# Покупка
|
||
"clear_saved_cart",
|
||
"subscription_confirm",
|
||
"subscription_cancel",
|
||
}
|
||
|
||
|
||
class ButtonStatsMiddleware(BaseMiddleware):
|
||
"""Middleware для автоматического логирования статистики кликов по кнопкам."""
|
||
|
||
async def __call__(
|
||
self,
|
||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||
event: TelegramObject,
|
||
data: Dict[str, Any]
|
||
) -> Any:
|
||
"""Перехватывает CallbackQuery и логирует клики по кнопкам."""
|
||
|
||
# Обрабатываем только CallbackQuery
|
||
if not isinstance(event, CallbackQuery):
|
||
return await handler(event, data)
|
||
|
||
# Пропускаем, если статистика отключена
|
||
if not settings.MENU_LAYOUT_ENABLED:
|
||
return await handler(event, data)
|
||
|
||
# Логируем клик асинхронно, не блокируя обработку
|
||
try:
|
||
# Получаем callback_data
|
||
callback_data = event.data
|
||
if not callback_data:
|
||
return await handler(event, data)
|
||
|
||
# Получаем user_id
|
||
user_id = event.from_user.id if event.from_user else None
|
||
|
||
# Определяем тип кнопки по callback_data
|
||
button_type = self._determine_button_type(callback_data)
|
||
|
||
# Получаем текст кнопки, если возможно
|
||
button_text = None
|
||
if event.message and hasattr(event.message, 'reply_markup'):
|
||
button_text = self._extract_button_text(event.message.reply_markup, callback_data)
|
||
|
||
# Логируем в фоне, не блокируя обработку
|
||
asyncio.create_task(
|
||
self._log_button_click_async(
|
||
button_id=callback_data,
|
||
user_id=user_id,
|
||
callback_data=callback_data,
|
||
button_type=button_type,
|
||
button_text=button_text
|
||
)
|
||
)
|
||
except Exception as e:
|
||
# Не прерываем обработку при ошибке логирования
|
||
logger.error(f"Ошибка логирования клика по кнопке: {e}", exc_info=True)
|
||
|
||
# Продолжаем обработку
|
||
return await handler(event, data)
|
||
|
||
def _determine_button_type(self, callback_data: str) -> str:
|
||
"""Определяет тип кнопки по callback_data.
|
||
|
||
Примечание: URL и MiniApp кнопки не имеют callback_data,
|
||
поэтому они не отслеживаются через этот middleware.
|
||
Для их отслеживания нужен отдельный механизм на стороне клиента.
|
||
"""
|
||
# Проверяем по известному списку builtin кнопок
|
||
if callback_data in BUILTIN_CALLBACKS:
|
||
return "builtin"
|
||
|
||
# Дополнительная проверка по префиксам для динамических callback_data
|
||
builtin_prefixes = (
|
||
"menu_",
|
||
"admin_",
|
||
"subscription_",
|
||
"balance_",
|
||
"referral_",
|
||
"device_guide_",
|
||
"happ_download_",
|
||
)
|
||
if callback_data.startswith(builtin_prefixes):
|
||
return "builtin"
|
||
|
||
# Всё остальное - кастомные callback кнопки
|
||
return "callback"
|
||
|
||
def _extract_button_text(self, reply_markup, callback_data: str) -> str:
|
||
"""Извлекает текст кнопки из клавиатуры."""
|
||
try:
|
||
if not reply_markup or not hasattr(reply_markup, 'inline_keyboard'):
|
||
return None
|
||
|
||
for row in reply_markup.inline_keyboard:
|
||
for button in row:
|
||
if hasattr(button, 'callback_data') and button.callback_data == callback_data:
|
||
if hasattr(button, 'text'):
|
||
return button.text
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
async def _log_button_click_async(
|
||
self,
|
||
button_id: str,
|
||
user_id: int = None,
|
||
callback_data: str = None,
|
||
button_type: str = None,
|
||
button_text: str = None
|
||
):
|
||
"""Асинхронно логирует клик по кнопке."""
|
||
try:
|
||
async with AsyncSessionLocal() as db:
|
||
try:
|
||
from app.services.menu_layout_service import MenuLayoutService
|
||
|
||
await MenuLayoutService.log_button_click(
|
||
db,
|
||
button_id=button_id,
|
||
user_id=user_id,
|
||
callback_data=callback_data,
|
||
button_type=button_type,
|
||
button_text=button_text
|
||
)
|
||
except Exception as e:
|
||
logger.debug(f"Ошибка записи клика в БД {button_id}: {e}")
|
||
except Exception as e:
|
||
logger.debug(f"Ошибка создания сессии БД для логирования клика: {e}")
|
||
|