Сохраняем треды макса в телеграмме
This commit is contained in:
+51
-3
@@ -63,6 +63,15 @@ class MaxToTelegramBridge:
|
||||
text=parsed.text,
|
||||
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)
|
||||
if not has_any_payload:
|
||||
logger.debug("Skip empty message %s/%s", parsed.chat_id, parsed.message_id)
|
||||
@@ -76,6 +85,7 @@ class MaxToTelegramBridge:
|
||||
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")
|
||||
@@ -99,7 +109,9 @@ class MaxToTelegramBridge:
|
||||
|
||||
sent_any = False
|
||||
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
|
||||
if mid is not None:
|
||||
self._storage.save_mapping(
|
||||
@@ -112,7 +124,12 @@ 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)
|
||||
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
|
||||
if mid is not None:
|
||||
self._storage.save_mapping(
|
||||
@@ -125,7 +142,12 @@ 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)
|
||||
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
|
||||
if mid is not None:
|
||||
self._storage.save_mapping(
|
||||
@@ -145,6 +167,32 @@ class MaxToTelegramBridge:
|
||||
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
|
||||
def _format_caption(*, sender_name: str, chat_name: str, text: str, include_chat_name: bool) -> str:
|
||||
sender_name = (sender_name or "").strip() or "unknown"
|
||||
|
||||
@@ -77,6 +77,23 @@ def _extract_media_urls(message: Any) -> tuple[list[str], list[str]]:
|
||||
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:
|
||||
sender = _get_attr(message, ["sender", "sender_name", "author"], default="unknown")
|
||||
sender_data = _as_dict(sender)
|
||||
@@ -99,6 +116,7 @@ def parse_message(message: Any) -> ParsedMessage:
|
||||
text = _stringify(_get_attr(message, ["text", "message", "body"]))
|
||||
|
||||
image_urls, video_urls = _extract_media_urls(message)
|
||||
reply_mid, reply_preview = _extract_max_reply(message)
|
||||
return ParsedMessage(
|
||||
message_id=message_id,
|
||||
chat_id=chat_id,
|
||||
@@ -107,4 +125,6 @@ def parse_message(message: Any) -> ParsedMessage:
|
||||
text=text,
|
||||
image_urls=image_urls,
|
||||
video_urls=video_urls,
|
||||
reply_to_max_message_id=reply_mid,
|
||||
reply_preview_text=reply_preview,
|
||||
)
|
||||
|
||||
@@ -10,3 +10,6 @@ class ParsedMessage:
|
||||
text: str
|
||||
image_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
|
||||
|
||||
@@ -105,6 +105,25 @@ class BridgeStorage:
|
||||
return None
|
||||
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(
|
||||
self,
|
||||
*,
|
||||
|
||||
+41
-12
@@ -27,26 +27,48 @@ class TelegramClient:
|
||||
return chat_id, True
|
||||
return self._fallback_user_id, False
|
||||
|
||||
async def send_text(self, chat_id: str, text: str) -> dict[str, Any]:
|
||||
return await self._request(
|
||||
"sendMessage",
|
||||
{
|
||||
async def send_text(
|
||||
self,
|
||||
chat_id: str,
|
||||
text: str,
|
||||
*,
|
||||
reply_to_message_id: int | None = None,
|
||||
) -> 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] = {
|
||||
"chat_id": chat_id,
|
||||
"photo": photo_url,
|
||||
}
|
||||
if 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)
|
||||
|
||||
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] = {
|
||||
"chat_id": chat_id,
|
||||
"video": video_url,
|
||||
@@ -54,6 +76,8 @@ class TelegramClient:
|
||||
}
|
||||
if 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)
|
||||
|
||||
async def send_media_group(
|
||||
@@ -62,6 +86,8 @@ class TelegramClient:
|
||||
image_urls: list[str],
|
||||
video_urls: list[str],
|
||||
caption: str | None = None,
|
||||
*,
|
||||
reply_to_message_id: int | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
media: list[dict[str, Any]] = []
|
||||
|
||||
@@ -77,12 +103,15 @@ class TelegramClient:
|
||||
if caption:
|
||||
media[0]["caption"] = caption
|
||||
|
||||
data = await self._request(
|
||||
"sendMediaGroup",
|
||||
{
|
||||
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(
|
||||
"sendMediaGroup",
|
||||
mg_payload,
|
||||
)
|
||||
result = data.get("result", [])
|
||||
if not isinstance(result, list):
|
||||
|
||||
Reference in New Issue
Block a user