refactor(nostr): 整理代码结构并新增多项功能优化
1. 整理多个文件的导入顺序,移除冗余空行和重复导入 2. 为Nostr订阅添加until参数防止重复拉取 3. 实现长消息分块发送并添加间隔等待 4. 新增Relay健康分数同步任务 5. 新增TLS强制检查配置项并支持从环境变量加载 6. 重构状态恢复逻辑适配异步存储 7. 修复反应发送的参数错误 8. 新增线程模拟自动补全消息上下文 9. 添加解密指标统计和更完善的错误日志
This commit is contained in:
parent
b317387e0c
commit
7002557cf4
@ -1,16 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json as _json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
from yuxi.channels.adapters.nostr.config import NostrConfig
|
||||
from yuxi.channels.adapters.nostr.crypto import NostrCrypto, NostrCryptoError, normalize_pubkey
|
||||
from yuxi.channels.adapters.nostr.guard import GuardPolicy, NostrGuard
|
||||
from yuxi.channels.adapters.nostr.health import RelayHealthTracker
|
||||
from yuxi.channels.adapters.nostr.metrics import NostrMetrics
|
||||
from yuxi.channels.adapters.nostr.monitor import NostrMonitor
|
||||
from yuxi.channels.adapters.nostr.probe import probe_relay
|
||||
from yuxi.channels.adapters.nostr.relay_manager import RelayManager
|
||||
from yuxi.channels.adapters.nostr.send import NostrSender
|
||||
from yuxi.channels.adapters.nostr.send_cache import SendCache
|
||||
from yuxi.channels.adapters.nostr.state_store import NostrStateStore
|
||||
from yuxi.channels.adapters.nostr.thread_simulator import NostrThreadSimulator
|
||||
from yuxi.channels.base import BaseChannelAdapter
|
||||
from yuxi.channels.capabilities import ChannelCapabilities
|
||||
from yuxi.channels.meta import ChannelMeta
|
||||
from yuxi.channels.exceptions import (
|
||||
ChannelAuthenticationError,
|
||||
ChannelException,
|
||||
)
|
||||
from yuxi.channels.meta import ChannelMeta
|
||||
from yuxi.channels.models import (
|
||||
Attachment,
|
||||
ChannelIdentity,
|
||||
@ -25,24 +40,9 @@ from yuxi.channels.models import (
|
||||
MessageType,
|
||||
)
|
||||
from yuxi.channels.registry import register_builtin_adapter
|
||||
from yuxi.channels.adapters.nostr.config import NostrConfig
|
||||
from yuxi.channels.adapters.nostr.crypto import NostrCrypto, NostrCryptoError
|
||||
from yuxi.channels.adapters.nostr.relay_manager import RelayManager
|
||||
from yuxi.channels.adapters.nostr.send import NostrSender
|
||||
from yuxi.channels.adapters.nostr.monitor import NostrMonitor
|
||||
from yuxi.channels.adapters.nostr.probe import probe_relay
|
||||
from yuxi.channels.adapters.nostr.guard import NostrGuard, GuardPolicy
|
||||
from yuxi.channels.adapters.nostr.health import RelayHealthTracker
|
||||
from yuxi.channels.adapters.nostr.crypto import normalize_pubkey
|
||||
from yuxi.channels.adapters.nostr.state_store import NostrStateStore
|
||||
from yuxi.channels.adapters.nostr.metrics import NostrMetrics
|
||||
from yuxi.channels.adapters.nostr.send_cache import SendCache
|
||||
from yuxi.utils.datetime_utils import utc_now_naive
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
import aiohttp
|
||||
import json as _json
|
||||
|
||||
|
||||
@dataclass
|
||||
class _AccountComponents:
|
||||
@ -114,8 +114,10 @@ class NostrAdapter(BaseChannelAdapter):
|
||||
self._order_buffer: list[dict] = []
|
||||
self._order_lock = asyncio.Lock()
|
||||
self._ordering_task: asyncio.Task | None = None
|
||||
self._score_sync_task: asyncio.Task | None = None
|
||||
self._account_managers: dict[str, _AccountComponents] = {}
|
||||
self._active_account_id: str = "default"
|
||||
self._thread_simulator: NostrThreadSimulator | None = None
|
||||
|
||||
async def connect(self) -> None:
|
||||
self._status = ChannelStatus.CONNECTING
|
||||
@ -143,6 +145,8 @@ class NostrAdapter(BaseChannelAdapter):
|
||||
|
||||
await self._relay_manager.connect_all()
|
||||
|
||||
self._thread_simulator = NostrThreadSimulator(relay_pool=self._relay_manager, max_depth=10)
|
||||
|
||||
for components in self._account_managers.values():
|
||||
if components.relay_manager is not self._relay_manager:
|
||||
await components.relay_manager.connect_all()
|
||||
@ -163,6 +167,8 @@ class NostrAdapter(BaseChannelAdapter):
|
||||
if self._nostr_config.message_ordering:
|
||||
self._ordering_task = asyncio.create_task(self._run_order_buffer())
|
||||
|
||||
self._score_sync_task = asyncio.create_task(self._run_score_sync())
|
||||
|
||||
self._status = ChannelStatus.CONNECTED
|
||||
account_count = 1 + len(self._account_managers)
|
||||
logger.info(f"[Nostr] Channel connected. npub: {self._crypto.npub}, 账户数: {account_count}")
|
||||
@ -192,7 +198,7 @@ class NostrAdapter(BaseChannelAdapter):
|
||||
guard_policy = self._build_guard_policy(cfg)
|
||||
self._guard = NostrGuard(self._crypto.pubkey_hex(), guard_policy)
|
||||
|
||||
self._restore_state()
|
||||
await self._restore_state()
|
||||
|
||||
async def _init_additional_account(self, account_id: str, cfg: NostrConfig) -> None:
|
||||
try:
|
||||
@ -247,20 +253,28 @@ class NostrAdapter(BaseChannelAdapter):
|
||||
allow_from=cfg.allow_from,
|
||||
)
|
||||
|
||||
def _restore_state(self) -> None:
|
||||
saved_events = self._state_store.load_seen_events()
|
||||
async def _restore_state(self) -> None:
|
||||
saved_events = await self.state_get("seen_events", namespace="dedup")
|
||||
if saved_events is None:
|
||||
saved_events = self._state_store.load_seen_events()
|
||||
for event_id in saved_events:
|
||||
self._guard._seen.mark_seen(event_id)
|
||||
saved_inflight = self._state_store.load_inflight()
|
||||
|
||||
saved_inflight = await self.state_get("inflight", namespace="dedup")
|
||||
if saved_inflight is None:
|
||||
saved_inflight = self._state_store.load_inflight()
|
||||
for event_id in saved_inflight:
|
||||
self._guard._inflight.add(event_id)
|
||||
|
||||
if saved_events or saved_inflight:
|
||||
logger.info(
|
||||
"[Nostr] 从 state_store 恢复状态: seen=%s, inflight=%s",
|
||||
len(saved_events),
|
||||
len(saved_inflight),
|
||||
)
|
||||
last_ts = self._state_store.load_last_processed_at()
|
||||
last_ts = await self.state_get("last_processed_at", namespace="connection")
|
||||
if last_ts is None:
|
||||
last_ts = self._state_store.load_last_processed_at()
|
||||
if last_ts and self._monitor._last_subscribe_ts == 0:
|
||||
self._monitor._last_subscribe_ts = last_ts
|
||||
|
||||
@ -296,16 +310,42 @@ class NostrAdapter(BaseChannelAdapter):
|
||||
if failed:
|
||||
logger.warning(f"[Nostr] NIP-42 AUTH 最终失败 {len(failed)} 个 Relay: {failed}")
|
||||
|
||||
total_urls = len(auth_urls)
|
||||
success_count = total_urls - len(failed)
|
||||
self._metrics.increment("nip42_auth.success", success_count)
|
||||
self._metrics.increment("nip42_auth.failure", len(failed))
|
||||
self._metrics.set_gauge("nip42_auth.success_rate", success_count / max(total_urls, 1))
|
||||
|
||||
if failed and len(failed) == total_urls:
|
||||
logger.warning("[Nostr] 所有 Relay NIP-42 AUTH 失败,适配器进入 degraded 状态")
|
||||
self._status = ChannelStatus.CONNECTED
|
||||
for url in failed:
|
||||
self._health_tracker.record_failure(url)
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
if self._ordering_task:
|
||||
self._ordering_task.cancel()
|
||||
self._ordering_task = None
|
||||
|
||||
if self._score_sync_task:
|
||||
self._score_sync_task.cancel()
|
||||
self._score_sync_task = None
|
||||
|
||||
if self._state_store:
|
||||
self._state_store.save_last_processed_at(self._monitor._last_subscribe_ts if self._monitor else 0)
|
||||
last_ts = self._monitor._last_subscribe_ts if self._monitor else 0
|
||||
self._state_store.save_last_processed_at(last_ts)
|
||||
await self.state_set("last_processed_at", last_ts, namespace="connection")
|
||||
|
||||
self._state_store.save_send_cache(self._send_cache.to_dict_list())
|
||||
send_cache_data = self._send_cache.to_dict_list()
|
||||
await self.state_set("send_cache", send_cache_data, namespace="cache", ttl_seconds=3600)
|
||||
|
||||
if self._guard:
|
||||
seen_events = list(self._guard._seen._store.keys())
|
||||
self._state_store.save_inflight(list(self._guard.inflight))
|
||||
inflight_data = list(self._guard.inflight)
|
||||
await self.state_set("seen_events", seen_events, namespace="dedup", ttl_seconds=600)
|
||||
await self.state_set("inflight", inflight_data, namespace="dedup", ttl_seconds=600)
|
||||
tasks = []
|
||||
if self._monitor:
|
||||
tasks.append(self._monitor.stop())
|
||||
@ -390,20 +430,18 @@ class NostrAdapter(BaseChannelAdapter):
|
||||
if not self._sender:
|
||||
return DeliveryResult(success=False, error="Sender 未初始化")
|
||||
|
||||
target_pubkey = msg_id
|
||||
if ":" in msg_id:
|
||||
parts = msg_id.split(":")
|
||||
target_pubkey = parts[-1]
|
||||
return await self._sender.send_reaction(chat_id, target_pubkey, emoji)
|
||||
target_pubkey = ""
|
||||
if ":" in chat_id:
|
||||
target_pubkey = chat_id.split(":")[-1]
|
||||
return await self._sender.send_reaction(msg_id, target_pubkey, emoji)
|
||||
|
||||
async def remove_reaction(self, chat_id: str, msg_id: str) -> DeliveryResult:
|
||||
if not self._sender:
|
||||
return DeliveryResult(success=False, error="Sender 未初始化")
|
||||
|
||||
target_pubkey = msg_id
|
||||
if ":" in msg_id:
|
||||
parts = msg_id.split(":")
|
||||
target_pubkey = parts[-1]
|
||||
target_pubkey = ""
|
||||
if ":" in chat_id:
|
||||
target_pubkey = chat_id.split(":")[-1]
|
||||
return await self._sender.remove_reaction(msg_id, target_pubkey)
|
||||
|
||||
async def send_chat_action(self, chat_id: str, action: str) -> DeliveryResult:
|
||||
@ -424,43 +462,38 @@ class NostrAdapter(BaseChannelAdapter):
|
||||
|
||||
if streaming_mode == "off":
|
||||
if finished:
|
||||
identity = self._build_stream_identity(chat_id, msg_id)
|
||||
response = ChannelResponse(identity=identity, content=chunk)
|
||||
return await self.send(response)
|
||||
return await self._flush_stream_final(chat_id, msg_id, chunk, receiver_pubkey)
|
||||
return DeliveryResult(success=True, message_id=None)
|
||||
|
||||
if streaming_mode == "block":
|
||||
if finished:
|
||||
if msg_id and self._sender:
|
||||
return await self._sender.send_edit(
|
||||
original_event_id=msg_id,
|
||||
new_content=chunk,
|
||||
receiver_pubkey=receiver_pubkey,
|
||||
)
|
||||
identity = self._build_stream_identity(chat_id, msg_id)
|
||||
response = ChannelResponse(identity=identity, content=chunk)
|
||||
return await self.send(response)
|
||||
return await self._flush_stream_final(chat_id, msg_id, chunk, receiver_pubkey)
|
||||
return DeliveryResult(success=True, message_id=None)
|
||||
|
||||
# progress 模式:每块都发送编辑
|
||||
if finished:
|
||||
if msg_id and self._sender:
|
||||
return await self._sender.send_edit(
|
||||
original_event_id=msg_id,
|
||||
new_content=chunk,
|
||||
receiver_pubkey=receiver_pubkey,
|
||||
)
|
||||
identity = self._build_stream_identity(chat_id, msg_id)
|
||||
response = ChannelResponse(identity=identity, content=chunk)
|
||||
return await self.send(response)
|
||||
return await self._flush_stream_final(chat_id, msg_id, chunk, receiver_pubkey)
|
||||
return await self._flush_stream_edit_or_send(chat_id, msg_id, chunk, receiver_pubkey)
|
||||
|
||||
async def _flush_stream_final(self, chat_id: str, msg_id: str, chunk: str, receiver_pubkey: str) -> DeliveryResult:
|
||||
if msg_id and self._sender:
|
||||
return await self._sender.send_edit(
|
||||
original_event_id=msg_id,
|
||||
new_content=chunk,
|
||||
receiver_pubkey=receiver_pubkey,
|
||||
)
|
||||
identity = self._build_stream_identity(chat_id, msg_id)
|
||||
response = ChannelResponse(identity=identity, content=chunk)
|
||||
return await self.send(response)
|
||||
|
||||
async def _flush_stream_edit_or_send(
|
||||
self, chat_id: str, msg_id: str, chunk: str, receiver_pubkey: str
|
||||
) -> DeliveryResult:
|
||||
if msg_id and self._sender:
|
||||
return await self._sender.send_edit(
|
||||
original_event_id=msg_id,
|
||||
new_content=chunk,
|
||||
receiver_pubkey=receiver_pubkey,
|
||||
)
|
||||
identity = self._build_stream_identity(chat_id, msg_id)
|
||||
response = ChannelResponse(identity=identity, content=chunk)
|
||||
return await self.send(response)
|
||||
@ -807,22 +840,26 @@ class NostrAdapter(BaseChannelAdapter):
|
||||
try:
|
||||
decrypted = await self._crypto.decrypt_nip17(raw_event.get("content", ""))
|
||||
raw_event = {**raw_event, "content": decrypted, "kind": 1059}
|
||||
self._metrics.record_decrypt_success("nip17")
|
||||
if self._guard:
|
||||
plaintext_reject = self._guard.check_plaintext_size(decrypted)
|
||||
if plaintext_reject:
|
||||
logger.debug("NIP-17 plaintext rejected: %s", plaintext_reject)
|
||||
raw_event["content"] = f"(plaintext too large: {len(decrypted)} bytes)"
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
self._metrics.record_decrypt_failure("nip17")
|
||||
logger.warning(
|
||||
"NIP-17 解密失败: event=%s, sender=%s, error=%s",
|
||||
event_id[:8],
|
||||
raw_event.get("pubkey", "?")[:8],
|
||||
e,
|
||||
)
|
||||
return
|
||||
|
||||
if kind == 4 and self._crypto:
|
||||
try:
|
||||
content = self._crypto.decrypt_nip04(raw_event.get("content", ""), raw_event.get("pubkey", ""))
|
||||
self._metrics.record_decrypt_success("nip04")
|
||||
if self._guard:
|
||||
plaintext_reject = self._guard.check_plaintext_size(content)
|
||||
if plaintext_reject:
|
||||
@ -830,15 +867,28 @@ class NostrAdapter(BaseChannelAdapter):
|
||||
content = f"(plaintext too large: {len(content)} bytes)"
|
||||
raw_event["content"] = content
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
self._metrics.record_decrypt_failure("nip04")
|
||||
logger.warning(
|
||||
"NIP-04 解密失败: event=%s, sender=%s, error=%s",
|
||||
event_id[:8],
|
||||
raw_event.get("pubkey", "?")[:8],
|
||||
e,
|
||||
)
|
||||
return
|
||||
|
||||
self._metrics.record_event_accepted()
|
||||
message = self.normalize_inbound(raw_event)
|
||||
|
||||
if self._thread_simulator and kind not in (5, 7):
|
||||
try:
|
||||
thread_chain = await self._thread_simulator.build_thread_from_event(raw_event)
|
||||
if len(thread_chain) > 1:
|
||||
parent_ids = [e.get("id", "") for e in thread_chain[:-1]]
|
||||
message.metadata = message.metadata or {}
|
||||
message.metadata["thread_parent_ids"] = parent_ids
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await self._handle_message(message)
|
||||
finally:
|
||||
if self._guard and event_id:
|
||||
@ -882,6 +932,16 @@ class NostrAdapter(BaseChannelAdapter):
|
||||
self._pending_tasks = [t for t in self._pending_tasks if not t.done()]
|
||||
self._pending_tasks.append(task)
|
||||
|
||||
async def _run_score_sync(self) -> None:
|
||||
interval = 30.0
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(interval)
|
||||
if self._relay_manager:
|
||||
self._relay_manager.sync_scores_from_health_tracker(self._health_tracker)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
def _resolve_state_dir(self) -> str:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
@ -53,6 +54,7 @@ class NostrConfig:
|
||||
message_ordering: bool = False,
|
||||
message_ordering_window_ms: int = 500,
|
||||
multi_account_enabled: bool = False,
|
||||
require_tls: bool = True,
|
||||
):
|
||||
self.private_key = private_key or ""
|
||||
self.relays = relays or [
|
||||
@ -80,6 +82,7 @@ class NostrConfig:
|
||||
self.message_ordering = message_ordering
|
||||
self.message_ordering_window_ms = message_ordering_window_ms
|
||||
self.multi_account_enabled = multi_account_enabled
|
||||
self.require_tls = require_tls
|
||||
self._validate()
|
||||
|
||||
def _validate(self):
|
||||
@ -104,6 +107,10 @@ class NostrConfig:
|
||||
for url in self.relays:
|
||||
if not url.startswith(("ws://", "wss://")):
|
||||
raise NostrConfigError(f"Relay URL 必须以 ws:// 或 wss:// 开头: {url}")
|
||||
if self.require_tls and url.startswith("ws://"):
|
||||
raise NostrConfigError(
|
||||
f"Relay URL 不允许明文 ws:// (require_tls=True): {url}。请使用 wss:// 或将 require_tls 设为 False。"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, config: dict[str, Any] | None) -> NostrConfig:
|
||||
@ -157,6 +164,88 @@ class NostrConfig:
|
||||
message_ordering=default_account.get("message_ordering", False),
|
||||
message_ordering_window_ms=default_account.get("message_ordering_window_ms", 500),
|
||||
multi_account_enabled=default_account.get("multi_account_enabled", False),
|
||||
require_tls=default_account.get("require_tls", True),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls, prefix: str = "NOSTR_") -> NostrConfig:
|
||||
def _env(key: str, default: Any = None) -> Any:
|
||||
return os.getenv(prefix + key, default)
|
||||
|
||||
def _env_bool(key: str, default: bool = False) -> bool:
|
||||
val = os.getenv(prefix + key)
|
||||
if val is None:
|
||||
return default
|
||||
return val.lower() in ("1", "true", "yes", "on")
|
||||
|
||||
def _env_int(key: str, default: int = 0) -> int:
|
||||
val = os.getenv(prefix + key)
|
||||
if val is None:
|
||||
return default
|
||||
try:
|
||||
return int(val)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
def _env_float(key: str, default: float = 0.0) -> float:
|
||||
val = os.getenv(prefix + key)
|
||||
if val is None:
|
||||
return default
|
||||
try:
|
||||
return float(val)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
def _env_list(key: str, default: list[str] | None = None) -> list[str] | None:
|
||||
val = os.getenv(prefix + key)
|
||||
if val is None:
|
||||
return default
|
||||
return [item.strip() for item in val.split(",") if item.strip()]
|
||||
|
||||
relays = _env_list("RELAYS") or [
|
||||
"wss://relay.damus.io",
|
||||
"wss://relay.primal.net",
|
||||
"wss://relay.nostr.info",
|
||||
"wss://nos.lol",
|
||||
]
|
||||
|
||||
guard_policy = GuardPolicyConfig(
|
||||
allowed_kinds=_env_list("GUARD_ALLOWED_KINDS") or [1, 4, 5, 7, 1059],
|
||||
max_ciphertext_bytes=_env_int("GUARD_MAX_CIPHERTEXT_BYTES", 50_000),
|
||||
max_plaintext_bytes=_env_int("GUARD_MAX_PLAINTEXT_BYTES", 10_000),
|
||||
max_future_skew_sec=_env_int("GUARD_MAX_FUTURE_SKEW_SEC", 30),
|
||||
rate_limit=RateLimitConfig(
|
||||
window_ms=_env_int("RATE_LIMIT_WINDOW_MS", 10_000),
|
||||
max_per_sender_per_window=_env_int("RATE_LIMIT_MAX_PER_SENDER", 20),
|
||||
max_global_per_window=_env_int("RATE_LIMIT_MAX_GLOBAL", 200),
|
||||
),
|
||||
)
|
||||
|
||||
allow_from = _env_list("ALLOW_FROM")
|
||||
|
||||
return cls(
|
||||
private_key=_env("PRIVATE_KEY", ""),
|
||||
relays=relays,
|
||||
dm_policy=_env("DM_POLICY", "pairing"),
|
||||
nip17_enabled=_env_bool("NIP17_ENABLED", True),
|
||||
streaming_mode=_env("STREAMING_MODE", "block"),
|
||||
reconnect_interval_sec=_env_int("RECONNECT_INTERVAL_SEC", 5),
|
||||
relay_timeout_sec=_env_int("RELAY_TIMEOUT_SEC", 30),
|
||||
relay_degraded_threshold=_env_float("RELAY_DEGRADED_THRESHOLD", 0.5),
|
||||
nip42_auth_enabled=_env_bool("NIP42_AUTH_ENABLED", False),
|
||||
nip42_auth_urls=_env_list("NIP42_AUTH_URLS"),
|
||||
guard_policy=guard_policy,
|
||||
backfill_window_sec=_env_int("BACKFILL_WINDOW_SEC", 120),
|
||||
allow_from=allow_from,
|
||||
markdown_table_mode=_env("MARKDOWN_TABLE_MODE", "off"),
|
||||
profile=None,
|
||||
enabled=_env_bool("ENABLED", True),
|
||||
name=_env("NAME", ""),
|
||||
send_message_cache_size=_env_int("SEND_CACHE_SIZE", 100),
|
||||
message_ordering=_env_bool("MESSAGE_ORDERING", False),
|
||||
message_ordering_window_ms=_env_int("MESSAGE_ORDERING_WINDOW_MS", 500),
|
||||
multi_account_enabled=_env_bool("MULTI_ACCOUNT_ENABLED", False),
|
||||
require_tls=_env_bool("REQUIRE_TLS", True),
|
||||
)
|
||||
|
||||
def get_account_configs(self, raw_config: dict[str, Any] | None = None) -> dict[str, NostrConfig]:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json as _json
|
||||
import time
|
||||
from collections import OrderedDict, defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
@ -7,8 +8,6 @@ from dataclasses import dataclass, field
|
||||
from yuxi.channels.adapters.nostr.crypto import normalize_pubkey
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
import json as _json
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditRecord:
|
||||
|
||||
@ -55,7 +55,7 @@ class NostrMonitor:
|
||||
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}]
|
||||
filters = [{"kinds": [1, 4, 5, 7, 1059], "since": since, "until": now + 60}]
|
||||
self._relay_manager.set_pubkey_filter([self._crypto.pubkey_hex()])
|
||||
await self._relay_manager.subscribe(filters)
|
||||
self._subscription_active = True
|
||||
|
||||
@ -2,17 +2,16 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import json as _json
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
|
||||
from yuxi.channels.adapters.nostr.relay_manager import RelayManager
|
||||
from yuxi.channels.adapters.nostr.models import NostrProfile
|
||||
from yuxi.channels.adapters.nostr.relay_manager import RelayManager
|
||||
from yuxi.channels.adapters.nostr.state_store import NostrStateStore
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
import json as _json
|
||||
|
||||
|
||||
def _validate_profile_url(url: str, field_name: str = "url") -> str | None:
|
||||
if not url:
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from yuxi.channels.adapters.nostr.models import NostrProfile
|
||||
|
||||
@ -296,9 +296,19 @@ class RelayManager:
|
||||
pass
|
||||
self._schedule_reconnect(url)
|
||||
|
||||
async def query(self, filters: list[dict], timeout: float = 10.0) -> list[dict]:
|
||||
async def query(
|
||||
self, filters: list[dict], timeout: float = 10.0, since: int | None = None, until: int | None = None
|
||||
) -> list[dict]:
|
||||
sub_id = "forcepilot_nostr_query"
|
||||
req = json.dumps(["REQ", sub_id, *filters])
|
||||
effective_filters: list[dict] = []
|
||||
for f in filters:
|
||||
f_copy = dict(f)
|
||||
if since is not None:
|
||||
f_copy["since"] = since
|
||||
if until is not None:
|
||||
f_copy["until"] = until
|
||||
effective_filters.append(f_copy)
|
||||
req = json.dumps(["REQ", sub_id, *effective_filters])
|
||||
close_req = json.dumps(["CLOSE", sub_id])
|
||||
|
||||
active_ws = [(url, ws) for url, ws in self._connections.items() if ws and ws.open]
|
||||
@ -329,6 +339,11 @@ class RelayManager:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
await _ws.send(close_req)
|
||||
except Exception:
|
||||
pass
|
||||
return results
|
||||
|
||||
receive_tasks = [_receive_from_relay(url, ws) for url, ws in active_ws]
|
||||
@ -343,12 +358,6 @@ class RelayManager:
|
||||
seen.add(event_id)
|
||||
events.append(event)
|
||||
|
||||
for url, ws in active_ws:
|
||||
try:
|
||||
await ws.send(close_req)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return events
|
||||
|
||||
async def send_auth(self, raw_signed_auth_event: dict, urls: list[str] | None = None) -> dict[str, bool]:
|
||||
@ -378,5 +387,15 @@ class RelayManager:
|
||||
def set_relay_scores(self, scores: dict[str, float]) -> None:
|
||||
self._relay_scores.update(scores)
|
||||
|
||||
def sync_scores_from_health_tracker(self, health_tracker, urls: list[str] | None = None) -> None:
|
||||
target_urls = urls or list(self._relay_urls)
|
||||
for url in target_urls:
|
||||
score = health_tracker.get_score(url)
|
||||
self._relay_scores[url] = score
|
||||
|
||||
def get_relay_score(self, url: str) -> float:
|
||||
return self._relay_scores.get(url, 0.5)
|
||||
|
||||
async def query_event(self, event_id: str, timeout: float = 10.0) -> dict | None:
|
||||
events = await self.query([{"ids": [event_id], "limit": 1}], timeout=timeout)
|
||||
return events[0] if events else None
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json as _json
|
||||
import re
|
||||
import time
|
||||
|
||||
from yuxi.channels.models import ChannelResponse, DeliveryResult
|
||||
from yuxi.channels.adapters.nostr.crypto import NostrCrypto, NostrCryptoError
|
||||
from yuxi.channels.adapters.nostr.config import NostrConfig
|
||||
from yuxi.channels.adapters.nostr.crypto import NostrCrypto, NostrCryptoError
|
||||
from yuxi.channels.adapters.nostr.relay_manager import RelayManager
|
||||
from yuxi.channels.models import ChannelResponse, DeliveryResult
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
|
||||
@ -22,8 +23,9 @@ class NostrSender:
|
||||
async def send(self, response: ChannelResponse) -> DeliveryResult:
|
||||
try:
|
||||
chunks = self._chunk_text(response.content)
|
||||
total = len(chunks)
|
||||
results = []
|
||||
for chunk in chunks:
|
||||
for i, chunk in enumerate(chunks):
|
||||
chunk_response = ChannelResponse(
|
||||
identity=response.identity,
|
||||
content=chunk,
|
||||
@ -31,8 +33,14 @@ class NostrSender:
|
||||
reply_to_message_id=response.reply_to_message_id,
|
||||
)
|
||||
event = await self.format_outbound(chunk_response)
|
||||
if total > 1:
|
||||
tags = event.get("tags", [])
|
||||
tags.append(["chunk", f"{i + 1}/{total}"])
|
||||
event["tags"] = tags
|
||||
success_count = await self._relay_manager.broadcast(event)
|
||||
results.append(success_count > 0)
|
||||
if total > 1 and i < total - 1:
|
||||
await asyncio.sleep(0.2)
|
||||
all_success = all(results)
|
||||
event = await self.format_outbound(response)
|
||||
event_id = event.get("id", "")
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from yuxi.channels.adapters.nostr.crypto import NostrCrypto, NostrCryptoError
|
||||
from yuxi.channels.adapters.nostr.probe import probe_relay
|
||||
from yuxi.utils.logging_config import logger
|
||||
import asyncio
|
||||
|
||||
|
||||
class NostrSetupAdapter:
|
||||
|
||||
@ -2,9 +2,9 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from yuxi.channels.adapters.nostr.setup import NostrSetupAdapter
|
||||
from yuxi.channels.adapters.nostr.crypto import NostrCrypto, NostrCryptoError
|
||||
from yuxi.channels.adapters.nostr.config import NostrConfig
|
||||
from yuxi.channels.adapters.nostr.crypto import NostrCrypto, NostrCryptoError
|
||||
from yuxi.channels.adapters.nostr.setup import NostrSetupAdapter
|
||||
|
||||
|
||||
class NostrSetupPlugin:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user