新增 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
143 lines
4.8 KiB
Python
143 lines
4.8 KiB
Python
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",
|
|
}
|