ForcePilot/backend/package/yuxi/channels/adapters/nostr/monitor.py

108 lines
4.2 KiB
Python
Raw Normal View History

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")