新增 Nextcloud Talk 渠道扩展,支持在 Yuxi 平台中集成 Nextcloud Talk 即时通讯渠道。 包含以下功能模块: - accounts: 账户管理 - bot_admin: Bot 管理 - chat_api: 聊天 API 封装 - config_schema: 配置模式 - conversation: 会话管理 - gateway: SSE/WebSocket 网关接入 - inbound: 入站消息处理 - send: 消息发送 - webhook_server: Webhook 服务 - format: 消息格式转换 - normalize: 消息规范化 - poll_api: 投票 API - reaction_api: 表情反应 API - policy: 安全策略 - probe: 健康探测 - replay_guard: 重放防护 - signature: 签名验证 - room_info: 房间信息 - types: 类型定义
160 lines
5.4 KiB
Python
160 lines
5.4 KiB
Python
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
|