"""Configuration schema validation for Synology Chat channel. Provides Pydantic-based configuration validation for the Synology Chat adapter, ensuring required fields are present and types are correct before connection. """ from __future__ import annotations from typing import Any from pydantic import BaseModel, Field, model_validator class SynologyChatSecurityConfig(BaseModel): dm_policy: str = Field(default="open", pattern=r"^(open|allowlist|pairing|disabled)$") group_policy: str = Field(default="allowlist", pattern=r"^(open|allowlist|disabled)$") allow_from: list[str] = Field(default_factory=list) group_allow_from: list[str] = Field(default_factory=list) approvers: list[str] = Field(default_factory=list) class SynologyChatRetryConfig(BaseModel): attempts: int = Field(default=3, ge=1, le=10) min_delay_ms: int = Field(default=1000, ge=100) max_delay_ms: int = Field(default=30000, ge=1000) jitter: float = Field(default=0.1, ge=0.0, le=1.0) class SynologyChatRateLimitConfig(BaseModel): max_per_minute: int = Field(default=30, ge=1, le=120) window_seconds: int = Field(default=60, ge=10, le=300) class SynologyChatConfig(BaseModel): dsm_url: str = Field( default="", description="Synology NAS URL (e.g. https://192.168.1.100:5001)", json_schema_extra={"label": "DSM URL", "category": "connection"}, ) username: str = Field( default="", description="DSM account username for Chat integration", json_schema_extra={"label": "DSM Username", "category": "connection"}, ) password: str = Field( default="", description="DSM account password", json_schema_extra={"label": "DSM Password", "category": "credentials", "secret": True}, ) password_file: str = Field( default="", description="Path to file containing DSM password (Docker secrets compatible)", json_schema_extra={"label": "Password File Path", "category": "credentials"}, ) verify_ssl: bool = Field( default=True, description="Verify SSL certificates (disable for self-signed NAS certs)", json_schema_extra={"label": "Verify SSL", "category": "security"}, ) session_name: str = Field( default="Chat", description="DSM session name for the Chat connection", json_schema_extra={"label": "Session Name", "category": "connection"}, ) enable_syno_token: bool = Field( default=False, description="Enable SynoToken for CSRF protection (DSM 6+)", json_schema_extra={"label": "Enable SynoToken", "category": "security"}, ) name: str = Field( default="synologychat", description="Channel instance name", json_schema_extra={"label": "Channel Name", "category": "general"}, ) default_agent_id: str = Field( default="default", description="Default agent ID for routing incoming messages", json_schema_extra={"label": "Default Agent ID", "category": "routing"}, ) trigger_word: str = Field( default="", description="Optional prefix that triggers bot responses (e.g. '/bot')", json_schema_extra={"label": "Trigger Word", "category": "behavior"}, ) request_timeout_seconds: float = Field( default=30.0, description="HTTP request timeout in seconds", json_schema_extra={"label": "Request Timeout (s)", "category": "advanced"}, ) polling_interval_seconds: int = Field( default=3, ge=1, le=60, description="Polling interval in seconds for message retrieval", json_schema_extra={"label": "Polling Interval (s)", "category": "polling"}, ) polling_max_backoff_seconds: int = Field( default=60, ge=10, le=300, description="Maximum backoff seconds when polling encounters errors", json_schema_extra={"label": "Max Polling Backoff (s)", "category": "polling"}, ) text_chunk_limit: int = Field( default=4000, ge=100, le=4000, description="Maximum characters per message (Synology Chat limit: 4000)", json_schema_extra={"label": "Text Chunk Limit", "category": "messaging"}, ) min_send_interval_ms: int = Field( default=500, ge=100, le=5000, description="Minimum interval between consecutive sends (ms)", json_schema_extra={"label": "Min Send Interval (ms)", "category": "messaging"}, ) reply_to_mode: str = Field( default="first", pattern=r"^(off|first)$", description="Reply quoting mode: 'off' or 'first'", json_schema_extra={"label": "Reply-to Mode", "category": "messaging"}, ) security: SynologyChatSecurityConfig = Field( default_factory=SynologyChatSecurityConfig, description="Access control and security policy configuration", json_schema_extra={"label": "Security Policy", "category": "security"}, ) retry: SynologyChatRetryConfig = Field( default_factory=SynologyChatRetryConfig, description="Send retry configuration", json_schema_extra={"label": "Retry Policy", "category": "advanced"}, ) rate_limit: SynologyChatRateLimitConfig = Field( default_factory=SynologyChatRateLimitConfig, description="Per-user rate limiting configuration", json_schema_extra={"label": "Rate Limit", "category": "security"}, ) user_list_cache_ttl_seconds: int = Field( default=300, ge=30, le=3600, description="User list cache TTL in seconds", json_schema_extra={"label": "User List Cache TTL (s)", "category": "performance"}, ) agent_timeout_seconds: int = Field( default=120, ge=30, le=600, description="Agent response timeout in seconds", json_schema_extra={"label": "Agent Timeout (s)", "category": "behavior"}, ) enable_experimental_message_actions: bool = Field( default=False, description="Enable experimental edit/delete/reaction API calls (not officially supported)", json_schema_extra={"label": "Experimental Actions", "category": "advanced", "experimental": True}, ) send_mode: str = Field( default="dsm_api", pattern=r"^(dsm_api|webhook)$", description="Outbound send mode: 'dsm_api' (direct) or 'webhook' (via incoming webhook)", json_schema_extra={"label": "Send Mode", "category": "connection"}, ) connect_mode: str = Field( default="polling", pattern=r"^(polling|webhook)$", description="Connection mode: 'polling' (DSM API) or 'webhook' (HTTP endpoint)", json_schema_extra={"label": "Connect Mode", "category": "connection"}, ) webhook_path: str = Field( default="/webhook/synology", description="HTTP path for incoming webhook events", json_schema_extra={"label": "Webhook Path", "category": "connection"}, ) webhook_token: str = Field( default="", description="Token for validating incoming webhook requests", json_schema_extra={"label": "Webhook Token", "category": "credentials", "secret": True}, ) incoming_webhook_url: str = Field( default="", description="Incoming webhook URL for sending messages via webhook mode", json_schema_extra={"label": "Incoming Webhook URL", "category": "connection"}, ) dangerously_allow_name_matching: bool = Field( default=False, description="Allow matching users by username across dual ID spaces (webhook vs Chat API). " "Enabling this bypasses strict ID-based security and may allow impersonation.", json_schema_extra={"label": "Dangerous: Allow Name Matching", "category": "security", "dangerous": True}, ) dangerously_allow_inherited_webhook_path: bool = Field( default=False, description="Allow named accounts to inherit the default account's webhook_path. " "Disable to require explicit webhook_path for each named account.", json_schema_extra={ "label": "Dangerous: Allow Inherited Webhook Path", "category": "security", "dangerous": True, }, ) @model_validator(mode="after") def check_credentials(self) -> SynologyChatConfig: if self.connect_mode == "webhook": return self if not self.password and not self.password_file: raise ValueError("Either password or password_file must be configured for polling mode") return self @model_validator(mode="after") def check_dsm_url(self) -> SynologyChatConfig: if self.dsm_url and not (self.dsm_url.startswith("http://") or self.dsm_url.startswith("https://")): raise ValueError("dsm_url must start with http:// or https://") return self def validate_config(config: dict[str, Any]) -> tuple[bool, list[str]]: try: SynologyChatConfig(**config) return True, [] except Exception as e: return False, [str(e)]