ForcePilot/backend/package/yuxi/channels/adapters/nostr/profile_api.py
Kris a1f8288d20 feat(nostr): 实现完整的 Nostr 协议适配器模块
该提交新增了完整的 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 部署,内置安全防护与流量控制机制。
2026-05-12 00:47:41 +08:00

133 lines
5.0 KiB
Python

from __future__ import annotations
from fastapi import APIRouter, HTTPException
from yuxi.channels.adapters.nostr.models import NostrProfile
from yuxi.utils.logging_config import logger
profile_router = APIRouter(prefix="/nostr/profile", tags=["nostr-profile"])
class NostrProfileAPI:
"""Nostr Profile HTTP API — 管理 Kind 0 元数据"""
def __init__(self, adapter):
self._adapter = adapter
@property
def _crypto(self):
return self._adapter._crypto if self._adapter else None
@property
def _relay_manager(self):
return self._adapter._relay_manager if self._adapter else None
async def get_profile(self, account_id: str = "default") -> dict:
if not self._crypto:
raise HTTPException(status_code=503, detail="Adapter 未初始化")
pubkey = self._crypto.pubkey_hex()
publish_state_raw = (
self._adapter._state_store.load_profile("last_publish_state") if self._adapter._state_store else None
)
profile = await self._adapter.import_profile(pubkey)
if profile:
return {
"status": "ok",
"profile": profile,
"publishState": publish_state_raw
or {
"lastPublishedAt": None,
"lastPublishedEventId": None,
"lastPublishResults": None,
},
}
from yuxi.channels.adapters.nostr.profile import NostrProfileManager
if self._adapter._state_store:
manager = NostrProfileManager(self._crypto, self._relay_manager, self._adapter._state_store)
local = manager.load_local_profile(account_id)
if local:
return {
"status": "ok",
"profile": local.model_dump(),
"source": "local",
"publishState": manager.publish_state
or {
"lastPublishedAt": None,
"lastPublishedEventId": None,
"lastPublishResults": None,
},
}
return {
"status": "ok",
"profile": None,
"message": "Profile 未设置",
"publishState": {
"lastPublishedAt": None,
"lastPublishedEventId": None,
"lastPublishResults": None,
},
}
async def publish_profile(self, profile_data: dict, account_id: str = "default") -> dict:
if not self._crypto:
raise HTTPException(status_code=503, detail="Adapter 未初始化")
try:
result = await self._adapter.publish_profile(profile_data, account_id)
if result:
publish_state = {
"lastPublishedAt": result.get("created_at"),
"lastPublishedEventId": result.get("id"),
"lastPublishResults": {"success": True},
}
return {"status": "ok", "message": "Profile 已发布", "publishState": publish_state}
return {"status": "error", "message": "发布失败:所有 Relay 不可达"}
except Exception as e:
logger.error(f"[NostrProfile] 发布失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
async def import_profile(self, pubkey: str) -> dict:
if not self._crypto:
raise HTTPException(status_code=503, detail="Adapter 未初始化")
try:
from yuxi.channels.adapters.nostr.profile import NostrProfileManager
manager = NostrProfileManager(self._crypto, self._relay_manager)
profile = await manager.import_profile_from_relays(pubkey)
if profile:
return {"status": "ok", "profile": profile.model_dump()}
return {"status": "not_found", "message": f"未找到 pubkey {pubkey[:12]}... 的 Profile"}
except Exception as e:
logger.error(f"[NostrProfile] 导入失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
async def merge_profile(self, local_data: dict, imported_pubkey: str) -> dict:
if not self._crypto:
raise HTTPException(status_code=503, detail="Adapter 未初始化")
try:
from yuxi.channels.adapters.nostr.profile import NostrProfileManager
manager = NostrProfileManager(self._crypto, self._relay_manager)
imported = await manager.import_profile_from_relays(imported_pubkey)
if not imported:
return {
"status": "not_found",
"message": f"未找到 pubkey {imported_pubkey[:12]}... 的 Profile",
}
local = NostrProfile(**local_data)
merged = manager.merge_profiles(local, imported)
return {"status": "ok", "profile": merged.model_dump()}
except Exception as e:
logger.error(f"[NostrProfile] 合并失败: {e}")
raise HTTPException(status_code=500, detail=str(e))