实现了完整的Nextcloud Talk聊天适配器,包含以下核心功能: 1. 基础客户端通信与认证 2. 会话路由与聊天类型解析 3. 消息分块与格式转换 4. 用户配对与权限校验 5. 轮询式消息监听 6. 配置验证与诊断工具 7. 安全策略与去重缓存 8. 指标统计与状态快照 新增配套的配对用户缓存文件与模块导出入口。
189 lines
8.1 KiB
Python
189 lines
8.1 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, Field, model_validator
|
|
|
|
|
|
class NextcloudTalkRoomConfig(BaseModel):
|
|
enabled: bool = Field(default=True, description="Enable bot responses in this room")
|
|
system_prompt: str | None = Field(
|
|
default=None, alias="systemPrompt", description="Override system prompt for this room"
|
|
)
|
|
require_mention: bool = Field(
|
|
default=True, alias="requireMention", description="Only respond when bot is @mentioned"
|
|
)
|
|
allow_from: list[str] = Field(
|
|
default_factory=list, alias="allowFrom", description="Allowlist of user IDs for this room"
|
|
)
|
|
skills: list[str] = Field(default_factory=list, description="Skills filter for this room")
|
|
history_limit: int = Field(
|
|
default=0, alias="historyLimit", description="Max history messages to load (0=unlimited)"
|
|
)
|
|
|
|
model_config = {"populate_by_name": True, "extra": "allow"}
|
|
|
|
|
|
class NextcloudTalkDMConfig(BaseModel):
|
|
enabled: bool = Field(default=True, description="Enable bot responses in this DM")
|
|
system_prompt: str | None = Field(
|
|
default=None, alias="systemPrompt", description="Override system prompt for this DM"
|
|
)
|
|
allow_from: list[str] = Field(
|
|
default_factory=list, alias="allowFrom", description="Allowlist of user IDs for this DM"
|
|
)
|
|
skills: list[str] = Field(default_factory=list, description="Skills filter for this DM")
|
|
history_limit: int = Field(
|
|
default=0, alias="historyLimit", description="Max history messages to load (0=unlimited)"
|
|
)
|
|
|
|
model_config = {"populate_by_name": True, "extra": "allow"}
|
|
|
|
|
|
class NextcloudTalkWebhookConfig(BaseModel):
|
|
enabled: bool = Field(default=False, description="Enable standalone webhook server")
|
|
port: int = Field(default=8788, description="Webhook server listen port")
|
|
host: str = Field(default="0.0.0.0", description="Webhook server bind address")
|
|
path: str = Field(default="/nextcloud-talk-webhook", description="Webhook endpoint path")
|
|
rate_limit_max_requests: int = Field(
|
|
default=10,
|
|
alias="rateLimitMaxRequests",
|
|
description="Max failed auth attempts in window",
|
|
)
|
|
rate_limit_window_s: int = Field(
|
|
default=60,
|
|
alias="rateLimitWindowS",
|
|
description="Rate limiting window in seconds",
|
|
)
|
|
rate_limit_lockout_s: int = Field(
|
|
default=300,
|
|
alias="rateLimitLockoutS",
|
|
description="Lockout duration after exceeding rate limit",
|
|
)
|
|
max_body_size: int = Field(
|
|
default=1 * 1024 * 1024,
|
|
alias="maxBodySize",
|
|
description="Maximum webhook request body size in bytes (default: 1MB)",
|
|
)
|
|
pre_auth_max_body_size: int = Field(
|
|
default=64 * 1024,
|
|
alias="preAuthMaxBodySize",
|
|
description="Maximum body size before HMAC auth verification (default: 64KB)",
|
|
)
|
|
|
|
model_config = {"populate_by_name": True, "extra": "allow"}
|
|
|
|
|
|
class NextcloudTalkAccountConfig(BaseModel):
|
|
server_url: str = Field(default="", alias="serverUrl", description="Nextcloud server base URL (https://...)")
|
|
bot_user: str = Field(default="", alias="botUser", description="Bot username for authentication")
|
|
app_password: str = Field(
|
|
default="",
|
|
alias="appPassword",
|
|
description="App password or device-specific password",
|
|
)
|
|
display_name: str = Field(
|
|
default="ForcePilot Bot",
|
|
alias="displayName",
|
|
description="Bot display name shown in chats",
|
|
)
|
|
dm_policy: str = Field(
|
|
default="pairing",
|
|
alias="dmPolicy",
|
|
description="DM policy: open|pairing|allowlist|disabled",
|
|
)
|
|
group_policy: str = Field(
|
|
default="allowlist",
|
|
alias="groupPolicy",
|
|
description="Group chat policy: open|allowlist|disabled",
|
|
)
|
|
try_websocket: bool = Field(default=True, alias="tryWebsocket", description="Attempt WebSocket monitor mode")
|
|
reaction_level: str = Field(
|
|
default="minimal",
|
|
alias="reactionLevel",
|
|
description="Reaction verbosity: minimal|normal|verbose",
|
|
)
|
|
poll_interval: float = Field(default=0.5, description="Polling interval in seconds")
|
|
poll_timeout: int = Field(default=30, description="Polling long-poll timeout in seconds")
|
|
allow_from: list[str] = Field(default_factory=list, alias="allowFrom", description="Global allowlist of user IDs")
|
|
group_allow_from: list[str] = Field(
|
|
default_factory=list, alias="groupAllowFrom", description="Global group allowlist"
|
|
)
|
|
rooms: dict[str, NextcloudTalkRoomConfig] = Field(
|
|
default_factory=dict,
|
|
description="Per-room configuration overrides, keyed by room token",
|
|
)
|
|
dms: dict[str, NextcloudTalkDMConfig] = Field(
|
|
default_factory=dict,
|
|
description="Per-user DM config overrides, keyed by user ID",
|
|
)
|
|
groups: dict[str, Any] = Field(default_factory=dict, description="Group policy details")
|
|
accounts: dict[str, Any] = Field(default_factory=dict, alias="accounts", description="Multi-account configurations")
|
|
retry: dict[str, int] = Field(
|
|
default_factory=lambda: {"attempts": 3, "min_delay_ms": 400, "max_delay_ms": 30000},
|
|
description="Send retry policy: attempts, min/max delay",
|
|
)
|
|
history_limit: int = Field(default=0, alias="historyLimit", description="Global history limit for group chats")
|
|
dm_history_limit: int = Field(default=0, alias="dmHistoryLimit", description="Global history limit for DMs")
|
|
markdown: dict[str, Any] = Field(default_factory=dict, description="Markdown rendering options")
|
|
block_streaming_coalesce: dict[str, Any] = Field(
|
|
default_factory=dict,
|
|
alias="blockStreamingCoalesce",
|
|
description="Block streaming coalesce config (min_chars, max_delay_ms)",
|
|
)
|
|
response_prefix: str = Field(
|
|
default="",
|
|
alias="responsePrefix",
|
|
description="Text prepended to every bot response",
|
|
)
|
|
silent: bool = Field(default=False, description="Send messages with silent flag")
|
|
chunk_mode: str = Field(
|
|
default="split",
|
|
alias="chunkMode",
|
|
description="Chunking strategy: split|stop|truncate",
|
|
)
|
|
media_max_mb: int = Field(default=20, alias="mediaMaxMb", description="Max media file size in MB")
|
|
webhook: NextcloudTalkWebhookConfig = Field(
|
|
default_factory=NextcloudTalkWebhookConfig,
|
|
description="Standalone webhook server configuration",
|
|
)
|
|
dangerously_allow_private_network: bool = Field(
|
|
default=False,
|
|
alias="dangerouslyAllowPrivateNetwork",
|
|
description="Allow connections to private/reserved IPs (SSRF risk)",
|
|
)
|
|
network: dict[str, Any] = Field(default_factory=dict, description="Network config: proxy, etc.")
|
|
|
|
model_config = {"populate_by_name": True, "extra": "allow"}
|
|
|
|
@model_validator(mode="after")
|
|
def validate_server_url(self) -> NextcloudTalkAccountConfig:
|
|
if self.server_url and not (self.server_url.startswith("https://") or self.server_url.startswith("http://")):
|
|
raise ValueError(f"server_url must start with https:// or http://, got: {self.server_url}")
|
|
return self
|
|
|
|
@model_validator(mode="after")
|
|
def validate_dm_policy(self) -> NextcloudTalkAccountConfig:
|
|
valid = {"open", "pairing", "allowlist", "disabled"}
|
|
if self.dm_policy not in valid:
|
|
raise ValueError(f"dm_policy must be one of {sorted(valid)}, got: {self.dm_policy}")
|
|
return self
|
|
|
|
@model_validator(mode="after")
|
|
def validate_group_policy(self) -> NextcloudTalkAccountConfig:
|
|
valid = {"open", "allowlist", "disabled"}
|
|
if self.group_policy not in valid:
|
|
raise ValueError(f"group_policy must be one of {sorted(valid)}, got: {self.group_policy}")
|
|
return self
|
|
|
|
@model_validator(mode="after")
|
|
def validate_chunk_mode(self) -> NextcloudTalkAccountConfig:
|
|
valid = {"split", "stop", "truncate"}
|
|
if self.chunk_mode not in valid:
|
|
raise ValueError(f"chunk_mode must be one of {sorted(valid)}, got: {self.chunk_mode}")
|
|
return self
|
|
|
|
|
|
def validate_config(config: dict[str, Any]) -> NextcloudTalkAccountConfig:
|
|
return NextcloudTalkAccountConfig.model_validate(config)
|