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}, )