import logging import os from pathlib import Path logger = logging.getLogger(__name__) ENV_NEYNAR_API_KEY = "NEYNAR_API_KEY" ENV_NEYNAR_SIGNER_UUID = "NEYNAR_SIGNER_UUID" class FarcasterConfigAdapter: def __init__(self): self._config: dict = {} def update_config(self, config: dict) -> None: self._config = config def list_account_ids(self, config: dict) -> list[str]: self._config = config fc_cfg = self._resolve_fc_config(config) accounts = fc_cfg.get("accounts", {}) if isinstance(fc_cfg, dict) else {} if accounts: return list(accounts.keys()) return ["default"] async def resolve_account(self, account_id: str) -> dict: fc_cfg = self._get_farcaster_config() accounts = fc_cfg.get("accounts", {}) if isinstance(fc_cfg, dict) else {} raw = accounts.get(account_id, {}) if isinstance(accounts, dict) else {} base = {k: v for k, v in fc_cfg.items() if k != "accounts"} api_key = ( raw.get("api_key") or base.get("api_key", "") or _read_file(raw.get("api_key_file", "") or base.get("api_key_file", "")) or os.environ.get(ENV_NEYNAR_API_KEY, "") ) signer_uuid = ( raw.get("signer_uuid") or base.get("signer_uuid", "") or os.environ.get(ENV_NEYNAR_SIGNER_UUID, "") ) return { "account_id": account_id, "name": raw.get("name", account_id), "enabled": raw.get("enabled", base.get("enabled", True)), "configured": bool(api_key), "api_key": api_key, "signer_uuid": signer_uuid, "fid": raw.get("fid", base.get("fid", 0)), "fname": raw.get("fname", base.get("fname", "")), "dm_policy": raw.get("dm_policy", base.get("dm_policy", "pairing")), "allow_from": raw.get("allow_from", base.get("allow_from", [])), "webhook_base_url": raw.get("webhook_base_url", base.get("webhook_base_url", "")), "channel_id": raw.get("channel_id", base.get("channel_id", "")), } def is_configured(self, account: dict) -> bool: return bool(account.get("api_key")) def is_enabled(self, account: dict) -> bool: return account.get("enabled", True) def disabled_reason(self, account: dict) -> str: if not account.get("api_key"): return "Neynar API Key is required" if not account.get("signer_uuid"): return "Signer UUID is required for write operations" return "" def describe_account(self, account: dict) -> dict: return { "account_id": account.get("account_id", ""), "name": account.get("name", ""), "configured": account.get("configured", False), "fname": account.get("fname", ""), } def config_schema(self) -> dict: return { "type": "object", "properties": { "enabled": {"type": "boolean", "default": True, "title": "启用"}, "api_key": { "type": "string", "title": "Neynar API Key", "description": "从 dev.neynar.com 获取的 API Key", "sensitive": True, }, "signer_uuid": { "type": "string", "title": "Signer UUID", "description": "Neynar 托管签名的 UUID(写操作必需)", "sensitive": True, }, "fid": { "type": "integer", "title": "Bot FID", "description": "Bot 的 Farcaster FID", }, "fname": { "type": "string", "title": "Bot Username", "description": "Bot 的 Farcaster 用户名(用于 @提及匹配)", }, "channel_id": { "type": "string", "title": "默认频道 ID", "description": "Farcaster Channel ID,所有公开 Cast 默认发布到此频道", }, "dm_policy": { "type": "string", "enum": ["pairing", "allowlist", "open", "disabled"], "default": "pairing", "title": "DM 安全策略", "description": "pairing=配对码模式, allowlist=白名单, open=开放, disabled=禁用", }, "allow_from": { "type": "array", "items": {"type": "string"}, "title": "允许列表", "description": 'FID 白名单(如 ["1234", "5678"])', }, "webhook_base_url": { "type": "string", "title": "Webhook 回调地址", "description": "Neynar Webhook 推送的公网地址(如 https://forcepilot.example.com)", }, "default_account": {"type": "string", "default": "default"}, "accounts": { "type": "object", "additionalProperties": { "type": "object", "properties": { "name": {"type": "string"}, "enabled": {"type": "boolean", "default": True}, "api_key": {"type": "string", "sensitive": True}, "signer_uuid": {"type": "string", "sensitive": True}, "fid": {"type": "integer"}, "fname": {"type": "string"}, "channel_id": {"type": "string"}, "dm_policy": { "type": "string", "enum": ["pairing", "allowlist", "open", "disabled"], }, "allow_from": {"type": "array", "items": {"type": "string"}}, "webhook_base_url": {"type": "string"}, }, }, }, }, } @staticmethod def _resolve_fc_config(config: dict) -> dict: channels = config.get("channels", {}) fc_cfg = channels.get("farcaster", {}) return fc_cfg if isinstance(fc_cfg, dict) else {} def _get_farcaster_config(self) -> dict: return self._resolve_fc_config(self._config) def _read_file(filepath: str) -> str: if not filepath: return "" try: path = Path(filepath) if path.exists(): return path.read_text(encoding="utf-8").strip() except OSError: pass return ""