ForcePilot/backend/package/yuxi/channel/extensions/rocketchat/client.py
Kris 043e75d787 feat(channel): 添加 RocketChat 渠道扩展
新增 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: 类型定义
2026-05-21 11:39:24 +08:00

661 lines
20 KiB
Python

from __future__ import annotations
import logging
from typing import Any
import httpx
from yuxi.channel.extensions.rocketchat.config import normalize_rocketchat_server_url
from yuxi.channel.extensions.rocketchat.errors import parse_rocketchat_error
logger = logging.getLogger(__name__)
class RocketChatClient:
def __init__(
self,
server_url: str,
user_id: str,
auth_token: str,
timeout: float = 30.0,
):
self.server_url = normalize_rocketchat_server_url(server_url)
self.api_url = f"{self.server_url}/api/v1"
self.user_id = user_id
self.auth_token = auth_token
limits = httpx.Limits(max_keepalive_connections=5, max_connections=20)
transport = httpx.AsyncHTTPTransport(
limits=limits,
retries=0,
)
self._client = httpx.AsyncClient(
base_url=self.api_url,
headers={
"X-Auth-Token": auth_token,
"X-User-Id": user_id,
"Content-Type": "application/json",
},
timeout=httpx.Timeout(timeout),
transport=transport,
)
async def close(self) -> None:
await self._client.aclose()
async def _request(
self,
method: str,
path: str,
body: dict | None = None,
params: dict | None = None,
) -> dict:
response = await self._client.request(method, path, json=body, params=params)
if response.status_code >= 400:
raise parse_rocketchat_error(response)
data = response.json() if response.text else {}
if isinstance(data, dict) and not data.get("success", True):
raise parse_rocketchat_error(response)
return data
async def fetch_me(self) -> dict:
return await self._request("GET", "/me")
async def fetch_user_info(
self,
user_id: str | None = None,
username: str | None = None,
) -> dict:
if user_id:
return await self._request("GET", "/users.info", params={"userId": user_id})
if username:
return await self._request("GET", "/users.info", params={"username": username})
raise ValueError("user_id or username required")
async def set_user_status(self, status: str, message: str = "") -> dict:
return await self._request(
"POST",
"/users.setStatus",
body={
"status": status,
"message": message,
},
)
async def get_user_status(self, user_id: str) -> dict:
return await self._request("GET", "/users.getStatus", params={"userId": user_id})
async def get_user_presence(self, user_id: str) -> dict:
return await self._request("GET", "/users.getPresence", params={"userId": user_id})
async def post_message(
self,
room_id: str,
text: str,
*,
channel: str | None = None,
thread_id: str | None = None,
alias: str | None = None,
emoji: str | None = None,
avatar: str | None = None,
attachments: list[dict] | None = None,
tshow: bool | None = None,
custom_fields: dict | None = None,
) -> dict:
payload: dict[str, Any] = {"roomId": room_id, "text": text}
if channel:
payload["channel"] = channel
if thread_id:
payload["tmid"] = thread_id
if alias:
payload["alias"] = alias
if emoji:
payload["emoji"] = emoji
if avatar:
payload["avatar"] = avatar
if attachments:
payload["attachments"] = attachments
if tshow is not None:
payload["tshow"] = tshow
if custom_fields:
payload["customFields"] = custom_fields
return await self._request("POST", "/chat.postMessage", body=payload)
async def send_message(
self,
room_id: str,
text: str = "",
*,
thread_id: str | None = None,
alias: str | None = None,
avatar: str | None = None,
emoji: str | None = None,
attachments: list[dict] | None = None,
custom_fields: dict | None = None,
) -> dict:
message: dict[str, Any] = {
"rid": room_id,
"msg": text,
}
if thread_id:
message["tmid"] = thread_id
if alias:
message["alias"] = alias
if avatar:
message["avatar"] = avatar
if emoji:
message["emoji"] = emoji
if attachments:
message["attachments"] = attachments
if custom_fields:
message["customFields"] = custom_fields
return await self._request("POST", "/chat.sendMessage", body={"message": message})
async def update_message(self, room_id: str, msg_id: str, text: str) -> dict:
return await self._request(
"POST",
"/chat.update",
body={
"roomId": room_id,
"msgId": msg_id,
"text": text,
},
)
async def delete_message(
self,
room_id: str,
msg_id: str,
*,
as_user: bool = False,
) -> dict:
payload: dict[str, Any] = {"roomId": room_id, "msgId": msg_id}
if as_user:
payload["asUser"] = True
return await self._request("POST", "/chat.delete", body=payload)
async def fetch_message(self, msg_id: str) -> dict:
return await self._request("GET", "/chat.getMessage", params={"msgId": msg_id})
async def fetch_thread_messages(
self,
thread_msg_id: str,
*,
count: int = 50,
offset: int = 0,
) -> dict:
return await self._request(
"GET",
"/chat.getThreadMessages",
params={
"tmid": thread_msg_id,
"count": min(count, 100),
"offset": offset,
},
)
async def fetch_threads_list(
self,
room_id: str,
*,
count: int = 50,
offset: int = 0,
) -> dict:
return await self._request(
"GET",
"/chat.getThreadsList",
params={
"rid": room_id,
"count": min(count, 100),
"offset": offset,
},
)
async def send_typing(self, room_id: str, username: str | None = None) -> dict:
payload: dict[str, Any] = {"roomId": room_id}
if username:
payload["username"] = username
return await self._request("POST", "/chat.typing", body=payload)
async def upload_file(
self,
room_id: str,
file_data: bytes,
filename: str,
*,
thread_id: str | None = None,
description: str | None = None,
msg: str | None = None,
) -> dict:
files = {"file": (filename, file_data)}
data: dict[str, Any] = {"room_id": room_id}
if thread_id:
data["tmid"] = thread_id
if description:
data["description"] = description
if msg:
data["msg"] = msg
response = await self._client.post(
f"/rooms.upload/{room_id}",
data=data,
files=files,
)
if response.status_code >= 400:
raise parse_rocketchat_error(response)
return response.json()
async def add_reaction(self, msg_id: str, emoji: str) -> dict:
return await self._request(
"POST",
"/chat.react",
body={
"messageId": msg_id,
"emoji": emoji,
},
)
async def fetch_room_info(self, room_id: str) -> dict:
return await self._request("GET", "/rooms.info", params={"roomId": room_id})
async def fetch_rooms(self) -> list[dict]:
data = await self._request("GET", "/rooms.get")
return data.get("update", []) or data.get("rooms", [])
async def fetch_channel_history(
self,
room_id: str,
*,
count: int = 50,
latest: str | None = None,
oldest: str | None = None,
inclusive: bool = False,
) -> dict:
params: dict[str, Any] = {"roomId": room_id, "count": min(count, 100)}
if latest:
params["latest"] = latest
if oldest:
params["oldest"] = oldest
if inclusive:
params["inclusive"] = "true"
return await self._request("GET", "/channels.history", params=params)
async def fetch_group_history(
self,
room_id: str,
*,
count: int = 50,
latest: str | None = None,
oldest: str | None = None,
inclusive: bool = False,
) -> dict:
params: dict[str, Any] = {"roomId": room_id, "count": min(count, 100)}
if latest:
params["latest"] = latest
if oldest:
params["oldest"] = oldest
if inclusive:
params["inclusive"] = "true"
return await self._request("GET", "/groups.history", params=params)
async def fetch_dm_history(
self,
room_id: str,
*,
count: int = 50,
latest: str | None = None,
oldest: str | None = None,
inclusive: bool = False,
) -> dict:
params: dict[str, Any] = {"roomId": room_id, "count": min(count, 100)}
if latest:
params["latest"] = latest
if oldest:
params["oldest"] = oldest
if inclusive:
params["inclusive"] = "true"
return await self._request("GET", "/im.history", params=params)
async def fetch_channels(self) -> list[dict]:
data = await self._request("GET", "/channels.list")
return data.get("channels", [])
async def fetch_groups(self) -> list[dict]:
data = await self._request("GET", "/groups.list")
return data.get("groups", [])
async def create_direct_message(self, username: str) -> dict:
return await self._request("POST", "/im.create", body={"username": username})
async def create_channel(
self,
name: str,
*,
members: list[str] | None = None,
read_only: bool = False,
) -> dict:
payload: dict[str, Any] = {"name": name}
if members:
payload["members"] = members
if read_only:
payload["readOnly"] = True
return await self._request("POST", "/channels.create", body=payload)
async def create_group(
self,
name: str,
*,
members: list[str] | None = None,
read_only: bool = False,
) -> dict:
payload: dict[str, Any] = {"name": name}
if members:
payload["members"] = members
if read_only:
payload["readOnly"] = True
return await self._request("POST", "/groups.create", body=payload)
async def fetch_channel_info(
self,
room_id: str | None = None,
room_name: str | None = None,
) -> dict:
params: dict[str, Any] = {}
if room_id:
params["roomId"] = room_id
elif room_name:
params["roomName"] = room_name
else:
raise ValueError("room_id or room_name required")
return await self._request("GET", "/channels.info", params=params)
async def fetch_group_info(
self,
room_id: str | None = None,
room_name: str | None = None,
) -> dict:
params: dict[str, Any] = {}
if room_id:
params["roomId"] = room_id
elif room_name:
params["roomName"] = room_name
else:
raise ValueError("room_id or room_name required")
return await self._request("GET", "/groups.info", params=params)
async def fetch_channel_members(
self,
room_id: str,
*,
count: int = 50,
offset: int = 0,
) -> dict:
return await self._request(
"GET",
"/channels.members",
params={"roomId": room_id, "count": min(count, 100), "offset": offset},
)
async def fetch_group_members(
self,
room_id: str,
*,
count: int = 50,
offset: int = 0,
) -> dict:
return await self._request(
"GET",
"/groups.members",
params={"roomId": room_id, "count": min(count, 100), "offset": offset},
)
async def join_channel(self, room_id: str) -> dict:
return await self._request("POST", "/channels.join", body={"roomId": room_id})
async def leave_channel(self, room_id: str) -> dict:
return await self._request("POST", "/channels.leave", body={"roomId": room_id})
async def invite_to_channel(self, room_id: str, user_id: str) -> dict:
return await self._request(
"POST",
"/channels.invite",
body={"roomId": room_id, "userId": user_id},
)
async def fetch_joined_channels(self) -> list[dict]:
data = await self._request("GET", "/channels.list.joined")
return data.get("channels", [])
async def search_messages(
self,
room_id: str,
search_text: str,
*,
count: int = 50,
) -> dict:
return await self._request(
"GET",
"/chat.search",
params={
"roomId": room_id,
"searchText": search_text,
"count": min(count, 100),
},
)
async def pin_message(self, msg_id: str) -> dict:
return await self._request(
"POST",
"/chat.pinMessage",
body={"messageId": msg_id},
)
async def unpin_message(self, msg_id: str) -> dict:
return await self._request(
"POST",
"/chat.unPinMessage",
body={"messageId": msg_id},
)
async def fetch_pinned_messages(
self,
room_id: str,
*,
count: int = 50,
offset: int = 0,
) -> dict:
return await self._request(
"GET",
"/chat.getPinnedMessages",
params={
"roomId": room_id,
"count": min(count, 100),
"offset": offset,
},
)
async def download_file(self, file_url: str) -> bytes:
response = await self._client.get(file_url)
if response.status_code >= 400:
raise parse_rocketchat_error(response)
return response.content
async def leave_room(self, room_id: str) -> dict:
return await self._request("POST", "/rooms.leave", body={"roomId": room_id})
async def favorite_room(self, room_id: str, favorite: bool = True) -> dict:
return await self._request(
"POST",
"/rooms.favorite",
body={"roomId": room_id, "favorite": favorite},
)
async def save_room_notification(
self,
room_id: str,
notifications: dict,
) -> dict:
return await self._request(
"POST",
"/rooms.saveNotification",
body={"roomId": room_id, "notifications": notifications},
)
async def save_room_settings(
self,
room_id: str,
*,
room_name: str | None = None,
room_topic: str | None = None,
room_description: str | None = None,
room_read_only: bool | None = None,
react_when_read_only: bool | None = None,
system_messages: list[str] | None = None,
default: bool | None = None,
) -> dict:
payload: dict[str, Any] = {"rid": room_id}
if room_name is not None:
payload["roomName"] = room_name
if room_topic is not None:
payload["roomTopic"] = room_topic
if room_description is not None:
payload["roomDescription"] = room_description
if room_read_only is not None:
payload["readOnly"] = room_read_only
if react_when_read_only is not None:
payload["reactWhenReadOnly"] = react_when_read_only
if system_messages is not None:
payload["systemMessages"] = system_messages
if default is not None:
payload["default"] = default
return await self._request("POST", "/rooms.saveRoomSettings", body=payload)
async def fetch_users_list(
self,
*,
count: int = 50,
offset: int = 0,
query: str | None = None,
) -> dict:
params: dict[str, Any] = {"count": min(count, 100), "offset": offset}
if query:
params["query"] = query
return await self._request("GET", "/users.list", params=params)
async def set_avatar(self, avatar_url: str) -> dict:
return await self._request(
"POST",
"/users.setAvatar",
body={"avatarUrl": avatar_url},
)
async def get_avatar(self, user_id: str | None = None, username: str | None = None) -> dict:
params: dict[str, Any] = {}
if user_id:
params["userId"] = user_id
elif username:
params["username"] = username
else:
raise ValueError("user_id or username required")
return await self._request("GET", "/users.getAvatar", params=params)
async def star_message(self, msg_id: str) -> dict:
return await self._request(
"POST",
"/chat.starMessage",
body={"messageId": msg_id},
)
async def unstar_message(self, msg_id: str) -> dict:
return await self._request(
"POST",
"/chat.unStarMessage",
body={"messageId": msg_id},
)
async def fetch_starred_messages(
self,
room_id: str,
*,
count: int = 50,
offset: int = 0,
) -> dict:
return await self._request(
"GET",
"/chat.getStarredMessages",
params={"roomId": room_id, "count": min(count, 100), "offset": offset},
)
async def follow_message(self, msg_id: str) -> dict:
return await self._request(
"POST",
"/chat.followMessage",
body={"mid": msg_id},
)
async def unfollow_message(self, msg_id: str) -> dict:
return await self._request(
"POST",
"/chat.unFollowMessage",
body={"mid": msg_id},
)
async def report_message(self, msg_id: str, description: str) -> dict:
return await self._request(
"POST",
"/chat.report",
body={"messageId": msg_id, "description": description},
)
async def fetch_deleted_messages(
self,
room_id: str,
since: str,
*,
count: int = 50,
offset: int = 0,
) -> dict:
return await self._request(
"GET",
"/chat.getDeletedMessages",
params={"roomId": room_id, "since": since, "count": min(count, 100), "offset": offset},
)
async def fetch_read_receipts(self, msg_id: str) -> dict:
return await self._request(
"GET",
"/chat.getMessageReadReceipts",
params={"messageId": msg_id},
)
async def create_discussion(
self,
parent_room_id: str,
discussion_name: str,
message: str = "",
*,
users: list[str] | None = None,
) -> dict:
payload: dict[str, Any] = {
"prid": parent_room_id,
"t_name": discussion_name,
}
if message:
payload["message"] = message
if users:
payload["users"] = users
return await self._request("POST", "/rooms.createDiscussion", body=payload)
async def fetch_discussions(
self,
room_id: str,
*,
count: int = 50,
offset: int = 0,
) -> dict:
return await self._request(
"GET",
"/rooms.getDiscussions",
params={"roomId": room_id, "count": min(count, 100), "offset": offset},
)