Files
remnawave-bedolaga-telegram…/app/middlewares/button_stats.py
Egor 151ce092b9 Enhance button stats middleware with builtin callbacks
Added a set of known builtin callback data for button statistics logging.
2025-12-21 07:16:55 +03:00

209 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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}")