Сохраняем треды макса в телеграмме

This commit is contained in:
kislovdm
2026-04-09 23:15:45 +03:00
parent fece0123c8
commit 54cb857f58
5 changed files with 137 additions and 18 deletions
+51 -3
View File
@@ -63,6 +63,15 @@ class MaxToTelegramBridge:
text=parsed.text, text=parsed.text,
include_chat_name=is_fallback, include_chat_name=is_fallback,
) )
reply_telegram_mid = self._resolve_telegram_reply_to(
telegram_chat_id=str(target_chat_id),
max_chat_id=str(parsed.chat_id),
parsed=parsed,
)
if parsed.reply_to_max_message_id and reply_telegram_mid is None:
text = self._prepend_max_reply_context(parsed, text)
has_any_payload = bool(text.strip()) or bool(parsed.image_urls) or bool(parsed.video_urls) has_any_payload = bool(text.strip()) or bool(parsed.image_urls) or bool(parsed.video_urls)
if not has_any_payload: if not has_any_payload:
logger.debug("Skip empty message %s/%s", parsed.chat_id, parsed.message_id) logger.debug("Skip empty message %s/%s", parsed.chat_id, parsed.message_id)
@@ -76,6 +85,7 @@ class MaxToTelegramBridge:
image_urls=parsed.image_urls, image_urls=parsed.image_urls,
video_urls=parsed.video_urls, video_urls=parsed.video_urls,
caption=text, caption=text,
reply_to_message_id=reply_telegram_mid,
) )
for sent in sent_messages: for sent in sent_messages:
mid = sent.get("message_id") mid = sent.get("message_id")
@@ -99,7 +109,9 @@ class MaxToTelegramBridge:
sent_any = False sent_any = False
if parsed.text.strip() and total_media == 0: if parsed.text.strip() and total_media == 0:
sent = await self._telegram.send_text(target_chat_id, text) sent = await self._telegram.send_text(
target_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 mid = sent.get("result", {}).get("message_id") if isinstance(sent.get("result"), dict) else None
if mid is not None: if mid is not None:
self._storage.save_mapping( self._storage.save_mapping(
@@ -112,7 +124,12 @@ class MaxToTelegramBridge:
for index, image_url in enumerate(parsed.image_urls): for index, image_url in enumerate(parsed.image_urls):
caption = text if not sent_any and index == 0 else None caption = text if not sent_any and index == 0 else None
sent = await self._telegram.send_photo(target_chat_id, image_url, caption=caption) 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,
)
mid = sent.get("result", {}).get("message_id") if isinstance(sent.get("result"), dict) else None mid = sent.get("result", {}).get("message_id") if isinstance(sent.get("result"), dict) else None
if mid is not None: if mid is not None:
self._storage.save_mapping( self._storage.save_mapping(
@@ -125,7 +142,12 @@ class MaxToTelegramBridge:
for index, video_url in enumerate(parsed.video_urls): for index, video_url in enumerate(parsed.video_urls):
caption = text if not sent_any and index == 0 else None caption = text if not sent_any and index == 0 else None
sent = await self._telegram.send_video(target_chat_id, video_url, caption=caption) 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,
)
mid = sent.get("result", {}).get("message_id") if isinstance(sent.get("result"), dict) else None mid = sent.get("result", {}).get("message_id") if isinstance(sent.get("result"), dict) else None
if mid is not None: if mid is not None:
self._storage.save_mapping( self._storage.save_mapping(
@@ -145,6 +167,32 @@ class MaxToTelegramBridge:
len(parsed.video_urls), len(parsed.video_urls),
) )
def _resolve_telegram_reply_to(
self, *, telegram_chat_id: str, max_chat_id: str, parsed: ParsedMessage
) -> int | None:
if not parsed.reply_to_max_message_id:
return None
raw = self._storage.get_telegram_message_id_for_max(
telegram_chat_id=telegram_chat_id,
max_chat_id=max_chat_id,
max_message_id=str(parsed.reply_to_max_message_id),
)
if not raw:
return None
try:
return int(raw)
except ValueError:
return None
@staticmethod
def _prepend_max_reply_context(parsed: ParsedMessage, body: str) -> str:
"""Если в Telegram нет исходного сообщения — сохраняем контекст ответа текстом."""
prev = (parsed.reply_preview_text or "").strip()
if prev:
quoted = "\n".join(f"> {line}" for line in prev.splitlines()[:25])
return f"↪ ответ в MAX:\n{quoted}\n\n{body}"
return f"↪ ответ в MAX (сообщение id={parsed.reply_to_max_message_id})\n\n{body}"
@staticmethod @staticmethod
def _format_caption(*, sender_name: str, chat_name: str, text: str, include_chat_name: bool) -> str: def _format_caption(*, sender_name: str, chat_name: str, text: str, include_chat_name: bool) -> str:
sender_name = (sender_name or "").strip() or "unknown" sender_name = (sender_name or "").strip() or "unknown"
+20
View File
@@ -77,6 +77,23 @@ def _extract_media_urls(message: Any) -> tuple[list[str], list[str]]:
return image_urls, video_urls return image_urls, video_urls
def _extract_max_reply(message: Any) -> tuple[str | None, str | None]:
"""Извлекает id сообщения MAX, на которое ответили, и короткий превью-текст (если есть)."""
link = _get_attr(message, ["link"], default=None)
if link is None:
return None, None
inner = _get_attr(link, ["message"], default=None)
if inner is None:
return None, None
mid = _stringify(_get_attr(inner, ["id", "message_id", "mid"]))
if not mid:
return None, None
preview = _stringify(_get_attr(inner, ["text", "message", "body"]))
if preview and len(preview) > 500:
preview = preview[:497] + "..."
return mid, preview or None
def parse_message(message: Any) -> ParsedMessage: def parse_message(message: Any) -> ParsedMessage:
sender = _get_attr(message, ["sender", "sender_name", "author"], default="unknown") sender = _get_attr(message, ["sender", "sender_name", "author"], default="unknown")
sender_data = _as_dict(sender) sender_data = _as_dict(sender)
@@ -99,6 +116,7 @@ def parse_message(message: Any) -> ParsedMessage:
text = _stringify(_get_attr(message, ["text", "message", "body"])) text = _stringify(_get_attr(message, ["text", "message", "body"]))
image_urls, video_urls = _extract_media_urls(message) image_urls, video_urls = _extract_media_urls(message)
reply_mid, reply_preview = _extract_max_reply(message)
return ParsedMessage( return ParsedMessage(
message_id=message_id, message_id=message_id,
chat_id=chat_id, chat_id=chat_id,
@@ -107,4 +125,6 @@ def parse_message(message: Any) -> ParsedMessage:
text=text, text=text,
image_urls=image_urls, image_urls=image_urls,
video_urls=video_urls, video_urls=video_urls,
reply_to_max_message_id=reply_mid,
reply_preview_text=reply_preview,
) )
+3
View File
@@ -10,3 +10,6 @@ class ParsedMessage:
text: str text: str
image_urls: list[str] = field(default_factory=list) image_urls: list[str] = field(default_factory=list)
video_urls: list[str] = field(default_factory=list) video_urls: list[str] = field(default_factory=list)
# Ответ в MAX: Message.link указывает на исходное сообщение (тред).
reply_to_max_message_id: str | None = None
reply_preview_text: str | None = None
+19
View File
@@ -105,6 +105,25 @@ class BridgeStorage:
return None return None
return str(row[0]) return str(row[0])
def get_telegram_message_id_for_max(
self, *, telegram_chat_id: str, max_chat_id: str, max_message_id: str
) -> str | None:
"""Обратный поиск: какое сообщение в Telegram соответствует паре MAX chat/message."""
with closing(self._connect()) as conn:
row = conn.execute(
"""
SELECT telegram_message_id
FROM message_mapping
WHERE telegram_chat_id = ? AND max_chat_id = ? AND max_message_id = ?
ORDER BY telegram_message_id
LIMIT 1
""",
(telegram_chat_id, max_chat_id, max_message_id),
).fetchone()
if not row:
return None
return str(row[0])
def set_chat_route( def set_chat_route(
self, self,
*, *,
+44 -15
View File
@@ -27,26 +27,48 @@ class TelegramClient:
return chat_id, True return chat_id, True
return self._fallback_user_id, False return self._fallback_user_id, False
async def send_text(self, chat_id: str, text: str) -> dict[str, Any]: async def send_text(
return await self._request( self,
"sendMessage", chat_id: str,
{ text: str,
"chat_id": chat_id, *,
"text": text, reply_to_message_id: int | None = None,
"disable_web_page_preview": True, ) -> dict[str, Any]:
}, payload: dict[str, Any] = {
) "chat_id": chat_id,
"text": text,
"disable_web_page_preview": True,
}
if reply_to_message_id is not None:
payload["reply_to_message_id"] = reply_to_message_id
return await self._request("sendMessage", payload)
async def send_photo(self, chat_id: str, photo_url: str, caption: str | None = None) -> dict[str, Any]: async def send_photo(
self,
chat_id: str,
photo_url: str,
caption: str | None = None,
*,
reply_to_message_id: int | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = { payload: dict[str, Any] = {
"chat_id": chat_id, "chat_id": chat_id,
"photo": photo_url, "photo": photo_url,
} }
if caption: if caption:
payload["caption"] = caption payload["caption"] = caption
if reply_to_message_id is not None:
payload["reply_to_message_id"] = reply_to_message_id
return await self._request("sendPhoto", payload) return await self._request("sendPhoto", payload)
async def send_video(self, chat_id: str, video_url: str, caption: str | None = None) -> dict[str, Any]: async def send_video(
self,
chat_id: str,
video_url: str,
caption: str | None = None,
*,
reply_to_message_id: int | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = { payload: dict[str, Any] = {
"chat_id": chat_id, "chat_id": chat_id,
"video": video_url, "video": video_url,
@@ -54,6 +76,8 @@ class TelegramClient:
} }
if caption: if caption:
payload["caption"] = caption payload["caption"] = caption
if reply_to_message_id is not None:
payload["reply_to_message_id"] = reply_to_message_id
return await self._request("sendVideo", payload) return await self._request("sendVideo", payload)
async def send_media_group( async def send_media_group(
@@ -62,6 +86,8 @@ class TelegramClient:
image_urls: list[str], image_urls: list[str],
video_urls: list[str], video_urls: list[str],
caption: str | None = None, caption: str | None = None,
*,
reply_to_message_id: int | None = None,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
media: list[dict[str, Any]] = [] media: list[dict[str, Any]] = []
@@ -77,12 +103,15 @@ class TelegramClient:
if caption: if caption:
media[0]["caption"] = caption media[0]["caption"] = caption
mg_payload: dict[str, Any] = {
"chat_id": chat_id,
"media": media,
}
if reply_to_message_id is not None:
mg_payload["reply_to_message_id"] = reply_to_message_id
data = await self._request( data = await self._request(
"sendMediaGroup", "sendMediaGroup",
{ mg_payload,
"chat_id": chat_id,
"media": media,
},
) )
result = data.get("result", []) result = data.get("result", [])
if not isinstance(result, list): if not isinstance(result, list):