from __future__ import annotations import logging from yuxi.channel.extensions.nextcloud_talk.config_schema import ( DmPolicy, GroupPolicy, NextcloudTalkConfig, NextcloudTalkRoomConfig, ) from yuxi.channel.extensions.nextcloud_talk.policy import ( check_nextcloud_talk_group_sender_allowlist, check_nextcloud_talk_sender_allowlist, resolve_nextcloud_talk_room_match, ) from yuxi.channel.extensions.nextcloud_talk.room_info import RoomInfoResolver from yuxi.channel.extensions.nextcloud_talk.types import NextcloudTalkInboundMessage logger = logging.getLogger(__name__) async def handle_nextcloud_talk_inbound( message: NextcloudTalkInboundMessage, cfg: NextcloudTalkConfig, room_resolver: RoomInfoResolver, ) -> dict | None: if not message.text.strip(): logger.debug("Nextcloud Talk: empty message, discarding") return None room_kind = await room_resolver.resolve(message.room_token) if room_kind is None: room_kind = "group" room_cfg = resolve_nextcloud_talk_room_match(cfg.rooms, message.room_token) if room_kind == "direct": return await _handle_direct_message(message, cfg) else: return await _handle_group_message(message, cfg, room_cfg) async def _handle_direct_message( message: NextcloudTalkInboundMessage, cfg: NextcloudTalkConfig, ) -> dict | None: if cfg.dm_policy == DmPolicy.DISABLED: logger.debug("Nextcloud Talk: DM disabled, discarding") return None if cfg.dm_policy == DmPolicy.ALLOWLIST: if not check_nextcloud_talk_sender_allowlist(message.sender_id, cfg): logger.debug("Nextcloud Talk: sender not in DM allowlist") return None if cfg.dm_policy == DmPolicy.PAIRING: allowed = _is_sender_in_list(message.sender_id, cfg.allow_from) if not allowed: logger.info( "Nextcloud Talk: sender '%s' not paired, initiating pairing challenge", message.sender_id, ) return { "action": "pairing_challenge", "sender_id": message.sender_id, "sender_name": message.sender_name, "room_token": message.room_token, } return _build_context_payload(message, "direct", was_mentioned=False) async def _handle_group_message( message: NextcloudTalkInboundMessage, cfg: NextcloudTalkConfig, room_cfg: NextcloudTalkRoomConfig | None, ) -> dict | None: if cfg.group_policy == GroupPolicy.DISABLED: logger.debug("Nextcloud Talk: group disabled, discarding") return None if cfg.group_policy == GroupPolicy.ALLOWLIST: if room_cfg is None: logger.debug("Nextcloud Talk: room '%s' not in allowlist", message.room_token) return None if not room_cfg.enabled: logger.debug("Nextcloud Talk: room '%s' disabled", message.room_token) return None if not check_nextcloud_talk_group_sender_allowlist(message.sender_id, cfg): logger.debug("Nextcloud Talk: sender '%s' not in group allowlist", message.sender_id) return None if room_cfg and not room_cfg.enabled: logger.debug("Nextcloud Talk: room '%s' disabled", message.room_token) return None require_mention = room_cfg.require_mention if room_cfg else True was_mentioned = _check_mention(message.text) if require_mention and not was_mentioned: logger.debug("Nextcloud Talk: require_mention set, but no @mention found") return None return _build_context_payload(message, "group", was_mentioned=was_mentioned, room_cfg=room_cfg) def _build_context_payload( message: NextcloudTalkInboundMessage, chat_type: str, *, was_mentioned: bool = False, room_cfg: NextcloudTalkRoomConfig | None = None, ) -> dict: payload = { "From": f"nextcloud-talk:{message.sender_id}", "To": f"nextcloud-talk:{message.room_token}", "ChatType": chat_type, "ConversationLabel": message.sender_name if chat_type == "direct" else f"room:{message.room_name}", "SenderId": message.sender_id, "SenderName": message.sender_name, "Text": message.text, "MessageId": message.message_id, "Provider": "nextcloud-talk", "Surface": "nextcloud-talk", } if chat_type == "group": payload["WasMentioned"] = was_mentioned payload["GroupSubject"] = message.room_name payload["From"] = f"nextcloud-talk:room:{message.room_token}" if room_cfg and room_cfg.system_prompt: payload["GroupSystemPrompt"] = room_cfg.system_prompt return payload def _check_mention(text: str) -> bool: import re return bool(re.search(r"@\S+", text)) def _is_sender_in_list(sender_id: str, allow_list: list[str]) -> bool: if not allow_list: return False normalized = sender_id.strip().lower() for prefix in ("nextcloud-talk:", "nc-talk:", "nc:"): if normalized.startswith(prefix): normalized = normalized[len(prefix) :] break for entry in allow_list: entry_normalized = entry.strip().lower() for prefix in ("nextcloud-talk:", "nc-talk:", "nc:"): if entry_normalized.startswith(prefix): entry_normalized = entry_normalized[len(prefix) :] break if normalized == entry_normalized: return True return False