ForcePilot/backend/package/yuxi/channel/extensions/farcaster/config.py

173 lines
6.7 KiB
Python
Raw Normal View History

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 ""