diff --git a/handlers/utils.py b/handlers/utils.py index 4c6c0294..067e02cd 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -38,6 +38,12 @@ _CALLBACK_ANSWER_IGNORE = ( "query id is invalid", ) +_MESSAGE_NOT_MODIFIED = "message is not modified" + + +def _is_message_not_modified(exc: BaseException) -> bool: + return isinstance(exc, TelegramBadRequest) and _MESSAGE_NOT_MODIFIED in str(exc).lower() + async def safe_answer_callback(callback_query: CallbackQuery, text: str | None = None, show_alert: bool = False, **kwargs) -> None: """ @@ -298,7 +304,9 @@ async def edit_or_send_message( InputMediaAnimation(media=cached_id, caption=text), reply_markup=reply_markup ) return - except Exception: + except Exception as e: + if _is_message_not_modified(e): + return try: if media_type == "photo": await target_message.answer_photo( @@ -342,7 +350,9 @@ async def edit_or_send_message( msg = await target_message.edit_media( InputMediaAnimation(media=upload, caption=text), reply_markup=reply_markup ) - except Exception: + except Exception as e: + if _is_message_not_modified(e): + return if media_type == "photo": msg = await target_message.answer_photo( photo=upload, @@ -385,7 +395,9 @@ async def edit_or_send_message( try: await target_message.edit_caption(caption=text, reply_markup=reply_markup) return - except Exception: + except Exception as e: + if _is_message_not_modified(e): + return pass try: await target_message.edit_text( @@ -394,7 +406,9 @@ async def edit_or_send_message( disable_web_page_preview=disable_web_page_preview, ) return - except Exception: + except Exception as e: + if _is_message_not_modified(e): + return await target_message.answer( text=text, reply_markup=reply_markup, diff --git a/middlewares/__init__.py b/middlewares/__init__.py index 11154548..2962560f 100644 --- a/middlewares/__init__.py +++ b/middlewares/__init__.py @@ -7,7 +7,7 @@ from middlewares.ban_checker import BanCheckerMiddleware from middlewares.subscription import SubscriptionMiddleware from .admin import AdminMiddleware -from .answer import CallbackAnswerMiddleware +from .answer import CallbackAnswerMiddleware, EarlyCallbackAnswerMiddleware from .concurrency import ConcurrencyLimiterMiddleware from .direct_start_blocker import DirectStartBlockerMiddleware from .loggings import LoggingMiddleware @@ -60,6 +60,9 @@ def register_middleware( if PROBE_LOGGING: dispatcher.update.outer_middleware(StreamProbeMiddleware("global")) + # Первым делом отвечаем на callback, чтобы не уйти в «query is too old» при очереди + dispatcher.update.outer_middleware(EarlyCallbackAnswerMiddleware()) + if middleware_enabled("runtime_config_sync"): dispatcher.update.outer_middleware(wrap(RuntimeConfigSyncMiddleware(), "runtime_config_sync")) if sessionmaker and middleware_enabled("concurrency"): diff --git a/middlewares/answer.py b/middlewares/answer.py index f7094cd0..f0d5f181 100644 --- a/middlewares/answer.py +++ b/middlewares/answer.py @@ -1,12 +1,46 @@ from collections.abc import Awaitable, Callable from typing import Any -from aiogram import BaseMiddleware +from aiogram import BaseMiddleware, Bot +from aiogram.exceptions import TelegramBadRequest from aiogram.types import CallbackQuery, InaccessibleMessage, TelegramObject from bot import bot +class EarlyCallbackAnswerMiddleware(BaseMiddleware): + """ + Регистрируется первым в цепочке update. Для CallbackQuery сразу вызывает + answer_callback_query. + """ + + async def __call__( + self, + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], + event: TelegramObject, + data: dict[str, Any], + ) -> Any: + if isinstance(event, CallbackQuery): + bot_instance: Bot | None = data.get("bot") + if bot_instance: + try: + await bot_instance.answer_callback_query(event.id, show_alert=False) + data["callback_answered_early"] = True + except TelegramBadRequest as e: + msg = str(e).lower() + if ( + "query is too old" in msg + or "response timeout expired" in msg + or "query id is invalid" in msg + ): + pass + else: + raise + except Exception: + pass + return await handler(event, data) + + class CallbackAnswerMiddleware(BaseMiddleware): async def __call__( self, @@ -14,7 +48,7 @@ class CallbackAnswerMiddleware(BaseMiddleware): event: TelegramObject, data: dict[str, Any], ) -> Any: - if isinstance(event, CallbackQuery) and not data.get("callback_answered_by_concurrency"): + if isinstance(event, CallbackQuery) and not data.get("callback_answered_by_concurrency") and not data.get("callback_answered_early"): try: await event.answer() except Exception: diff --git a/middlewares/concurrency.py b/middlewares/concurrency.py index c85e52cf..f4b5302e 100644 --- a/middlewares/concurrency.py +++ b/middlewares/concurrency.py @@ -33,7 +33,7 @@ class ConcurrencyLimiterMiddleware(BaseMiddleware): event: TelegramObject, data: dict[str, Any], ) -> Any: - if isinstance(event, CallbackQuery): + if isinstance(event, CallbackQuery) and not data.get("callback_answered_early"): bot: Bot | None = data.get("bot") if bot: try: