此提交对Synology Chat适配器进行了全面改进: 1. 新增消息去重、分布式轮询租约、bot名称配置等功能 2. 优化URL提取逻辑,自动清理尾部标点符号 3. 重构发送逻辑,提取通用重试工具函数并优化SID缓存 4. 完善文档提示与配置项,新增轮询租约类型支持 5. 修复认证API路径硬编码问题,调整交互组件提示文案 6. 增加Webhook模式下的DSM客户端兜底初始化 7. 优化导入顺序与代码结构,清理冗余空行
217 lines
8.5 KiB
Python
217 lines
8.5 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, connect_mode: str = "polling") -> str:
|
|
prefix = f"[Named Account: {account_id}] " if account_id != DEFAULT_ACCOUNT_ID else ""
|
|
mode_note = (
|
|
"Note: This adapter uses DSM API Polling mode, no incoming/outgoing webhook URLs needed."
|
|
if connect_mode == "polling"
|
|
else "Note: This adapter uses Webhook mode. Ensure Synology Chat Outgoing Webhook is configured."
|
|
)
|
|
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"
|
|
f"{mode_note}"
|
|
+ (
|
|
"\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 ""
|
|
)
|
|
)
|