from __future__ import annotations import logging from datetime import datetime, UTC logger = logging.getLogger(__name__) MESSAGE_TYPE_MAP = { "text": "TEXT", "photo": "IMAGE", "video": "FILE", "voice": "VOICE", "audio": "VOICE", "document": "FILE", "sticker": "IMAGE", "location": "TEXT", "contact": "TEXT", "poll": "EVENT", "callback_query": "EVENT", } def convert_update_to_unified(update: dict, account_id: str) -> dict | None: if "message" in update: return _convert_message(update["message"], update.get("update_id", 0), account_id) if "callback_query" in update: return _convert_callback_query(update["callback_query"], update.get("update_id", 0), account_id) if "edited_message" in update: return _convert_edited_message(update["edited_message"], update.get("update_id", 0), account_id) if "my_chat_member" in update: return _convert_my_chat_member(update["my_chat_member"], update.get("update_id", 0), account_id) if "poll" in update: return _convert_poll_update(update["poll"], update.get("update_id", 0), account_id) if "poll_answer" in update: return _convert_poll_answer(update["poll_answer"], update.get("update_id", 0), account_id) if "message_reaction" in update: return _convert_message_reaction(update["message_reaction"], update.get("update_id", 0), account_id) if "channel_post" in update: return _convert_message(update["channel_post"], update.get("update_id", 0), account_id) if "chat_member" in update: return _convert_chat_member(update["chat_member"], update.get("update_id", 0), account_id) if "chat_join_request" in update: return _convert_chat_join_request(update["chat_join_request"], update.get("update_id", 0), account_id) return None def convert_fake_message_to_unified(fake_msg: object, account_id: str) -> dict | None: from yuxi.channel.extensions.telegram.types import FakeUpdateMessage msg = fake_msg if isinstance(fake_msg, FakeUpdateMessage) else FakeUpdateMessage(**fake_msg) sender_kind = "DIRECT" if msg.chat_type == "private" else "GROUP" return { "msg_id": f"tg:{msg.msg_id}", "channel_type": "telegram", "account_id": account_id, "content": msg.text or msg.caption or "", "message_type": MESSAGE_TYPE_MAP.get(msg.media_type or "text", "TEXT"), "media_urls": [msg.file_id] if msg.file_id else [], "sender": { "id": str(msg.from_id), "username": msg.from_username, "display_name": msg.from_first_name + (f" {msg.from_last_name}" if msg.from_last_name else ""), "is_bot": msg.from_is_bot, "kind": sender_kind, }, "group": { "id": str(msg.chat_id), "title": msg.chat_title, "kind": msg.chat_type, } if msg.chat_type != "private" else None, "timestamp": datetime.fromtimestamp(msg.date, tz=UTC) if msg.date else None, "reply_to_id": str(msg.reply_to_message_id) if msg.reply_to_message_id else None, "thread_id": str(msg.message_thread_id) if msg.message_thread_id else None, "is_topic_message": msg.is_topic_message, "forwarded_from": { "id": str(msg.forward_from_id), "name": msg.forward_from_name, } if msg.forward_from_id else None, "raw_payload": {}, } def _convert_message(message: dict, update_id: int, account_id: str) -> dict | None: msg_id = f"tg:{message.get('message_id', update_id)}" chat = message.get("chat", {}) from_user = message.get("from", {}) chat_type = chat.get("type", "private") msg_type = _detect_message_type(message) text = message.get("text", "") or message.get("caption", "") media_urls: list[str] = [] file_id = None if "photo" in message: photos = message["photo"] if photos: file_id = photos[-1].get("file_id", "") media_urls.append(file_id) elif "video" in message: file_id = message["video"].get("file_id", "") media_urls.append(file_id) elif "document" in message: file_id = message["document"].get("file_id", "") media_urls.append(file_id) elif "voice" in message: file_id = message["voice"].get("file_id", "") media_urls.append(file_id) elif "audio" in message: file_id = message["audio"].get("file_id", "") media_urls.append(file_id) reply_to = message.get("reply_to_message", {}) reply_to_id = str(reply_to.get("message_id")) if reply_to else None is_topic = bool(message.get("is_topic_message", False)) thread_id = str(message.get("message_thread_id")) if message.get("message_thread_id") else None sender_kind = "GROUP" if chat_type in ("group", "supergroup") else "DIRECT" forward_origin = message.get("forward_origin", {}) forwarded_from = None if forward_origin: forwarded_from = { "type": forward_origin.get("type", ""), "sender_user_id": ( str(forward_origin.get("sender_user", {}).get("id", "")) if forward_origin.get("sender_user") else None ), "sender_user_name": forward_origin.get("sender_user_name", ""), "chat_id": str(forward_origin.get("chat", {}).get("id", "")) if forward_origin.get("chat") else None, "chat_title": forward_origin.get("chat", {}).get("title", "") if forward_origin.get("chat") else "", } return { "msg_id": msg_id, "channel_type": "telegram", "account_id": account_id, "content": text, "message_type": msg_type, "media_urls": media_urls, "sender": { "id": str(from_user.get("id", "")), "username": from_user.get("username", ""), "display_name": _build_display_name(from_user), "is_bot": from_user.get("is_bot", False), "kind": sender_kind, }, "group": { "id": str(chat.get("id", "")), "title": chat.get("title", chat.get("username", "")), "kind": chat_type, } if chat_type != "private" else None, "timestamp": datetime.fromtimestamp(message.get("date", 0), tz=UTC) if message.get("date") else None, "reply_to_id": reply_to_id, "thread_id": thread_id, "is_topic_message": is_topic, "forwarded_from": forwarded_from, "raw_payload": message, } def _convert_callback_query(callback: dict, update_id: int, account_id: str) -> dict | None: from_user = callback.get("from", {}) message = callback.get("message", {}) chat = message.get("chat", {}) data = callback.get("data", "") return { "msg_id": f"tg:cb:{callback.get('id', update_id)}", "channel_type": "telegram", "account_id": account_id, "content": data, "message_type": "EVENT", "media_urls": [], "sender": { "id": str(from_user.get("id", "")), "username": from_user.get("username", ""), "display_name": _build_display_name(from_user), "is_bot": from_user.get("is_bot", False), "kind": "DIRECT" if chat.get("type") == "private" else "GROUP", }, "group": { "id": str(chat.get("id", "")), "title": chat.get("title", ""), "kind": chat.get("type", "private"), } if chat.get("type") != "private" else None, "timestamp": None, "reply_to_id": str(message.get("message_id")) if message else None, "thread_id": str(message.get("message_thread_id")) if message.get("message_thread_id") else None, "raw_payload": callback, } def _detect_message_type(message: dict) -> str: if "text" in message: return "TEXT" if "photo" in message: return "IMAGE" if "video" in message: return "FILE" if "voice" in message or "audio" in message: return "VOICE" if "document" in message: return "FILE" if "sticker" in message: return "IMAGE" if "location" in message: return "TEXT" if "contact" in message: return "TEXT" if "poll" in message: return "EVENT" return "TEXT" def _build_display_name(from_user: dict) -> str: first = from_user.get("first_name", "") last = from_user.get("last_name", "") if first and last: return f"{first} {last}" if first: return first return from_user.get("username", str(from_user.get("id", ""))) def _convert_edited_message(message: dict, update_id: int, account_id: str) -> dict | None: unified = _convert_message(message, update_id, account_id) if unified: unified["message_type"] = "EDITED" return unified def _convert_my_chat_member(chat_member: dict, update_id: int, account_id: str) -> dict | None: chat = chat_member.get("chat", {}) new_status = chat_member.get("new_chat_member", {}).get("status", "") old_status = chat_member.get("old_chat_member", {}).get("status", "") from_user = chat_member.get("from", {}) return { "msg_id": f"tg:member:{update_id}", "channel_type": "telegram", "account_id": account_id, "content": f"bot_status_change: {old_status} → {new_status}", "message_type": "SYSTEM", "media_urls": [], "sender": { "id": str(from_user.get("id", "")), "username": from_user.get("username", ""), "display_name": _build_display_name(from_user), "is_bot": from_user.get("is_bot", False), "kind": "GROUP", }, "group": { "id": str(chat.get("id", "")), "title": chat.get("title", ""), "kind": chat.get("type", ""), } if chat.get("type") != "private" else None, "timestamp": ( datetime.fromtimestamp(chat_member.get("date", 0), tz=UTC) if chat_member.get("date") else None ), "reply_to_id": None, "thread_id": None, "raw_payload": chat_member, } def _convert_poll_update(poll: dict, update_id: int, account_id: str) -> dict | None: return { "msg_id": f"tg:poll:{poll.get('id', update_id)}", "channel_type": "telegram", "account_id": account_id, "content": poll.get("question", ""), "message_type": "EVENT", "media_urls": [], "sender": {"id": "", "username": "", "display_name": "", "is_bot": False, "kind": "GROUP"}, "group": None, "timestamp": ( datetime.fromtimestamp(poll.get("close_date", 0), tz=UTC) if poll.get("close_date") else None ), "reply_to_id": None, "thread_id": None, "raw_payload": poll, } def _convert_poll_answer(poll_answer: dict, update_id: int, account_id: str) -> dict | None: from_user = poll_answer.get("user", {}) return { "msg_id": f"tg:poll_answer:{poll_answer.get('poll_id', update_id)}", "channel_type": "telegram", "account_id": account_id, "content": "poll_answer", "message_type": "EVENT", "media_urls": [], "sender": { "id": str(from_user.get("id", "")), "username": from_user.get("username", ""), "display_name": _build_display_name(from_user), "is_bot": from_user.get("is_bot", False), "kind": "GROUP", }, "group": None, "timestamp": None, "reply_to_id": None, "thread_id": None, "raw_payload": poll_answer, } def _convert_message_reaction(reaction: dict, update_id: int, account_id: str) -> dict | None: chat = reaction.get("chat", {}) from_user = reaction.get("user", {}) return { "msg_id": f"tg:reaction:{reaction.get('message_id', update_id)}", "channel_type": "telegram", "account_id": account_id, "content": "message_reaction", "message_type": "EVENT", "media_urls": [], "sender": { "id": str(from_user.get("id", "")), "username": from_user.get("username", ""), "display_name": _build_display_name(from_user), "is_bot": from_user.get("is_bot", False), "kind": "GROUP" if chat.get("type") in ("group", "supergroup") else "DIRECT", }, "group": { "id": str(chat.get("id", "")), "title": chat.get("title", ""), "kind": chat.get("type", ""), } if chat.get("type") != "private" else None, "timestamp": ( datetime.fromtimestamp(reaction.get("date", 0), tz=UTC) if reaction.get("date") else None ), "reply_to_id": None, "thread_id": None, "raw_payload": reaction, } def _convert_chat_member(member_update: dict, update_id: int, account_id: str) -> dict | None: chat = member_update.get("chat", {}) from_user = member_update.get("from", {}) return { "msg_id": f"tg:cm:{update_id}", "channel_type": "telegram", "account_id": account_id, "content": "chat_member_update", "message_type": "SYSTEM", "media_urls": [], "sender": { "id": str(from_user.get("id", "")), "username": from_user.get("username", ""), "display_name": _build_display_name(from_user), "is_bot": from_user.get("is_bot", False), "kind": "GROUP", }, "group": { "id": str(chat.get("id", "")), "title": chat.get("title", ""), "kind": chat.get("type", ""), } if chat.get("type") != "private" else None, "timestamp": ( datetime.fromtimestamp(member_update.get("date", 0), tz=UTC) if member_update.get("date") else None ), "reply_to_id": None, "thread_id": None, "raw_payload": member_update, } def _convert_chat_join_request(join_request: dict, update_id: int, account_id: str) -> dict | None: chat = join_request.get("chat", {}) from_user = join_request.get("from", {}) return { "msg_id": f"tg:cjr:{update_id}", "channel_type": "telegram", "account_id": account_id, "content": "chat_join_request", "message_type": "SYSTEM", "media_urls": [], "sender": { "id": str(from_user.get("id", "")), "username": from_user.get("username", ""), "display_name": _build_display_name(from_user), "is_bot": from_user.get("is_bot", False), "kind": "GROUP", }, "group": { "id": str(chat.get("id", "")), "title": chat.get("title", ""), "kind": chat.get("type", ""), } if chat.get("type") != "private" else None, "timestamp": ( datetime.fromtimestamp(join_request.get("date", 0), tz=UTC) if join_request.get("date") else None ), "reply_to_id": None, "thread_id": None, "raw_payload": join_request, }