Fixed: back button in stars/ traffic display/ trial in country mode. Added error duplicate handling and request queueing.
This commit is contained in:
@@ -9,6 +9,9 @@ from config import DATABASE_URL, DB_MAX_OVERFLOW, DB_POOL_SIZE
|
|||||||
|
|
||||||
CONCURRENT_UPDATES_LIMIT = DB_POOL_SIZE + DB_MAX_OVERFLOW
|
CONCURRENT_UPDATES_LIMIT = DB_POOL_SIZE + DB_MAX_OVERFLOW
|
||||||
MAX_UPDATE_AGE_SEC = 28
|
MAX_UPDATE_AGE_SEC = 28
|
||||||
|
CONCURRENT_UPDATES_WAIT_TIMEOUT_SEC = 8
|
||||||
|
CONCURRENT_UPDATES_GATE_LIMIT = 150
|
||||||
|
CONCURRENT_UPDATES_GATE_WAIT_SEC = 2
|
||||||
|
|
||||||
engine = create_async_engine(
|
engine = create_async_engine(
|
||||||
DATABASE_URL,
|
DATABASE_URL,
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ async def add_user(
|
|||||||
|
|
||||||
async def update_balance(session: AsyncSession, tg_id: int, amount: float) -> None:
|
async def update_balance(session: AsyncSession, tg_id: int, amount: float) -> None:
|
||||||
try:
|
try:
|
||||||
|
if amount < 0:
|
||||||
|
current = await get_balance(session, tg_id)
|
||||||
|
if current + amount < 0:
|
||||||
|
logger.warning(f"[DB] Недостаточно средств: tg_id={tg_id} balance={current} списание={amount}")
|
||||||
|
await session.rollback()
|
||||||
|
raise ValueError(f"Недостаточно средств: баланс {current}, списание {amount}")
|
||||||
res = await session.execute(
|
res = await session.execute(
|
||||||
update(User)
|
update(User)
|
||||||
.where(User.tg_id == tg_id)
|
.where(User.tg_id == tg_id)
|
||||||
|
|||||||
@@ -1453,6 +1453,22 @@ async def render_config_menu(callback_query: CallbackQuery, state: FSMContext, s
|
|||||||
base_traffic = data.get("cfg_base_traffic")
|
base_traffic = data.get("cfg_base_traffic")
|
||||||
extra_traffic = data.get("cfg_extra_traffic") or 0
|
extra_traffic = data.get("cfg_extra_traffic") or 0
|
||||||
|
|
||||||
|
traffic_to_show = base_traffic
|
||||||
|
if traffic_to_show is None and email:
|
||||||
|
result = await session.execute(select(Key).where(Key.email == email))
|
||||||
|
key_obj = result.scalar_one_or_none()
|
||||||
|
if key_obj:
|
||||||
|
traffic_to_show = key_obj.selected_traffic_limit or key_obj.current_traffic_limit
|
||||||
|
if traffic_to_show is None and tariff:
|
||||||
|
raw = tariff.get("traffic_limit")
|
||||||
|
if raw is not None:
|
||||||
|
try:
|
||||||
|
val = int(raw)
|
||||||
|
if val > 0:
|
||||||
|
traffic_to_show = val
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
f"<b>⚙️ Конфигурация ключа</b>\n\n"
|
f"<b>⚙️ Конфигурация ключа</b>\n\n"
|
||||||
f"🔑 <b>Ключ:</b> <code>{email}</code>\n"
|
f"🔑 <b>Ключ:</b> <code>{email}</code>\n"
|
||||||
@@ -1462,9 +1478,9 @@ async def render_config_menu(callback_query: CallbackQuery, state: FSMContext, s
|
|||||||
extra_dev_str = f" + {extra_devices} (докуплено)" if extra_devices > 0 else ""
|
extra_dev_str = f" + {extra_devices} (докуплено)" if extra_devices > 0 else ""
|
||||||
text += f"📱 <b>Устройства:</b> {base_devices}{extra_dev_str}\n"
|
text += f"📱 <b>Устройства:</b> {base_devices}{extra_dev_str}\n"
|
||||||
|
|
||||||
if base_traffic:
|
if traffic_to_show:
|
||||||
extra_traf_str = f" + {extra_traffic} ГБ (докуплено)" if extra_traffic > 0 else ""
|
extra_traf_str = f" + {extra_traffic} ГБ (докуплено)" if extra_traffic > 0 else ""
|
||||||
text += f"📊 <b>Трафик:</b> {base_traffic} ГБ{extra_traf_str}\n"
|
text += f"📊 <b>Трафик:</b> {traffic_to_show} ГБ{extra_traf_str}\n"
|
||||||
else:
|
else:
|
||||||
text += "📊 <b>Трафик:</b> безлимит\n"
|
text += "📊 <b>Трафик:</b> безлимит\n"
|
||||||
|
|
||||||
|
|||||||
@@ -195,13 +195,6 @@ async def key_country_mode(
|
|||||||
bound_servers = [s for s in servers if special in (s.get("special_groups") or [])]
|
bound_servers = [s for s in servers if special in (s.get("special_groups") or [])]
|
||||||
if bound_servers:
|
if bound_servers:
|
||||||
servers = bound_servers
|
servers = bound_servers
|
||||||
else:
|
|
||||||
text = f"❌ Нет доступных серверов для тарифа с группой '{special}'."
|
|
||||||
if safe_to_edit:
|
|
||||||
await edit_or_send_message(target_message=target_message, text=text, reply_markup=None)
|
|
||||||
else:
|
|
||||||
await bot.send_message(chat_id=tg_id, text=text)
|
|
||||||
return
|
|
||||||
|
|
||||||
available_servers: list[str] = []
|
available_servers: list[str] = []
|
||||||
tasks = [asyncio.create_task(check_server_availability(dict(server), session)) for server in servers]
|
tasks = [asyncio.create_task(check_server_availability(dict(server), session)) for server in servers]
|
||||||
@@ -368,15 +361,6 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
|
|||||||
bound_servers = [s for s in available_servers_dict if special in (s.get("special_groups") or [])]
|
bound_servers = [s for s in available_servers_dict if special in (s.get("special_groups") or [])]
|
||||||
if bound_servers:
|
if bound_servers:
|
||||||
available_servers = [s["server_name"] for s in bound_servers]
|
available_servers = [s["server_name"] for s in bound_servers]
|
||||||
else:
|
|
||||||
builder = InlineKeyboardBuilder()
|
|
||||||
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{old_key_name}"))
|
|
||||||
await edit_or_send_message(
|
|
||||||
target_message=callback_query.message,
|
|
||||||
text="❌ Нет доступных стран для смены локации.",
|
|
||||||
reply_markup=builder.as_markup(),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not available_servers:
|
if not available_servers:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
@@ -425,7 +409,10 @@ async def handle_country_selection(callback_query: CallbackQuery, session: Any,
|
|||||||
return
|
return
|
||||||
|
|
||||||
old_key_name = data[3] if len(data) > 3 and data[3] else None
|
old_key_name = data[3] if len(data) > 3 and data[3] else None
|
||||||
|
try:
|
||||||
tariff_id = int(data[4]) if len(data) > 4 and data[4] else None
|
tariff_id = int(data[4]) if len(data) > 4 and data[4] else None
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
tariff_id = None
|
||||||
|
|
||||||
tg_id = callback_query.from_user.id
|
tg_id = callback_query.from_user.id
|
||||||
|
|
||||||
|
|||||||
@@ -250,6 +250,38 @@ async def try_fast_payment_flow(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "fastflow_back")
|
||||||
|
async def fastflow_back(callback_query: CallbackQuery, state: FSMContext, session: Any):
|
||||||
|
"""Возврат из экрана выбора суммы в потоке /buy к выбору способа оплаты (без перехода на экран баланса)."""
|
||||||
|
amount_not_found_text = "Сумма не найдена"
|
||||||
|
data = await state.get_data()
|
||||||
|
temp_key = data.get("temp_key")
|
||||||
|
temp_payload = data.get("temp_payload")
|
||||||
|
required_amount = data.get("required_amount")
|
||||||
|
|
||||||
|
if not temp_key or not isinstance(temp_payload, dict) or required_amount is None:
|
||||||
|
await edit_or_send_message(
|
||||||
|
target_message=callback_query.message,
|
||||||
|
text=amount_not_found_text,
|
||||||
|
reply_markup=InlineKeyboardBuilder()
|
||||||
|
.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
|
||||||
|
.as_markup(),
|
||||||
|
)
|
||||||
|
await callback_query.answer()
|
||||||
|
return
|
||||||
|
|
||||||
|
await try_fast_payment_flow(
|
||||||
|
callback_query,
|
||||||
|
session,
|
||||||
|
state,
|
||||||
|
tg_id=callback_query.from_user.id,
|
||||||
|
temp_key=str(temp_key),
|
||||||
|
temp_payload=dict(temp_payload),
|
||||||
|
required_amount=int(required_amount),
|
||||||
|
)
|
||||||
|
await callback_query.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "fastflow_coupon_back")
|
@router.callback_query(F.data == "fastflow_coupon_back")
|
||||||
async def fastflow_coupon_back(callback_query: CallbackQuery, state: FSMContext, session: Any):
|
async def fastflow_coupon_back(callback_query: CallbackQuery, state: FSMContext, session: Any):
|
||||||
amount_not_found_text = "Сумма не найдена"
|
amount_not_found_text = "Сумма не найдена"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiogram import F, Router, types
|
from aiogram import F, Router, types
|
||||||
|
|||||||
@@ -179,7 +179,23 @@ async def handle_pay_currency(callback_query: CallbackQuery, state: FSMContext,
|
|||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "balance")
|
@router.callback_query(F.data == "balance")
|
||||||
async def balance_handler(callback_query: CallbackQuery, session: AsyncSession):
|
async def balance_handler(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||||||
|
data = await state.get_data()
|
||||||
|
if data.get("temp_key") and data.get("required_amount") is not None:
|
||||||
|
from handlers.payments.fast_payment_flow import try_fast_payment_flow
|
||||||
|
|
||||||
|
await try_fast_payment_flow(
|
||||||
|
callback_query,
|
||||||
|
session,
|
||||||
|
state,
|
||||||
|
tg_id=callback_query.from_user.id,
|
||||||
|
temp_key=str(data["temp_key"]),
|
||||||
|
temp_payload=dict(data.get("temp_payload") or {}),
|
||||||
|
required_amount=int(data["required_amount"]),
|
||||||
|
)
|
||||||
|
await callback_query.answer()
|
||||||
|
return
|
||||||
|
|
||||||
stmt = select(User.balance).where(User.tg_id == callback_query.from_user.id)
|
stmt = select(User.balance).where(User.tg_id == callback_query.from_user.id)
|
||||||
result = await session.execute(stmt)
|
result = await session.execute(stmt)
|
||||||
balance_rub = result.scalar_one_or_none() or 0.0
|
balance_rub = result.scalar_one_or_none() or 0.0
|
||||||
|
|||||||
@@ -14,15 +14,17 @@ class CallbackAnswerMiddleware(BaseMiddleware):
|
|||||||
event: TelegramObject,
|
event: TelegramObject,
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if isinstance(event, CallbackQuery):
|
if isinstance(event, CallbackQuery) and isinstance(event.message, InaccessibleMessage):
|
||||||
try:
|
|
||||||
await event.answer()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if isinstance(event.message, InaccessibleMessage):
|
|
||||||
try:
|
try:
|
||||||
new_message = await bot.send_message(event.message.chat.id, "⏳")
|
new_message = await bot.send_message(event.message.chat.id, "⏳")
|
||||||
object.__setattr__(event, "message", new_message)
|
object.__setattr__(event, "message", new_message)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
|
finally:
|
||||||
|
if isinstance(event, CallbackQuery) and not data.get("callback_answered_early"):
|
||||||
|
try:
|
||||||
|
await event.answer()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|||||||
+90
-17
@@ -4,18 +4,27 @@ from collections.abc import Awaitable, Callable
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiogram import BaseMiddleware, Bot
|
from aiogram import BaseMiddleware, Bot
|
||||||
from aiogram.types import CallbackQuery, Message, TelegramObject
|
from aiogram.types import CallbackQuery, Message, TelegramObject, Update
|
||||||
|
|
||||||
from database.db import CONCURRENT_UPDATES_LIMIT, MAX_UPDATE_AGE_SEC
|
from database.db import (
|
||||||
|
CONCURRENT_UPDATES_GATE_LIMIT,
|
||||||
|
CONCURRENT_UPDATES_GATE_WAIT_SEC,
|
||||||
|
CONCURRENT_UPDATES_LIMIT,
|
||||||
|
CONCURRENT_UPDATES_WAIT_TIMEOUT_SEC,
|
||||||
|
MAX_UPDATE_AGE_SEC,
|
||||||
|
)
|
||||||
|
from logger import logger
|
||||||
|
|
||||||
|
|
||||||
class ConcurrencyLimiterMiddleware(BaseMiddleware):
|
class ConcurrencyLimiterMiddleware(BaseMiddleware):
|
||||||
"""
|
"""
|
||||||
Регистрируется до SessionMiddleware. Ограничивает число апдейтов, одновременно
|
Регистрируется до SessionMiddleware. Шлюз (gate) ограничивает число апдейтов
|
||||||
получающих сессию, и отсекает апдейты, ждавшие слишком долго.
|
в конвейере; семафор — число одновременно обрабатываемых с БД. Лишние
|
||||||
|
апдейты сразу получают «высокая нагрузка» и не создают тысячи ожидающих задач.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
self._gate = asyncio.Semaphore(CONCURRENT_UPDATES_GATE_LIMIT)
|
||||||
self._semaphore = asyncio.Semaphore(CONCURRENT_UPDATES_LIMIT)
|
self._semaphore = asyncio.Semaphore(CONCURRENT_UPDATES_LIMIT)
|
||||||
|
|
||||||
async def __call__(
|
async def __call__(
|
||||||
@@ -25,35 +34,99 @@ class ConcurrencyLimiterMiddleware(BaseMiddleware):
|
|||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
) -> Any:
|
) -> Any:
|
||||||
data["request_time"] = time.monotonic()
|
data["request_time"] = time.monotonic()
|
||||||
await self._semaphore.acquire()
|
if isinstance(event, CallbackQuery):
|
||||||
|
await self._answer_callback_early(event, data)
|
||||||
|
gate_wait = CONCURRENT_UPDATES_GATE_WAIT_SEC if CONCURRENT_UPDATES_GATE_WAIT_SEC else 0
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self._gate.acquire(), timeout=gate_wait)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.warning("[Concurrency] Reject: gate full (очередь переполнена)")
|
||||||
|
await self._reject_overload(event, data)
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(
|
||||||
|
self._semaphore.acquire(),
|
||||||
|
timeout=CONCURRENT_UPDATES_WAIT_TIMEOUT_SEC,
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.warning("[Concurrency] Reject: semaphore timeout (все слоты БД заняты)")
|
||||||
|
await self._reject_overload(event, data)
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
age = time.monotonic() - data["request_time"]
|
age = time.monotonic() - data["request_time"]
|
||||||
if age > MAX_UPDATE_AGE_SEC:
|
if age > MAX_UPDATE_AGE_SEC:
|
||||||
|
logger.warning("[Concurrency] Reject: update too old (age %.1fs)", age)
|
||||||
await self._reject_stale(event, data)
|
await self._reject_stale(event, data)
|
||||||
return None
|
return None
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
finally:
|
finally:
|
||||||
self._semaphore.release()
|
self._semaphore.release()
|
||||||
|
finally:
|
||||||
|
self._gate.release()
|
||||||
|
|
||||||
|
async def _answer_callback_early(self, event: CallbackQuery, data: dict[str, Any]) -> None:
|
||||||
|
"""Отвечает на callback сразу, снимая таймаут «устаревший запрос» при долгой очереди."""
|
||||||
|
if data.get("callback_answered_early"):
|
||||||
|
return
|
||||||
|
bot: Bot = data.get("bot")
|
||||||
|
if not bot:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await bot.answer_callback_query(
|
||||||
|
event.id,
|
||||||
|
text="⏳",
|
||||||
|
show_alert=False,
|
||||||
|
)
|
||||||
|
data["callback_answered_early"] = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
async def _reject_stale(self, event: TelegramObject, data: dict[str, Any]) -> None:
|
async def _reject_stale(self, event: TelegramObject, data: dict[str, Any]) -> None:
|
||||||
if isinstance(event, CallbackQuery):
|
await self._send_reject_message(event, data)
|
||||||
|
|
||||||
|
async def _reject_overload(self, event: TelegramObject, data: dict[str, Any]) -> None:
|
||||||
|
await self._send_reject_message(event, data)
|
||||||
|
|
||||||
|
async def _send_reject_message(self, event: TelegramObject, data: dict[str, Any]) -> None:
|
||||||
|
"""Отправляет пользователю сообщение «высокая нагрузка / нажмите ещё раз»."""
|
||||||
bot: Bot = data.get("bot")
|
bot: Bot = data.get("bot")
|
||||||
if bot:
|
if not bot:
|
||||||
|
return
|
||||||
|
text = "Сейчас высокая нагрузка. Попробуйте ещё раз через несколько секунд."
|
||||||
try:
|
try:
|
||||||
|
if isinstance(event, Update):
|
||||||
|
chat_id, callback = self._chat_and_callback_from_update(event)
|
||||||
|
if chat_id is None:
|
||||||
|
return
|
||||||
|
if callback and not data.get("callback_answered_early"):
|
||||||
|
await bot.answer_callback_query(
|
||||||
|
callback.id,
|
||||||
|
text="Время ожидания истекло. Нажмите ещё раз.",
|
||||||
|
show_alert=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await bot.send_message(chat_id, text)
|
||||||
|
elif isinstance(event, CallbackQuery):
|
||||||
|
if data.get("callback_answered_early"):
|
||||||
|
if event.message and event.message.chat:
|
||||||
|
await bot.send_message(event.message.chat.id, text)
|
||||||
|
else:
|
||||||
await bot.answer_callback_query(
|
await bot.answer_callback_query(
|
||||||
event.id,
|
event.id,
|
||||||
text="Время ожидания истекло. Нажмите ещё раз.",
|
text="Время ожидания истекло. Нажмите ещё раз.",
|
||||||
show_alert=False,
|
show_alert=False,
|
||||||
)
|
)
|
||||||
|
elif isinstance(event, Message) and event.chat:
|
||||||
|
await bot.send_message(event.chat.id, text)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
elif isinstance(event, Message) and event.text and event.chat:
|
|
||||||
bot: Bot = data.get("bot")
|
@staticmethod
|
||||||
if bot:
|
def _chat_and_callback_from_update(update: Update) -> tuple[int | None, CallbackQuery | None]:
|
||||||
try:
|
"""Извлекает chat_id и callback (если есть) из Update для отправки сообщения."""
|
||||||
await bot.send_message(
|
if update.message and update.message.chat:
|
||||||
event.chat.id,
|
return update.message.chat.id, None
|
||||||
"Сейчас высокая нагрузка. Отправьте команду ещё раз через пару секунд.",
|
if update.callback_query and update.callback_query.message and update.callback_query.message.chat:
|
||||||
)
|
return update.callback_query.message.chat.id, update.callback_query
|
||||||
except Exception:
|
return None, None
|
||||||
pass
|
|
||||||
|
|||||||
+37
-5
@@ -1,6 +1,9 @@
|
|||||||
|
import asyncio
|
||||||
import html
|
import html
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||||
@@ -17,11 +20,41 @@ _OBFUSCATED_MIN_SEQ = 15
|
|||||||
_PLACEHOLDER = "<obfuscated>"
|
_PLACEHOLDER = "<obfuscated>"
|
||||||
|
|
||||||
|
|
||||||
|
_ERROR_NOTIFY_MAX_PER_MINUTE = 2
|
||||||
|
_ERROR_DEDUPE_SEC = 120
|
||||||
|
_ERROR_MSG_PREFIX_LEN = 200
|
||||||
|
_error_send_times: deque[float] = deque(maxlen=500)
|
||||||
|
_error_dedup: dict[tuple[str, str], float] = {}
|
||||||
|
_error_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_traceback(text: str) -> str:
|
def _sanitize_traceback(text: str) -> str:
|
||||||
"""Убирает из текста длинные последовательности \\xNN (обфусцированный код)."""
|
"""Убирает из текста длинные последовательности \\xNN (обфусцированный код)."""
|
||||||
return re.sub(r"(\\x[0-9a-fA-F]{2}){" + str(_OBFUSCATED_MIN_SEQ) + r",}", _PLACEHOLDER, text)
|
return re.sub(r"(\\x[0-9a-fA-F]{2}){" + str(_OBFUSCATED_MIN_SEQ) + r",}", _PLACEHOLDER, text)
|
||||||
|
|
||||||
|
|
||||||
|
async def _should_send_error_to_admins(exc_type: type[BaseException], exc_message: str) -> bool:
|
||||||
|
"""
|
||||||
|
Разрешает отправку уведомления админу только если не превышен лимит в минуту
|
||||||
|
и такая же ошибка не отправлялась недавно (дедуп). Сбрасывает старые записи.
|
||||||
|
"""
|
||||||
|
now = time.monotonic()
|
||||||
|
key = (exc_type.__name__, (exc_message or "")[:_ERROR_MSG_PREFIX_LEN])
|
||||||
|
async with _error_lock:
|
||||||
|
while _error_send_times and _error_send_times[0] < now - 60:
|
||||||
|
_error_send_times.popleft()
|
||||||
|
for k, t in list(_error_dedup.items()):
|
||||||
|
if t < now - _ERROR_DEDUPE_SEC:
|
||||||
|
del _error_dedup[k]
|
||||||
|
if len(_error_send_times) >= _ERROR_NOTIFY_MAX_PER_MINUTE:
|
||||||
|
return False
|
||||||
|
if key in _error_dedup:
|
||||||
|
return False
|
||||||
|
_error_dedup[key] = now
|
||||||
|
_error_send_times.append(now)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def setup_error_handlers(dp: Dispatcher) -> None:
|
def setup_error_handlers(dp: Dispatcher) -> None:
|
||||||
@dp.errors(ExceptionTypeFilter(Exception))
|
@dp.errors(ExceptionTypeFilter(Exception))
|
||||||
async def errors_handler(event: ErrorEvent, bot: Bot) -> bool:
|
async def errors_handler(event: ErrorEvent, bot: Bot) -> bool:
|
||||||
@@ -50,7 +83,7 @@ def setup_error_handlers(dp: Dispatcher) -> None:
|
|||||||
logger.warning(f"Показываем стартовое меню из-за TelegramBadRequest: {error_message}")
|
logger.warning(f"Показываем стартовое меню из-за TelegramBadRequest: {error_message}")
|
||||||
logger.error(f"Traceback:\n{tb}")
|
logger.error(f"Traceback:\n{tb}")
|
||||||
|
|
||||||
if ADMIN_ID:
|
if ADMIN_ID and await _should_send_error_to_admins(type(event.exception), error_message):
|
||||||
if "query is too old and response timeout expired or query ID is invalid" in error_message:
|
if "query is too old and response timeout expired or query ID is invalid" in error_message:
|
||||||
caption = (
|
caption = (
|
||||||
f"{hbold('TelegramBadRequest: устаревший callback-запрос')}\n\n"
|
f"{hbold('TelegramBadRequest: устаревший callback-запрос')}\n\n"
|
||||||
@@ -68,9 +101,8 @@ def setup_error_handlers(dp: Dispatcher) -> None:
|
|||||||
else:
|
else:
|
||||||
caption = f"{hbold(type(event.exception).__name__)}: {error_message[:1021]}..."
|
caption = f"{hbold(type(event.exception).__name__)}: {error_message[:1021]}..."
|
||||||
|
|
||||||
for admin_id in ADMIN_ID:
|
|
||||||
await bot.send_document(
|
await bot.send_document(
|
||||||
chat_id=admin_id,
|
chat_id=ADMIN_ID[0],
|
||||||
document=BufferedInputFile(
|
document=BufferedInputFile(
|
||||||
tb.encode(),
|
tb.encode(),
|
||||||
filename=f"error_{event.update.update_id}.txt",
|
filename=f"error_{event.update.update_id}.txt",
|
||||||
@@ -122,11 +154,11 @@ def setup_error_handlers(dp: Dispatcher) -> None:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if await _should_send_error_to_admins(type(event.exception), str(event.exception)):
|
||||||
tb_text = _sanitize_traceback(traceback.format_exc())
|
tb_text = _sanitize_traceback(traceback.format_exc())
|
||||||
for admin_id in ADMIN_ID:
|
|
||||||
exc_text = html.escape(str(event.exception)[:1021])
|
exc_text = html.escape(str(event.exception)[:1021])
|
||||||
await bot.send_document(
|
await bot.send_document(
|
||||||
chat_id=admin_id,
|
chat_id=ADMIN_ID[0],
|
||||||
document=BufferedInputFile(
|
document=BufferedInputFile(
|
||||||
tb_text.encode(),
|
tb_text.encode(),
|
||||||
filename=f"error_{event.update.update_id}.txt",
|
filename=f"error_{event.update.update_id}.txt",
|
||||||
|
|||||||
Reference in New Issue
Block a user