mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-27 14:51:19 +00:00
- Add pyproject.toml with uv and ruff configuration - Pin Python version to 3.13 via .python-version - Add Makefile commands: lint, format, fix - Apply ruff formatting to entire codebase - Remove unused imports (base64 in yookassa/simple_subscription) - Update .gitignore for new config files
210 lines
7.3 KiB
Python
210 lines
7.3 KiB
Python
"""Middleware для автоматического логирования кликов по кнопкам."""
|
||
|
||
import asyncio
|
||
import logging
|
||
from collections.abc import Awaitable, Callable
|
||
from typing import Any
|
||
|
||
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}')
|