from __future__ import annotations import json from yuxi.channels.models import ( Attachment, ChannelIdentity, ChannelMessage, ChannelType, ChatType, EventType, MessageType, ) from .mentions import parse_mentions from .slash_commands import extract_command def map_event_type(event_str: str) -> EventType: _map = { "MESSAGE": EventType.MESSAGE_RECEIVED, "MESSAGE_UPDATE": EventType.MESSAGE_UPDATED, "MESSAGE_DELETE": EventType.MESSAGE_DELETED, "ADDED_TO_SPACE": EventType.BOT_ADDED, "REMOVED_FROM_SPACE": EventType.BOT_REMOVED, "CARD_CLICKED": EventType.CARD_ACTION, } return _map.get(event_str, EventType.MESSAGE_RECEIVED) def normalize_inbound( channel_id: str, channel_type: ChannelType, raw_payload: dict, bot_user: str = "", ) -> ChannelMessage: event = raw_payload.get("event", {}) event_type_str = event.get("type", "") message = event.get("message", {}) space = event.get("space", {}) user = event.get("user", {}) sender = message.get("sender", {}) event_type = map_event_type(event_type_str) space_name = space.get("name", "") space_type = space.get("spaceType", "SPACE") chat_type = _resolve_chat_type(space_type, message, raw_payload, event_type) thread_key = message.get("thread", {}).get("threadKey", "") if thread_key: chat_type = ChatType.THREAD chat_id = f"{space_name}/threads/{thread_key}" else: chat_id = space_name mentions = parse_mentions(message, bot_user) content = _extract_content(message, event_type, raw_payload) msg_type = map_msg_type(message, event_type) attachments = extract_attachments(message) argument_text = _extract_argument_text(message) context = _build_context(channel_id, event, message, space, user, sender) route_envelope = resolve_route_envelope(chat_type, space_name, user.get("name", "")) command, command_args = extract_command(content) return ChannelMessage( identity=ChannelIdentity( channel_id=channel_id, channel_type=channel_type, channel_user_id=user.get("name", ""), channel_chat_id=chat_id, channel_message_id=message.get("name", ""), ), chat_type=chat_type, event_type=event_type, message_type=MessageType.COMMAND if command else msg_type, content=content, attachments=attachments, mentions=mentions, metadata={ "space_name": space_name, "space_type": space_type, "thread_key": thread_key, "sender_name": sender.get("name", ""), "sender_type": sender.get("type", ""), "argument_text": argument_text, "context": context, "route_envelope": route_envelope, "slash_command": command, "slash_args": command_args, }, ) def is_bot_message(message: dict) -> bool: sender = message.get("sender", {}) return sender.get("type", "") == "BOT" def _extract_argument_text(message: dict) -> str: annotations = message.get("annotations", []) for annotation in annotations: if annotation.get("type") == "SLASH_COMMAND": return message.get("argumentText", "") return "" def _build_context( channel_id: str, event: dict, message: dict, space: dict, user: dict, sender: dict, ) -> dict: return { "channel": channel_id, "accountId": event.get("accountId", ""), "messageId": message.get("name", ""), "from": sender.get("name", ""), "sender": sender, "conversation": space, "reply": message.get("thread", {}), } def _resolve_chat_type( space_type: str, message: dict, raw_payload: dict, event_type: EventType, ) -> ChatType: if space_type == "DIRECT_MESSAGE": return ChatType.DIRECT if raw_payload.get("is_card_clicked"): return ChatType.SPACE if event_type == EventType.CARD_ACTION: return ChatType.SPACE return ChatType.SPACE def is_forwarded_message(message: dict) -> bool: return bool(message.get("retentionSettings")) or bool(message.get("lastUpdateTime") and message.get("createTime")) def detect_message_edit(message: dict) -> bool: create_time = message.get("createTime", "") update_time = message.get("lastUpdateTime", "") if create_time and update_time and create_time != update_time: return True return False def resolve_route_envelope(chat_type: ChatType, space_name: str, user_name: str) -> dict: if chat_type == ChatType.DIRECT: user_id = user_name.removeprefix("users/") if user_name.startswith("users/") else user_name return { "peer_kind": "direct", "peer_id": user_name, "peer_key": f"direct:{user_id}", "route": { "session_key": f"direct:{user_id}", "content": {"type": "direct", "id": user_name}, }, } space_id = space_name.removeprefix("spaces/") if space_name.startswith("spaces/") else space_name return { "peer_kind": "group", "peer_id": space_name, "peer_key": f"space:{space_id}", "route": { "session_key": f"space:{space_id}", "content": {"type": "space", "id": space_name}, }, } def map_msg_type(message: dict, event_type: EventType) -> MessageType: if event_type == EventType.CARD_ACTION: return MessageType.CARD attachments = message.get("attachment", message.get("attachments", [])) if attachments: first = attachments[0] if attachments else {} content_type = first.get("contentType", "") if content_type.startswith("image/"): return MessageType.IMAGE if content_type.startswith("video/"): return MessageType.VIDEO if content_type.startswith("audio/"): return MessageType.AUDIO if content_type: return MessageType.FILE text = message.get("text", "") if message.get("cardsV2") or message.get("cards_v2"): return MessageType.CARD if text: return MessageType.TEXT return MessageType.TEXT def extract_attachments(message: dict) -> list[Attachment]: attachments = message.get("attachment", message.get("attachments", [])) if not attachments: return [] result: list[Attachment] = [] for att in attachments: name = att.get("name", "") content_name = att.get("contentName", "") content_type = att.get("contentType", "") att_type = "file" if content_type.startswith("image/"): att_type = "image" elif content_type.startswith("video/"): att_type = "video" elif content_type.startswith("audio/"): att_type = "audio" result.append( Attachment( type=att_type, filename=content_name, mime_type=content_type, file_id=_encode_file_id(name), metadata={"attachment_name": name}, ) ) return result def _encode_file_id(attachment_name: str) -> str: return json.dumps({"name": attachment_name}) def _extract_content( message: dict, event_type: EventType, raw_payload: dict, ) -> str: if event_type == EventType.CARD_ACTION: action = raw_payload.get("action", {}) return json.dumps(action) return message.get("text", "")