from __future__ import annotations import logging from yuxi.channel.extensions.rocketchat.client import RocketChatClient from yuxi.channel.extensions.rocketchat.errors import RocketChatError from yuxi.channel.extensions.rocketchat.format import ( safe_split_markdown, truncate_markdown, ) from yuxi.channel.extensions.rocketchat.types import DeliveryTarget, SendContext logger = logging.getLogger(__name__) class RocketChatOutbound: def __init__(self, client: RocketChatClient, account_id: str = ""): self.client = client self.account_id = account_id self._dm_cache: dict[str, str] = {} async def send_text( self, room_id: str, content: str, *, thread_id: str | None = None, file_ids: list[str] | None = None, text_chunk_limit: int = 4000, ) -> dict | None: chunks = safe_split_markdown(content, text_chunk_limit) last_post: dict | None = None for chunk in chunks: try: resp = await self.client.post_message( room_id, chunk, thread_id=thread_id, ) last_post = resp except RocketChatError as e: logger.error("Failed to send text to room %s: %s", room_id, e) raise return last_post async def send_rich_text( self, room_id: str, content: str = "", *, thread_id: str | None = None, alias: str | None = None, avatar: str | None = None, emoji: str | None = None, attachments: list[dict] | None = None, ) -> dict | None: try: return await self.client.send_message( room_id, text=content, thread_id=thread_id, alias=alias, avatar=avatar, emoji=emoji, attachments=attachments, ) except RocketChatError as e: logger.error("Failed to send rich message to room %s: %s", room_id, e) return None async def edit_message(self, room_id: str, msg_id: str, content: str) -> dict | None: try: return await self.client.update_message( room_id, msg_id, truncate_markdown(content), ) except RocketChatError as e: logger.error("Failed to edit message %s: %s", msg_id, e) return None async def delete_message(self, room_id: str, msg_id: str) -> bool: try: await self.client.delete_message(room_id, msg_id) return True except RocketChatError: return False async def send_media( self, room_id: str, file_data: bytes, filename: str, *, thread_id: str | None = None, caption: str | None = None, ) -> dict | None: try: upload = await self.client.upload_file( room_id, file_data, filename, thread_id=thread_id, ) if caption: try: return await self.client.post_message( room_id, caption, thread_id=thread_id, ) except RocketChatError: pass return upload except RocketChatError as e: logger.error("Failed to send media to room %s: %s", room_id, e) return None async def send_reaction(self, msg_id: str, emoji: str) -> dict | None: try: return await self.client.add_reaction(msg_id, emoji) except RocketChatError as e: logger.error("Failed to add reaction: %s", e) return None async def start_typing(self, room_id: str, username: str | None = None) -> None: try: await self.client.send_typing(room_id, username) except RocketChatError as e: logger.debug("Failed to send typing indicator: %s", e) async def stop_typing(self, room_id: str) -> None: pass async def create_dm_room(self, username: str) -> str | None: if username in self._dm_cache: return self._dm_cache[username] try: result = await self.client.create_direct_message(username) room_id = result.get("room", {}).get("_id", "") if room_id: self._dm_cache[username] = room_id return room_id except RocketChatError as e: logger.error("Failed to create DM room: %s", e) return None def parse_rocketchat_target(raw: str) -> DeliveryTarget: raw = raw.strip() if raw.startswith("room:"): return DeliveryTarget(kind="room", id=raw[len("room:") :]) if raw.startswith("user:"): return DeliveryTarget(kind="user", id=raw[len("user:") :]) if raw.startswith("@"): return DeliveryTarget(kind="user", name=raw[1:]) if raw.startswith("#"): return DeliveryTarget(kind="room", name=raw[1:]) return DeliveryTarget(kind="room", id=raw) async def resolve_send_context( target_str: str, client: RocketChatClient, account_id: str, ) -> SendContext: target = parse_rocketchat_target(target_str) if target.kind == "room" and target.id: return SendContext( room_id=target.id, auth_token=client.auth_token, user_id=client.user_id, server_url=client.server_url, account_id=account_id, ) if target.kind == "user" and target.name: try: room_id = await _resolve_dm_room(client, target.name) return SendContext( room_id=room_id or "", auth_token=client.auth_token, user_id=client.user_id, server_url=client.server_url, account_id=account_id, ) except RocketChatError: pass return SendContext( room_id="", auth_token=client.auth_token, user_id=client.user_id, server_url=client.server_url, account_id=account_id, ) async def _resolve_dm_room(client: RocketChatClient, username: str) -> str | None: try: result = await client.create_direct_message(username) return result.get("room", {}).get("_id", "") except RocketChatError: return None def map_room_type_to_chat_type(room_type: str) -> str: mapping = {"c": "channel", "p": "group", "d": "direct"} return mapping.get(room_type, "channel")