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