实现了完整的Nextcloud Talk聊天适配器,包含以下核心功能: 1. 基础客户端通信与认证 2. 会话路由与聊天类型解析 3. 消息分块与格式转换 4. 用户配对与权限校验 5. 轮询式消息监听 6. 配置验证与诊断工具 7. 安全策略与去重缓存 8. 指标统计与状态快照 新增配套的配对用户缓存文件与模块导出入口。
147 lines
5.2 KiB
Python
147 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
WIZARD_STEPS = [
|
|
{
|
|
"step": 0,
|
|
"title": "Bot Installation Guide",
|
|
"description": (
|
|
"Before configuring the adapter, install a Talk bot on your Nextcloud server.\n\n"
|
|
"Run this command as an administrator on your Nextcloud server:\n\n"
|
|
"```bash\n"
|
|
"occ talk:bot:install <bot_name> <bot_secret> <bot_url>\n"
|
|
"```\n\n"
|
|
"Example:\n```bash\n"
|
|
'occ talk:bot:install "ForcePilot Bot" my_secret_key https://your-server.example.com\n'
|
|
"```\n\n"
|
|
"After installation, use the bot username and the secret as app_password in Step 2."
|
|
),
|
|
"field": None,
|
|
"required": False,
|
|
},
|
|
{
|
|
"step": 1,
|
|
"title": "Nextcloud Instance URL",
|
|
"description": "Enter the base URL of your Nextcloud server (e.g., https://cloud.example.com).",
|
|
"field": "server_url",
|
|
"required": True,
|
|
},
|
|
{
|
|
"step": 2,
|
|
"title": "Bot Credentials",
|
|
"description": (
|
|
"Enter the bot username and application password. "
|
|
"Create an app password in Nextcloud at Settings > Security.\n\n"
|
|
"Environment variables (alternative to config):\n"
|
|
"- `NEXTCLOUD_TALK_BOT_USER` for bot_user\n"
|
|
"- `NEXTCLOUD_TALK_APP_PASSWORD` or `NEXTCLOUD_TALK_API_PASSWORD` for app_password"
|
|
),
|
|
"fields": ["bot_user", "app_password"],
|
|
"required": True,
|
|
"preferredEnvVar": {
|
|
"app_password": "NEXTCLOUD_TALK_API_PASSWORD",
|
|
},
|
|
},
|
|
{
|
|
"step": 3,
|
|
"title": "API Credentials (Optional)",
|
|
"description": "Optionally provide separate API credentials for room/peer queries. Leave blank to reuse bot credentials.",
|
|
"fields": ["api_user", "api_password"],
|
|
"required": False,
|
|
},
|
|
{
|
|
"step": 4,
|
|
"title": "DM Policy",
|
|
"description": "Choose how the bot handles direct messages: open (anyone), pairing (require admin approval), allowlist (specified users only), or disabled.",
|
|
"field": "dm_policy",
|
|
"required": True,
|
|
"options": [
|
|
{"value": "open", "label": "Open - anyone can message the bot"},
|
|
{"value": "pairing", "label": "Pairing - users must be approved by admin"},
|
|
{"value": "allowlist", "label": "Allowlist - only specified users"},
|
|
{"value": "disabled", "label": "Disabled - no DMs allowed"},
|
|
],
|
|
},
|
|
{
|
|
"step": 5,
|
|
"title": "Allow From",
|
|
"description": "Configure allowed user IDs and room tokens. Format: nc:{user_id} for users.",
|
|
"field": "allow_from",
|
|
"required": False,
|
|
},
|
|
]
|
|
|
|
|
|
def generate_wizard_steps() -> list[dict[str, Any]]:
|
|
return WIZARD_STEPS
|
|
|
|
|
|
def validate_step(step_num: int, data: dict[str, Any]) -> dict[str, Any]:
|
|
step = next((s for s in WIZARD_STEPS if s["step"] == step_num), None)
|
|
if not step:
|
|
return {"valid": False, "error": f"Unknown step: {step_num}"}
|
|
|
|
errors: list[str] = []
|
|
|
|
if step_num == 1:
|
|
server_url = data.get("server_url", "").strip()
|
|
if step["required"] and not server_url:
|
|
errors.append("Server URL is required")
|
|
elif server_url and not (server_url.startswith("https://") or server_url.startswith("http://")):
|
|
errors.append("Server URL must start with https:// or http://")
|
|
|
|
elif step_num == 2:
|
|
bot_user = data.get("bot_user", "").strip()
|
|
app_password = data.get("app_password", "").strip()
|
|
if not bot_user:
|
|
errors.append("Bot username is required")
|
|
if not app_password:
|
|
errors.append("App password is required")
|
|
|
|
elif step_num == 3:
|
|
pass
|
|
|
|
elif step_num == 4:
|
|
dm_policy = data.get("dm_policy", "").strip()
|
|
valid_policies = {"open", "pairing", "allowlist", "disabled"}
|
|
if dm_policy and dm_policy not in valid_policies:
|
|
errors.append(f"DM policy must be one of: {', '.join(sorted(valid_policies))}")
|
|
|
|
elif step_num == 5:
|
|
allow_from = data.get("allow_from", [])
|
|
if isinstance(allow_from, list):
|
|
for entry in allow_from:
|
|
if not isinstance(entry, str) or not entry.startswith("nc:"):
|
|
errors.append(f"Each allow_from entry must start with 'nc:', got: {entry}")
|
|
|
|
if errors:
|
|
return {"valid": False, "errors": errors}
|
|
return {"valid": True}
|
|
|
|
|
|
def build_config_from_wizard(wizard_data: dict[str, Any]) -> dict[str, Any]:
|
|
config: dict[str, Any] = {
|
|
"server_url": wizard_data.get("server_url", "").rstrip("/"),
|
|
"bot_user": wizard_data.get("bot_user", ""),
|
|
"app_password": wizard_data.get("app_password", ""),
|
|
"dm_policy": wizard_data.get("dm_policy", "pairing"),
|
|
"try_websocket": True,
|
|
}
|
|
|
|
api_user = wizard_data.get("api_user", "")
|
|
if api_user:
|
|
config["api_user"] = api_user
|
|
api_password = wizard_data.get("api_password", "")
|
|
if api_password:
|
|
config["api_password"] = api_password
|
|
|
|
allow_from = wizard_data.get("allow_from", [])
|
|
if allow_from:
|
|
config["allow_from"] = allow_from
|
|
|
|
return config
|