Сохраняем треды макса в телеграмме
This commit is contained in:
+51
-3
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user