ForcePilot/backend/package/yuxi/channels/adapters/nostr/config.py
Kris 7002557cf4 refactor(nostr): 整理代码结构并新增多项功能优化
1.  整理多个文件的导入顺序,移除冗余空行和重复导入
2.  为Nostr订阅添加until参数防止重复拉取
3.  实现长消息分块发送并添加间隔等待
4.  新增Relay健康分数同步任务
5.  新增TLS强制检查配置项并支持从环境变量加载
6.  重构状态恢复逻辑适配异步存储
7.  修复反应发送的参数错误
8.  新增线程模拟自动补全消息上下文
9.  添加解密指标统计和更完善的错误日志
2026-05-13 16:13:03 +08:00

280 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import os
from dataclasses import dataclass, field
from typing import Any
class NostrConfigError(Exception):
pass
@dataclass
class RateLimitConfig:
window_ms: int = 10_000
max_per_sender_per_window: int = 20
max_global_per_window: int = 200
@dataclass
class GuardPolicyConfig:
allowed_kinds: list[int] = field(default_factory=lambda: [1, 4, 5, 7, 1059])
max_ciphertext_bytes: int = 50_000
max_plaintext_bytes: int = 10_000
max_future_skew_sec: int = 30
rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig)
class NostrConfig:
VALID_DM_POLICIES = {"pairing", "open", "whitelist", "allowlist", "disabled"}
VALID_STREAMING_MODES = {"off", "block", "progress"}
VALID_MARKDOWN_TABLE_MODES = {"off", "convert"}
CHANNEL_ENV_VARS = ["NOSTR_PRIVATE_KEY"]
def __init__(
self,
private_key: str = "",
relays: list[str] | None = None,
dm_policy: str = "pairing",
nip17_enabled: bool = True,
streaming_mode: str = "block",
reconnect_interval_sec: int = 5,
relay_timeout_sec: int = 30,
relay_degraded_threshold: float = 0.5,
nip42_auth_enabled: bool = False,
nip42_auth_urls: list[str] | None = None,
guard_policy: GuardPolicyConfig | None = None,
backfill_window_sec: int = 120,
allow_from: list[str] | None = None,
markdown_table_mode: str = "off",
profile: dict | None = None,
enabled: bool = True,
name: str = "",
send_message_cache_size: int = 100,
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 [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://relay.nostr.info",
"wss://nos.lol",
]
self.dm_policy = dm_policy
self.nip17_enabled = nip17_enabled
self.streaming_mode = streaming_mode
self.reconnect_interval_sec = reconnect_interval_sec
self.relay_timeout_sec = relay_timeout_sec
self.relay_degraded_threshold = relay_degraded_threshold
self.nip42_auth_enabled = nip42_auth_enabled
self.nip42_auth_urls = nip42_auth_urls or []
self.guard_policy = guard_policy or GuardPolicyConfig()
self.backfill_window_sec = backfill_window_sec
self.allow_from = allow_from or []
self.markdown_table_mode = markdown_table_mode
self.profile = profile
self.enabled = enabled
self.name = name
self.send_message_cache_size = send_message_cache_size
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):
if self.dm_policy == "whitelist":
self.dm_policy = "allowlist"
if self.dm_policy not in self.VALID_DM_POLICIES:
raise NostrConfigError(f"dm_policy 值无效: '{self.dm_policy}',应为 {self.VALID_DM_POLICIES}")
if self.streaming_mode not in self.VALID_STREAMING_MODES:
raise NostrConfigError(f"streaming_mode 值无效: '{self.streaming_mode}',应为 {self.VALID_STREAMING_MODES}")
if self.markdown_table_mode not in self.VALID_MARKDOWN_TABLE_MODES:
raise NostrConfigError(
f"markdown_table_mode 值无效: '{self.markdown_table_mode}',应为 {self.VALID_MARKDOWN_TABLE_MODES}"
)
if self.reconnect_interval_sec < 1:
raise NostrConfigError(f"reconnect_interval_sec 不能小于 1当前值: {self.reconnect_interval_sec}")
if self.relay_timeout_sec < 1:
raise NostrConfigError(f"relay_timeout_sec 不能小于 1当前值: {self.relay_timeout_sec}")
if not (0 < self.relay_degraded_threshold <= 1):
raise NostrConfigError(
f"relay_degraded_threshold 应在 (0, 1] 范围内,当前值: {self.relay_degraded_threshold}"
)
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:
if not config:
return cls()
accounts = config.get("accounts", {})
default_account = accounts.get("default", config)
guard_dict = default_account.get("guard_policy", None)
guard_policy = None
if guard_dict:
rate_limit_dict = guard_dict.get("rate_limit", {})
if not rate_limit_dict:
rate_limit_dict = {
"window_ms": guard_dict.get("rate_limit_window_ms", 10_000),
"max_per_sender_per_window": guard_dict.get("rate_limit_max_per_sender_per_window", 20),
"max_global_per_window": guard_dict.get("rate_limit_max_global_per_window", 200),
}
rate_limit = RateLimitConfig(
window_ms=rate_limit_dict.get("window_ms", 10_000),
max_per_sender_per_window=rate_limit_dict.get("max_per_sender_per_window", 20),
max_global_per_window=rate_limit_dict.get("max_global_per_window", 200),
)
guard_policy = GuardPolicyConfig(
allowed_kinds=guard_dict.get("allowed_kinds", [1, 4, 5, 7, 1059]),
max_ciphertext_bytes=guard_dict.get("max_ciphertext_bytes", 50_000),
max_plaintext_bytes=guard_dict.get("max_plaintext_bytes", 10_000),
max_future_skew_sec=guard_dict.get("max_future_skew_sec", 30),
rate_limit=rate_limit,
)
return cls(
private_key=default_account.get("private_key", ""),
relays=default_account.get("relays"),
dm_policy=default_account.get("dm_policy", "pairing"),
nip17_enabled=default_account.get("nip17_enabled", True),
streaming_mode=default_account.get("streaming_mode", "block"),
reconnect_interval_sec=default_account.get("reconnect_interval_sec", 5),
relay_timeout_sec=default_account.get("relay_timeout_sec", 30),
relay_degraded_threshold=default_account.get("relay_degraded_threshold", 0.5),
nip42_auth_enabled=default_account.get("nip42_auth_enabled", False),
nip42_auth_urls=default_account.get("nip42_auth_urls"),
guard_policy=guard_policy,
backfill_window_sec=default_account.get("backfill_window_sec", 120),
allow_from=default_account.get("allow_from"),
markdown_table_mode=default_account.get("markdown_table_mode", "off"),
profile=default_account.get("profile"),
enabled=default_account.get("enabled", True),
name=default_account.get("name", ""),
send_message_cache_size=default_account.get("send_message_cache_size", 100),
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]:
cfg = raw_config or {}
accounts = cfg.get("accounts", {})
if not accounts:
return {"default": NostrConfig.from_dict(cfg) if cfg else NostrConfig()}
result: dict[str, NostrConfig] = {}
for account_id, account_cfg in accounts.items():
if isinstance(account_cfg, dict) and account_cfg.get("enabled", True) is not False:
result[account_id] = NostrConfig.from_dict({"accounts": {account_id: account_cfg}})
if not result:
result["default"] = NostrConfig()
return result
def resolve_default_account_id(self) -> str:
return "default"
def list_account_ids(self, config: dict | None = None) -> list[str]:
cfg = config or {}
accounts = cfg.get("accounts", {})
if not accounts:
return ["default"]
result = []
for account_id, account_cfg in accounts.items():
if isinstance(account_cfg, dict) and account_cfg.get("enabled", True) is not False:
result.append(account_id)
if not result:
return ["default"]
return result