from __future__ import annotations import logging import os from .types import MatrixAccount logger = logging.getLogger(__name__) ENV_MAP = { "MATRIX_HOMESERVER": "homeserver", "MATRIX_ACCESS_TOKEN": "access_token", "MATRIX_USER_ID": "user_id", "MATRIX_PASSWORD": "password", "MATRIX_DEVICE_ID": "device_id", "MATRIX_DEVICE_NAME": "device_name", "MATRIX_RECOVERY_KEY": "recovery_key", } def _apply_env_overrides(account: MatrixAccount) -> MatrixAccount: for env_key, attr_name in ENV_MAP.items(): env_val = os.environ.get(env_key) if env_val is not None: setattr(account, attr_name, env_val) env_dm_policy = os.environ.get("MATRIX_DM_POLICY") if env_dm_policy: account.dm_policy = env_dm_policy env_group_policy = os.environ.get("MATRIX_GROUP_POLICY") if env_group_policy: account.group_policy = env_group_policy return account def _dict_to_account(data: dict) -> MatrixAccount: return MatrixAccount( account_id=data.get("account_id", "default"), enabled=data.get("enabled", True), homeserver=data.get("homeserver", ""), allow_private_network=data.get("allow_private_network", False), access_token=data.get("access_token", ""), user_id=data.get("user_id", ""), password=data.get("password", ""), device_id=data.get("device_id", ""), device_name=data.get("device_name", "ForcePilot Gateway"), encryption=data.get("encryption", False), recovery_key=data.get("recovery_key", ""), initial_sync_limit=data.get("initial_sync_limit", 50), dm_policy=data.get("dm_policy", "pairing"), dm_allow_from=data.get("dm_allow_from", []), dm_session_scope=data.get("dm_session_scope", "per-user"), group_policy=data.get("group_policy", "allowlist"), group_allow_from=data.get("group_allow_from", []), rooms=data.get("rooms", {}), auto_join=data.get("auto_join", "off"), auto_join_allowlist=data.get("auto_join_allowlist", []), streaming=data.get("streaming", "partial"), block_streaming=data.get("block_streaming", False), text_chunk_limit=data.get("text_chunk_limit", 4000), media_max_mb=data.get("media_max_mb", 20), thread_replies=data.get("thread_replies", "inbound"), reply_to_mode=data.get("reply_to_mode", "off"), ack_reaction=data.get("ack_reaction", ""), reaction_notifications=data.get("reaction_notifications", "own"), ) def list_account_ids(config: dict) -> list[str]: accounts = config.get("accounts", {}) if not accounts: return ["default"] return list(accounts.keys()) def resolve_account(account_id: str, config: dict | None = None) -> dict: if config is None: config = {} accounts = config.get("accounts", {}) data = accounts.get(account_id, {}) if not data: data = {"account_id": account_id} elif "account_id" not in data: data = {**data, "account_id": account_id} return data def is_configured(account: dict) -> bool: acct = _dict_to_account(account) acct = _apply_env_overrides(acct) return bool(acct.homeserver and acct.access_token and acct.user_id) def is_enabled(account: dict, config: dict | None = None) -> bool: return account.get("enabled", True) def describe_account(account: dict, config: dict | None = None) -> dict: acct = _dict_to_account(account) acct = _apply_env_overrides(acct) return { "account_id": acct.account_id, "homeserver": acct.homeserver, "user_id": acct.user_id, "device_id": acct.device_id, "device_name": acct.device_name, "encryption": acct.encryption, "dm_policy": acct.dm_policy, "group_policy": acct.group_policy, "enabled": acct.enabled, } def inspect_account(config: dict, account_id: str | None = None) -> dict: aid = account_id or "default" accounts = config.get("accounts", {}) data = accounts.get(aid, {}) acct = _dict_to_account(data) acct = _apply_env_overrides(acct) return { "account_id": acct.account_id, "homeserver": acct.homeserver, "user_id": acct.user_id, "configured": bool(acct.homeserver and acct.access_token and acct.user_id), "encryption": acct.encryption, } def default_account_id(config: dict) -> str: return "default" def set_account_enabled(config: dict, account_id: str, enabled: bool) -> dict: accounts = config.setdefault("accounts", {}) acct = accounts.setdefault(account_id, {}) acct["enabled"] = enabled return config def delete_account(config: dict, account_id: str) -> dict: accounts = config.get("accounts", {}) accounts.pop(account_id, None) return config def disabled_reason(account: dict, config: dict | None = None) -> str: if account.get("enabled", True): return "" return "Account is disabled" def unconfigured_reason(account: dict, config: dict | None = None) -> str: acct = _dict_to_account(account) acct = _apply_env_overrides(acct) reasons = [] if not acct.homeserver: reasons.append("homeserver not set") if not acct.access_token: reasons.append("access_token not set") if not acct.user_id: reasons.append("user_id not set") return "; ".join(reasons) if reasons else "" def resolve_allow_from(config: dict, account_id: str | None = None) -> list[str] | None: aid = account_id or "default" accounts = config.get("accounts", {}) data = accounts.get(aid, {}) acct = _dict_to_account(data) acct = _apply_env_overrides(acct) return list(acct.dm_allow_from) def format_allow_from(config: dict, account_id: str | None, allow_from: list[str]) -> list[str]: return allow_from def has_configured_state(config: dict) -> bool: accounts = config.get("accounts", {}) for data in accounts.values(): if is_configured(data): return True return False def has_persisted_auth_state(config: dict) -> bool: return has_configured_state(config) def resolve_default_to(config: dict, account_id: str | None = None) -> str | None: return None def config_schema() -> dict: return { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "accounts": { "type": "object", "default": {"default": {}}, "additionalProperties": { "type": "object", "properties": { "account_id": {"type": "string", "default": "default"}, "enabled": {"type": "boolean", "default": True}, "homeserver": { "type": "string", "description": "Matrix Homeserver URL", "placeholder": "https://matrix.example.org", }, "access_token": {"type": "string", "description": "Matrix Access Token"}, "user_id": {"type": "string", "description": "Matrix User ID (@bot:example.org)"}, "password": {"type": "string", "description": "Login password (fallback)"}, "device_id": {"type": "string"}, "device_name": {"type": "string", "default": "ForcePilot Gateway"}, "encryption": {"type": "boolean", "default": False}, "recovery_key": {"type": "string"}, "sync_token": {"type": "string", "description": "Last sync token for incremental sync"}, "dm_policy": { "type": "string", "enum": ["pairing", "allowlist", "open", "disabled"], "default": "pairing", }, "dm_allow_from": { "type": "array", "items": {"type": "string"}, "default": [], }, "dm_session_scope": { "type": "string", "enum": ["per-user", "per-room"], "default": "per-user", }, "group_policy": { "type": "string", "enum": ["open", "allowlist", "disabled"], "default": "allowlist", }, "group_allow_from": { "type": "array", "items": {"type": "string"}, "default": [], }, "auto_join": { "type": "string", "enum": ["off", "allowlist", "all"], "default": "off", }, "auto_join_allowlist": { "type": "array", "items": {"type": "string"}, "default": [], }, "streaming": { "type": "string", "enum": ["partial", "quiet", "off"], "default": "partial", }, "block_streaming": {"type": "boolean", "default": False}, "text_chunk_limit": {"type": "integer", "default": 4000}, "media_max_mb": {"type": "integer", "default": 20}, "thread_replies": { "type": "string", "enum": ["off", "inbound", "always"], "default": "inbound", }, "reply_to_mode": { "type": "string", "enum": ["off", "inbound"], "default": "off", }, "ack_reaction": {"type": "string", "default": ""}, "reaction_notifications": { "type": "string", "enum": ["off", "own", "all"], "default": "own", }, }, }, } }, }