Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d32bf4807 | ||
|
|
d019e884b6 | ||
|
|
e40297787e |
+165
-27
@@ -5,7 +5,7 @@ from typing import Any
|
||||
from max_parser import parse_message
|
||||
from models import ParsedMessage
|
||||
from pymax import MaxClient
|
||||
from pymax.types import PhotoAttach, VideoAttach
|
||||
from pymax.types import AudioAttach, FileAttach, Message, PhotoAttach, StickerAttach, VideoAttach
|
||||
from storage import BridgeStorage
|
||||
from telegram_api import TelegramApiError, TelegramClient
|
||||
|
||||
@@ -19,9 +19,9 @@ class MaxToTelegramBridge:
|
||||
self._storage = storage
|
||||
|
||||
async def forward_message(self, max_message: Any) -> None:
|
||||
if self._is_self_message(max_message):
|
||||
logger.debug("Skip self message %s/%s", getattr(max_message, "chat_id", "?"), getattr(max_message, "id", "?"))
|
||||
return
|
||||
#if self._is_self_message(max_message):
|
||||
# logger.debug("Skip self message %s/%s", getattr(max_message, "chat_id", "?"), getattr(max_message, "id", "?"))
|
||||
# return
|
||||
|
||||
parsed = parse_message(max_message)
|
||||
parsed = await self._enrich_from_max(max_message, parsed)
|
||||
@@ -110,7 +110,8 @@ class MaxToTelegramBridge:
|
||||
)
|
||||
return
|
||||
|
||||
if parsed.text.strip() and total_media == 0:
|
||||
should_send_plain_text = total_media == 0 and not parsed.file_urls and bool(text.strip())
|
||||
if should_send_plain_text:
|
||||
target_chat_id, sent = await self._send_with_migration_retry(
|
||||
target_chat_id=target_chat_id,
|
||||
max_chat_title_norm=normalized,
|
||||
@@ -200,12 +201,13 @@ class MaxToTelegramBridge:
|
||||
|
||||
if not sent_any:
|
||||
# Последняя страховка: гарантируем уведомление в Telegram даже для пустых/неизвестных payload.
|
||||
fallback_text = text.strip() or self._build_fallback_unknown_notice(parsed)
|
||||
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
|
||||
chat_id, fallback_text, reply_to_message_id=reply_telegram_mid
|
||||
),
|
||||
)
|
||||
mid = sent.get("result", {}).get("message_id") if isinstance(sent.get("result"), dict) else None
|
||||
@@ -338,28 +340,24 @@ class MaxToTelegramBridge:
|
||||
except Exception:
|
||||
logger.debug("Cannot resolve chat title", exc_info=True)
|
||||
|
||||
attaches = getattr(max_message, "attaches", None) or []
|
||||
for attach in attaches:
|
||||
if isinstance(attach, PhotoAttach):
|
||||
parsed.image_urls.extend(self._extract_photo_urls(attach))
|
||||
elif isinstance(attach, VideoAttach):
|
||||
try:
|
||||
video = await self._max_client.get_video_by_id(
|
||||
chat_id=max_message.chat_id,
|
||||
message_id=max_message.id,
|
||||
video_id=attach.video_id,
|
||||
await self._collect_message_attachments(
|
||||
message=max_message,
|
||||
parsed=parsed,
|
||||
source_tag="root",
|
||||
)
|
||||
|
||||
link = getattr(max_message, "link", None)
|
||||
linked_message = getattr(link, "message", None)
|
||||
if linked_message is not None:
|
||||
if not (parsed.text or "").strip():
|
||||
linked_text = str(getattr(linked_message, "text", "") or "").strip()
|
||||
if linked_text:
|
||||
parsed.text = linked_text
|
||||
await self._collect_message_attachments(
|
||||
message=linked_message,
|
||||
parsed=parsed,
|
||||
source_tag="forward",
|
||||
)
|
||||
video_url = getattr(video, "url", None)
|
||||
if video_url:
|
||||
parsed.video_urls.append(str(video_url))
|
||||
except Exception:
|
||||
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 нашли одинаковые вложения.
|
||||
parsed.image_urls = list(dict.fromkeys(parsed.image_urls))
|
||||
@@ -368,6 +366,127 @@ class MaxToTelegramBridge:
|
||||
parsed.unknown_attachments = list(dict.fromkeys(parsed.unknown_attachments))
|
||||
return parsed
|
||||
|
||||
async def _collect_message_attachments(
|
||||
self,
|
||||
*,
|
||||
message: Any,
|
||||
parsed: ParsedMessage,
|
||||
source_tag: str,
|
||||
) -> None:
|
||||
attaches = getattr(message, "attaches", None) or []
|
||||
message_chat_id = getattr(message, "chat_id", None)
|
||||
message_id = getattr(message, "id", None)
|
||||
if message_chat_id is None:
|
||||
message_chat_id = parsed.chat_id
|
||||
|
||||
for attach in attaches:
|
||||
if isinstance(attach, PhotoAttach):
|
||||
parsed.image_urls.extend(self._extract_photo_urls(attach))
|
||||
continue
|
||||
|
||||
if isinstance(attach, VideoAttach):
|
||||
try:
|
||||
video = await self._max_client.get_video_by_id(
|
||||
chat_id=message_chat_id,
|
||||
message_id=message_id,
|
||||
video_id=attach.video_id,
|
||||
)
|
||||
video_url = getattr(video, "url", None)
|
||||
if video_url:
|
||||
parsed.video_urls.append(str(video_url))
|
||||
except Exception:
|
||||
logger.exception("Cannot resolve video URL from Max (%s)", source_tag)
|
||||
continue
|
||||
|
||||
if isinstance(attach, FileAttach):
|
||||
resolved = await self._resolve_file_attach_url(
|
||||
message_chat_id=message_chat_id,
|
||||
message_id=message_id,
|
||||
attach=attach,
|
||||
)
|
||||
if resolved:
|
||||
parsed.file_urls.append(resolved)
|
||||
else:
|
||||
fallback = str(getattr(attach, "name", "") or "").strip()
|
||||
if fallback:
|
||||
parsed.text = self._append_missing_file_note(parsed.text, fallback)
|
||||
else:
|
||||
parsed.unknown_attachments.append(type(attach).__name__)
|
||||
continue
|
||||
|
||||
if isinstance(attach, AudioAttach):
|
||||
audio_url = str(getattr(attach, "url", "") or "").strip()
|
||||
if audio_url:
|
||||
parsed.file_urls.append(audio_url)
|
||||
else:
|
||||
parsed.unknown_attachments.append(type(attach).__name__)
|
||||
continue
|
||||
|
||||
if isinstance(attach, StickerAttach):
|
||||
sticker_url = str(getattr(attach, "url", "") or "").strip()
|
||||
if sticker_url:
|
||||
parsed.image_urls.append(sticker_url)
|
||||
else:
|
||||
parsed.unknown_attachments.append(type(attach).__name__)
|
||||
continue
|
||||
|
||||
# Fallback на случай сырого Attach/нестандартного типа:
|
||||
if await self._resolve_generic_file_attach(
|
||||
message_chat_id=message_chat_id,
|
||||
message_id=message_id,
|
||||
attach=attach,
|
||||
parsed=parsed,
|
||||
):
|
||||
continue
|
||||
|
||||
urls = self._extract_any_urls(attach)
|
||||
if urls:
|
||||
parsed.file_urls.extend(urls)
|
||||
continue
|
||||
|
||||
parsed.unknown_attachments.append(type(attach).__name__)
|
||||
|
||||
async def _resolve_file_attach_url(self, *, message_chat_id: Any, message_id: Any, attach: FileAttach) -> str | None:
|
||||
file_id = getattr(attach, "file_id", None)
|
||||
if file_id is None or message_id is None:
|
||||
return None
|
||||
try:
|
||||
file_info = await self._max_client.get_file_by_id(
|
||||
chat_id=message_chat_id,
|
||||
message_id=message_id,
|
||||
file_id=file_id,
|
||||
)
|
||||
url = getattr(file_info, "url", None)
|
||||
return str(url) if url else None
|
||||
except Exception:
|
||||
logger.exception("Cannot resolve file URL from Max (file_id=%s)", file_id)
|
||||
return None
|
||||
|
||||
async def _resolve_generic_file_attach(
|
||||
self,
|
||||
*,
|
||||
message_chat_id: Any,
|
||||
message_id: Any,
|
||||
attach: Any,
|
||||
parsed: ParsedMessage,
|
||||
) -> bool:
|
||||
file_id = getattr(attach, "file_id", None)
|
||||
if file_id is None or message_id is None:
|
||||
return False
|
||||
try:
|
||||
file_info = await self._max_client.get_file_by_id(
|
||||
chat_id=message_chat_id,
|
||||
message_id=message_id,
|
||||
file_id=file_id,
|
||||
)
|
||||
url = getattr(file_info, "url", None)
|
||||
if url:
|
||||
parsed.file_urls.append(str(url))
|
||||
return True
|
||||
except Exception:
|
||||
logger.debug("Cannot resolve generic file attach from Max", exc_info=True)
|
||||
return False
|
||||
|
||||
def _is_self_message(self, max_message: Any) -> bool:
|
||||
sender = getattr(max_message, "sender", None)
|
||||
me = getattr(self._max_client, "me", None)
|
||||
@@ -445,6 +564,25 @@ class MaxToTelegramBridge:
|
||||
walk(node)
|
||||
return list(dict.fromkeys(urls))
|
||||
|
||||
@staticmethod
|
||||
def _is_forward_attach_like(attach: Any) -> bool:
|
||||
name = type(attach).__name__.lower()
|
||||
if "forward" in name or "share" in name or "quote" in name:
|
||||
return True
|
||||
if hasattr(attach, "__dict__"):
|
||||
keys = {str(k).lower() for k in vars(attach).keys()}
|
||||
if {"forward", "forwarded", "forwards", "link", "message", "messages", "origin", "payload"} & keys:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _append_missing_file_note(current_text: str, file_name: str) -> str:
|
||||
text = (current_text or "").strip()
|
||||
note = f"[MAX forwarded file without direct URL] {file_name}"
|
||||
if not text:
|
||||
return note
|
||||
return f"{text}\n{note}"
|
||||
|
||||
@staticmethod
|
||||
def _append_unknown_attachment_notice(*, parsed: ParsedMessage, text: str) -> str:
|
||||
if not parsed.unknown_attachments:
|
||||
|
||||
@@ -36,6 +36,64 @@ def _is_video(media_type: str) -> bool:
|
||||
return "video" in value or value in {"mp4", "mov", "mkv", "avi"}
|
||||
|
||||
|
||||
def _is_forward_like(data: dict[str, Any]) -> bool:
|
||||
media_type = _stringify(data.get("type") or data.get("media_type") or data.get("kind")).lower()
|
||||
if "forward" in media_type or "share" in media_type or "quote" in media_type:
|
||||
return True
|
||||
forward_keys = {
|
||||
"forward",
|
||||
"forwarded",
|
||||
"forwards",
|
||||
"link",
|
||||
"message",
|
||||
"messages",
|
||||
"payload",
|
||||
"quote",
|
||||
"origin",
|
||||
}
|
||||
return any(key in data for key in forward_keys)
|
||||
|
||||
|
||||
def _classify_url(url: str, media_type: str) -> str:
|
||||
lowered = url.lower()
|
||||
if _is_image(media_type) or lowered.endswith((".jpg", ".jpeg", ".png", ".webp", ".gif")):
|
||||
return "image"
|
||||
if _is_video(media_type) or lowered.endswith((".mp4", ".mov", ".mkv", ".avi", ".webm")):
|
||||
return "video"
|
||||
return "file"
|
||||
|
||||
|
||||
def _collect_urls(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.strip())
|
||||
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))
|
||||
|
||||
|
||||
def _extract_media_urls(message: Any) -> tuple[list[str], list[str], list[str], list[str]]:
|
||||
image_urls: list[str] = []
|
||||
video_urls: list[str] = []
|
||||
@@ -47,6 +105,7 @@ def _extract_media_urls(message: Any) -> tuple[list[str], list[str], list[str],
|
||||
for item in raw_attachments:
|
||||
data = _as_dict(item)
|
||||
media_type = _stringify(data.get("type") or data.get("media_type") or data.get("kind"))
|
||||
is_forward_like = _is_forward_like(data)
|
||||
url = _stringify(
|
||||
data.get("base_url")
|
||||
or data.get("url")
|
||||
@@ -69,6 +128,23 @@ def _extract_media_urls(message: Any) -> tuple[list[str], list[str], list[str],
|
||||
media_type = _stringify(nested_data.get("type") or nested_data.get("media_type"))
|
||||
|
||||
if not url:
|
||||
nested_urls = _collect_urls(item)
|
||||
for nested_url in nested_urls:
|
||||
kind = _classify_url(nested_url, media_type)
|
||||
if kind == "image":
|
||||
image_urls.append(nested_url)
|
||||
elif kind == "video":
|
||||
video_urls.append(nested_url)
|
||||
else:
|
||||
file_urls.append(nested_url)
|
||||
if nested_urls:
|
||||
continue
|
||||
|
||||
if not url:
|
||||
if is_forward_like:
|
||||
# Forward-пакет может не содержать прямого URL в верхнем уровне;
|
||||
# текст/медиа достанем рекурсивно в других этапах.
|
||||
continue
|
||||
kind = media_type or _stringify(type(item).__name__) or "unknown"
|
||||
unknown_attachments.append(kind)
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user