mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-22 20:31:47 +00:00
267 lines
11 KiB
Python
267 lines
11 KiB
Python
import functools
|
||
import logging
|
||
from collections.abc import Callable
|
||
from typing import Any
|
||
|
||
from aiogram import types
|
||
from aiogram.exceptions import TelegramBadRequest
|
||
from aiogram.fsm.context import FSMContext
|
||
|
||
from app.config import settings
|
||
from app.localization.texts import get_texts
|
||
from app.middlewares.global_error import schedule_error_notification
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def admin_required(func: Callable) -> Callable:
|
||
@functools.wraps(func)
|
||
async def wrapper(event: types.Update, *args, **kwargs) -> Any:
|
||
user = None
|
||
if isinstance(event, (types.Message, types.CallbackQuery)):
|
||
user = event.from_user
|
||
|
||
if not user or not settings.is_admin(user.id):
|
||
texts = get_texts()
|
||
|
||
try:
|
||
if isinstance(event, types.Message):
|
||
await event.answer(texts.ACCESS_DENIED)
|
||
elif isinstance(event, types.CallbackQuery):
|
||
await event.answer(texts.ACCESS_DENIED, show_alert=True)
|
||
except TelegramBadRequest as e:
|
||
if 'query is too old' in str(e).lower():
|
||
logger.warning(f'Попытка ответить на устаревший callback query от {user.id if user else "Unknown"}')
|
||
else:
|
||
raise
|
||
|
||
logger.warning(f'Попытка доступа к админской функции от {user.id if user else "Unknown"}')
|
||
return None
|
||
|
||
return await func(event, *args, **kwargs)
|
||
|
||
return wrapper
|
||
|
||
|
||
def auth_required(func: Callable) -> Callable:
|
||
"""
|
||
Простая проверка на наличие пользователя в апдейте. Middleware уже подтягивает db_user,
|
||
но здесь страхуемся от вызовов без from_user.
|
||
"""
|
||
|
||
@functools.wraps(func)
|
||
async def wrapper(event: types.Update, *args, **kwargs) -> Any:
|
||
user = None
|
||
if isinstance(event, (types.Message, types.CallbackQuery)):
|
||
user = event.from_user
|
||
if not user:
|
||
logger.warning('auth_required: нет from_user, пропускаем')
|
||
return None
|
||
return await func(event, *args, **kwargs)
|
||
|
||
return wrapper
|
||
|
||
|
||
def error_handler(func: Callable) -> Callable:
|
||
@functools.wraps(func)
|
||
async def wrapper(*args, **kwargs) -> Any:
|
||
try:
|
||
return await func(*args, **kwargs)
|
||
except TelegramBadRequest as e:
|
||
error_message = str(e).lower()
|
||
|
||
if 'query is too old' in error_message or 'query id is invalid' in error_message:
|
||
event = _extract_event(args)
|
||
if event and isinstance(event, types.CallbackQuery):
|
||
user_info = (
|
||
f'@{event.from_user.username}' if event.from_user.username else f'ID:{event.from_user.id}'
|
||
)
|
||
logger.warning(f"🕐 Игнорируем устаревший callback '{event.data}' от {user_info} в {func.__name__}")
|
||
else:
|
||
logger.warning(f'🕐 Игнорируем устаревший запрос в {func.__name__}: {e}')
|
||
return None
|
||
|
||
if 'message is not modified' in error_message:
|
||
logger.debug(f'📝 Сообщение не изменено в {func.__name__}')
|
||
event = _extract_event(args)
|
||
if event and isinstance(event, types.CallbackQuery):
|
||
try:
|
||
await event.answer()
|
||
except TelegramBadRequest as answer_error:
|
||
if 'query is too old' not in str(answer_error).lower():
|
||
logger.error(f'Ошибка при ответе на callback: {answer_error}')
|
||
# Отправляем уведомление в админский чат
|
||
bot = kwargs.get('bot') or (event.bot if event else None)
|
||
if bot:
|
||
schedule_error_notification(bot, answer_error, f'Callback answer error в {func.__name__}')
|
||
return None
|
||
|
||
logger.error(f'Telegram API error в {func.__name__}: {e}')
|
||
# Уведомление отправляется в _send_error_message
|
||
await _send_error_message(args, kwargs, e, func.__name__)
|
||
|
||
except Exception as e:
|
||
logger.error(f'Ошибка в {func.__name__}: {e}', exc_info=True)
|
||
await _send_error_message(args, kwargs, e, func.__name__)
|
||
|
||
return wrapper
|
||
|
||
|
||
def _extract_event(args) -> types.TelegramObject:
|
||
for arg in args:
|
||
if isinstance(arg, (types.Message, types.CallbackQuery)):
|
||
return arg
|
||
return None
|
||
|
||
|
||
async def _send_error_message(args, kwargs, original_error, func_name: str = 'unknown'):
|
||
event = _extract_event(args)
|
||
db_user = kwargs.get('db_user')
|
||
|
||
# Отправляем уведомление в админский чат
|
||
bot = kwargs.get('bot') or (event.bot if event else None)
|
||
if bot:
|
||
user_info = 'Unknown'
|
||
if event and hasattr(event, 'from_user') and event.from_user:
|
||
user_info = f'@{event.from_user.username}' if event.from_user.username else f'ID:{event.from_user.id}'
|
||
context = f'Функция: {func_name}\nПользователь: {user_info}'
|
||
schedule_error_notification(bot, original_error, context)
|
||
|
||
# Отправляем сообщение пользователю
|
||
try:
|
||
if not event:
|
||
return
|
||
|
||
texts = get_texts(db_user.language if db_user else 'ru')
|
||
|
||
if isinstance(event, types.Message):
|
||
await event.answer(texts.ERROR)
|
||
elif isinstance(event, types.CallbackQuery):
|
||
await event.answer(texts.ERROR, show_alert=True)
|
||
|
||
except TelegramBadRequest as e:
|
||
if 'query is too old' in str(e).lower():
|
||
logger.warning('Не удалось отправить сообщение об ошибке - callback query устарел')
|
||
else:
|
||
logger.error(f'Ошибка при отправке сообщения об ошибке: {e}')
|
||
# Не отправляем уведомление здесь, т.к. это уже ошибка при отправке ошибки
|
||
except Exception as e:
|
||
logger.error(f'Критическая ошибка при отправке сообщения об ошибке: {e}')
|
||
# Пытаемся отправить уведомление о критической ошибке
|
||
bot = kwargs.get('bot') or (event.bot if event else None)
|
||
if bot:
|
||
schedule_error_notification(bot, e, f'Критическая ошибка отправки сообщения: {func_name}')
|
||
|
||
|
||
def state_cleanup(func: Callable) -> Callable:
|
||
@functools.wraps(func)
|
||
async def wrapper(*args, **kwargs) -> Any:
|
||
state = kwargs.get('state')
|
||
|
||
try:
|
||
return await func(*args, **kwargs)
|
||
except Exception as e:
|
||
if state and isinstance(state, FSMContext):
|
||
await state.clear()
|
||
raise e
|
||
|
||
return wrapper
|
||
|
||
|
||
def typing_action(func: Callable) -> Callable:
|
||
@functools.wraps(func)
|
||
async def wrapper(event: types.Update, *args, **kwargs) -> Any:
|
||
if isinstance(event, types.Message):
|
||
try:
|
||
await event.bot.send_chat_action(chat_id=event.chat.id, action='typing')
|
||
except Exception as e:
|
||
logger.warning(f'Не удалось отправить typing action: {e}')
|
||
|
||
return await func(event, *args, **kwargs)
|
||
|
||
return wrapper
|
||
|
||
|
||
def rate_limit(rate: float = 1.0, key: str = None):
|
||
def decorator(func: Callable) -> Callable:
|
||
@functools.wraps(func)
|
||
async def wrapper(event: types.Update, *args, **kwargs) -> Any:
|
||
return await func(event, *args, **kwargs)
|
||
|
||
return wrapper
|
||
|
||
return decorator
|
||
|
||
|
||
def modem_available(for_enable: bool = False, for_disable: bool = False):
|
||
"""
|
||
Декоратор для проверки доступности модема.
|
||
|
||
Проверяет:
|
||
- Наличие подписки
|
||
- Подписка не триальная
|
||
- Функция модема включена в настройках
|
||
- (опционально) Модем ещё не подключен (for_enable=True)
|
||
- (опционально) Модем уже подключен (for_disable=True)
|
||
|
||
Args:
|
||
for_enable: Проверять, что модем ещё не подключен
|
||
for_disable: Проверять, что модем подключен
|
||
|
||
Usage:
|
||
@modem_available()
|
||
async def handle_modem_menu(callback, db_user, db): ...
|
||
|
||
@modem_available(for_enable=True)
|
||
async def handle_modem_enable(callback, db_user, db): ...
|
||
"""
|
||
|
||
def decorator(func: Callable) -> Callable:
|
||
@functools.wraps(func)
|
||
async def wrapper(event: types.Update, *args, **kwargs) -> Any:
|
||
db_user = kwargs.get('db_user')
|
||
|
||
if not db_user:
|
||
logger.warning('modem_available: нет db_user в kwargs')
|
||
return None
|
||
|
||
from app.services.modem_service import ModemError, get_modem_service
|
||
|
||
service = get_modem_service()
|
||
result = service.check_availability(db_user, for_enable=for_enable, for_disable=for_disable)
|
||
|
||
if not result.available:
|
||
texts = get_texts(db_user.language if db_user else 'ru')
|
||
|
||
error_messages = {
|
||
ModemError.NO_SUBSCRIPTION: texts.t(
|
||
'MODEM_PAID_ONLY', 'Модем доступен только для платных подписок'
|
||
),
|
||
ModemError.TRIAL_SUBSCRIPTION: texts.t(
|
||
'MODEM_PAID_ONLY', 'Модем доступен только для платных подписок'
|
||
),
|
||
ModemError.MODEM_DISABLED: texts.t('MODEM_DISABLED', 'Функция модема отключена'),
|
||
ModemError.ALREADY_ENABLED: texts.t('MODEM_ALREADY_ENABLED', 'Модем уже подключен'),
|
||
ModemError.NOT_ENABLED: texts.t('MODEM_NOT_ENABLED', 'Модем не подключен'),
|
||
}
|
||
|
||
error_text = error_messages.get(result.error, texts.ERROR)
|
||
|
||
try:
|
||
if isinstance(event, types.CallbackQuery):
|
||
await event.answer(error_text, show_alert=True)
|
||
elif isinstance(event, types.Message):
|
||
await event.answer(error_text)
|
||
except TelegramBadRequest as e:
|
||
if 'query is too old' not in str(e).lower():
|
||
raise
|
||
|
||
return None
|
||
|
||
return await func(event, *args, **kwargs)
|
||
|
||
return wrapper
|
||
|
||
return decorator
|