Files
remnawave-bedolaga-telegram…/app/middlewares/global_error.py
2026-02-02 01:18:28 +03:00

296 lines
12 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.

import asyncio
import logging
import traceback
from collections.abc import Awaitable, Callable
from datetime import datetime, timedelta
from typing import Any
from aiogram import BaseMiddleware, Bot
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, TelegramObject
from app.config import settings
from app.utils.timezone import format_local_datetime
logger = logging.getLogger(__name__)
# Троттлинг для предотвращения спама ошибками
_last_error_notification: datetime | None = None
_error_notification_cooldown = timedelta(minutes=5) # Минимум 5 минут между уведомлениями
_error_buffer: list[tuple[str, str, str]] = [] # (error_type, error_message, traceback)
_max_buffer_size = 10
class GlobalErrorMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
try:
return await handler(event, data)
except TelegramBadRequest as e:
return await self._handle_telegram_error(event, e)
except Exception as e:
logger.error(f'Неожиданная ошибка в GlobalErrorMiddleware: {e}', exc_info=True)
# Отправляем уведомление об ошибке в админский чат
bot = data.get('bot')
if bot:
user_info = self._get_user_info(event)
schedule_error_notification(bot, e, f'Пользователь: {user_info}')
raise
async def _handle_telegram_error(self, event: TelegramObject, error: TelegramBadRequest):
error_message = str(error).lower()
if self._is_old_query_error(error_message):
return await self._handle_old_query(event, error)
if self._is_message_not_modified_error(error_message):
return await self._handle_message_not_modified(event, error)
if self._is_topic_required_error(error_message):
# Канал с топиками — просто игнорируем
logger.debug(f'📋 [GlobalErrorMiddleware] Игнорируем ошибку топика: {error}')
return None
if self._is_bad_request_error(error_message):
return await self._handle_bad_request(event, error)
logger.error(f'Неизвестная Telegram API ошибка: {error}')
raise error
def _is_old_query_error(self, error_message: str) -> bool:
return any(
phrase in error_message
for phrase in ['query is too old', 'query id is invalid', 'response timeout expired']
)
def _is_message_not_modified_error(self, error_message: str) -> bool:
return 'message is not modified' in error_message
def _is_bad_request_error(self, error_message: str) -> bool:
return any(
phrase in error_message
for phrase in ['message not found', 'chat not found', 'bot was blocked by the user', 'user is deactivated']
)
def _is_topic_required_error(self, error_message: str) -> bool:
return any(
phrase in error_message
for phrase in ['topic must be specified', 'topic_closed', 'topic_deleted', 'forum_closed']
)
async def _handle_old_query(self, event: TelegramObject, error: TelegramBadRequest):
if isinstance(event, CallbackQuery):
user_info = self._get_user_info(event)
logger.warning(f"🕐 [GlobalErrorMiddleware] Игнорируем устаревший callback '{event.data}' от {user_info}")
else:
logger.warning(f'🕐 [GlobalErrorMiddleware] Игнорируем устаревший запрос: {error}')
async def _handle_message_not_modified(self, event: TelegramObject, error: TelegramBadRequest):
logger.debug(f'📝 [GlobalErrorMiddleware] Сообщение не было изменено: {error}')
if isinstance(event, CallbackQuery):
try:
await event.answer()
logger.debug("✅ Успешно ответили на callback после 'message not modified'")
except TelegramBadRequest as answer_error:
if not self._is_old_query_error(str(answer_error).lower()):
logger.error(f'❌ Ошибка при ответе на callback: {answer_error}')
async def _handle_bad_request(self, event: TelegramObject, error: TelegramBadRequest):
error_message = str(error).lower()
if 'bot was blocked' in error_message:
user_info = self._get_user_info(event) if hasattr(event, 'from_user') else 'Unknown'
logger.info(f'🚫 [GlobalErrorMiddleware] Бот заблокирован пользователем {user_info}')
return
if 'user is deactivated' in error_message:
user_info = self._get_user_info(event) if hasattr(event, 'from_user') else 'Unknown'
logger.info(f'👻 [GlobalErrorMiddleware] Пользователь деактивирован {user_info}')
return
if 'chat not found' in error_message or 'message not found' in error_message:
logger.warning(f'🔍 [GlobalErrorMiddleware] Чат или сообщение не найдено: {error}')
return
logger.error(f'❌ [GlobalErrorMiddleware] Неизвестная bad request ошибка: {error}')
raise error
def _get_user_info(self, event: TelegramObject) -> str:
if hasattr(event, 'from_user') and event.from_user:
if event.from_user.username:
return f'@{event.from_user.username}'
return f'ID:{event.from_user.id}'
return 'Unknown'
class ErrorStatisticsMiddleware(BaseMiddleware):
def __init__(self):
self.error_counts = {
'old_queries': 0,
'message_not_modified': 0,
'bot_blocked': 0,
'user_deactivated': 0,
'other_errors': 0,
}
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
try:
return await handler(event, data)
except TelegramBadRequest as e:
self._count_error(e)
raise
def _count_error(self, error: TelegramBadRequest):
error_message = str(error).lower()
if 'query is too old' in error_message:
self.error_counts['old_queries'] += 1
elif 'message is not modified' in error_message:
self.error_counts['message_not_modified'] += 1
elif 'bot was blocked' in error_message:
self.error_counts['bot_blocked'] += 1
elif 'user is deactivated' in error_message:
self.error_counts['user_deactivated'] += 1
else:
self.error_counts['other_errors'] += 1
def get_statistics(self) -> dict:
return self.error_counts.copy()
def reset_statistics(self):
for key in self.error_counts:
self.error_counts[key] = 0
async def send_error_to_admin_chat(bot: Bot, error: Exception, context: str = '') -> bool:
"""
Отправляет уведомление об ошибке в админский чат с троттлингом.
Args:
bot: Экземпляр бота
error: Исключение
context: Дополнительный контекст (например, информация о пользователе)
Returns:
bool: True если уведомление отправлено
"""
global _last_error_notification
chat_id = getattr(settings, 'ADMIN_NOTIFICATIONS_CHAT_ID', None)
topic_id = getattr(settings, 'ADMIN_NOTIFICATIONS_TOPIC_ID', None)
enabled = getattr(settings, 'ADMIN_NOTIFICATIONS_ENABLED', False)
if not enabled or not chat_id:
return False
error_type = type(error).__name__
error_message = str(error)[:500]
tb_str = traceback.format_exc()
# Добавляем в буфер
_error_buffer.append((error_type, error_message, tb_str))
if len(_error_buffer) > _max_buffer_size:
_error_buffer.pop(0)
# Проверяем троттлинг
now = datetime.utcnow()
if _last_error_notification and (now - _last_error_notification) < _error_notification_cooldown:
logger.debug(f'Ошибка добавлена в буфер, троттлинг активен: {error_type}')
return False
_last_error_notification = now
try:
timestamp = format_local_datetime(now, '%d.%m.%Y %H:%M:%S')
# Формируем лог-файл со всеми ошибками из буфера
log_lines = [
'ERROR REPORT',
'=' * 50,
f'Timestamp: {timestamp}',
f'Errors in buffer: {len(_error_buffer)}',
'',
]
for i, (err_type, err_msg, err_tb) in enumerate(_error_buffer, 1):
log_lines.extend(
[
f'{"=" * 50}',
f'ERROR #{i}: {err_type}',
f'{"=" * 50}',
f'Message: {err_msg}',
'',
'Traceback:',
err_tb,
'',
]
)
log_content = '\n'.join(log_lines)
# Очищаем буфер после отправки
errors_count = len(_error_buffer)
_error_buffer.clear()
file_name = f'error_report_{now.strftime("%Y%m%d_%H%M%S")}.txt'
file = BufferedInputFile(
file=log_content.encode('utf-8'),
filename=file_name,
)
message_text = (
f'<b>Remnawave Bedolaga Bot</b>\n\n'
f'⚠️ Ошибка во время работы\n\n'
f'<b>Тип:</b> <code>{error_type}</code>\n'
f'<b>Ошибок в отчёте:</b> {errors_count}\n'
)
if context:
message_text += f'<b>Контекст:</b> {context}\n'
message_text += f'\n<i>{timestamp}</i>'
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text='💬 Сообщить разработчику',
url='https://t.me/fringg',
),
],
]
)
message_kwargs: dict = {
'chat_id': chat_id,
'document': file,
'caption': message_text,
'parse_mode': 'HTML',
'reply_markup': keyboard,
}
if topic_id:
message_kwargs['message_thread_id'] = topic_id
await bot.send_document(**message_kwargs)
logger.info(f'Уведомление об ошибке отправлено в чат {chat_id}')
return True
except Exception as e:
logger.error(f'Ошибка отправки уведомления об ошибке: {e}')
return False
def schedule_error_notification(bot: Bot, error: Exception, context: str = '') -> None:
"""
Планирует отправку уведомления об ошибке в фоне (не блокирует).
Args:
bot: Экземпляр бота
error: Исключение
context: Дополнительный контекст
"""
asyncio.create_task(send_error_to_admin_chat(bot, error, context))