ForcePilot/backend/package/yuxi/channels/adapters/synologychat/setup_wizard.py

212 lines
8.3 KiB
Python
Raw Normal View History

"""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 ""
)
)