173 lines
6.7 KiB
Python
173 lines
6.7 KiB
Python
|
|
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 ""
|