新增 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: 状态存储
157 lines
4.9 KiB
Python
157 lines
4.9 KiB
Python
import re
|
|
|
|
import bech32
|
|
from coincurve import PrivateKey
|
|
|
|
|
|
def validate_private_key(key: str) -> bytes:
|
|
key = key.strip()
|
|
if key.startswith("nsec1"):
|
|
hrp, data = bech32.bech32_decode(key)
|
|
if hrp is None or data is None:
|
|
raise ValueError("Invalid nsec1 format")
|
|
sk_bytes = bytes(bech32.convertbits(data, 5, 8, False))
|
|
elif re.match(r"^[0-9a-fA-F]{64}$", key):
|
|
sk_bytes = bytes.fromhex(key)
|
|
else:
|
|
raise ValueError("Invalid private key format: expected nsec1... or 64-char hex")
|
|
if len(sk_bytes) != 32:
|
|
raise ValueError(f"Private key must be 32 bytes, got {len(sk_bytes)}")
|
|
return sk_bytes
|
|
|
|
|
|
def get_public_key(sk: str | bytes) -> str:
|
|
if isinstance(sk, str):
|
|
sk = validate_private_key(sk)
|
|
pk = PrivateKey(sk)
|
|
pubkey_bytes = pk.public_key.format(compressed=False)
|
|
return pubkey_bytes[1:33].hex()
|
|
|
|
|
|
def normalize_pubkey(pubkey: str) -> str:
|
|
pubkey = pubkey.strip()
|
|
if pubkey.startswith("npub1"):
|
|
hrp, data = bech32.bech32_decode(pubkey)
|
|
if hrp is None or data is None:
|
|
raise ValueError(f"Invalid npub1 format: {pubkey[:20]}...")
|
|
pk_bytes = bytes(bech32.convertbits(data, 5, 8, False))
|
|
return pk_bytes.hex().lower()
|
|
if re.match(r"^[0-9a-fA-F]{64}$", pubkey):
|
|
return pubkey.lower()
|
|
raise ValueError(f"Invalid pubkey format: {pubkey[:20]}...")
|
|
|
|
|
|
def pubkey_to_npub(hex_pk: str) -> str:
|
|
data = bech32.convertbits(bytes.fromhex(hex_pk), 8, 5, True)
|
|
return bech32.bech32_encode("npub", data)
|
|
|
|
|
|
def is_valid_pubkey(pubkey: str) -> bool:
|
|
try:
|
|
normalize_pubkey(pubkey)
|
|
return True
|
|
except (ValueError, IndexError, TypeError):
|
|
return False
|
|
|
|
|
|
def _encode_tlv(items: list[tuple[int, bytes]]) -> bytes:
|
|
result = bytearray()
|
|
for typ, data in items:
|
|
result.append(typ)
|
|
result.extend(len(data).to_bytes(2, "big"))
|
|
result.extend(data)
|
|
return bytes(result)
|
|
|
|
|
|
def _decode_tlv(data: bytes) -> list[tuple[int, bytes]]:
|
|
items = []
|
|
i = 0
|
|
while i < len(data):
|
|
if i + 2 >= len(data):
|
|
break
|
|
typ = data[i]
|
|
length = int.from_bytes(data[i + 1:i + 3], "big")
|
|
value = data[i + 3:i + 3 + length]
|
|
items.append((typ, value))
|
|
i += 3 + length
|
|
return items
|
|
|
|
|
|
def pubkey_to_nprofile(hex_pk: str, relays: list[str] | None = None) -> str:
|
|
items = [(0, bytes.fromhex(hex_pk))]
|
|
if relays:
|
|
for r in relays:
|
|
items.append((1, r.encode("utf-8")))
|
|
data = _encode_tlv(items)
|
|
converted = bech32.convertbits(data, 8, 5, True)
|
|
return bech32.bech32_encode("nprofile", converted)
|
|
|
|
|
|
def event_to_nevent(
|
|
event_id_hex: str, relays: list[str] | None = None,
|
|
author_pk: str | None = None, kind: int | None = None,
|
|
) -> str:
|
|
items = [(0, bytes.fromhex(event_id_hex))]
|
|
if relays:
|
|
for r in relays:
|
|
items.append((1, r.encode("utf-8")))
|
|
if author_pk:
|
|
items.append((2, bytes.fromhex(author_pk)))
|
|
if kind is not None:
|
|
items.append((3, kind.to_bytes(4, "big")))
|
|
data = _encode_tlv(items)
|
|
converted = bech32.convertbits(data, 8, 5, True)
|
|
return bech32.bech32_encode("nevent", converted)
|
|
|
|
|
|
def kind_pk_d_to_naddr(kind: int, pubkey: str, d_tag: str = "", relays: list[str] | None = None) -> str:
|
|
items = [(0, d_tag.encode("utf-8"))]
|
|
if relays:
|
|
for r in relays:
|
|
items.append((1, r.encode("utf-8")))
|
|
items.append((2, bytes.fromhex(pubkey)))
|
|
items.append((3, kind.to_bytes(4, "big")))
|
|
data = _encode_tlv(items)
|
|
converted = bech32.convertbits(data, 8, 5, True)
|
|
return bech32.bech32_encode("naddr", converted)
|
|
|
|
|
|
def event_to_note(event_id_hex: str) -> str:
|
|
data = bech32.convertbits(bytes.fromhex(event_id_hex), 8, 5, True)
|
|
return bech32.bech32_encode("note", data)
|
|
|
|
|
|
def decode_nprofile(bech32_str: str) -> tuple[str, list[str]]:
|
|
hrp, data = bech32.bech32_decode(bech32_str)
|
|
if hrp is None or data is None:
|
|
raise ValueError("Invalid nprofile")
|
|
raw = bytes(bech32.convertbits(data, 5, 8, False))
|
|
items = _decode_tlv(raw)
|
|
pubkey = ""
|
|
relays = []
|
|
for typ, value in items:
|
|
if typ == 0:
|
|
pubkey = value.hex()
|
|
elif typ == 1:
|
|
relays.append(value.decode("utf-8"))
|
|
return pubkey, relays
|
|
|
|
|
|
def decode_nevent(bech32_str: str) -> dict:
|
|
hrp, data = bech32.bech32_decode(bech32_str)
|
|
if hrp is None or data is None:
|
|
raise ValueError("Invalid nevent")
|
|
raw = bytes(bech32.convertbits(data, 5, 8, False))
|
|
items = _decode_tlv(raw)
|
|
result: dict = {"event_id": "", "relays": [], "author": None, "kind": None}
|
|
for typ, value in items:
|
|
if typ == 0:
|
|
result["event_id"] = value.hex()
|
|
elif typ == 1:
|
|
result["relays"].append(value.decode("utf-8"))
|
|
elif typ == 2:
|
|
result["author"] = value.hex()
|
|
elif typ == 3:
|
|
result["kind"] = int.from_bytes(value, "big")
|
|
return result
|