diff --git a/app/middlewares/global_error.py b/app/middlewares/global_error.py
index 48fb7069..f1f5846d 100644
--- a/app/middlewares/global_error.py
+++ b/app/middlewares/global_error.py
@@ -1,14 +1,26 @@
+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
+from aiogram import BaseMiddleware, Bot
from aiogram.exceptions import TelegramBadRequest
-from aiogram.types import CallbackQuery, TelegramObject
+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__(
@@ -23,6 +35,11 @@ class GlobalErrorMiddleware(BaseMiddleware):
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):
@@ -147,3 +164,132 @@ class ErrorStatisticsMiddleware(BaseMiddleware):
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'Remnawave Bedolaga Bot\n\n'
+ f'⚠️ Ошибка во время работы\n\n'
+ f'Тип: {error_type}\n'
+ f'Ошибок в отчёте: {errors_count}\n'
+ )
+ if context:
+ message_text += f'Контекст: {context}\n'
+ message_text += f'\n{timestamp}'
+
+ 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))