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, }, )