151ce092b9
Added a set of known builtin callback data for button statistics logging.
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}")
|
|
|