新增 Nostr 渠道扩展,支持在 Yuxi 平台中集成 Nostr 去中心化社交协议。 包含以下功能模块: - bus: 事件总线与中继通信 - account: 账户管理 - key_utils: 密钥工具 - gateway: SSE/WebSocket 网关接入 - outbound: 外发消息管理 - gift_wrap: Gift Wrap 加密 - nip44: NIP-44 加密协议 - config_schema: 配置模式 - defaults: 默认配置 - profile_core: 用户资料核心 - profile_publisher: 资料发布 - event_utils: 事件工具 - deletion: 事件删除 - reactions: 表情反应 - metrics: 指标监控 - seen_tracker: 已读追踪 - session_route: 会话路由 - state_store: 状态存储
126 lines
3.6 KiB
Python
126 lines
3.6 KiB
Python
import logging
|
|
import time
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from .account import ResolvedNostrAccount
|
|
from .bus import BusOptions, NostrBus
|
|
from .metrics import MetricsSnapshot
|
|
from .state_store import NostrBusState, load_bus_state, save_bus_state
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class NostrGatewayError(Exception):
|
|
pass
|
|
|
|
|
|
@dataclass
|
|
class NostrBusHandle:
|
|
bus: NostrBus
|
|
state: NostrBusState
|
|
|
|
|
|
class NostrGateway:
|
|
def __init__(self, state_dir: str = ""):
|
|
self._active_buses: dict[str, NostrBusHandle] = {}
|
|
self._metrics_snapshots: dict[str, MetricsSnapshot] = {}
|
|
self._state_dir = state_dir
|
|
|
|
def _on_metric(self, name: str, relay: str | None, value: int):
|
|
if relay:
|
|
logger.debug("Nostr metric %s relay=%s value=%s", name, relay, value)
|
|
else:
|
|
logger.debug("Nostr metric %s value=%s", name, value)
|
|
|
|
def _resolve_state_dir(self) -> Path:
|
|
if self._state_dir:
|
|
return Path(self._state_dir)
|
|
return Path.home() / ".forcepilot" / "nostr"
|
|
|
|
async def start(
|
|
self,
|
|
account_id: str,
|
|
account: ResolvedNostrAccount,
|
|
on_message: callable,
|
|
authorize_sender: callable,
|
|
) -> NostrBus:
|
|
if not account.configured:
|
|
raise NostrGatewayError(f"Nostr account '{account_id}' not configured: missing privateKey")
|
|
|
|
from .key_utils import get_public_key, validate_private_key
|
|
|
|
sk = validate_private_key(account.private_key)
|
|
pk = get_public_key(sk)
|
|
|
|
state_dir = self._resolve_state_dir()
|
|
state = load_bus_state(account_id, state_dir)
|
|
|
|
now = int(time.time())
|
|
if state.gateway_started_at is None:
|
|
state.gateway_started_at = now
|
|
|
|
since = max(
|
|
state.last_processed_at or 0,
|
|
state.gateway_started_at or 0,
|
|
) - 120
|
|
|
|
bus = NostrBus(
|
|
sk=sk,
|
|
pk=pk,
|
|
relays=account.relays,
|
|
account_id=account_id,
|
|
options=BusOptions(
|
|
on_metric=self._on_metric,
|
|
state_dir=str(state_dir),
|
|
encryption=account.config.get("encryption", "nip17"),
|
|
),
|
|
)
|
|
bus._set_state(state)
|
|
bus.seen.seed(state.recent_event_ids)
|
|
|
|
await bus.start(since=since, on_message=on_message, authorize_sender=authorize_sender)
|
|
|
|
handle = NostrBusHandle(bus=bus, state=state)
|
|
self._active_buses[account_id] = handle
|
|
return bus
|
|
|
|
async def stop(self, account_id: str):
|
|
handle = self._active_buses.pop(account_id, None)
|
|
if handle is None:
|
|
return
|
|
|
|
bus = handle.bus
|
|
state = handle.state
|
|
|
|
state.last_processed_at = int(time.time())
|
|
state.recent_event_ids = bus.seen.get_recent_ids()
|
|
|
|
await bus.close()
|
|
|
|
state_dir = self._resolve_state_dir()
|
|
save_bus_state(account_id, state, state_dir)
|
|
|
|
self._metrics_snapshots.pop(account_id, None)
|
|
|
|
def get_bus(self, account_id: str) -> NostrBus | None:
|
|
handle = self._active_buses.get(account_id)
|
|
return handle.bus if handle else None
|
|
|
|
def is_active(self, account_id: str) -> bool:
|
|
return account_id in self._active_buses
|
|
|
|
async def stop_all(self):
|
|
for account_id in list(self._active_buses.keys()):
|
|
await self.stop(account_id)
|
|
|
|
def capture_metrics(self, account_id: str) -> MetricsSnapshot | None:
|
|
bus = self.get_bus(account_id)
|
|
if bus is None:
|
|
return None
|
|
snapshot = bus.get_metrics()
|
|
self._metrics_snapshots[account_id] = snapshot
|
|
return snapshot
|
|
|
|
|