from __future__ import annotations import logging import time from yuxi.channel.extensions.zulip.format import clean_mentions from yuxi.channel.extensions.zulip.security import ZulipSecurity logger = logging.getLogger(__name__) _SEEN_TTL = 300_000 _SEEN_MAX = 2000 class ZulipMonitor: def __init__(self, account_id: str, account: dict): self.account_id = account_id self.account = account self.security = ZulipSecurity() self._seen: dict[int, float] = {} def convert_zulip_message_to_unified(self, msg: dict) -> dict | None: msg_id = msg.get("id") msg_type = msg.get("type", "") content = msg.get("content", "") sender_id = msg.get("sender_id") sender_email = msg.get("sender_email", "") sender_name = msg.get("sender_full_name", "") display_recipient = msg.get("display_recipient", "") subject = msg.get("subject", "") timestamp = msg.get("timestamp", 0) stream_id = msg.get("stream_id") if not msg_id or not content: return None if self._is_duplicate(msg_id): logger.debug("Duplicate Zulip message %s, dropping", msg_id) return None if msg_type == "stream": stream_name = display_recipient if isinstance(display_recipient, str) else "" if not self.security.is_stream_allowed(stream_name, self.account.get("stream_allowlist", [])): logger.debug("Stream %s not in allowlist, dropping", stream_name) return None chat_type = "group" group_id = f"stream:{stream_id or display_recipient}" elif msg_type == "private": allowed, reason = self.security.check_dm_access(sender_email, self.account) if not allowed: logger.info("DM access denied for %s: %s", sender_email, reason) return {"status": "blocked", "reason": reason} chat_type = "direct" group_id = None else: return None flags = msg.get("flags", []) was_mentioned = "mentioned" in flags or "wildcard_mentioned" in flags normalized = clean_mentions(content) unified = { "message_id": str(msg_id), "sender": { "id": str(sender_id), "name": sender_name, "email": sender_email, "kind": "direct" if msg_type == "private" else "group", }, "content": normalized, "content_type": "text/markdown", "chat_type": chat_type, "timestamp": timestamp, "account_id": self.account_id, "channel_id": "zulip", "was_mentioned": was_mentioned, "raw": msg, } if group_id: unified["group"] = { "id": group_id, "name": display_recipient if isinstance(display_recipient, str) else "", } if subject: unified["thread"] = { "id": subject, "name": subject, } if msg.get("reactions"): unified["reactions"] = msg["reactions"] if msg.get("edit_history"): unified["edit_history"] = msg["edit_history"] return unified def _is_duplicate(self, msg_id: int) -> bool: now = time.monotonic() if msg_id in self._seen: return True self._seen[msg_id] = now self._evict(now) return False def _evict(self, now: float) -> None: if len(self._seen) <= _SEEN_MAX: return cutoff = now - _SEEN_TTL / 1000.0 expired = [k for k, v in self._seen.items() if v < cutoff] for k in expired: del self._seen[k] def convert_reaction_event_to_unified(self, event: dict) -> dict | None: op = event.get("op") if op not in ("add", "remove"): return None return { "type": "reaction_" + op, "message_id": str(event.get("message_id")), "user_id": str(event.get("user_id")), "emoji_name": event.get("emoji_name", ""), "emoji_code": event.get("emoji_code", ""), "reaction_type": event.get("reaction_type", ""), "channel_id": "zulip", } def convert_edit_event_to_unified(self, event: dict) -> dict | None: message_id = event.get("message_id") if not message_id: return None return { "type": "message_updated", "message_id": str(message_id), "user_id": str(event.get("user_id", "")), "edit_timestamp": event.get("edit_timestamp", 0), "orig_content": event.get("orig_content", ""), "orig_subject": event.get("orig_subject", ""), "channel_id": "zulip", }