新增 RocketChat 渠道扩展,支持在 Yuxi 平台中集成 RocketChat 团队协作平台。 包含以下功能模块: - client: RocketChat API 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - websocket: WebSocket 实时连接 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedup: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - gating: 门控管理 - threading: 线程管理 - reactions: 表情反应 - types: 类型定义
176 lines
5.8 KiB
Python
176 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
|
|
from yuxi.channel.extensions.rocketchat.client import RocketChatClient
|
|
from yuxi.channel.extensions.rocketchat.dedup import ClaimableDedupe
|
|
from yuxi.channel.extensions.rocketchat.errors import RocketChatError
|
|
from yuxi.channel.extensions.rocketchat.format import normalize_message
|
|
from yuxi.channel.extensions.rocketchat.gating import RocketChatGating
|
|
from yuxi.channel.extensions.rocketchat.outbound import map_room_type_to_chat_type
|
|
from yuxi.channel.extensions.rocketchat.security import RocketChatSecurityAdapter
|
|
from yuxi.channel.extensions.rocketchat.threading import RocketChatThreadingAdapter
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RocketChatMonitor:
|
|
def __init__(
|
|
self,
|
|
client: RocketChatClient,
|
|
account_id: str,
|
|
account: dict,
|
|
on_message=None,
|
|
on_dispatched=None,
|
|
):
|
|
self.client = client
|
|
self.account_id = account_id
|
|
self.account = account
|
|
self._on_message = on_message
|
|
self._on_dispatched = on_dispatched
|
|
|
|
self.dedupe = ClaimableDedupe(ttl_ms=300_000, max_size=2000)
|
|
self.security = RocketChatSecurityAdapter()
|
|
self.gating = RocketChatGating(account)
|
|
self.threading = RocketChatThreadingAdapter(account)
|
|
|
|
self.bot_user_id: str = ""
|
|
self.bot_username: str = ""
|
|
|
|
async def handle_message(self, raw_msg: dict) -> dict | None:
|
|
try:
|
|
msg_id = raw_msg.get("_id", "")
|
|
room_id = raw_msg.get("rid", "")
|
|
user = raw_msg.get("u", {})
|
|
user_id = user.get("_id", "")
|
|
username = user.get("username", "")
|
|
|
|
if not msg_id or not room_id:
|
|
return None
|
|
|
|
if not user_id:
|
|
logger.debug("Message missing user_id, dropping")
|
|
return None
|
|
|
|
if user_id == self.bot_user_id:
|
|
return None
|
|
|
|
msg_type = raw_msg.get("t", "")
|
|
if msg_type:
|
|
logger.debug("Skipping system message type: %s", msg_type)
|
|
return None
|
|
|
|
dedupe_key = f"{self.account_id}:{msg_id}"
|
|
result = self.dedupe.claim(dedupe_key)
|
|
if result == "duplicate":
|
|
logger.debug("Duplicate message %s, dropping", msg_id)
|
|
return None
|
|
|
|
try:
|
|
room_info = await self.client.fetch_room_info(room_id)
|
|
room_t = room_info.get("room", {}).get("t", "c")
|
|
chat_type = map_room_type_to_chat_type(room_t)
|
|
except RocketChatError:
|
|
chat_type = "channel"
|
|
|
|
allowed, reason = self.security.check_sender_access(
|
|
chat_type,
|
|
user_id,
|
|
self.account,
|
|
username,
|
|
)
|
|
if not allowed:
|
|
logger.info(
|
|
"Access denied for %s in %s: %s",
|
|
user_id,
|
|
room_id,
|
|
reason,
|
|
)
|
|
return {
|
|
"status": "blocked",
|
|
"reason": reason,
|
|
"room_id": room_id,
|
|
"chat_type": chat_type,
|
|
}
|
|
|
|
raw_text = raw_msg.get("msg", "")
|
|
normalized = normalize_message(raw_text, self.bot_username)
|
|
|
|
was_mentioned = self.gating.was_mentioned(
|
|
raw_text,
|
|
self.bot_user_id,
|
|
self.bot_username,
|
|
)
|
|
|
|
should_respond, gate_reason = self.gating.should_respond(
|
|
chat_type,
|
|
normalized,
|
|
was_mentioned,
|
|
)
|
|
|
|
if not should_respond:
|
|
logger.debug("Gate blocked: %s", gate_reason)
|
|
return {
|
|
"status": "gated",
|
|
"reason": gate_reason,
|
|
"room_id": room_id,
|
|
"chat_type": chat_type,
|
|
}
|
|
|
|
thread_id = raw_msg.get("tmid", "")
|
|
|
|
thread_context = self.threading.build_thread_context(
|
|
thread_id,
|
|
msg_id,
|
|
room_id,
|
|
chat_type,
|
|
)
|
|
|
|
ts_field = raw_msg.get("ts", {})
|
|
if isinstance(ts_field, dict):
|
|
timestamp = ts_field.get("$date", time.time() * 1000) / 1000.0
|
|
else:
|
|
timestamp = time.time()
|
|
|
|
file_id = raw_msg.get("file", {}).get("_id", "")
|
|
mentions = [m.get("_id", "") for m in raw_msg.get("mentions", [])]
|
|
|
|
unified = {
|
|
"channel_type": "rocketchat",
|
|
"account_id": self.account_id,
|
|
"msg_id": msg_id,
|
|
"room_id": room_id,
|
|
"chat_type": chat_type,
|
|
"sender_id": user_id,
|
|
"sender_name": username,
|
|
"content": normalized,
|
|
"raw_content": raw_text,
|
|
"thread_id": thread_id,
|
|
"thread_context": thread_context,
|
|
"file_ids": [file_id] if file_id else [],
|
|
"mentions": mentions,
|
|
"timestamp": timestamp,
|
|
}
|
|
|
|
if self._on_message:
|
|
try:
|
|
await self._on_message(unified)
|
|
except Exception as e:
|
|
logger.error("Error in on_message handler: %s", e)
|
|
|
|
return {
|
|
"status": "processed",
|
|
"room_id": room_id,
|
|
"chat_type": chat_type,
|
|
"msg_id": msg_id,
|
|
"unified": unified,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Error handling message %s: %s", raw_msg.get("_id", "?"), e)
|
|
return None
|
|
|
|
def reset(self) -> None:
|
|
self.dedupe.reset()
|