新增 Telegram 渠道扩展,支持在 Yuxi 平台中集成 Telegram 即时通讯渠道。 包含以下功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - polling: 长轮询模式 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - actions: 动作处理 - inline_keyboard: 内联键盘 - native_commands: 原生指令 - chat: 聊天管理 - delivery: 消息送达确认 - media: 媒体资源处理 - profile: 用户资料 - reactions: 表情反应 - sticker: 贴纸处理 - types: 类型定义
411 lines
15 KiB
Python
411 lines
15 KiB
Python
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,
|
|
}
|