from __future__ import annotations import logging import os from .types import FlockAccount logger = logging.getLogger(__name__) ENV_MAP = { "FLOCK_BOT_TOKEN": "bot_token", "FLOCK_APP_TOKEN": "app_token", "FLOCK_APP_SECRET": "app_secret", "FLOCK_OUTGOING_WEBHOOK_TOKEN": "outgoing_webhook_token", "FLOCK_EVENT_LISTENER_TOKEN": "event_listener_token", "FLOCK_INCOMING_WEBHOOK_URL": "incoming_webhook_url", "FLOCK_DM_POLICY": "dm_policy", "FLOCK_GROUP_POLICY": "group_policy", "FLOCK_TEXT_CHUNK_LIMIT": "text_chunk_limit", } def _apply_env_overrides(account: FlockAccount) -> FlockAccount: 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) return account def _dict_to_account(data: dict) -> FlockAccount: return FlockAccount( account_id=data.get("account_id", "default"), enabled=data.get("enabled", True), bot_token=data.get("bot_token", ""), app_token=data.get("app_token", ""), app_secret=data.get("app_secret", ""), outgoing_webhook_token=data.get("outgoing_webhook_token", ""), event_listener_token=data.get("event_listener_token", ""), incoming_webhook_url=data.get("incoming_webhook_url", ""), dm_policy=data.get("dm_policy", "open"), dm_allow_from=data.get("dm_allow_from", []), group_policy=data.get("group_policy", "open"), group_allow_from=data.get("group_allow_from", []), require_mention=data.get("require_mention", False), text_chunk_limit=data.get("text_chunk_limit", 7000), media_max_mb=data.get("media_max_mb", 100), thread_replies=data.get("thread_replies", "inbound"), block_streaming=data.get("block_streaming", True), ) def _resolve_account_data(raw_config: dict, account_id: str) -> FlockAccount: accounts = raw_config.get("accounts", {}) data = accounts.get(account_id, {}) if "account_id" not in data: data = {**data, "account_id": account_id} account = _dict_to_account(data) return _apply_env_overrides(account) class FlockConfigAdapter: def __init__(self, raw_config: dict | None = None): self._raw_config: dict = raw_config or {} def set_config(self, config: dict) -> None: self._raw_config = config def list_account_ids(self, config: dict | None = None) -> list[str]: cfg = config if config is not None else self._raw_config accounts = cfg.get("accounts", {}) if not accounts: return ["default"] return list(accounts.keys()) def resolve_account(self, account_id: str) -> dict: accounts = self._raw_config.get("accounts", {}) data = accounts.get(account_id, {}) if not data: return {"account_id": account_id} if "account_id" not in data: return {**data, "account_id": account_id} return data def resolve_account_object(self, account_id: str) -> FlockAccount: return _resolve_account_data(self._raw_config, account_id) def is_configured(self, account: dict) -> bool: acct = _dict_to_account(account) acct = _apply_env_overrides(acct) return bool(acct.bot_token or acct.incoming_webhook_url) def is_enabled(self, account: dict, config: dict | None = None) -> bool: return account.get("enabled", True) def describe_account(self, account: dict, config: dict | None = None) -> dict: acct = _dict_to_account(account) acct = _apply_env_overrides(acct) return { "account_id": acct.account_id, "enabled": acct.enabled, "configured": bool(acct.bot_token or acct.incoming_webhook_url), "dm_policy": acct.dm_policy, "group_policy": acct.group_policy, } def disabled_reason(self, account: dict, config: dict | None = None) -> str: if account.get("enabled", True): return "" return "Account is disabled" def unconfigured_reason(self, account: dict, config: dict | None = None) -> str: acct = _dict_to_account(account) acct = _apply_env_overrides(acct) if not acct.bot_token and not acct.incoming_webhook_url: return "bot_token and incoming_webhook_url not set" return "" def default_account_id(self, config: dict) -> str: return "default" def resolve_allow_from(self, 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(self, config: dict, account_id: str | None, allow_from: list[str]) -> list[str]: return allow_from def has_configured_state(self, config: dict) -> bool: accounts = config.get("accounts", {}) for data in accounts.values(): if self.is_configured(data): return True return False def has_persisted_auth_state(self, config: dict) -> bool: return self.has_configured_state(config) def config_schema(self) -> 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}, "bot_token": {"type": "string", "description": "Flock Bot Token"}, "app_token": {"type": "string", "description": "Flock App Token (备选)"}, "app_secret": { "type": "string", "description": "Flock App Secret (Event Token HMAC 签名验证)", }, "outgoing_webhook_token": { "type": "string", "description": "Outgoing Webhook 验证 Token", }, "event_listener_token": { "type": "string", "description": "Event Listener 验证 Token", }, "incoming_webhook_url": { "type": "string", "description": "Incoming Webhook URL (备选发送方式)", }, "dm_policy": { "type": "string", "enum": ["open", "pairing", "allowlist", "disabled"], "default": "open", }, "dm_allow_from": { "type": "array", "items": {"type": "string"}, "default": [], }, "group_policy": { "type": "string", "enum": ["open", "allowlist", "disabled"], "default": "open", }, "group_allow_from": { "type": "array", "items": {"type": "string"}, "default": [], }, "require_mention": {"type": "boolean", "default": False}, "text_chunk_limit": {"type": "integer", "default": 7000}, "media_max_mb": {"type": "integer", "default": 100}, "thread_replies": { "type": "string", "enum": ["off", "inbound", "always"], "default": "inbound", }, "block_streaming": {"type": "boolean", "default": True}, }, }, } }, }