这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
224 lines
8.9 KiB
Python
224 lines
8.9 KiB
Python
"""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)]
|