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