该提交新增了完整的 Nostr 去中心化社交网络适配器实现,包含以下核心功能: 1. 基础加密与密钥处理:支持 nsec/npub/hex 格式密钥转换,实现 NIP-04/NIP-17 加解密 2. Relay 管理与健康监控:支持多 Relay 连接、自动重连、健康评分与自动选优 3. 事件与消息处理:实现事件校验、去重、速率限制、消息缓存与状态持久化 4. 个人资料管理:支持发布/导入/合并 Nostr Kind 0 元数据事件 5. 配对机制:实现安全的双向配对通信流程 6. Zap 功能:支持 NIP-57 打赏请求与收据解析 7. NIP-05 验证:实现域名身份验证 8. 配置系统:完整的配置校验与多账户支持 9. 监控与指标:提供连接监控、事件统计与健康快照 10. HTTP API:提供个人资料管理的 RESTful 接口 11. 安装向导:命令行配置向导与初始化流程 所有模块均遵循 Nostr 协议规范,支持多账户、多 Relay 部署,内置安全防护与流量控制机制。
189 lines
8.4 KiB
Python
189 lines
8.4 KiB
Python
from __future__ import annotations
|
||
|
||
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", "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,
|
||
):
|
||
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._validate()
|
||
|
||
def _validate(self):
|
||
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}")
|
||
|
||
@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),
|
||
)
|
||
|
||
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
|