ForcePilot/backend/package/yuxi/channel/extensions/nostr/key_utils.py
Kris 16455cb303 feat(channel): 添加 Nostr 渠道扩展
新增 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: 状态存储
2026-05-21 11:31:51 +08:00

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