该提交新增了完整的 Nostr 去中心化社交网络适配器实现,包含以下核心功能: 1. 基础加密与密钥处理:支持 nsec/npub/hex 格式密钥转换,实现 NIP-04/NIP-17 加解密 2. Relay 管理与健康监控:支持多 Relay 连接、自动重连、健康评分与自动选优 3. 事件与消息处理:实现事件校验、去重、速率限制、消息缓存与状态持久化 4. 个人资料管理:支持发布/导入/合并 Nostr Kind 0 元数据事件 5. 配对机制:实现安全的双向配对通信流程 6. Zap 功能:支持 NIP-57 打赏请求与收据解析 7. NIP-05 验证:实现域名身份验证 8. 配置系统:完整的配置校验与多账户支持 9. 监控与指标:提供连接监控、事件统计与健康快照 10. HTTP API:提供个人资料管理的 RESTful 接口 11. 安装向导:命令行配置向导与初始化流程 所有模块均遵循 Nostr 协议规范,支持多账户、多 Relay 部署,内置安全防护与流量控制机制。
277 lines
11 KiB
Python
277 lines
11 KiB
Python
from __future__ import annotations
|
||
|
||
from yuxi.channels.adapters.nostr.crypto import NostrCrypto, NostrCryptoError
|
||
from yuxi.channels.adapters.nostr.probe import probe_relay
|
||
from yuxi.utils.logging_config import logger
|
||
import asyncio
|
||
|
||
|
||
class NostrSetupAdapter:
|
||
def __init__(self, adapter=None):
|
||
self._adapter = adapter
|
||
|
||
def get_setup_steps(self) -> list[dict]:
|
||
return [
|
||
{
|
||
"step": 1,
|
||
"title": "生成或输入私钥",
|
||
"description": "Nostr 私钥用于签名和加密。可以留空自动生成,或输入现有的 nsec/hex 格式私钥",
|
||
"field_key": "private_key",
|
||
"required": False,
|
||
"help": "支持 nsec1... 或 64 位 hex 格式",
|
||
},
|
||
{
|
||
"step": 2,
|
||
"title": "配置 Relay 服务器",
|
||
"description": "Relay 是中继服务器,用于收发 Nostr 消息。支持 ws:// 和 wss:// 协议",
|
||
"field_key": "relays",
|
||
"required": False,
|
||
"default": [
|
||
"wss://relay.damus.io",
|
||
"wss://relay.primal.net",
|
||
"wss://relay.nostr.info",
|
||
"wss://nos.lol",
|
||
],
|
||
"help": "多个 Relay URL 以逗号分隔",
|
||
},
|
||
{
|
||
"step": 3,
|
||
"title": "选择 DM 策略",
|
||
"description": "控制谁可以向你的 Nostr Bot 发送私信",
|
||
"field_key": "dm_policy",
|
||
"required": True,
|
||
"default": "pairing",
|
||
"options": ["pairing", "open", "whitelist", "disabled"],
|
||
"help": "pairing=配对认证 | open=所有人 | whitelist=白名单 | disabled=禁用",
|
||
},
|
||
{
|
||
"step": 4,
|
||
"title": "配置高级选项",
|
||
"description": "配置 NIP-17 加密、流式输出模式、Markdown 表格转换等高级选项",
|
||
"field_key": "advanced",
|
||
"required": False,
|
||
"default": {
|
||
"nip17_enabled": True,
|
||
"streaming_mode": "block",
|
||
"markdown_table_mode": "off",
|
||
"nip42_auth_enabled": False,
|
||
},
|
||
},
|
||
{
|
||
"step": 5,
|
||
"title": "探测 Relay 连接",
|
||
"description": "验证 Relay 服务器是否可达",
|
||
"action": "probe_relays",
|
||
"field_key": "relays",
|
||
},
|
||
{
|
||
"step": 6,
|
||
"title": "验证私钥",
|
||
"description": "验证私钥格式并获取对应的 npub",
|
||
"action": "verify_key",
|
||
"field_key": "private_key",
|
||
},
|
||
]
|
||
|
||
async def probe_relays(self, relays: list[str]) -> dict:
|
||
probe_tasks = [probe_relay(url, timeout=8.0) for url in relays]
|
||
results = await asyncio.gather(*probe_tasks, return_exceptions=True)
|
||
|
||
reachable = 0
|
||
total = len(relays)
|
||
details: list[dict] = []
|
||
for result in results:
|
||
if isinstance(result, Exception):
|
||
details.append({"error": str(result)})
|
||
continue
|
||
entry = {
|
||
"url": result.url,
|
||
"connected": result.connected,
|
||
"latency_ms": round(result.latency_ms, 1) if result.latency_ms else None,
|
||
}
|
||
if result.connected:
|
||
reachable += 1
|
||
if result.error:
|
||
entry["error"] = result.error[:120]
|
||
details.append(entry)
|
||
|
||
return {
|
||
"success": reachable > 0,
|
||
"reachable": reachable,
|
||
"total": total,
|
||
"details": details,
|
||
}
|
||
|
||
async def verify_key(self, private_key: str) -> dict:
|
||
if not private_key:
|
||
crypto = NostrCrypto(None)
|
||
return {
|
||
"success": True,
|
||
"auto_generated": True,
|
||
"npub": crypto.npub,
|
||
"hex": crypto.pubkey_hex(),
|
||
"nsec": crypto.nsec(),
|
||
}
|
||
|
||
try:
|
||
crypto = NostrCrypto(private_key)
|
||
return {
|
||
"success": True,
|
||
"npub": crypto.npub,
|
||
"hex": crypto.pubkey_hex(),
|
||
}
|
||
except NostrCryptoError as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
async def run_interactive_setup(self) -> dict:
|
||
logger.info("[Nostr Setup] Starting interactive setup wizard")
|
||
steps = self.get_setup_steps()
|
||
return {
|
||
"status": "setup_required",
|
||
"channel": "nostr",
|
||
"steps": steps,
|
||
"help": "详细文档请参考: /docs/channels/nostr",
|
||
}
|
||
|
||
def generate_config_schema(self) -> dict:
|
||
return {
|
||
"private_key": {
|
||
"type": "str",
|
||
"required": False,
|
||
"label": "私钥 (nsec/hex)",
|
||
"description": "Nostr 私钥 (nsec/hex),留空自动生成",
|
||
"placeholder": "nsec1... 或 64 位 hex 格式",
|
||
"help": "建议使用 nsec1 格式。留空则自动生成一个新的临时密钥",
|
||
},
|
||
"relays": {
|
||
"type": "list[str]",
|
||
"required": False,
|
||
"label": "Relay 中继服务器",
|
||
"description": "Relay 中继服务器 URL 列表 (ws:// 或 wss://)",
|
||
"placeholder": "wss://relay.damus.io",
|
||
"help": "至少配置 1 个 Relay 用于收发消息。建议使用 wss:// 加密连接",
|
||
"default": [
|
||
"wss://relay.damus.io",
|
||
"wss://relay.primal.net",
|
||
"wss://relay.nostr.info",
|
||
"wss://nos.lol",
|
||
],
|
||
},
|
||
"dm_policy": {
|
||
"type": "str",
|
||
"required": False,
|
||
"default": "pairing",
|
||
"label": "DM 策略",
|
||
"description": "控制谁可以向您的 Nostr Bot 发送私信",
|
||
"help": "pairing=配对认证 | open=所有人 | whitelist=白名单 | disabled=禁用 DM",
|
||
"enum": ["pairing", "open", "whitelist", "disabled"],
|
||
},
|
||
"nip17_enabled": {
|
||
"type": "bool",
|
||
"required": False,
|
||
"default": True,
|
||
"label": "NIP-17 Gift Wrap 加密",
|
||
"description": "启用 NIP-17 Gift Wrap 加密(推荐)",
|
||
"help": "NIP-17 提供更好的隐私保护,建议开启",
|
||
},
|
||
"streaming_mode": {
|
||
"type": "str",
|
||
"required": False,
|
||
"default": "block",
|
||
"label": "流式输出模式",
|
||
"description": "控制消息的流式输出方式",
|
||
"help": "off=关闭流式 | block=分段输出 | progress=进度式输出",
|
||
"enum": ["off", "block", "progress"],
|
||
},
|
||
"markdown_table_mode": {
|
||
"type": "str",
|
||
"required": False,
|
||
"default": "off",
|
||
"label": "Markdown 表格转换",
|
||
"description": "将 Markdown 表格转换为适合 Nostr 的格式",
|
||
"help": "Nostr 客户端通常不支持原生的 Markdown 表格渲染",
|
||
"enum": ["off", "convert"],
|
||
},
|
||
"nip42_auth_enabled": {
|
||
"type": "bool",
|
||
"required": False,
|
||
"default": False,
|
||
"label": "NIP-42 AUTH 认证",
|
||
"description": "启用 Relay AUTH 认证 (NIP-42)",
|
||
"help": "需要 Relay 支持 NIP-42 才能生效",
|
||
},
|
||
"allow_from": {
|
||
"type": "list[str]",
|
||
"required": False,
|
||
"default": [],
|
||
"label": "DM 白名单",
|
||
"description": "DM 白名单 (npub/hex/pubkey)",
|
||
"placeholder": "npub1...",
|
||
"help": "当 DM 策略为 whitelist 时,只有此列表中的 pubkey 能发消息",
|
||
},
|
||
"profile": {
|
||
"type": "dict",
|
||
"required": False,
|
||
"label": "Bot Profile",
|
||
"description": "Nostr Kind 0 元数据 Profile",
|
||
"schema": {
|
||
"name": {
|
||
"type": "str",
|
||
"default": "",
|
||
"label": "用户名",
|
||
"description": "Nostr 用户名",
|
||
"placeholder": "my_nostr_bot",
|
||
},
|
||
"display_name": {
|
||
"type": "str",
|
||
"default": "",
|
||
"label": "显示名称",
|
||
"description": "Nostr 显示名称",
|
||
"placeholder": "My Nostr Bot",
|
||
},
|
||
"about": {
|
||
"type": "str",
|
||
"default": "",
|
||
"label": "简介",
|
||
"description": "Bot 简介",
|
||
"placeholder": "A Nostr bot powered by ForcePilot",
|
||
},
|
||
"picture": {
|
||
"type": "str",
|
||
"default": "",
|
||
"label": "头像 URL",
|
||
"description": "头像图片 URL(需要 https)",
|
||
"placeholder": "https://example.com/avatar.png",
|
||
},
|
||
"banner": {
|
||
"type": "str",
|
||
"default": "",
|
||
"label": "横幅图 URL",
|
||
"description": "Profile 横幅图 URL(需要 https)",
|
||
"placeholder": "https://example.com/banner.png",
|
||
},
|
||
"website": {
|
||
"type": "str",
|
||
"default": "",
|
||
"label": "个人网站",
|
||
"description": "个人/项目网站 URL",
|
||
"placeholder": "https://example.com",
|
||
},
|
||
"nip05": {
|
||
"type": "str",
|
||
"default": "",
|
||
"label": "NIP-05 标识",
|
||
"description": "NIP-05 验证标识 (name@domain)",
|
||
"placeholder": "bot@example.com",
|
||
},
|
||
"lud16": {
|
||
"type": "str",
|
||
"default": "",
|
||
"label": "Lightning Address",
|
||
"description": "Lightning 地址用于接收 Zap 打赏",
|
||
"placeholder": "bot@getalby.com",
|
||
},
|
||
},
|
||
},
|
||
}
|