新增群晖Chat渠道适配器的全套实现,包括: 1. 基础适配器与导出接口定义 2. DSM API认证、探测与会话管理 3. 轮询与Webhook两种消息接收方式 4. 消息去重、格式化与规范化处理 5. 多账号支持与权限安全策略 6. 目录用户/群组发现功能 7. 审批配对与流量控制机制 8. 安全审计与配置检查功能
212 lines
8.3 KiB
Python
212 lines
8.3 KiB
Python
"""Interactive setup wizard for Synology Chat channel configuration.
|
|
|
|
Guides users through DSM URL, username, password, webhook path, and
|
|
security policy configuration via CLI prompts.
|
|
|
|
Supports dual-path configuration: default account uses top-level config keys,
|
|
named accounts use accounts.<account_id>.* nested keys.
|
|
|
|
For named accounts:
|
|
- Environment variable credential injection is NOT supported (only explicit config)
|
|
- Adding users to allow_from automatically switches dm_policy to 'allowlist'
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
DEFAULT_ACCOUNT_ID = "default"
|
|
|
|
|
|
def patch_config(
|
|
updates: dict[str, Any],
|
|
existing_config: dict[str, Any] | None = None,
|
|
account_id: str = DEFAULT_ACCOUNT_ID,
|
|
) -> dict[str, Any]:
|
|
"""Apply user-provided updates to the channel configuration.
|
|
|
|
When an allowFrom value is provided, automatically switches dm_policy
|
|
to 'allowlist' to ensure the allowlist is actually enforced.
|
|
"""
|
|
config = dict(existing_config or {})
|
|
|
|
if account_id != DEFAULT_ACCOUNT_ID:
|
|
target = config.setdefault("accounts", {}).setdefault(account_id, {})
|
|
path_prefix = f"accounts.{account_id}."
|
|
else:
|
|
target = config
|
|
path_prefix = ""
|
|
|
|
for key, value in updates.items():
|
|
if key.startswith(path_prefix):
|
|
clean_key = key[len(path_prefix) :]
|
|
else:
|
|
clean_key = key
|
|
|
|
if "." in clean_key:
|
|
parts = clean_key.split(".")
|
|
nested = target
|
|
for part in parts[:-1]:
|
|
nested = nested.setdefault(part, {})
|
|
nested[parts[-1]] = value
|
|
else:
|
|
target[clean_key] = value
|
|
|
|
allow_from_key = f"{path_prefix}security.allow_from"
|
|
allow_from_val = updates.get(allow_from_key, "")
|
|
if allow_from_val:
|
|
allow_from_list = (
|
|
[v.strip() for v in allow_from_val.split(",") if v.strip()]
|
|
if isinstance(allow_from_val, str)
|
|
else allow_from_val
|
|
)
|
|
if allow_from_list:
|
|
sec = target.setdefault("security", {})
|
|
sec["allow_from"] = allow_from_list
|
|
if sec.get("dm_policy", "open") == "open":
|
|
sec["dm_policy"] = "allowlist"
|
|
|
|
return config
|
|
|
|
|
|
def build_setup_prompt(
|
|
existing_config: dict[str, Any] | None = None,
|
|
account_id: str = DEFAULT_ACCOUNT_ID,
|
|
) -> dict[str, Any]:
|
|
"""Return the setup wizard configuration including field descriptions,
|
|
defaults from existing config, and validation rules.
|
|
|
|
Args:
|
|
existing_config: Current channel configuration.
|
|
account_id: Account ID for multi-account setups. 'default' uses
|
|
top-level config keys; named accounts use nested paths.
|
|
"""
|
|
config = existing_config or {}
|
|
|
|
if account_id != DEFAULT_ACCOUNT_ID:
|
|
account_cfg = config.get("accounts", {}).get(account_id, {}) if isinstance(config.get("accounts"), dict) else {}
|
|
path_prefix = f"accounts.{account_id}."
|
|
default_allow_env = False
|
|
else:
|
|
account_cfg = config
|
|
path_prefix = ""
|
|
default_allow_env = True
|
|
|
|
return {
|
|
"channel": "synologychat",
|
|
"label": "Synology Chat",
|
|
"description": (
|
|
"Connect to Synology NAS DSM Chat via polling-based API integration."
|
|
if account_id == DEFAULT_ACCOUNT_ID
|
|
else f"Configure named account '{account_id}' for Synology Chat."
|
|
),
|
|
"account_id": account_id,
|
|
"path_prefix": path_prefix,
|
|
"is_named_account": account_id != DEFAULT_ACCOUNT_ID,
|
|
"steps": [
|
|
{
|
|
"key": f"{path_prefix}dsm_url",
|
|
"label": "DSM URL",
|
|
"description": "Synology NAS URL (e.g. https://192.168.1.100:5001)",
|
|
"type": "string",
|
|
"required": True,
|
|
"default": account_cfg.get("dsm_url", ""),
|
|
"env": "DSM_URL" if default_allow_env else None,
|
|
"validate": {"pattern": r"^https?://", "message": "Must start with http:// or https://"},
|
|
},
|
|
{
|
|
"key": f"{path_prefix}username",
|
|
"label": "DSM Username",
|
|
"description": "DSM account username for Chat integration",
|
|
"type": "string",
|
|
"required": True,
|
|
"default": account_cfg.get("username", ""),
|
|
"env": "DSM_USERNAME" if default_allow_env else None,
|
|
},
|
|
{
|
|
"key": f"{path_prefix}password",
|
|
"label": "DSM Password",
|
|
"description": (
|
|
"DSM account password" + (" (use password_file for Docker secrets)" if default_allow_env else "")
|
|
),
|
|
"type": "password",
|
|
"required": False,
|
|
"default": "",
|
|
"env": "DSM_PASSWORD" if default_allow_env else None,
|
|
},
|
|
{
|
|
"key": f"{path_prefix}password_file",
|
|
"label": "Password File Path",
|
|
"description": "Path to file containing DSM password (Docker secrets compatible)",
|
|
"type": "string",
|
|
"required": False,
|
|
"default": account_cfg.get("password_file", ""),
|
|
"env": "DSM_PASSWORD_FILE" if default_allow_env else None,
|
|
},
|
|
{
|
|
"key": f"{path_prefix}verify_ssl",
|
|
"label": "Verify SSL",
|
|
"description": "Verify SSL certificates (disable for self-signed NAS certs)",
|
|
"type": "boolean",
|
|
"required": False,
|
|
"default": account_cfg.get("verify_ssl", True),
|
|
},
|
|
{
|
|
"key": f"{path_prefix}trigger_word",
|
|
"label": "Trigger Word",
|
|
"description": "Optional prefix that triggers bot responses (e.g. '/bot')",
|
|
"type": "string",
|
|
"required": False,
|
|
"default": account_cfg.get("trigger_word", ""),
|
|
},
|
|
],
|
|
"security_steps": [
|
|
{
|
|
"key": f"{path_prefix}security.dm_policy",
|
|
"label": "DM Policy",
|
|
"description": "Direct message access policy",
|
|
"type": "select",
|
|
"options": ["open", "allowlist", "pairing", "disabled"],
|
|
"default": account_cfg.get("security", {}).get("dm_policy", "open"),
|
|
},
|
|
{
|
|
"key": f"{path_prefix}security.group_policy",
|
|
"label": "Group Policy",
|
|
"description": "Group message access policy",
|
|
"type": "select",
|
|
"options": ["open", "allowlist", "disabled"],
|
|
"default": account_cfg.get("security", {}).get("group_policy", "allowlist"),
|
|
},
|
|
{
|
|
"key": f"{path_prefix}security.allow_from",
|
|
"label": "Allow From (User IDs)",
|
|
"description": "Comma-separated user IDs or '*' for all users. "
|
|
"Adding users will automatically switch DM policy to 'allowlist'.",
|
|
"type": "string",
|
|
"required": False,
|
|
"default": ",".join(account_cfg.get("security", {}).get("allow_from", [])),
|
|
"env": "DSM_ALLOW_FROM" if default_allow_env else None,
|
|
"force_dm_allowlist": True,
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def get_setup_instructions(account_id: str = DEFAULT_ACCOUNT_ID) -> str:
|
|
prefix = f"[Named Account: {account_id}] " if account_id != DEFAULT_ACCOUNT_ID else ""
|
|
return (
|
|
f"{prefix}Synology Chat Setup Guide:\n"
|
|
"1. Install 'Synology Chat' package on your DSM via Package Center\n"
|
|
"2. Go to Chat > Integration > Bots and create a new Bot\n"
|
|
"3. Copy the Bot's credentials (DSM URL, username, password)\n"
|
|
"4. Configure the adapter with DSM URL, username, and password\n"
|
|
"5. Optionally set dm_policy to 'allowlist' or 'pairing' for access control\n"
|
|
"Note: This adapter uses DSM API Polling mode, no incoming/outgoing webhook URLs needed."
|
|
+ (
|
|
"\nEnvironment variables (DSM_URL, DSM_USERNAME, etc.) are only available "
|
|
"for the default account. Named accounts must use explicit configuration."
|
|
if account_id != DEFAULT_ACCOUNT_ID
|
|
else ""
|
|
)
|
|
)
|