from __future__ import annotations from datetime import datetime, UTC from typing import Any from yuxi.channels.models import ( Attachment, ChannelIdentity, ChannelMessage, ChannelType, ChatType, EventType, MentionsInfo, MessageType, ) _AS2_TYPE_MAP: dict[str, EventType] = { "Create": EventType.MESSAGE_RECEIVED, "Announce": EventType.MESSAGE_RECEIVED, "Update": EventType.MESSAGE_UPDATED, "Delete": EventType.MESSAGE_DELETED, "Like": EventType.SYSTEM_EVENT, "Dislike": EventType.SYSTEM_EVENT, "Add": EventType.MEMBER_JOINED, "Remove": EventType.MEMBER_LEFT, "Join": EventType.MEMBER_JOINED, "Leave": EventType.MEMBER_LEFT, } def _parse_as2_activity(raw: dict[str, Any]) -> dict[str, Any] | None: if "@context" not in raw: return None as2_type = raw.get("type", "") obj = raw.get("object", {}) if isinstance(obj, dict): content = obj.get("content", "") obj_id = obj.get("id", "") message_type = obj.get("messageType", raw.get("messageType", "comment")) else: content = raw.get("message", "") obj_id = "" message_type = raw.get("messageType", "comment") actor = raw.get("actor", {}) if isinstance(actor, dict): actor_id = actor.get("id", "") actor_display_name = actor.get("name", "") else: actor_id = raw.get("actorId", "") actor_display_name = raw.get("actorDisplayName", "") target = raw.get("target", {}) if isinstance(target, dict): token = target.get("id", "") else: token = raw.get("token", "") return { "id": obj_id or raw.get("id", ""), "token": token or raw.get("token", ""), "actorId": actor_id or raw.get("actorId", ""), "actorDisplayName": actor_display_name or raw.get("actorDisplayName", ""), "timestamp": raw.get("published", raw.get("timestamp", 0)), "message": content or raw.get("message", ""), "messageType": message_type, "_as2_type": as2_type, } def normalize_inbound(raw: dict[str, Any], channel_id: str, room_type: int | None = None) -> ChannelMessage: as2_parsed = _parse_as2_activity(raw) as2_type = "" if as2_parsed: raw = {**raw, **{k: v for k, v in as2_parsed.items() if not k.startswith("_")}} as2_type = as2_parsed["_as2_type"] msg_type = raw.get("messageType", raw.get("message_type", "comment")) identity = ChannelIdentity( channel_id=channel_id, channel_type=ChannelType.NEXTCLOUDTALK, channel_user_id=raw.get("actorId", raw.get("actor_id", "")), channel_chat_id=raw.get("token", ""), channel_message_id=str(raw.get("id", "")), ) if msg_type == "system": return _build_system_event(raw, identity, channel_id, as2_parsed) message_type = MessageType.COMMAND if msg_type == "command" else MessageType.TEXT content = raw.get("message", "") attachments = [] params = raw.get("messageParameters", {}) for param_data in params.values(): ptype = param_data.get("type", "") if ptype in ("file", "media"): attachments.append( Attachment( type="file", url=param_data.get("link", ""), filename=param_data.get("name", ""), mime_type=param_data.get("mimetype", ""), size_bytes=param_data.get("size", 0), ) ) elif ptype == "talk-poll": message_type = MessageType.POLL reply_to = None parent_data = raw.get("parent") if parent_data: reply_to = str(parent_data.get("id", "")) timestamp = datetime.fromtimestamp(raw.get("timestamp", 0), tz=UTC) metadata = { "nc_message_type": msg_type, "nc_actor_type": raw.get("actorType", raw.get("actor_type", "")), "nc_actor_display_name": raw.get("actorDisplayName", raw.get("actor_display_name", "")), "reference_id": raw.get("referenceId", raw.get("reference_id", "")), "nc_message_parameters": params, } if as2_parsed: metadata["as2_type"] = as2_parsed["_as2_type"] was_mentioned = False sender_name = raw.get("actorDisplayName", raw.get("actor_display_name", "")) for key, param_data in params.items(): if key.startswith("mention"): param_type = param_data.get("type", "") if param_type in ("user", "guest"): was_mentioned = True elif param_data.get("type") == "user" and param_data.get("id") == identity.channel_user_id: pass is_forwarded = msg_type == "comment" and "{mention-forwarded}" in content if is_forwarded: forward_actor = raw.get("forwardActorDisplayName", raw.get("forward_actor_display_name", "")) forward_actor_id = raw.get("forwardActorId", raw.get("forward_actor_id", "")) metadata["nc_forwarded"] = True metadata["nc_forward_actor_display_name"] = forward_actor metadata["nc_forward_actor_id"] = forward_actor_id mentions_info = MentionsInfo( mentioned_user_ids=[], is_bot_mentioned=was_mentioned, raw_text=content, ) chat_type = ChatType.DIRECT if room_type is not None and room_type in (2, 3): chat_type = ChatType.GROUP return ChannelMessage( identity=identity, event_type=EventType.MESSAGE_RECEIVED, message_type=message_type, chat_type=chat_type, content=content, attachments=attachments, mentions=mentions_info, reply_to_message_id=reply_to, metadata=metadata, timestamp=timestamp, ) def _build_system_event( raw: dict[str, Any], identity: ChannelIdentity, channel_id: str, as2_parsed: dict[str, Any] | None = None ) -> ChannelMessage: system_msg = raw.get("systemMessage", raw.get("system_message", "")) event_map = { "user_added": EventType.MEMBER_JOINED, "user_removed": EventType.MEMBER_LEFT, "conversation_created": EventType.SYSTEM_EVENT, "conversation_renamed": EventType.SYSTEM_EVENT, "conversation_description_changed": EventType.SYSTEM_EVENT, "call_started": EventType.SYSTEM_EVENT, "call_ended": EventType.SYSTEM_EVENT, "call_missed": EventType.SYSTEM_EVENT, "message_deleted": EventType.MESSAGE_DELETED, "reaction": EventType.SYSTEM_EVENT, "reaction_revoked": EventType.SYSTEM_EVENT, "reaction_added": EventType.SYSTEM_EVENT, "reaction_removed": EventType.SYSTEM_EVENT, "poll_created": EventType.SYSTEM_EVENT, "poll_closed": EventType.SYSTEM_EVENT, "poll_voted": EventType.SYSTEM_EVENT, "conversation_password_changed": EventType.SYSTEM_EVENT, "lobby_timer_reached": EventType.SYSTEM_EVENT, "lobby_none": EventType.SYSTEM_EVENT, "breakout_rooms_started": EventType.SYSTEM_EVENT, "breakout_rooms_stopped": EventType.SYSTEM_EVENT, "recording_started": EventType.SYSTEM_EVENT, "recording_stopped": EventType.SYSTEM_EVENT, "avatar_updated": EventType.SYSTEM_EVENT, "message_edited": EventType.MESSAGE_UPDATED, "message_pinned": EventType.SYSTEM_EVENT, "message_unpinned": EventType.SYSTEM_EVENT, } event_type = event_map.get(system_msg, EventType.SYSTEM_EVENT) description = system_msg if system_msg else "system" metadata: dict[str, Any] = {"nc_system_message": system_msg} if as2_parsed: metadata["as2_type"] = as2_parsed["_as2_type"] return ChannelMessage( identity=identity, event_type=event_type, message_type=MessageType.TEXT, chat_type=ChatType.GROUP, content=description, metadata=metadata, )