diff --git a/handlers/tariffs/buy/key_tariffs.py b/handlers/tariffs/buy/key_tariffs.py index 08e3629a..88349ca1 100644 --- a/handlers/tariffs/buy/key_tariffs.py +++ b/handlers/tariffs/buy/key_tariffs.py @@ -24,7 +24,7 @@ from handlers.texts import ( UNLIMITED_DEVICES_LABEL, UNLIMITED_TRAFFIC_LABEL, ) -from handlers.utils import edit_or_send_message +from handlers.utils import edit_or_send_message, safe_answer_callback from hooks.processors import process_check_discount_validity from logger import logger @@ -160,7 +160,7 @@ async def start_tariff_config( target_message=callback_query.message, text="❌ Указанный тариф не найден.", ) - await callback_query.answer() + await safe_answer_callback(callback_query) logger.warning(f"[TARIFF_CFG] start_tariff_config tariff_not_found: tariff_id={tariff_id}") return @@ -169,7 +169,7 @@ async def start_tariff_config( target_message=callback_query.message, text="❌ Этот тариф нельзя настроить.", ) - await callback_query.answer() + await safe_answer_callback(callback_query) logger.info(f"[TARIFF_CFG] start_tariff_config not_configurable: tariff_id={tariff_id}") return @@ -253,7 +253,7 @@ async def proceed_purchase_with_values( text=CREATING_CONNECTION_MSG, reply_markup=builder.as_markup(), ) - await callback_query.answer() + await safe_answer_callback(callback_query) expiry_time = datetime.now(moscow_tz) + timedelta(days=duration_days) @@ -306,6 +306,7 @@ async def render_user_config_screen( text="Подождите, создаём подписку…", reply_markup=None, ) + await safe_answer_callback(callback_query) await state.clear() logger.warning("[TARIFF_CFG] render_user_config_screen: config_tariff_id missing in state") return @@ -317,6 +318,7 @@ async def render_user_config_screen( text="❌ Тариф не найден.", reply_markup=None, ) + await safe_answer_callback(callback_query) await state.clear() logger.warning(f"[TARIFF_CFG] render_user_config_screen tariff_not_found: tariff_id={tariff_id}") return @@ -547,7 +549,7 @@ async def render_user_config_screen( text=text, reply_markup=builder.as_markup(), ) - await callback_query.answer() + await safe_answer_callback(callback_query) async def start_user_tariff_configurator( @@ -587,7 +589,7 @@ async def start_user_tariff_configurator( text="❌ Конфигуратор для этого тарифа не настроен. Попробуйте выбрать другой тариф.", reply_markup=None, ) - await callback_query.answer() + await safe_answer_callback(callback_query) logger.warning(f"[TARIFF_CFG] configurator_not_configured: tariff_id={tariff.get('id')}") return @@ -774,7 +776,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state: target_message=callback_query.message, text="❌ Указанный тариф не найден.", ) - await callback_query.answer() + await safe_answer_callback(callback_query) logger.warning(f"[TARIFF_CFG] select_tariff_plan tariff_not_found: tariff_id={tariff_id}") return @@ -788,7 +790,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state: text="❌ Скидка недоступна или истекла. Пожалуйста, выберите тариф заново.", reply_markup=builder.as_markup(), ) - await callback_query.answer() + await safe_answer_callback(callback_query) logger.info( "[TARIFF_CFG] select_tariff_plan discount_invalid: " f"tg_id={tg_id} tariff_id={tariff_id} info={discount_info}" @@ -809,7 +811,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state: text=validity_result["message"], reply_markup=builder.as_markup(), ) - await callback_query.answer() + await safe_answer_callback(callback_query) logger.info( "[TARIFF_CFG] select_tariff_plan discount_validity_failed: " f"tg_id={tg_id} tariff_id={tariff_id} result={validity_result}" @@ -867,6 +869,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state: ) async def handle_user_devices_choice(callback: CallbackQuery, state: FSMContext, session: Any): """Обрабатывает выбор лимита устройств в конфигураторе.""" + await safe_answer_callback(callback) _, _tariff_id_str, devices_str = callback.data.split("|", 2) devices = int(devices_str) @@ -875,7 +878,6 @@ async def handle_user_devices_choice(callback: CallbackQuery, state: FSMContext, await state.update_data(config_tariff_id=int(_tariff_id_str)) current = data.get("config_selected_device_limit") if current is not None and int(current) == devices: - await callback.answer() return await state.update_data(config_selected_device_limit=devices) @@ -888,6 +890,7 @@ async def handle_user_devices_choice(callback: CallbackQuery, state: FSMContext, ) async def handle_user_traffic_choice(callback: CallbackQuery, state: FSMContext, session: Any): """Обрабатывает выбор лимита трафика в конфигураторе.""" + await safe_answer_callback(callback) _, _tariff_id_str, traffic_str = callback.data.split("|", 2) traffic = int(traffic_str) @@ -896,7 +899,6 @@ async def handle_user_traffic_choice(callback: CallbackQuery, state: FSMContext, await state.update_data(config_tariff_id=int(_tariff_id_str)) current = data.get("config_selected_traffic_gb") if current is not None and int(current) == traffic: - await callback.answer() return await state.update_data(config_selected_traffic_gb=traffic) diff --git a/handlers/utils.py b/handlers/utils.py index 2a861793..4c6c0294 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -8,8 +8,10 @@ from datetime import datetime, timedelta import aiofiles +from aiogram.exceptions import TelegramBadRequest from aiogram.types import ( BufferedInputFile, + CallbackQuery, InlineKeyboardMarkup, InputMediaAnimation, InputMediaPhoto, @@ -30,6 +32,26 @@ from logger import logger ALLOWED_GROUP_CODES = ["trial", "discounts", "discounts_max", "gifts"] +_CALLBACK_ANSWER_IGNORE = ( + "query is too old", + "response timeout expired", + "query id is invalid", +) + + +async def safe_answer_callback(callback_query: CallbackQuery, text: str | None = None, show_alert: bool = False, **kwargs) -> None: + """ + Вызывает callback_query.answer(), не поднимая исключение при устаревшем/уже отвеченном callback. + Использовать в хендлерах после долгой обработки или при наплыве пользователей. + """ + try: + await callback_query.answer(text=text, show_alert=show_alert, **kwargs) + except TelegramBadRequest as e: + msg = str(e).lower() + if not any(phrase in msg for phrase in _CALLBACK_ANSWER_IGNORE): + raise + + async def generate_random_email( length: int = 8, session: AsyncSession | None = None, diff --git a/panels/remnawave_runtime.py b/panels/remnawave_runtime.py index 2d61e5ac..50723243 100644 --- a/panels/remnawave_runtime.py +++ b/panels/remnawave_runtime.py @@ -109,27 +109,20 @@ def _run_with_api_in_thread( loop.close() -def invalidate_remnawave_profile_cache(*, api_url: str | None = None, client_id: str | None = None) -> None: - """Invalidate cached Remnawave profiles by api_url/client_id (or both).""" - import asyncio - - async def _invalidate_async() -> None: - if api_url is None and client_id is None: - await cache_delete_pattern("remna_profile:*") - return - if api_url is not None and client_id is not None: - await cache_delete_pattern(f"remna_profile:{api_url}:{client_id}") - return - if api_url is not None: - await cache_delete_pattern(f"remna_profile:{api_url}:*") - return - await cache_delete_pattern(f"remna_profile:*:{client_id}") - - try: - loop = asyncio.get_running_loop() - loop.create_task(_invalidate_async()) - except RuntimeError: +async def invalidate_remnawave_profile_cache( + *, api_url: str | None = None, client_id: str | None = None +) -> None: + """Invalidate cached Remnawave profiles by api_url/client_id (or both). Await to avoid pending task on shutdown.""" + if api_url is None and client_id is None: + await cache_delete_pattern("remna_profile:*") return + if api_url is not None and client_id is not None: + await cache_delete_pattern(f"remna_profile:{api_url}:{client_id}") + return + if api_url is not None: + await cache_delete_pattern(f"remna_profile:{api_url}:*") + return + await cache_delete_pattern(f"remna_profile:*:{client_id}") async def resolve_remnawave_api_url( @@ -204,9 +197,9 @@ async def invalidate_remnawave_profile( ) -> None: api_url = await resolve_remnawave_api_url(session, server_ref, fallback_any=fallback_any) if api_url: - invalidate_remnawave_profile_cache(api_url=api_url, client_id=client_id) + await invalidate_remnawave_profile_cache(api_url=api_url, client_id=client_id) else: - invalidate_remnawave_profile_cache(client_id=client_id) + await invalidate_remnawave_profile_cache(client_id=client_id) async def with_remnawave_api(