296 lines
14 KiB
Python
296 lines
14 KiB
Python
import html
|
|
import re
|
|
import time
|
|
import traceback
|
|
|
|
from aiogram import Bot, Dispatcher
|
|
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
|
|
from aiogram.filters import ExceptionTypeFilter
|
|
from aiogram.types import BufferedInputFile, ErrorEvent
|
|
from aiogram.utils.markdown import hbold
|
|
from sqlalchemy.exc import InterfaceError as SQLAlchemyInterfaceError
|
|
from sqlalchemy.exc import OperationalError as SQLAlchemyOperationalError
|
|
|
|
from config import ADMIN_ID
|
|
from database import async_session_maker
|
|
from logger import logger
|
|
|
|
|
|
_OBFUSCATED_MIN_SEQ = 15
|
|
_PLACEHOLDER = "<obfuscated>"
|
|
|
|
_ERROR_LOG_THROTTLE: dict[str, float] = {}
|
|
_ERROR_LOG_THROTTLE_WINDOW_SEC = 60
|
|
_ERROR_LOG_THROTTLE_MAX_KEYS = 500
|
|
|
|
|
|
def _should_log_error(key: str, window_sec: float = _ERROR_LOG_THROTTLE_WINDOW_SEC) -> bool:
|
|
"""Возвращает True, если эту ошибку ещё можно залогировать (не троттлим). Обновляет время последнего лога."""
|
|
now = time.monotonic()
|
|
if len(_ERROR_LOG_THROTTLE) >= _ERROR_LOG_THROTTLE_MAX_KEYS:
|
|
by_ts = sorted((v, k) for k, v in _ERROR_LOG_THROTTLE.items())
|
|
for _, k in by_ts[: _ERROR_LOG_THROTTLE_MAX_KEYS // 2]:
|
|
_ERROR_LOG_THROTTLE.pop(k, None)
|
|
last = _ERROR_LOG_THROTTLE.get(key, 0.0)
|
|
if now - last >= window_sec:
|
|
_ERROR_LOG_THROTTLE[key] = now
|
|
return True
|
|
return False
|
|
|
|
|
|
def _sanitize_traceback(text: str) -> str:
|
|
"""Убирает из текста длинные последовательности."""
|
|
return re.sub(r"(\\x[0-9a-fA-F]{2}){" + str(_OBFUSCATED_MIN_SEQ) + r",}", _PLACEHOLDER, text)
|
|
|
|
|
|
def setup_error_handlers(dp: Dispatcher) -> None:
|
|
@dp.errors(ExceptionTypeFilter(Exception))
|
|
async def errors_handler(event: ErrorEvent, bot: Bot) -> bool:
|
|
if isinstance(event.exception, TelegramForbiddenError):
|
|
user = None
|
|
if event.update.message and event.update.message.from_user:
|
|
user = event.update.message.from_user
|
|
elif event.update.callback_query and event.update.callback_query.from_user:
|
|
user = event.update.callback_query.from_user
|
|
if user:
|
|
logger.info(f"User {user.id} заблокировал бота.")
|
|
else:
|
|
logger.info("Пользователь заблокировал бота.")
|
|
return True
|
|
|
|
if isinstance(event.exception, TelegramRetryAfter):
|
|
e = event.exception
|
|
chat_id = None
|
|
if event.update.message:
|
|
chat_id = event.update.message.chat.id
|
|
elif event.update.callback_query and event.update.callback_query.message:
|
|
chat_id = event.update.callback_query.message.chat.id
|
|
if _should_log_error("TelegramRetryAfter", 30):
|
|
logger.warning(
|
|
"Flood control (TelegramRetryAfter): retry_after={} сек, chat_id={}",
|
|
e.retry_after,
|
|
chat_id,
|
|
)
|
|
if chat_id:
|
|
try:
|
|
await bot.send_message(
|
|
chat_id,
|
|
f"⏳ Слишком много запросов. Повторите через {int(e.retry_after) + 1} сек.",
|
|
)
|
|
except Exception:
|
|
pass
|
|
return True
|
|
|
|
if isinstance(event.exception, OSError) and getattr(event.exception, "errno", None) == 24:
|
|
chat_id = None
|
|
if event.update.message:
|
|
chat_id = event.update.message.chat.id
|
|
elif event.update.callback_query and event.update.callback_query.message:
|
|
chat_id = event.update.callback_query.message.chat.id
|
|
if _should_log_error("OSError24", 15):
|
|
logger.warning(
|
|
"OSError(24) Too many open files при обработке запроса (chat_id={})",
|
|
chat_id,
|
|
)
|
|
if chat_id:
|
|
try:
|
|
await bot.send_message(
|
|
chat_id,
|
|
"⚠️ Временная перегрузка. Попробуйте ещё раз через пару секунд.",
|
|
)
|
|
except Exception:
|
|
pass
|
|
return True
|
|
|
|
if isinstance(
|
|
event.exception,
|
|
(SQLAlchemyInterfaceError, SQLAlchemyOperationalError),
|
|
):
|
|
chat_id = None
|
|
if event.update.message:
|
|
chat_id = event.update.message.chat.id
|
|
elif event.update.callback_query and event.update.callback_query.message:
|
|
chat_id = event.update.callback_query.message.chat.id
|
|
if _should_log_error("DB_Interface_Operational", 30):
|
|
logger.warning(
|
|
"Ошибка соединения с БД (InterfaceError/OperationalError): {}",
|
|
str(event.exception)[:200],
|
|
)
|
|
if chat_id:
|
|
try:
|
|
await bot.send_message(
|
|
chat_id,
|
|
"⚠️ Временная ошибка связи с базой данных. Попробуйте ещё раз через пару секунд.",
|
|
)
|
|
except Exception:
|
|
pass
|
|
return True
|
|
|
|
if isinstance(event.exception, TelegramBadRequest):
|
|
error_message = str(event.exception)
|
|
|
|
if (
|
|
"message is not modified" in error_message
|
|
or "message to edit not found" in error_message
|
|
or "message can't be edited" in error_message.lower()
|
|
):
|
|
logger.debug(
|
|
"TelegramBadRequest (edit/delete): {}",
|
|
error_message[:150],
|
|
)
|
|
return True
|
|
|
|
if (
|
|
"query is too old and response timeout expired or query ID is invalid" in error_message
|
|
or "message can't be deleted for everyone" in error_message
|
|
or "message to delete not found" in error_message
|
|
):
|
|
log_key = "TelegramBadRequest_old_or_delete"
|
|
if _should_log_error(log_key, 45):
|
|
try:
|
|
tb = _sanitize_traceback(
|
|
"".join(
|
|
traceback.format_exception(
|
|
type(event.exception),
|
|
event.exception,
|
|
event.exception.__traceback__,
|
|
)
|
|
)
|
|
)
|
|
logger.warning("Показываем стартовое меню из-за TelegramBadRequest: {}", error_message[:200])
|
|
logger.error("Traceback:\n{}", tb)
|
|
|
|
if ADMIN_ID:
|
|
if "query is too old and response timeout expired or query ID is invalid" in error_message:
|
|
caption = (
|
|
f"{hbold('TelegramBadRequest: устаревший callback-запрос')}\n\n"
|
|
"Что произошло:\n"
|
|
"• Пользователь нажал старую кнопку, или\n"
|
|
"• Telegram обработал callback уже после истечения таймаута.\n\n"
|
|
"Описание:\n"
|
|
"Такое может происходить из-за временной недоступности Telegram или "
|
|
"нестабильного подключения сервера к API (задержки, потери пакетов, очереди запросов).\n\n"
|
|
"Действия:\n"
|
|
"• Проверить стабильность интернет-соединения сервера.\n"
|
|
"• Оценить задержки/нагрузку на бота и частоту callback-запросов.\n"
|
|
"• При необходимости оптимизировать обработку или уменьшить время между нажатием кнопки и ответом."
|
|
)
|
|
else:
|
|
caption = f"{hbold(type(event.exception).__name__)}: {error_message[:1021]}..."
|
|
|
|
for admin_id in ADMIN_ID:
|
|
await bot.send_document(
|
|
chat_id=admin_id,
|
|
document=BufferedInputFile(
|
|
tb.encode(),
|
|
filename=f"error_{event.update.update_id}.txt",
|
|
),
|
|
caption=caption[:1024],
|
|
)
|
|
except Exception as e:
|
|
if _should_log_error("error_handler_admin_send", 60):
|
|
logger.error("Сбой при логировании/отправке ошибки админу: {}", e, exc_info=True)
|
|
|
|
try:
|
|
from handlers.start import start_entry
|
|
|
|
if event.update.message:
|
|
fsm_context = dp.fsm.get_context(
|
|
bot=bot,
|
|
chat_id=event.update.message.chat.id,
|
|
user_id=event.update.message.from_user.id,
|
|
)
|
|
async with async_session_maker() as session:
|
|
await start_entry(
|
|
event=event.update.message,
|
|
state=fsm_context,
|
|
session=session,
|
|
admin=False,
|
|
captcha=False,
|
|
)
|
|
await session.commit()
|
|
elif event.update.callback_query:
|
|
fsm_context = dp.fsm.get_context(
|
|
bot=bot,
|
|
chat_id=event.update.callback_query.message.chat.id,
|
|
user_id=event.update.callback_query.from_user.id,
|
|
)
|
|
async with async_session_maker() as session:
|
|
await start_entry(
|
|
event=event.update.callback_query,
|
|
state=fsm_context,
|
|
session=session,
|
|
admin=False,
|
|
captcha=False,
|
|
)
|
|
await session.commit()
|
|
except Exception as e:
|
|
if _should_log_error("error_handler_start_menu", 60):
|
|
logger.error("Ошибка при показе стартового меню после ошибки: {}", e, exc_info=True)
|
|
|
|
return True
|
|
|
|
exc = event.exception
|
|
generic_key = "err:{}:{}".format(type(exc).__name__, str(exc)[:80].replace("\n", " "))
|
|
if _should_log_error(generic_key, _ERROR_LOG_THROTTLE_WINDOW_SEC):
|
|
logger.exception("Update: {}\nException: {}", event.update, exc)
|
|
|
|
if not ADMIN_ID:
|
|
return True
|
|
|
|
try:
|
|
if _should_log_error("generic_admin_doc", 60):
|
|
tb_text = _sanitize_traceback(traceback.format_exc())
|
|
exc_text = html.escape(str(event.exception)[:1021])
|
|
for admin_id in ADMIN_ID:
|
|
await bot.send_document(
|
|
chat_id=admin_id,
|
|
document=BufferedInputFile(
|
|
tb_text.encode(),
|
|
filename=f"error_{event.update.update_id}.txt",
|
|
),
|
|
caption=f"{hbold(type(event.exception).__name__)}: {exc_text}...",
|
|
)
|
|
|
|
from handlers.start import start_entry
|
|
|
|
if event.update.message:
|
|
fsm_context = dp.fsm.get_context(
|
|
bot=bot,
|
|
chat_id=event.update.message.chat.id,
|
|
user_id=event.update.message.from_user.id,
|
|
)
|
|
async with async_session_maker() as session:
|
|
await start_entry(
|
|
event=event.update.message,
|
|
state=fsm_context,
|
|
session=session,
|
|
admin=False,
|
|
captcha=False,
|
|
)
|
|
await session.commit()
|
|
elif event.update.callback_query:
|
|
fsm_context = dp.fsm.get_context(
|
|
bot=bot,
|
|
chat_id=event.update.callback_query.message.chat.id,
|
|
user_id=event.update.callback_query.from_user.id,
|
|
)
|
|
async with async_session_maker() as session:
|
|
await start_entry(
|
|
event=event.update.callback_query,
|
|
state=fsm_context,
|
|
session=session,
|
|
admin=False,
|
|
captcha=False,
|
|
)
|
|
await session.commit()
|
|
|
|
except TelegramBadRequest as exception:
|
|
if _should_log_error("error_handler_telegram_bad", 60):
|
|
logger.warning("Не удалось отправить детали ошибки: {}", exception)
|
|
except Exception as exception:
|
|
if _should_log_error("error_handler_unexpected", 60):
|
|
logger.error("Неожиданная ошибка в error handler: {}", exception)
|
|
|
|
return True
|