该提交完整实现了ForcePilot的Farcaster通道插件,包含: 1. 基础配置适配器与多账户支持 2. Neynar API客户端封装,带重试机制与签名验证 3. 消息去重处理模块 4. 网关服务与健康检查、轮询降级逻辑 5. Webhook回调端点与事件解析 6. 收发消息、 reactions、媒体发送等完整交互能力 7. 插件元数据与系统集成适配
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 ""
|