From b5b5bc8b7213028f4158ffcce69b6fb80e767992 Mon Sep 17 00:00:00 2001 From: kislovdm Date: Thu, 9 Apr 2026 12:08:10 +0300 Subject: [PATCH] reverse bridge --- src/bridge.py | 22 ++++++++++++++-------- src/reverse_bridge.py | 38 ++++++++++++++++++++++++++++++++++++++ src/storage.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/src/bridge.py b/src/bridge.py index d2db386..015ebda 100644 --- a/src/bridge.py +++ b/src/bridge.py @@ -35,15 +35,21 @@ class MaxToTelegramBridge: logger.debug("Skip empty message %s/%s", parsed.chat_id, parsed.message_id) return - target_chat_id, matched_by_title = await self._telegram.resolve_target_chat_id(parsed.chat_name) - if matched_by_title: - logger.info("Route Max chat '%s' to Telegram chat %s", parsed.chat_name, target_chat_id) + normalized = parsed.chat_name.strip().casefold() + routed = self._storage.get_chat_route(max_chat_title_norm=normalized) + if routed: + target_chat_id = routed + logger.info("Route Max chat '%s' to Telegram chat %s (bound)", parsed.chat_name, target_chat_id) else: - logger.info( - "Telegram chat '%s' not found, route to fallback user %s", - parsed.chat_name, - target_chat_id, - ) + target_chat_id, matched_by_title = await self._telegram.resolve_target_chat_id(parsed.chat_name) + if matched_by_title: + logger.info("Route Max chat '%s' to Telegram chat %s", parsed.chat_name, target_chat_id) + else: + logger.info( + "Telegram chat '%s' not found, route to fallback user %s", + parsed.chat_name, + target_chat_id, + ) total_media = len(parsed.image_urls) + len(parsed.video_urls) if total_media > 1: diff --git a/src/reverse_bridge.py b/src/reverse_bridge.py index f3c18a3..07c4f76 100644 --- a/src/reverse_bridge.py +++ b/src/reverse_bridge.py @@ -99,6 +99,13 @@ class TelegramToMaxBridge: if not isinstance(chat, dict): return + # Команда привязки чата Telegram к названию чата в MAX (для Max->Telegram маршрутизации). + # Работает даже при privacy mode, т.к. команды приходят боту. + text = str(message.get("text") or "").strip() + if text.startswith("/bind_max"): + await self._handle_bind_max_command(message, chat) + return + chat_title = _telegram_chat_title(chat) normalized = _normalize_title(chat_title) if not normalized: @@ -132,6 +139,37 @@ class TelegramToMaxBridge: media_group_id=None, ) + async def _handle_bind_max_command(self, message: dict[str, Any], chat: dict[str, Any]) -> None: + raw = str(message.get("text") or "").strip() + # формат: /bind_max <точное название чата в MAX> + parts = raw.split(maxsplit=1) + if len(parts) < 2 or not parts[1].strip(): + logger.error("bind_max: missing MAX chat title. Use: /bind_max ") + return + + max_title = parts[1].strip() + norm = _normalize_title(max_title) + + # проверяем, что чат существует в MAX + max_chat_id = self._resolve_max_chat_id_by_title(norm) + if max_chat_id is None: + logger.error("bind_max: MAX чат '%s' не найден — привязку не сохраняю", max_title) + return + + telegram_chat_id = str(chat.get("id")) + telegram_title = _telegram_chat_title(chat) + self._storage.set_chat_route( + max_chat_title_norm=norm, + telegram_chat_id=telegram_chat_id, + telegram_chat_title=telegram_title, + ) + logger.info( + "bind_max: bound MAX '%s' -> Telegram '%s' (%s)", + max_title, + telegram_title, + telegram_chat_id, + ) + async def _flush_ready_media_groups(self) -> None: now = time.monotonic() ready: list[tuple[tuple[str, str], _MediaGroupBuffer]] = [] diff --git a/src/storage.py b/src/storage.py index b55a077..8bb85e3 100644 --- a/src/storage.py +++ b/src/storage.py @@ -41,6 +41,16 @@ class BridgeStorage: conn.execute( "CREATE INDEX IF NOT EXISTS idx_message_mapping_media_group ON message_mapping (telegram_chat_id, media_group_id)" ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS chat_routes ( + max_chat_title_norm TEXT PRIMARY KEY, + telegram_chat_id TEXT NOT NULL, + telegram_chat_title TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """ + ) conn.commit() def was_forwarded(self, message_id: str, chat_id: str) -> bool: @@ -94,3 +104,30 @@ class BridgeStorage: if not row: return None return str(row[0]) + + def set_chat_route( + self, + *, + max_chat_title_norm: str, + telegram_chat_id: str, + telegram_chat_title: str | None = None, + ) -> None: + with closing(self._connect()) as conn: + conn.execute( + """ + INSERT OR REPLACE INTO chat_routes (max_chat_title_norm, telegram_chat_id, telegram_chat_title) + VALUES (?, ?, ?) + """, + (max_chat_title_norm, telegram_chat_id, telegram_chat_title), + ) + conn.commit() + + def get_chat_route(self, *, max_chat_title_norm: str) -> str | None: + with closing(self._connect()) as conn: + row = conn.execute( + "SELECT telegram_chat_id FROM chat_routes WHERE max_chat_title_norm = ?", + (max_chat_title_norm,), + ).fetchone() + if not row: + return None + return str(row[0])