新增 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
209 lines
6.9 KiB
Python
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,
|
|
},
|
|
) |