from __future__ import annotations import asyncio import logging from datetime import UTC, datetime from yuxi.channel.extensions.ringcentral.formatting import sanitize_text from yuxi.channel.extensions.ringcentral.mentions import strip_mentions from yuxi.channel.extensions.ringcentral.types import RingCentralEvent, ResolvedRingCentralAccount from yuxi.channel.message.models import ( GroupContext, MessageType, PeerInfo, UnifiedMessage, ) from yuxi.channel.routing.models import PeerKind logger = logging.getLogger(__name__) _PERSON_FETCH_TIMEOUT = 2.0 def _infer_attachment_msg_type(attachment: dict) -> MessageType: mime = attachment.get("contentType", "").lower() if mime.startswith("image/"): return MessageType.IMAGE if mime.startswith("audio/"): return MessageType.VOICE return MessageType.FILE def _build_attachment_text(attachments: list[dict]) -> str: names = [a.get("name", "unknown") for a in attachments] return f"[{', '.join(names)}]" class RingCentralMonitor: async def event_to_unified_message( self, raw_payload: dict, event: RingCentralEvent, account: ResolvedRingCentralAccount | None = None, ) -> UnifiedMessage | None: post = event.body if not post: return None text = strip_mentions(sanitize_text(post.text or "")) attachments = post.attachments or [] if not text and not attachments: return None if attachments and not text: msg_type = _infer_attachment_msg_type(attachments[0]) display_text = _build_attachment_text(attachments) else: msg_type = MessageType.TEXT display_text = text chat_type = _infer_chat_type(post.group_id, raw_payload) is_dm = chat_type == "direct" sender_kind = PeerKind.DIRECT if is_dm else PeerKind.GROUP display_name = None avatar_url = None if post.creator_id: try: person = await self._fetch_person_safe(post.creator_id, account) if person: display_name = ( person.get("firstName", "") + " " + person.get("lastName", "") ).strip() or person.get("name") avatar_url = person.get("avatar") except Exception: pass sender_info = PeerInfo( kind=sender_kind, id=post.creator_id, display_name=display_name, avatar_url=avatar_url, is_bot=False, ) group = None if not is_dm: group = GroupContext( id=post.group_id, kind=chat_type, ) metadata = { "ChatType": chat_type, "WasMentioned": "@!" in (post.text or ""), "event_type": event.event, "subscription_id": event.subscription_id, } return UnifiedMessage( msg_id=post.id or f"rc:{int(datetime.now(tz=UTC).timestamp())}", channel_type="ringcentral", account_id=account.account_id if account else "default", content=display_text, sender=sender_info, message_type=msg_type, group=group, raw_payload=raw_payload, metadata=metadata, was_mentioned="@!" in (post.text or ""), ) async def _fetch_person_safe( self, person_id: str, account: ResolvedRingCentralAccount | None, ) -> dict | None: try: from yuxi.channel.plugins.registry import ChannelPluginRegistry plugin = ChannelPluginRegistry.get("ringcentral") if plugin and hasattr(plugin, "_outbound"): return await asyncio.wait_for( plugin._outbound.fetch_person( person_id, account_id=account.account_id if account else None, ), timeout=_PERSON_FETCH_TIMEOUT, ) except Exception: pass return None def _infer_chat_type(group_id: str, raw_payload: dict) -> str: body = raw_payload.get("body", raw_payload) mentions = body.get("mentions", {}) if isinstance(mentions, dict): for mid, mdata in mentions.items(): mtype = mdata.get("type", "") if isinstance(mdata, dict) else "" if mtype == "Team": return "team" if mtype == "Group": return "group" if mtype == "PersonalChat": return "direct" return "direct"