ForcePilot/backend/package/yuxi/channel/extensions/nextcloud_talk/inbound.py
Kris 6fc9a4a94c feat(channel): 添加 Nextcloud Talk 渠道扩展
新增 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: 类型定义
2026-05-21 11:29:56 +08:00

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