Files
max2telegram/app/max_layer/formatter.py
T
2026-06-12 01:31:01 +03:00

297 lines
7.5 KiB
Python

from typing import Any
import structlog
from pymax.types.domain.message import Message as MaxMessage
from app.models.domain import MaxChatMeta, MaxIncomingMessage
logger = structlog.get_logger(__name__)
def get_forward_link(message: MaxMessage) -> dict[str, Any] | None:
link = getattr(message, "link", None)
if not isinstance(link, dict):
return None
if str(link.get("type", "")).upper() != "FORWARD":
return None
return link
def get_reply_target(message: MaxMessage) -> int | None:
link = getattr(message, "link", None)
if not isinstance(link, dict):
return None
if str(link.get("type", "")).upper() != "REPLY":
return None
target = link.get("messageId") or link.get("message_id")
if target is None:
return None
try:
return int(target)
except (TypeError, ValueError):
return None
def extract_forwarded_content(
message: MaxMessage,
) -> tuple[str, list[dict[str, Any]]] | None:
link = get_forward_link(message)
if link is None:
return None
nested = link.get("message")
if not isinstance(nested, dict):
logger.warning(
"max_forward_missing_nested_message",
chat_id=message.chat_id,
message_id=message.id,
)
return None
text = nested.get("text") or ""
attaches = nested.get("attaches") or []
if not isinstance(attaches, list):
attaches = []
logger.info(
"max_forward_extracted",
chat_id=message.chat_id,
message_id=message.id,
nested_message_id=nested.get("id"),
nested_sender=nested.get("sender"),
text_len=len(text),
attach_count=len(attaches),
)
return text, attaches
def format_forwarded_text(
original_text: str,
original_sender_name: str | None = None,
) -> str:
header = "↪️ Переслано"
if original_sender_name:
header += f" от {original_sender_name}"
if original_text:
return f"{header}:\n{original_text}"
return header
def _attach_type(attach: dict[str, Any]) -> str:
return str(attach.get("_type") or attach.get("type") or "").upper()
async def resolve_raw_attaches(
client,
chat_id: int,
message_id: int,
attaches: list[dict[str, Any]],
) -> list[dict]:
items: list[dict] = []
for attach in attaches:
if not isinstance(attach, dict):
continue
attach_type = _attach_type(attach)
if attach_type == "PHOTO":
url = attach.get("baseUrl") or attach.get("base_url")
if url:
items.append(
{
"kind": "photo",
"url": url,
"max_chat_id": chat_id,
"max_message_id": message_id,
}
)
continue
if attach_type == "VIDEO":
video_id = attach.get("videoId") or attach.get("video_id")
if video_id is not None:
items.append(
{
"kind": "video",
"max_chat_id": chat_id,
"max_message_id": message_id,
"max_video_id": video_id,
}
)
continue
if attach_type == "FILE":
file_id = attach.get("fileId") or attach.get("file_id")
if file_id is not None:
items.append(
{
"kind": "document",
"file_name": attach.get("name"),
"max_chat_id": chat_id,
"max_message_id": message_id,
"max_file_id": file_id,
}
)
continue
url = attach.get("baseUrl") or attach.get("base_url") or attach.get("url")
if url:
items.append(
{
"kind": "document",
"url": url,
"max_chat_id": chat_id,
"max_message_id": message_id,
}
)
logger.debug(
"max_raw_attaches_resolved",
chat_id=chat_id,
message_id=message_id,
input_count=len(attaches),
resolved_count=len(items),
)
return items
async def resolve_media(client, message: MaxMessage) -> list[dict]:
from pymax.types.domain.attachments.file import FileAttachment
from pymax.types.domain.attachments.photo import PhotoAttachment
from pymax.types.domain.attachments.video import VideoAttachment
items: list[dict] = []
chat_id = message.chat_id
if chat_id is None:
return items
for attach in message.attaches:
if isinstance(attach, dict):
items.extend(
await resolve_raw_attaches(client, chat_id, message.id, [attach])
)
continue
if isinstance(attach, PhotoAttachment):
items.append(
{
"kind": "photo",
"url": attach.base_url,
"max_chat_id": chat_id,
"max_message_id": message.id,
}
)
elif isinstance(attach, VideoAttachment):
items.append(
{
"kind": "video",
"max_chat_id": chat_id,
"max_message_id": message.id,
"max_video_id": attach.video_id,
}
)
elif isinstance(attach, FileAttachment):
items.append(
{
"kind": "document",
"file_name": attach.name,
"max_chat_id": chat_id,
"max_message_id": message.id,
"max_file_id": attach.file_id,
}
)
else:
url = getattr(attach, "base_url", None) or getattr(attach, "url", None)
if url:
items.append(
{
"kind": "document",
"url": url,
"max_chat_id": chat_id,
"max_message_id": message.id,
}
)
return items
def _pick_image_url(*values: str | None) -> str | None:
for value in values:
if value and value.strip():
return value.strip()
return None
def _chat_icon_url(chat) -> str | None:
return _pick_image_url(
getattr(chat, "base_icon_url", None),
getattr(chat, "base_raw_icon_url", None),
)
def _user_icon_url(user) -> str | None:
return _pick_image_url(
getattr(user, "base_url", None),
getattr(user, "base_raw_url", None),
)
async def resolve_chat_icon_url(
client,
chat,
my_user_id: int | None,
) -> str | None:
is_dm = bool(getattr(chat, "is_dialog", False) or chat.type == "DIALOG")
if is_dm:
for user_id in chat.participants or {}:
if my_user_id and user_id == my_user_id:
continue
try:
user = await client.get_user(user_id)
icon_url = _user_icon_url(user)
if icon_url:
return icon_url
except Exception:
logger.debug(
"max_dm_icon_lookup_failed",
chat_id=chat.id,
user_id=user_id,
)
return None
return _chat_icon_url(chat)
def extract_chat_meta(chat, ls_prefix: str) -> MaxChatMeta:
is_dm = bool(getattr(chat, "is_dialog", False) or chat.type == "DIALOG")
chat_name = chat.title or ("Контакт" if is_dm else f"Чат {chat.id}")
topic_title = chat_name
if is_dm and not topic_title.startswith(ls_prefix.strip()):
topic_title = f"{ls_prefix}{topic_title}"
return MaxChatMeta(
topic_title=topic_title,
chat_name=chat_name,
is_dm=is_dm,
icon_url=_chat_icon_url(chat),
participants_count=getattr(chat, "participants_count", 0) or 0,
link=getattr(chat, "link", None),
)
def build_chat_title(chat, ls_prefix: str) -> tuple[str, bool]:
meta = extract_chat_meta(chat, ls_prefix)
return meta.topic_title, meta.is_dm
def resolve_sender_name(user) -> str:
if user is None:
return "Неизвестный"
if user.names:
name = user.names[0]
parts = [name.first_name, name.last_name]
return " ".join(p for p in parts if p) or f"User {user.id}"
return f"User {user.id}"
def format_max_text(message: MaxIncomingMessage) -> str:
if message.is_dm:
return message.text
sender = message.sender_name or "Неизвестный"
return f"{sender}:\n{message.text}"