from __future__ import annotations from typing import Any from pydantic import BaseModel, Field, model_validator class NextcloudTalkRoomConfig(BaseModel): enabled: bool = Field(default=True, description="Enable bot responses in this room") system_prompt: str | None = Field( default=None, alias="systemPrompt", description="Override system prompt for this room" ) require_mention: bool = Field( default=True, alias="requireMention", description="Only respond when bot is @mentioned" ) allow_from: list[str] = Field( default_factory=list, alias="allowFrom", description="Allowlist of user IDs for this room" ) skills: list[str] = Field(default_factory=list, description="Skills filter for this room") history_limit: int = Field( default=0, alias="historyLimit", description="Max history messages to load (0=unlimited)" ) model_config = {"populate_by_name": True, "extra": "allow"} class NextcloudTalkDMConfig(BaseModel): enabled: bool = Field(default=True, description="Enable bot responses in this DM") system_prompt: str | None = Field( default=None, alias="systemPrompt", description="Override system prompt for this DM" ) allow_from: list[str] = Field( default_factory=list, alias="allowFrom", description="Allowlist of user IDs for this DM" ) skills: list[str] = Field(default_factory=list, description="Skills filter for this DM") history_limit: int = Field( default=0, alias="historyLimit", description="Max history messages to load (0=unlimited)" ) model_config = {"populate_by_name": True, "extra": "allow"} class NextcloudTalkWebhookConfig(BaseModel): enabled: bool = Field(default=False, description="Enable standalone webhook server") port: int = Field(default=8788, description="Webhook server listen port") host: str = Field(default="0.0.0.0", description="Webhook server bind address") path: str = Field(default="/nextcloud-talk-webhook", description="Webhook endpoint path") rate_limit_max_requests: int = Field( default=10, alias="rateLimitMaxRequests", description="Max failed auth attempts in window", ) rate_limit_window_s: int = Field( default=60, alias="rateLimitWindowS", description="Rate limiting window in seconds", ) rate_limit_lockout_s: int = Field( default=300, alias="rateLimitLockoutS", description="Lockout duration after exceeding rate limit", ) max_body_size: int = Field( default=1 * 1024 * 1024, alias="maxBodySize", description="Maximum webhook request body size in bytes (default: 1MB)", ) pre_auth_max_body_size: int = Field( default=64 * 1024, alias="preAuthMaxBodySize", description="Maximum body size before HMAC auth verification (default: 64KB)", ) model_config = {"populate_by_name": True, "extra": "allow"} class NextcloudTalkAccountConfig(BaseModel): server_url: str = Field(default="", alias="serverUrl", description="Nextcloud server base URL (https://...)") bot_user: str = Field(default="", alias="botUser", description="Bot username for authentication") app_password: str = Field( default="", alias="appPassword", description="App password or device-specific password", ) display_name: str = Field( default="ForcePilot Bot", alias="displayName", description="Bot display name shown in chats", ) dm_policy: str = Field( default="pairing", alias="dmPolicy", description="DM policy: open|pairing|allowlist|disabled", ) group_policy: str = Field( default="allowlist", alias="groupPolicy", description="Group chat policy: open|allowlist|disabled", ) try_websocket: bool = Field(default=True, alias="tryWebsocket", description="Attempt WebSocket monitor mode") reaction_level: str = Field( default="minimal", alias="reactionLevel", description="Reaction verbosity: minimal|normal|verbose", ) poll_interval: float = Field(default=0.5, description="Polling interval in seconds") poll_timeout: int = Field(default=30, description="Polling long-poll timeout in seconds") allow_from: list[str] = Field(default_factory=list, alias="allowFrom", description="Global allowlist of user IDs") group_allow_from: list[str] = Field( default_factory=list, alias="groupAllowFrom", description="Global group allowlist" ) rooms: dict[str, NextcloudTalkRoomConfig] = Field( default_factory=dict, description="Per-room configuration overrides, keyed by room token", ) dms: dict[str, NextcloudTalkDMConfig] = Field( default_factory=dict, description="Per-user DM config overrides, keyed by user ID", ) groups: dict[str, Any] = Field(default_factory=dict, description="Group policy details") accounts: dict[str, Any] = Field(default_factory=dict, alias="accounts", description="Multi-account configurations") retry: dict[str, int] = Field( default_factory=lambda: {"attempts": 3, "min_delay_ms": 400, "max_delay_ms": 30000}, description="Send retry policy: attempts, min/max delay", ) history_limit: int = Field(default=0, alias="historyLimit", description="Global history limit for group chats") dm_history_limit: int = Field(default=0, alias="dmHistoryLimit", description="Global history limit for DMs") markdown: dict[str, Any] = Field(default_factory=dict, description="Markdown rendering options") block_streaming_coalesce: dict[str, Any] = Field( default_factory=dict, alias="blockStreamingCoalesce", description="Block streaming coalesce config (min_chars, max_delay_ms)", ) response_prefix: str = Field( default="", alias="responsePrefix", description="Text prepended to every bot response", ) silent: bool = Field(default=False, description="Send messages with silent flag") chunk_mode: str = Field( default="split", alias="chunkMode", description="Chunking strategy: split|stop|truncate", ) media_max_mb: int = Field(default=20, alias="mediaMaxMb", description="Max media file size in MB") webhook: NextcloudTalkWebhookConfig = Field( default_factory=NextcloudTalkWebhookConfig, description="Standalone webhook server configuration", ) dangerously_allow_private_network: bool = Field( default=False, alias="dangerouslyAllowPrivateNetwork", description="Allow connections to private/reserved IPs (SSRF risk)", ) network: dict[str, Any] = Field(default_factory=dict, description="Network config: proxy, etc.") model_config = {"populate_by_name": True, "extra": "allow"} @model_validator(mode="after") def validate_server_url(self) -> NextcloudTalkAccountConfig: if self.server_url and not (self.server_url.startswith("https://") or self.server_url.startswith("http://")): raise ValueError(f"server_url must start with https:// or http://, got: {self.server_url}") return self @model_validator(mode="after") def validate_dm_policy(self) -> NextcloudTalkAccountConfig: valid = {"open", "pairing", "allowlist", "disabled"} if self.dm_policy not in valid: raise ValueError(f"dm_policy must be one of {sorted(valid)}, got: {self.dm_policy}") return self @model_validator(mode="after") def validate_group_policy(self) -> NextcloudTalkAccountConfig: valid = {"open", "allowlist", "disabled"} if self.group_policy not in valid: raise ValueError(f"group_policy must be one of {sorted(valid)}, got: {self.group_policy}") return self @model_validator(mode="after") def validate_chunk_mode(self) -> NextcloudTalkAccountConfig: valid = {"split", "stop", "truncate"} if self.chunk_mode not in valid: raise ValueError(f"chunk_mode must be one of {sorted(valid)}, got: {self.chunk_mode}") return self def validate_config(config: dict[str, Any]) -> NextcloudTalkAccountConfig: return NextcloudTalkAccountConfig.model_validate(config)