from __future__ import annotations import logging import os from pathlib import Path from yuxi.channel.extensions.qqbot.credentials import CredentialBackup from yuxi.channel.extensions.qqbot.types import QQBotAccountConfig logger = logging.getLogger(__name__) class QQBotConfigAdapter: def __init__(self): self._credential_backup = CredentialBackup() def list_account_ids(self, config: dict) -> list[str]: accounts = config.get("channels", {}).get("qqbot", {}).get("accounts", {}) if accounts: return list(accounts.keys()) return ["default"] def default_account_id(self, config: dict) -> str: return config.get("channels", {}).get("qqbot", {}).get("default_account", "default") async def resolve_account(self, account_id: str, config: dict | None = None) -> QQBotAccountConfig: channel_cfg = (config or {}).get("channels", {}).get("qqbot", {}) accounts = channel_cfg.get("accounts", {}) account = accounts.get(account_id, {}) if accounts else {} app_id = account.get("appId") or account.get("app_id") or os.environ.get("QQBOT_APP_ID", "") client_secret = account.get("clientSecret") or account.get("client_secret") secret_file = account.get("clientSecretFile") or account.get("client_secret_file") if not client_secret and secret_file: try: client_secret = Path(secret_file).read_text(encoding="utf-8").strip() except OSError: logger.warning("Failed to read client secret file: %s", secret_file) if not client_secret and account_id == "default": client_secret = os.environ.get("QQBOT_CLIENT_SECRET") if not app_id or not client_secret: backup = self._credential_backup.load(account_id) if backup: app_id = backup.get("app_id", app_id) client_secret = backup.get("client_secret", client_secret) dm_policy = account.get("dm_policy") or account.get("dmPolicy", "open") group_policy = account.get("group_policy") or account.get("groupPolicy", "open") allow_from = account.get("allow_from") or account.get("allowFrom", []) group_allow_from = account.get("group_allow_from") or account.get("groupAllowFrom", []) streaming = account.get("streaming", True) return QQBotAccountConfig( app_id=app_id, client_secret=client_secret, secret_source=self._resolve_secret_source(account), account_id=account_id, dm_policy=dm_policy, group_policy=group_policy, allow_from=list(allow_from) if allow_from else [], group_allow_from=list(group_allow_from) if group_allow_from else [], markdown_support=account.get("markdown_support", True), url_direct_upload=account.get("url_direct_upload", True), streaming=streaming if isinstance(streaming, bool) else True, streaming_mode=streaming if isinstance(streaming, str) else None, audio_format_policy=account.get("audio_format_policy"), exec_approvals=account.get("exec_approvals"), config=account, ) def is_configured(self, account: dict | QQBotAccountConfig) -> bool: if isinstance(account, QQBotAccountConfig): return bool(account.app_id and account.client_secret) app_id = account.get("appId") or account.get("app_id") secret = account.get("clientSecret") or account.get("client_secret") if app_id and secret: return True backup = self._credential_backup.load(account.get("account_id", "default")) if backup: return bool(backup.get("app_id") and backup.get("client_secret")) return False def is_enabled(self, account: dict, config: dict) -> bool: return account.get("enabled", True) def disabled_reason(self, account: dict, config: dict) -> str: if not self.is_configured(account): return "App ID or Client Secret not configured" return "" def describe_account(self, account: dict, config: dict) -> dict: return { "account_id": account.get("account_id", "default"), "app_id": account.get("appId") or account.get("app_id", ""), "configured": self.is_configured(account), } def resolve_allow_from(self, config: dict, account_id: str | None = None) -> list[str] | None: channel_cfg = config.get("channels", {}).get("qqbot", {}) accounts = channel_cfg.get("accounts", {}) account = accounts.get(account_id or "default", {}) if accounts else {} return account.get("allow_from") or account.get("allowFrom") def _resolve_secret_source(self, account: dict) -> str | None: if account.get("clientSecret") or account.get("client_secret"): return "config" if account.get("clientSecretFile") or account.get("client_secret_file"): return "file" if os.environ.get("QQBOT_CLIENT_SECRET"): return "env" return None def get_channel_config(self, config: dict) -> dict: return config.get("channels", {}).get("qqbot", {}) def get_group_config(self, config: dict, group_openid: str) -> dict | None: groups = config.get("channels", {}).get("qqbot", {}).get("groups", {}) return groups.get(group_openid) def inspect_account(self, config: dict, account_id: str | None = None) -> dict: channel_cfg = config.get("channels", {}).get("qqbot", {}) accounts = channel_cfg.get("accounts", {}) account = accounts.get(account_id or "default", {}) if accounts else {} return { "account_id": account_id or "default", "configured": self.is_configured(account), "app_id": account.get("appId") or account.get("app_id", ""), "dm_policy": account.get("dm_policy", "open"), } def set_account_enabled(self, config: dict, account_id: str, enabled: bool) -> dict: config.setdefault("channels", {}).setdefault("qqbot", {}).setdefault("accounts", {}).setdefault(account_id, {}) config["channels"]["qqbot"]["accounts"][account_id]["enabled"] = enabled return config def delete_account(self, config: dict, account_id: str) -> dict: accounts = config.get("channels", {}).get("qqbot", {}).get("accounts", {}) if account_id in accounts: del accounts[account_id] return config def has_configured_state(self, config: dict) -> bool: channel_cfg = config.get("channels", {}).get("qqbot", {}) accounts = channel_cfg.get("accounts", {}) if accounts: return any(self.is_configured(a) for a in accounts.values()) return bool(os.environ.get("QQBOT_APP_ID")) def has_persisted_auth_state(self, config: dict) -> bool: return self._credential_backup.exists("default") async def logout_account(self, account_id: str, config: dict | None = None) -> dict: channel_cfg = (config or {}).get("channels", {}).get("qqbot", {}) accounts = channel_cfg.get("accounts", {}) account = accounts.get(account_id, {}) if accounts else {} if account: account.pop("appId", None) account.pop("app_id", None) account.pop("clientSecret", None) account.pop("client_secret", None) account.pop("clientSecretFile", None) account.pop("client_secret_file", None) self._credential_backup.clear(account_id) has_env_token = bool(os.environ.get("QQBOT_APP_ID") and os.environ.get("QQBOT_CLIENT_SECRET")) logger.info("Account '%s' logged out, env_token=%s", account_id, has_env_token) return { "account_id": account_id, "env_token": has_env_token, }