From 1fd5e4ea39d3888a8333b663af157f60c19c38b4 Mon Sep 17 00:00:00 2001 From: kislovdm Date: Tue, 14 Apr 2026 11:13:21 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=BC=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B3=D1=80=D1=83=D0=BF?= =?UTF-8?q?=D0=BF=D1=8B=20=D0=B2=20=D1=81=D1=83=D0=BF=D0=B5=D1=80=D0=B3?= =?UTF-8?q?=D1=80=D1=83=D0=BF=D0=BF=D1=83=20=D0=BD=D0=B5=D0=BE=D0=B1=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D0=B8=D0=BC=D0=BE=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B5=D1=91=20id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bridge.py | 88 +++++++++++++++++++++++++++++++++++---------- src/telegram_api.py | 55 ++++++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 22 deletions(-) diff --git a/src/bridge.py b/src/bridge.py index fa359ee..e2f4595 100644 --- a/src/bridge.py +++ b/src/bridge.py @@ -1,4 +1,5 @@ import logging +from collections.abc import Awaitable, Callable from typing import Any from max_parser import parse_message @@ -6,7 +7,7 @@ from models import ParsedMessage from pymax import MaxClient from pymax.types import PhotoAttach, VideoAttach from storage import BridgeStorage -from telegram_api import TelegramClient +from telegram_api import TelegramApiError, TelegramClient logger = logging.getLogger(__name__) @@ -80,12 +81,17 @@ class MaxToTelegramBridge: total_media = len(parsed.image_urls) + len(parsed.video_urls) if total_media > 1: # Отправляем одним альбомом в Telegram (единое сообщение). - sent_messages = await self._telegram.send_media_group( - chat_id=target_chat_id, - image_urls=parsed.image_urls, - video_urls=parsed.video_urls, - caption=text, - reply_to_message_id=reply_telegram_mid, + target_chat_id, sent_messages = await self._send_with_migration_retry( + target_chat_id=target_chat_id, + max_chat_title_norm=normalized, + max_chat_title=parsed.chat_name, + send_action=lambda chat_id: self._telegram.send_media_group( + chat_id=chat_id, + image_urls=parsed.image_urls, + video_urls=parsed.video_urls, + caption=text, + reply_to_message_id=reply_telegram_mid, + ), ) for sent in sent_messages: mid = sent.get("message_id") @@ -109,8 +115,13 @@ class MaxToTelegramBridge: sent_any = False if parsed.text.strip() and total_media == 0: - sent = await self._telegram.send_text( - target_chat_id, text, reply_to_message_id=reply_telegram_mid + target_chat_id, sent = await self._send_with_migration_retry( + target_chat_id=target_chat_id, + max_chat_title_norm=normalized, + max_chat_title=parsed.chat_name, + send_action=lambda chat_id: self._telegram.send_text( + chat_id, text, reply_to_message_id=reply_telegram_mid + ), ) mid = sent.get("result", {}).get("message_id") if isinstance(sent.get("result"), dict) else None if mid is not None: @@ -124,11 +135,16 @@ class MaxToTelegramBridge: for index, image_url in enumerate(parsed.image_urls): caption = text if not sent_any and index == 0 else None - sent = await self._telegram.send_photo( - target_chat_id, - image_url, - caption=caption, - reply_to_message_id=reply_telegram_mid if not sent_any and index == 0 else None, + target_chat_id, sent = await self._send_with_migration_retry( + target_chat_id=target_chat_id, + max_chat_title_norm=normalized, + max_chat_title=parsed.chat_name, + send_action=lambda chat_id: self._telegram.send_photo( + chat_id, + image_url, + caption=caption, + reply_to_message_id=reply_telegram_mid if not sent_any and index == 0 else None, + ), ) mid = sent.get("result", {}).get("message_id") if isinstance(sent.get("result"), dict) else None if mid is not None: @@ -142,11 +158,16 @@ class MaxToTelegramBridge: for index, video_url in enumerate(parsed.video_urls): caption = text if not sent_any and index == 0 else None - sent = await self._telegram.send_video( - target_chat_id, - video_url, - caption=caption, - reply_to_message_id=reply_telegram_mid if not sent_any and index == 0 else None, + target_chat_id, sent = await self._send_with_migration_retry( + target_chat_id=target_chat_id, + max_chat_title_norm=normalized, + max_chat_title=parsed.chat_name, + send_action=lambda chat_id: self._telegram.send_video( + chat_id, + video_url, + caption=caption, + reply_to_message_id=reply_telegram_mid if not sent_any and index == 0 else None, + ), ) mid = sent.get("result", {}).get("message_id") if isinstance(sent.get("result"), dict) else None if mid is not None: @@ -167,6 +188,35 @@ class MaxToTelegramBridge: len(parsed.video_urls), ) + async def _send_with_migration_retry( + self, + *, + target_chat_id: str, + max_chat_title_norm: str, + max_chat_title: str, + send_action: Callable[[str], Awaitable[Any]], + ) -> tuple[str, Any]: + try: + sent = await send_action(target_chat_id) + return target_chat_id, sent + except TelegramApiError as exc: + migrated_chat_id = exc.migrate_to_chat_id + if not migrated_chat_id or migrated_chat_id == str(target_chat_id): + raise + logger.warning( + "Telegram chat %s upgraded to %s for MAX chat '%s'; update route and retry", + target_chat_id, + migrated_chat_id, + max_chat_title, + ) + self._storage.set_chat_route( + max_chat_title_norm=max_chat_title_norm, + telegram_chat_id=migrated_chat_id, + telegram_chat_title=max_chat_title, + ) + sent = await send_action(migrated_chat_id) + return migrated_chat_id, sent + def _resolve_telegram_reply_to( self, *, telegram_chat_id: str, max_chat_id: str, parsed: ParsedMessage ) -> int | None: diff --git a/src/telegram_api.py b/src/telegram_api.py index 66d3e7f..ba249a6 100644 --- a/src/telegram_api.py +++ b/src/telegram_api.py @@ -1,11 +1,34 @@ import asyncio +import json from typing import Any import requests class TelegramApiError(RuntimeError): - pass + def __init__( + self, + message: str, + *, + method: str | None = None, + status_code: int | None = None, + error_code: int | None = None, + description: str | None = None, + parameters: dict[str, Any] | None = None, + ) -> None: + super().__init__(message) + self.method = method + self.status_code = status_code + self.error_code = error_code + self.description = description + self.parameters = parameters or {} + + @property + def migrate_to_chat_id(self) -> str | None: + value = self.parameters.get("migrate_to_chat_id") + if value is None: + return None + return str(value) class TelegramClient: @@ -209,10 +232,36 @@ class TelegramClient: response = await asyncio.to_thread(_do_request) if response.status_code >= 400: + error_code: int | None = None + description: str | None = None + parameters: dict[str, Any] = {} + try: + payload_data = response.json() + if isinstance(payload_data, dict): + if isinstance(payload_data.get("error_code"), int): + error_code = payload_data.get("error_code") + if isinstance(payload_data.get("description"), str): + description = payload_data.get("description") + if isinstance(payload_data.get("parameters"), dict): + parameters = payload_data.get("parameters", {}) + except (json.JSONDecodeError, ValueError): + payload_data = None + raise TelegramApiError( - f"Telegram HTTP error on {method}: {response.status_code} {response.text}" + f"Telegram HTTP error on {method}: {response.status_code} {response.text}", + method=method, + status_code=response.status_code, + error_code=error_code, + description=description, + parameters=parameters, ) data = response.json() if not data.get("ok"): - raise TelegramApiError(f"Telegram API error on {method}: {data}") + raise TelegramApiError( + f"Telegram API error on {method}: {data}", + method=method, + error_code=data.get("error_code") if isinstance(data.get("error_code"), int) else None, + description=data.get("description") if isinstance(data.get("description"), str) else None, + parameters=data.get("parameters") if isinstance(data.get("parameters"), dict) else None, + ) return data