mirror of
https://github.com/artemium428/428th-exchange-bot.git
synced 2026-09-15 16:56:20 +00:00
Retry Telegram flood waits so client messages are not dropped.
Also fix reply-target error detection so fallback without reply_to works when Telegram says the replied message is gone. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
501c2a5985
commit
897651abe9
1 changed files with 66 additions and 15 deletions
|
|
@ -7,7 +7,12 @@ from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from aiogram.enums import ChatType, ParseMode
|
from aiogram.enums import ChatType, ParseMode
|
||||||
from aiogram.exceptions import TelegramAPIError, TelegramBadRequest, TelegramForbiddenError
|
from aiogram.exceptions import (
|
||||||
|
TelegramAPIError,
|
||||||
|
TelegramBadRequest,
|
||||||
|
TelegramForbiddenError,
|
||||||
|
TelegramRetryAfter,
|
||||||
|
)
|
||||||
from aiogram.types import Message, MessageReactionUpdated, ReactionTypeCustomEmoji, ReactionTypeEmoji, User
|
from aiogram.types import Message, MessageReactionUpdated, ReactionTypeCustomEmoji, ReactionTypeEmoji, User
|
||||||
from aiogram.utils.text_decorations import html_decoration
|
from aiogram.utils.text_decorations import html_decoration
|
||||||
|
|
||||||
|
|
@ -27,6 +32,34 @@ GENERAL_TOPIC_ID = 1
|
||||||
|
|
||||||
TOPIC_NAME_MAX_LEN = 128
|
TOPIC_NAME_MAX_LEN = 128
|
||||||
CAPTION_MAX_LEN = 1024
|
CAPTION_MAX_LEN = 1024
|
||||||
|
# How many times to wait out Telegram flood control before giving up.
|
||||||
|
FLOOD_RETRY_ATTEMPTS = 5
|
||||||
|
|
||||||
|
|
||||||
|
async def _with_flood_retry(api_call, *args, **kwargs):
|
||||||
|
"""Call Telegram API; on 429 wait retry_after and try again."""
|
||||||
|
for attempt in range(1, FLOOD_RETRY_ATTEMPTS + 1):
|
||||||
|
try:
|
||||||
|
return await api_call(*args, **kwargs)
|
||||||
|
except TelegramRetryAfter as exc:
|
||||||
|
wait = max(int(exc.retry_after), 1)
|
||||||
|
method = getattr(api_call, "__name__", type(api_call).__name__)
|
||||||
|
if attempt >= FLOOD_RETRY_ATTEMPTS:
|
||||||
|
logger.error(
|
||||||
|
"Rate limited on %s after %s attempt(s); giving up (retry_after=%ss)",
|
||||||
|
method,
|
||||||
|
attempt,
|
||||||
|
wait,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
logger.warning(
|
||||||
|
"Rate limited on %s; waiting %ss (attempt %s/%s)",
|
||||||
|
method,
|
||||||
|
wait,
|
||||||
|
attempt,
|
||||||
|
FLOOD_RETRY_ATTEMPTS,
|
||||||
|
)
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
|
||||||
|
|
||||||
def build_topic_name(user: User) -> str:
|
def build_topic_name(user: User) -> str:
|
||||||
|
|
@ -129,9 +162,15 @@ def _is_message_gone_error(exc: BaseException) -> bool:
|
||||||
|
|
||||||
def _is_reply_error(exc: BaseException) -> bool:
|
def _is_reply_error(exc: BaseException) -> bool:
|
||||||
text = str(exc).lower()
|
text = str(exc).lower()
|
||||||
return "reply" in text and (
|
# Telegram uses "replied" (y→i), so "reply" is not a substring of "replied".
|
||||||
"not found" in text or "message to be replied" in text or "replied message" in text
|
return any(
|
||||||
)
|
marker in text
|
||||||
|
for marker in (
|
||||||
|
"message to be replied",
|
||||||
|
"replied message",
|
||||||
|
"message to reply not found",
|
||||||
|
)
|
||||||
|
) or ("reply" in text and "not found" in text)
|
||||||
|
|
||||||
|
|
||||||
def _is_topic_missing_error(exc: BaseException) -> bool:
|
def _is_topic_missing_error(exc: BaseException) -> bool:
|
||||||
|
|
@ -224,7 +263,8 @@ class RelayService:
|
||||||
return fresh.topic_id
|
return fresh.topic_id
|
||||||
await self._repo.clear_topic_id(user.id)
|
await self._repo.clear_topic_id(user.id)
|
||||||
|
|
||||||
topic = await bot.create_forum_topic(
|
topic = await _with_flood_retry(
|
||||||
|
bot.create_forum_topic,
|
||||||
chat_id=self._manager_group_id,
|
chat_id=self._manager_group_id,
|
||||||
name=build_topic_name(user),
|
name=build_topic_name(user),
|
||||||
)
|
)
|
||||||
|
|
@ -277,12 +317,12 @@ class RelayService:
|
||||||
async def _call_with_optional_reply(self, api_call, kwargs: Dict[str, Any]):
|
async def _call_with_optional_reply(self, api_call, kwargs: Dict[str, Any]):
|
||||||
"""Call Telegram API; retry once without reply_to if the reply target is gone."""
|
"""Call Telegram API; retry once without reply_to if the reply target is gone."""
|
||||||
try:
|
try:
|
||||||
return await api_call(**kwargs)
|
return await _with_flood_retry(api_call, **kwargs)
|
||||||
except TelegramBadRequest as exc:
|
except TelegramBadRequest as exc:
|
||||||
if kwargs.get("reply_to_message_id") is not None and _is_reply_error(exc):
|
if kwargs.get("reply_to_message_id") is not None and _is_reply_error(exc):
|
||||||
retry = dict(kwargs)
|
retry = dict(kwargs)
|
||||||
retry.pop("reply_to_message_id", None)
|
retry.pop("reply_to_message_id", None)
|
||||||
return await api_call(**retry)
|
return await _with_flood_retry(api_call, **retry)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def _copy_client_message(
|
async def _copy_client_message(
|
||||||
|
|
@ -460,7 +500,8 @@ class RelayService:
|
||||||
|
|
||||||
async def _notify_managers(self, bot: Bot, topic_id: int, key: str) -> None:
|
async def _notify_managers(self, bot: Bot, topic_id: int, key: str) -> None:
|
||||||
try:
|
try:
|
||||||
await bot.send_message(
|
await _with_flood_retry(
|
||||||
|
bot.send_message,
|
||||||
chat_id=self._manager_group_id,
|
chat_id=self._manager_group_id,
|
||||||
message_thread_id=topic_id,
|
message_thread_id=topic_id,
|
||||||
text=t(DEFAULT_LANGUAGE, key),
|
text=t(DEFAULT_LANGUAGE, key),
|
||||||
|
|
@ -513,7 +554,8 @@ class RelayService:
|
||||||
try:
|
try:
|
||||||
if message.text is not None and not _is_captionable(message):
|
if message.text is not None and not _is_captionable(message):
|
||||||
body = _html_body(message.text, message.entities)
|
body = _html_body(message.text, message.entities)
|
||||||
await bot.edit_message_text(
|
await _with_flood_retry(
|
||||||
|
bot.edit_message_text,
|
||||||
chat_id=self._manager_group_id,
|
chat_id=self._manager_group_id,
|
||||||
message_id=link.manager_message_id,
|
message_id=link.manager_message_id,
|
||||||
text=format_labeled_html(f"{client_display_name(user)}:", body),
|
text=format_labeled_html(f"{client_display_name(user)}:", body),
|
||||||
|
|
@ -527,7 +569,8 @@ class RelayService:
|
||||||
message.caption,
|
message.caption,
|
||||||
message.caption_entities,
|
message.caption_entities,
|
||||||
)
|
)
|
||||||
await bot.edit_message_caption(
|
await _with_flood_retry(
|
||||||
|
bot.edit_message_caption,
|
||||||
chat_id=self._manager_group_id,
|
chat_id=self._manager_group_id,
|
||||||
message_id=link.manager_message_id,
|
message_id=link.manager_message_id,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
|
|
@ -566,7 +609,8 @@ class RelayService:
|
||||||
try:
|
try:
|
||||||
if message.text is not None and not _is_captionable(message):
|
if message.text is not None and not _is_captionable(message):
|
||||||
body = _html_body(message.text, message.entities)
|
body = _html_body(message.text, message.entities)
|
||||||
await bot.edit_message_text(
|
await _with_flood_retry(
|
||||||
|
bot.edit_message_text,
|
||||||
chat_id=client.telegram_id,
|
chat_id=client.telegram_id,
|
||||||
message_id=link.client_message_id,
|
message_id=link.client_message_id,
|
||||||
text=format_manager_html(client.language, body),
|
text=format_manager_html(client.language, body),
|
||||||
|
|
@ -580,7 +624,8 @@ class RelayService:
|
||||||
message.caption,
|
message.caption,
|
||||||
message.caption_entities,
|
message.caption_entities,
|
||||||
)
|
)
|
||||||
await bot.edit_message_caption(
|
await _with_flood_retry(
|
||||||
|
bot.edit_message_caption,
|
||||||
chat_id=client.telegram_id,
|
chat_id=client.telegram_id,
|
||||||
message_id=link.client_message_id,
|
message_id=link.client_message_id,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
|
|
@ -666,7 +711,11 @@ class RelayService:
|
||||||
|
|
||||||
async def _safe_delete(self, bot: Bot, chat_id: int, message_id: int) -> None:
|
async def _safe_delete(self, bot: Bot, chat_id: int, message_id: int) -> None:
|
||||||
try:
|
try:
|
||||||
await bot.delete_message(chat_id=chat_id, message_id=message_id)
|
await _with_flood_retry(
|
||||||
|
bot.delete_message,
|
||||||
|
chat_id=chat_id,
|
||||||
|
message_id=message_id,
|
||||||
|
)
|
||||||
except TelegramAPIError:
|
except TelegramAPIError:
|
||||||
logger.info("Could not delete message %s in chat %s", message_id, chat_id)
|
logger.info("Could not delete message %s in chat %s", message_id, chat_id)
|
||||||
|
|
||||||
|
|
@ -684,7 +733,8 @@ class RelayService:
|
||||||
if link is None:
|
if link is None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
await bot.set_message_reaction(
|
await _with_flood_retry(
|
||||||
|
bot.set_message_reaction,
|
||||||
chat_id=self._manager_group_id,
|
chat_id=self._manager_group_id,
|
||||||
message_id=link.manager_message_id,
|
message_id=link.manager_message_id,
|
||||||
reaction=reactions,
|
reaction=reactions,
|
||||||
|
|
@ -703,7 +753,8 @@ class RelayService:
|
||||||
if link is None:
|
if link is None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
await bot.set_message_reaction(
|
await _with_flood_retry(
|
||||||
|
bot.set_message_reaction,
|
||||||
chat_id=link.client_telegram_id,
|
chat_id=link.client_telegram_id,
|
||||||
message_id=link.client_message_id,
|
message_id=link.client_message_id,
|
||||||
reaction=reactions,
|
reaction=reactions,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue