ForcePilot/backend/package/yuxi/channel/extensions/zalouser/monitor.py
Kris d5e36d33b7 feat(channel): 添加 Zalo OA、Zoom Chat 和 Zulip 渠道扩展
新增 Zalo OA、Zoom Chat、Zulip 三个渠道扩展。

Zalo OA 渠道扩展主要模块:sidecar_client, config, gateway, outbound, streaming, pairing, security, auth, dedupe, directory, monitor, status, session, reactions, tools

Zoom Chat 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, crypto, dedupe, actions, media, mentions, monitor, status, session, reactions, threading

Zulip 渠道扩展主要模块:client, config, gateway, outbound, streaming, pairing, security, monitor, status
2026-05-21 12:06:26 +08:00

209 lines
6.9 KiB
Python

from __future__ import annotations
import logging
from datetime import datetime
from yuxi.channel.extensions.zalouser.dedupe import DedupeCache
from yuxi.channel.extensions.zalouser.format import strip_own_mentions
from yuxi.channel.extensions.zalouser.types import ZaloUserInboundMessage
from yuxi.channel.message.models import (
GroupContext,
MessageType,
PeerInfo,
UnifiedMessage,
)
from yuxi.channel.routing.models import PeerKind
logger = logging.getLogger(__name__)
class ZaloUserMonitor:
def __init__(self):
self._dedupe = DedupeCache(maxsize=10000, ttl_ms=600_000)
self._bot_name: str = ""
def set_bot_name(self, name: str) -> None:
self._bot_name = name
def parse_event(self, raw: dict, account_id: str) -> UnifiedMessage | None:
event_type = raw.get("event_type", "message")
if event_type == "reaction":
return self._parse_reaction_event(raw, account_id)
if event_type == "undo":
return self._parse_undo_event(raw, account_id)
if event_type == "group_event":
return self._parse_group_event(raw, account_id)
return self._parse_message_event(raw, account_id)
def _parse_message_event(self, raw: dict, account_id: str) -> UnifiedMessage | None:
msg = ZaloUserInboundMessage.from_sidecar_event(raw)
if not msg.content and not raw.get("attachments"):
return None
msg_key = DedupeCache.make_key(
msg.msg_id or f"{msg.sender_id}:{msg.timestamp_ms}",
msg.cli_msg_id,
)
if msg_key in self._dedupe:
return None
self._dedupe.add(msg_key)
command_content = msg.content
if self._bot_name and msg.content:
command_content = strip_own_mentions(msg.content, self._bot_name)
sender = PeerInfo(
kind=PeerKind.GROUP if msg.is_group else PeerKind.DIRECT,
id=msg.sender_id,
display_name=msg.sender_name,
is_bot=False,
)
group: GroupContext | None = None
if msg.is_group:
group = GroupContext(
id=msg.thread_id,
name=msg.group_name,
)
msg_type = MessageType.TEXT
attachments = raw.get("attachments", [])
if attachments:
mime = attachments[0].get("mimeType", "") if attachments else ""
if mime.startswith("image/"):
msg_type = MessageType.IMAGE
elif mime.startswith("audio/"):
msg_type = MessageType.VOICE
elif mime.startswith("video/"):
msg_type = MessageType.FILE
else:
msg_type = MessageType.FILE
media_urls: list[str] = [
att.get("url", att.get("path", "")) for att in attachments if att.get("url") or att.get("path")
]
timestamp = None
if msg.timestamp_ms:
timestamp = datetime.fromtimestamp(msg.timestamp_ms / 1000.0)
return UnifiedMessage(
msg_id=f"zlu:{msg.msg_id or msg_key}",
channel_type="zalouser",
account_id=account_id,
content=command_content,
sender=sender,
message_type=msg_type,
media_urls=media_urls,
group=group,
timestamp=timestamp,
raw_payload=raw,
was_mentioned=msg.was_explicitly_mentioned or msg.has_any_mention,
explicitly_mentioned_bot=msg.was_explicitly_mentioned,
metadata={
"cli_msg_id": msg.cli_msg_id,
"is_group": msg.is_group,
"has_any_mention": msg.has_any_mention,
"implicit_mention": msg.implicit_mention,
"raw_content": msg.content,
},
)
def _parse_reaction_event(self, raw: dict, account_id: str) -> UnifiedMessage | None:
sender_id = raw.get("senderId", "")
msg_id = raw.get("msgId", "")
icon = raw.get("icon", "")
thread_id = raw.get("threadId", "")
is_group = raw.get("threadType") == "Group"
sender = PeerInfo(
kind=PeerKind.GROUP if is_group else PeerKind.DIRECT,
id=sender_id,
display_name=raw.get("senderName", ""),
)
group = None
if is_group:
group = GroupContext(id=thread_id, name=raw.get("groupName"))
return UnifiedMessage(
msg_id=f"zlu:reaction:{msg_id}",
channel_type="zalouser",
account_id=account_id,
content=f"[reaction:{icon}]",
sender=sender,
message_type=MessageType.TEXT,
group=group,
raw_payload=raw,
metadata={
"event_type": "reaction",
"icon": icon,
"msg_id": msg_id,
"is_group": is_group,
},
)
def _parse_undo_event(self, raw: dict, account_id: str) -> UnifiedMessage | None:
sender_id = raw.get("senderId", "")
msg_id = raw.get("msgId", "")
thread_id = raw.get("threadId", "")
is_group = raw.get("threadType") == "Group"
sender = PeerInfo(
kind=PeerKind.GROUP if is_group else PeerKind.DIRECT,
id=sender_id,
display_name=raw.get("senderName", ""),
)
group = None
if is_group:
group = GroupContext(id=thread_id, name=raw.get("groupName"))
return UnifiedMessage(
msg_id=f"zlu:undo:{msg_id}",
channel_type="zalouser",
account_id=account_id,
content="[undo:message_recalled]",
sender=sender,
message_type=MessageType.TEXT,
group=group,
raw_payload=raw,
metadata={
"event_type": "undo",
"original_msg_id": msg_id,
"is_group": is_group,
},
)
def _parse_group_event(self, raw: dict, account_id: str) -> UnifiedMessage | None:
sub_type = raw.get("subType", "")
group_id = raw.get("groupId", "")
group_name = raw.get("groupName", "")
member_ids = raw.get("memberIds", [])
actor_id = raw.get("actorId", "")
sender = PeerInfo(
kind=PeerKind.GROUP,
id=actor_id,
display_name=raw.get("actorName", ""),
)
return UnifiedMessage(
msg_id=f"zlu:group_event:{group_id}:{sub_type}",
channel_type="zalouser",
account_id=account_id,
content=f"[group_event:{sub_type}]",
sender=sender,
message_type=MessageType.TEXT,
group=GroupContext(id=group_id, name=group_name),
raw_payload=raw,
metadata={
"event_type": "group_event",
"sub_type": sub_type,
"group_id": group_id,
"member_ids": member_ids,
},
)