该提交新增了完整的 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 部署,内置安全防护与流量控制机制。
108 lines
4.2 KiB
Python
108 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import time
|
|
from collections.abc import Awaitable, Callable
|
|
|
|
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
|
|
from yuxi.channels.adapters.nostr.relay_manager import RelayManager
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
|
|
class NostrMonitor:
|
|
def __init__(
|
|
self,
|
|
crypto: NostrCrypto,
|
|
relay_manager: RelayManager,
|
|
backfill_window_sec: int = 120,
|
|
):
|
|
self._crypto = crypto
|
|
self._relay_manager = relay_manager
|
|
self._listeners: list[asyncio.Task] = []
|
|
self._running = False
|
|
self._last_subscribe_ts: int = 0
|
|
self._backfill_window_sec = backfill_window_sec
|
|
self._eose_received: set[str] = set()
|
|
self._on_eose_handlers: list[Callable[[str, int], Awaitable[None]]] = []
|
|
self._onclose_handlers: list[Callable[[str], Awaitable[None]]] = []
|
|
self._subscription_active = False
|
|
|
|
@property
|
|
def running(self) -> bool:
|
|
return self._running
|
|
|
|
def on_event(self, handler: Callable[[dict], Awaitable[None]]) -> None:
|
|
self._relay_manager.on_event(handler)
|
|
|
|
def on_eose(self, handler: Callable[[str, int], Awaitable[None]]) -> None:
|
|
self._on_eose_handlers.append(handler)
|
|
|
|
def on_subscription_close(self, handler: Callable[[str], Awaitable[None]]) -> None:
|
|
self._onclose_handlers.append(handler)
|
|
|
|
async def _handle_subscription_close(self, reason: str) -> None:
|
|
self._subscription_active = False
|
|
logger.warning("[NostrMonitor] 订阅关闭: reason=%s", reason)
|
|
for handler in self._onclose_handlers:
|
|
try:
|
|
await handler(reason)
|
|
except Exception:
|
|
logger.debug("onclose handler error", exc_info=True)
|
|
|
|
async def subscribe(self, dm_policy: str) -> None:
|
|
now = int(time.time())
|
|
if self._last_subscribe_ts == 0:
|
|
self._last_subscribe_ts = now - self._backfill_window_sec
|
|
since = self._last_subscribe_ts
|
|
self._last_subscribe_ts = now
|
|
filters = [{"kinds": [1, 4, 5, 7, 1059], "since": since}]
|
|
self._relay_manager.set_pubkey_filter([self._crypto.pubkey_hex()])
|
|
await self._relay_manager.subscribe(filters)
|
|
self._subscription_active = True
|
|
|
|
async def start(self) -> None:
|
|
self._running = True
|
|
self._relay_manager.on_eose(self._handle_eose)
|
|
connections = self._relay_manager.list_connections()
|
|
for url, ws in connections.items():
|
|
if ws and ws.open:
|
|
task = asyncio.create_task(self._listen_relay(url))
|
|
self._listeners.append(task)
|
|
logger.info(f"Nostr Monitor 已启动,监听 {len(self._listeners)} 个 Relay")
|
|
|
|
async def stop(self) -> None:
|
|
self._running = False
|
|
for task in self._listeners:
|
|
task.cancel()
|
|
self._listeners.clear()
|
|
if self._subscription_active:
|
|
await self._handle_subscription_close("stop called")
|
|
|
|
async def _handle_eose(self, url: str) -> None:
|
|
self._eose_received.add(url)
|
|
total_connections = len(self._relay_manager.list_connections())
|
|
logger.debug(f"Nostr EOSE from {url} ({len(self._eose_received)}/{total_connections})")
|
|
for handler in self._on_eose_handlers:
|
|
try:
|
|
await handler(url, len(self._eose_received))
|
|
except Exception:
|
|
logger.debug("EOSE handler error", exc_info=True)
|
|
|
|
async def _listen_relay(self, url: str) -> None:
|
|
while self._running:
|
|
connections = self._relay_manager.list_connections()
|
|
ws = connections.get(url)
|
|
if ws and ws.open:
|
|
await self._relay_manager.listen(url)
|
|
await asyncio.sleep(self._relay_manager._reconnect_interval)
|
|
else:
|
|
await asyncio.sleep(self._relay_manager._reconnect_interval)
|
|
|
|
all_disconnected = True
|
|
for ws in self._relay_manager.list_connections().values():
|
|
if ws and ws.open:
|
|
all_disconnected = False
|
|
break
|
|
if all_disconnected and self._subscription_active:
|
|
await self._handle_subscription_close("all relays disconnected")
|