新增 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: 类型定义
661 lines
20 KiB
Python
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},
|
|
)
|