Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e40297787e | ||
|
|
dcf4441340 |
+131
-13
@@ -19,9 +19,9 @@ class MaxToTelegramBridge:
|
|||||||
self._storage = storage
|
self._storage = storage
|
||||||
|
|
||||||
async def forward_message(self, max_message: Any) -> None:
|
async def forward_message(self, max_message: Any) -> None:
|
||||||
if self._is_self_message(max_message):
|
#if self._is_self_message(max_message):
|
||||||
logger.debug("Skip self message %s/%s", getattr(max_message, "chat_id", "?"), getattr(max_message, "id", "?"))
|
# logger.debug("Skip self message %s/%s", getattr(max_message, "chat_id", "?"), getattr(max_message, "id", "?"))
|
||||||
return
|
# return
|
||||||
|
|
||||||
parsed = parse_message(max_message)
|
parsed = parse_message(max_message)
|
||||||
parsed = await self._enrich_from_max(max_message, parsed)
|
parsed = await self._enrich_from_max(max_message, parsed)
|
||||||
@@ -34,11 +34,6 @@ class MaxToTelegramBridge:
|
|||||||
# - если найден целевой Telegram-чат (не fallback): "Ирина:\n<текст>"
|
# - если найден целевой Telegram-чат (не fallback): "Ирина:\n<текст>"
|
||||||
# - если fallback: "Ирина / Свободный микрофон:\n<текст>"
|
# - если fallback: "Ирина / Свободный микрофон:\n<текст>"
|
||||||
# Решение о том, включать ли название чата, принимаем после определения маршрута.
|
# Решение о том, включать ли название чата, принимаем после определения маршрута.
|
||||||
has_any_payload = bool((parsed.text or "").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)
|
|
||||||
return
|
|
||||||
|
|
||||||
normalized = parsed.chat_name.strip().casefold()
|
normalized = parsed.chat_name.strip().casefold()
|
||||||
routed = self._storage.get_chat_route(max_chat_title_norm=normalized)
|
routed = self._storage.get_chat_route(max_chat_title_norm=normalized)
|
||||||
if routed:
|
if routed:
|
||||||
@@ -64,6 +59,7 @@ class MaxToTelegramBridge:
|
|||||||
text=parsed.text,
|
text=parsed.text,
|
||||||
include_chat_name=is_fallback,
|
include_chat_name=is_fallback,
|
||||||
)
|
)
|
||||||
|
text = self._append_unknown_attachment_notice(parsed=parsed, text=text)
|
||||||
|
|
||||||
reply_telegram_mid = self._resolve_telegram_reply_to(
|
reply_telegram_mid = self._resolve_telegram_reply_to(
|
||||||
telegram_chat_id=str(target_chat_id),
|
telegram_chat_id=str(target_chat_id),
|
||||||
@@ -73,12 +69,12 @@ class MaxToTelegramBridge:
|
|||||||
if parsed.reply_to_max_message_id and reply_telegram_mid is None:
|
if parsed.reply_to_max_message_id and reply_telegram_mid is None:
|
||||||
text = self._prepend_max_reply_context(parsed, text)
|
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) or bool(parsed.file_urls)
|
||||||
if not has_any_payload:
|
if not has_any_payload:
|
||||||
logger.debug("Skip empty message %s/%s", parsed.chat_id, parsed.message_id)
|
text = self._build_fallback_unknown_notice(parsed)
|
||||||
return
|
|
||||||
|
|
||||||
total_media = len(parsed.image_urls) + len(parsed.video_urls)
|
total_media = len(parsed.image_urls) + len(parsed.video_urls)
|
||||||
|
sent_any = False
|
||||||
if total_media > 1:
|
if total_media > 1:
|
||||||
# Отправляем одним альбомом в Telegram (единое сообщение).
|
# Отправляем одним альбомом в Telegram (единое сообщение).
|
||||||
target_chat_id, sent_messages = await self._send_with_migration_retry(
|
target_chat_id, sent_messages = await self._send_with_migration_retry(
|
||||||
@@ -103,6 +99,7 @@ class MaxToTelegramBridge:
|
|||||||
max_chat_id=str(parsed.chat_id),
|
max_chat_id=str(parsed.chat_id),
|
||||||
max_message_id=str(parsed.message_id),
|
max_message_id=str(parsed.message_id),
|
||||||
)
|
)
|
||||||
|
sent_any = True
|
||||||
self._storage.mark_forwarded(parsed.message_id, parsed.chat_id)
|
self._storage.mark_forwarded(parsed.message_id, parsed.chat_id)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Forwarded media group %s/%s (images=%s, videos=%s)",
|
"Forwarded media group %s/%s (images=%s, videos=%s)",
|
||||||
@@ -113,7 +110,6 @@ class MaxToTelegramBridge:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
sent_any = False
|
|
||||||
if parsed.text.strip() and total_media == 0:
|
if parsed.text.strip() and total_media == 0:
|
||||||
target_chat_id, sent = await self._send_with_migration_retry(
|
target_chat_id, sent = await self._send_with_migration_retry(
|
||||||
target_chat_id=target_chat_id,
|
target_chat_id=target_chat_id,
|
||||||
@@ -179,15 +175,80 @@ class MaxToTelegramBridge:
|
|||||||
)
|
)
|
||||||
sent_any = True
|
sent_any = True
|
||||||
|
|
||||||
|
for index, file_url in enumerate(parsed.file_urls):
|
||||||
|
caption = text 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_document(
|
||||||
|
chat_id,
|
||||||
|
file_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(
|
||||||
|
telegram_chat_id=str(target_chat_id),
|
||||||
|
telegram_message_id=str(mid),
|
||||||
|
max_chat_id=str(parsed.chat_id),
|
||||||
|
max_message_id=str(parsed.message_id),
|
||||||
|
)
|
||||||
|
sent_any = True
|
||||||
|
|
||||||
|
if not sent_any:
|
||||||
|
# Последняя страховка: гарантируем уведомление в Telegram даже для пустых/неизвестных payload.
|
||||||
|
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, self._build_fallback_unknown_notice(parsed), 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(
|
||||||
|
telegram_chat_id=str(target_chat_id),
|
||||||
|
telegram_message_id=str(mid),
|
||||||
|
max_chat_id=str(parsed.chat_id),
|
||||||
|
max_message_id=str(parsed.message_id),
|
||||||
|
)
|
||||||
|
sent_any = True
|
||||||
|
|
||||||
self._storage.mark_forwarded(parsed.message_id, parsed.chat_id)
|
self._storage.mark_forwarded(parsed.message_id, parsed.chat_id)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Forwarded message %s/%s (images=%s, videos=%s)",
|
"Forwarded message %s/%s (images=%s, videos=%s, files=%s, unknown=%s)",
|
||||||
parsed.chat_id,
|
parsed.chat_id,
|
||||||
parsed.message_id,
|
parsed.message_id,
|
||||||
len(parsed.image_urls),
|
len(parsed.image_urls),
|
||||||
len(parsed.video_urls),
|
len(parsed.video_urls),
|
||||||
|
len(parsed.file_urls),
|
||||||
|
len(parsed.unknown_attachments),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def notify_delivery_failure(self, max_message: Any, error: Exception) -> None:
|
||||||
|
"""Best-effort аварийное уведомление, если основной форвардинг упал."""
|
||||||
|
try:
|
||||||
|
parsed = parse_message(max_message)
|
||||||
|
parsed = await self._enrich_from_max(max_message, parsed)
|
||||||
|
body = self._build_fallback_unknown_notice(parsed)
|
||||||
|
body = f"{body}\n\n[bridge-error] {type(error).__name__}: {error}"
|
||||||
|
except Exception:
|
||||||
|
body = f"[!] Сообщение из MAX не доставлено в Telegram из-за ошибки bridge: {type(error).__name__}: {error}"
|
||||||
|
|
||||||
|
fallback_chat_id = self._telegram.fallback_user_id
|
||||||
|
if not fallback_chat_id:
|
||||||
|
logger.error("Cannot send emergency notice: Telegram fallback user id is empty")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await self._telegram.send_text(chat_id=fallback_chat_id, text=body)
|
||||||
|
logger.warning("Sent emergency notice to Telegram fallback chat %s", fallback_chat_id)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Cannot send emergency notice to Telegram")
|
||||||
|
|
||||||
async def _send_with_migration_retry(
|
async def _send_with_migration_retry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -293,10 +354,18 @@ class MaxToTelegramBridge:
|
|||||||
parsed.video_urls.append(str(video_url))
|
parsed.video_urls.append(str(video_url))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Cannot resolve video URL from Max")
|
logger.exception("Cannot resolve video URL from Max")
|
||||||
|
else:
|
||||||
|
urls = self._extract_any_urls(attach)
|
||||||
|
if urls:
|
||||||
|
parsed.file_urls.extend(urls)
|
||||||
|
else:
|
||||||
|
parsed.unknown_attachments.append(type(attach).__name__)
|
||||||
|
|
||||||
# Убираем дубли URL, если парсер и enrich нашли одинаковые вложения.
|
# Убираем дубли URL, если парсер и enrich нашли одинаковые вложения.
|
||||||
parsed.image_urls = list(dict.fromkeys(parsed.image_urls))
|
parsed.image_urls = list(dict.fromkeys(parsed.image_urls))
|
||||||
parsed.video_urls = list(dict.fromkeys(parsed.video_urls))
|
parsed.video_urls = list(dict.fromkeys(parsed.video_urls))
|
||||||
|
parsed.file_urls = list(dict.fromkeys(parsed.file_urls))
|
||||||
|
parsed.unknown_attachments = list(dict.fromkeys(parsed.unknown_attachments))
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
def _is_self_message(self, max_message: Any) -> bool:
|
def _is_self_message(self, max_message: Any) -> bool:
|
||||||
@@ -345,3 +414,52 @@ class MaxToTelegramBridge:
|
|||||||
|
|
||||||
walk(attach)
|
walk(attach)
|
||||||
return list(dict.fromkeys(urls))
|
return list(dict.fromkeys(urls))
|
||||||
|
|
||||||
|
def _extract_any_urls(self, node: Any) -> list[str]:
|
||||||
|
urls: list[str] = []
|
||||||
|
seen_ids: set[int] = set()
|
||||||
|
|
||||||
|
def walk(value: Any) -> None:
|
||||||
|
if value is None:
|
||||||
|
return
|
||||||
|
obj_id = id(value)
|
||||||
|
if obj_id in seen_ids:
|
||||||
|
return
|
||||||
|
seen_ids.add(obj_id)
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
if value.startswith("http://") or value.startswith("https://"):
|
||||||
|
urls.append(value)
|
||||||
|
return
|
||||||
|
if isinstance(value, (list, tuple, set)):
|
||||||
|
for item in value:
|
||||||
|
walk(item)
|
||||||
|
return
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for nested in value.values():
|
||||||
|
walk(nested)
|
||||||
|
return
|
||||||
|
if hasattr(value, "__dict__"):
|
||||||
|
walk(vars(value))
|
||||||
|
|
||||||
|
walk(node)
|
||||||
|
return list(dict.fromkeys(urls))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _append_unknown_attachment_notice(*, parsed: ParsedMessage, text: str) -> str:
|
||||||
|
if not parsed.unknown_attachments:
|
||||||
|
return text
|
||||||
|
unknown_preview = ", ".join(parsed.unknown_attachments[:5])
|
||||||
|
suffix = f"\n\n[!] Неизвестный тип вложения из MAX: {unknown_preview}"
|
||||||
|
return f"{text}{suffix}" if text else suffix.strip()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_fallback_unknown_notice(parsed: ParsedMessage) -> str:
|
||||||
|
base = MaxToTelegramBridge._format_caption(
|
||||||
|
sender_name=parsed.sender_name,
|
||||||
|
chat_name=parsed.chat_name,
|
||||||
|
text=parsed.text,
|
||||||
|
include_chat_name=True,
|
||||||
|
)
|
||||||
|
unknown = ", ".join(parsed.unknown_attachments[:5]) if parsed.unknown_attachments else "unknown"
|
||||||
|
return f"{base}\n\n[!] Неизвестный или пустой тип сообщения из MAX (attachments={unknown})."
|
||||||
|
|||||||
+2
-1
@@ -72,8 +72,9 @@ def main() -> None:
|
|||||||
health.mark_max_event()
|
health.mark_max_event()
|
||||||
try:
|
try:
|
||||||
await bridge.forward_message(message)
|
await bridge.forward_message(message)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
logger.exception("Failed to forward Max message")
|
logger.exception("Failed to forward Max message")
|
||||||
|
await bridge.notify_delivery_failure(message, exc)
|
||||||
|
|
||||||
asyncio.run(max_client.start())
|
asyncio.run(max_client.start())
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -36,9 +36,11 @@ def _is_video(media_type: str) -> bool:
|
|||||||
return "video" in value or value in {"mp4", "mov", "mkv", "avi"}
|
return "video" in value or value in {"mp4", "mov", "mkv", "avi"}
|
||||||
|
|
||||||
|
|
||||||
def _extract_media_urls(message: Any) -> tuple[list[str], list[str]]:
|
def _extract_media_urls(message: Any) -> tuple[list[str], list[str], list[str], list[str]]:
|
||||||
image_urls: list[str] = []
|
image_urls: list[str] = []
|
||||||
video_urls: list[str] = []
|
video_urls: list[str] = []
|
||||||
|
file_urls: list[str] = []
|
||||||
|
unknown_attachments: list[str] = []
|
||||||
|
|
||||||
# В PyMax рабочее поле для вложений обычно называется attaches.
|
# В PyMax рабочее поле для вложений обычно называется attaches.
|
||||||
raw_attachments = _get_attr(message, ["attaches", "attachments", "media", "files"], default=[]) or []
|
raw_attachments = _get_attr(message, ["attaches", "attachments", "media", "files"], default=[]) or []
|
||||||
@@ -67,14 +69,18 @@ def _extract_media_urls(message: Any) -> tuple[list[str], list[str]]:
|
|||||||
media_type = _stringify(nested_data.get("type") or nested_data.get("media_type"))
|
media_type = _stringify(nested_data.get("type") or nested_data.get("media_type"))
|
||||||
|
|
||||||
if not url:
|
if not url:
|
||||||
|
kind = media_type or _stringify(type(item).__name__) or "unknown"
|
||||||
|
unknown_attachments.append(kind)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if _is_image(media_type):
|
if _is_image(media_type):
|
||||||
image_urls.append(url)
|
image_urls.append(url)
|
||||||
elif _is_video(media_type):
|
elif _is_video(media_type):
|
||||||
video_urls.append(url)
|
video_urls.append(url)
|
||||||
|
else:
|
||||||
|
file_urls.append(url)
|
||||||
|
|
||||||
return image_urls, video_urls
|
return image_urls, video_urls, file_urls, unknown_attachments
|
||||||
|
|
||||||
|
|
||||||
def _extract_max_reply(message: Any) -> tuple[str | None, str | None]:
|
def _extract_max_reply(message: Any) -> tuple[str | None, str | None]:
|
||||||
@@ -115,7 +121,7 @@ def parse_message(message: Any) -> ParsedMessage:
|
|||||||
chat_id = _stringify(_get_attr(message, ["chat_id", "dialog_id", "peer_id"])) or "unknown-chat"
|
chat_id = _stringify(_get_attr(message, ["chat_id", "dialog_id", "peer_id"])) or "unknown-chat"
|
||||||
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, file_urls, unknown_attachments = _extract_media_urls(message)
|
||||||
reply_mid, reply_preview = _extract_max_reply(message)
|
reply_mid, reply_preview = _extract_max_reply(message)
|
||||||
return ParsedMessage(
|
return ParsedMessage(
|
||||||
message_id=message_id,
|
message_id=message_id,
|
||||||
@@ -125,6 +131,8 @@ 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,
|
||||||
|
file_urls=file_urls,
|
||||||
|
unknown_attachments=unknown_attachments,
|
||||||
reply_to_max_message_id=reply_mid,
|
reply_to_max_message_id=reply_mid,
|
||||||
reply_preview_text=reply_preview,
|
reply_preview_text=reply_preview,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ 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)
|
||||||
|
file_urls: list[str] = field(default_factory=list)
|
||||||
|
unknown_attachments: list[str] = field(default_factory=list)
|
||||||
# Ответ в MAX: Message.link указывает на исходное сообщение (тред).
|
# Ответ в MAX: Message.link указывает на исходное сообщение (тред).
|
||||||
reply_to_max_message_id: str | None = None
|
reply_to_max_message_id: str | None = None
|
||||||
reply_preview_text: str | None = None
|
reply_preview_text: str | None = None
|
||||||
|
|||||||
+38
-4
@@ -64,6 +64,11 @@ def _is_supported_telegram_message(message: dict[str, Any]) -> bool:
|
|||||||
if has_video:
|
if has_video:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
for key in ("document", "audio", "voice", "animation", "sticker", "video_note"):
|
||||||
|
value = message.get(key)
|
||||||
|
if isinstance(value, dict) and value.get("file_id"):
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -314,8 +319,13 @@ class TelegramToMaxBridge:
|
|||||||
reply_to = self._resolve_reply_to_max_id(max_chat_id=max_chat_id, message=messages[0])
|
reply_to = self._resolve_reply_to_max_id(max_chat_id=max_chat_id, message=messages[0])
|
||||||
|
|
||||||
attachments: list[Any] = []
|
attachments: list[Any] = []
|
||||||
|
extra_links: list[str] = []
|
||||||
for m in messages:
|
for m in messages:
|
||||||
attachments.extend(await self._extract_attachments(m))
|
extracted, links = await self._extract_attachments(m)
|
||||||
|
attachments.extend(extracted)
|
||||||
|
extra_links.extend(links)
|
||||||
|
|
||||||
|
text = self._append_file_links(text=text, links=extra_links)
|
||||||
|
|
||||||
if not text.strip() and not attachments:
|
if not text.strip() and not attachments:
|
||||||
return
|
return
|
||||||
@@ -375,7 +385,8 @@ class TelegramToMaxBridge:
|
|||||||
) -> None:
|
) -> None:
|
||||||
raw_text = str(message.get("text") or message.get("caption") or "").strip()
|
raw_text = str(message.get("text") or message.get("caption") or "").strip()
|
||||||
text = _format_forward_text(sender=message.get("from"), text=raw_text)
|
text = _format_forward_text(sender=message.get("from"), text=raw_text)
|
||||||
attachments = await self._extract_attachments(message)
|
attachments, extra_links = await self._extract_attachments(message)
|
||||||
|
text = self._append_file_links(text=text, links=extra_links)
|
||||||
if not text.strip() and not attachments:
|
if not text.strip() and not attachments:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -430,8 +441,9 @@ class TelegramToMaxBridge:
|
|||||||
# reply_to в MAX — это id сообщения; если не нашли, просто отправляем без reply
|
# reply_to в MAX — это id сообщения; если не нашли, просто отправляем без reply
|
||||||
return mapped
|
return mapped
|
||||||
|
|
||||||
async def _extract_attachments(self, message: dict[str, Any]) -> list[Any]:
|
async def _extract_attachments(self, message: dict[str, Any]) -> tuple[list[Any], list[str]]:
|
||||||
attachments: list[Any] = []
|
attachments: list[Any] = []
|
||||||
|
file_links: list[str] = []
|
||||||
|
|
||||||
# photo: массив размеров, берём последний (самый большой)
|
# photo: массив размеров, берём последний (самый большой)
|
||||||
photos = message.get("photo")
|
photos = message.get("photo")
|
||||||
@@ -459,7 +471,29 @@ class TelegramToMaxBridge:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Cannot fetch Telegram video URL")
|
logger.exception("Cannot fetch Telegram video URL")
|
||||||
|
|
||||||
return attachments
|
for key in ("document", "audio", "voice", "animation", "sticker", "video_note"):
|
||||||
|
value = message.get(key)
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
continue
|
||||||
|
file_id = str(value.get("file_id") or "")
|
||||||
|
if not file_id:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
url = await self._telegram.get_file_url(file_id)
|
||||||
|
file_links.append(url)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Cannot fetch Telegram %s URL", key)
|
||||||
|
|
||||||
|
return attachments, file_links
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _append_file_links(*, text: str, links: list[str]) -> str:
|
||||||
|
uniq_links = list(dict.fromkeys([str(link).strip() for link in links if str(link).strip()]))
|
||||||
|
if not uniq_links:
|
||||||
|
return text
|
||||||
|
links_block = "\n".join(f"- {url}" for url in uniq_links)
|
||||||
|
suffix = f"\n\n[Telegram files]\n{links_block}"
|
||||||
|
return f"{text}{suffix}" if text else suffix.strip()
|
||||||
|
|
||||||
def _refresh_max_chat_cache(self) -> None:
|
def _refresh_max_chat_cache(self) -> None:
|
||||||
title_to_id: dict[str, int] = {}
|
title_to_id: dict[str, int] = {}
|
||||||
|
|||||||
@@ -103,6 +103,24 @@ class TelegramClient:
|
|||||||
payload["reply_to_message_id"] = reply_to_message_id
|
payload["reply_to_message_id"] = reply_to_message_id
|
||||||
return await self._request("sendVideo", payload)
|
return await self._request("sendVideo", payload)
|
||||||
|
|
||||||
|
async def send_document(
|
||||||
|
self,
|
||||||
|
chat_id: str,
|
||||||
|
document_url: str,
|
||||||
|
caption: str | None = None,
|
||||||
|
*,
|
||||||
|
reply_to_message_id: int | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload: dict[str, Any] = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"document": document_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("sendDocument", payload)
|
||||||
|
|
||||||
async def send_media_group(
|
async def send_media_group(
|
||||||
self,
|
self,
|
||||||
chat_id: str,
|
chat_id: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user