新增 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: 状态存储
133 lines
3.7 KiB
Python
133 lines
3.7 KiB
Python
import json
|
|
import random
|
|
import time
|
|
from dataclasses import dataclass
|
|
|
|
from coincurve import PrivateKey
|
|
|
|
from .event_utils import sign_event
|
|
from .nip44 import nip44_encrypt, nip44_decrypt
|
|
|
|
SEAL_KIND = 13
|
|
GIFT_WRAP_KIND = 1059
|
|
DM_CHAT_KIND = 14
|
|
DM_FILE_KIND = 15
|
|
CREATED_AT_JITTER_SEC = 172800
|
|
|
|
|
|
@dataclass
|
|
class GiftWrapResult:
|
|
event: dict
|
|
gift_wrap_pubkey: str
|
|
|
|
|
|
@dataclass
|
|
class UnwrappedDM:
|
|
pubkey: str
|
|
kind: int
|
|
content: str
|
|
tags: list[list[str]]
|
|
created_at: int
|
|
|
|
|
|
def _jittered_now() -> int:
|
|
offset = random.randint(-CREATED_AT_JITTER_SEC, CREATED_AT_JITTER_SEC)
|
|
return int(time.time()) + offset
|
|
|
|
|
|
def create_rumor(
|
|
sender_pk: str, content: str,
|
|
kind: int = DM_CHAT_KIND,
|
|
tags: list[list[str]] | None = None,
|
|
created_at: int | None = None,
|
|
) -> dict:
|
|
return {
|
|
"pubkey": sender_pk,
|
|
"created_at": created_at if created_at is not None else int(time.time()),
|
|
"kind": kind,
|
|
"tags": tags or [],
|
|
"content": content,
|
|
}
|
|
|
|
|
|
def create_seal(sender_sk_hex: str, sender_pk: str, receiver_pk: str, rumor: dict) -> dict:
|
|
if isinstance(sender_sk_hex, str) and len(sender_sk_hex) == 64:
|
|
sk_bytes = bytes.fromhex(sender_sk_hex)
|
|
else:
|
|
sk_bytes = sender_sk_hex
|
|
if isinstance(sk_bytes, str):
|
|
sk_bytes = bytes.fromhex(sk_bytes)
|
|
rumor_json = json.dumps(rumor, separators=(",", ":"), ensure_ascii=False)
|
|
encrypted_rumor = nip44_encrypt(sk_bytes, receiver_pk, rumor_json)
|
|
return sign_event(
|
|
sk=sk_bytes,
|
|
pubkey=sender_pk,
|
|
kind=SEAL_KIND,
|
|
content=encrypted_rumor,
|
|
tags=[],
|
|
)
|
|
|
|
|
|
def create_gift_wrap(
|
|
sender_sk_hex: str, sender_pk: str, receiver_pk: str,
|
|
content: str, kind: int = DM_CHAT_KIND,
|
|
tags: list[list[str]] | None = None,
|
|
) -> GiftWrapResult:
|
|
rumor = create_rumor(sender_pk, content, kind, tags)
|
|
seal = create_seal(sender_sk_hex, sender_pk, receiver_pk, rumor)
|
|
|
|
random_sk = PrivateKey()
|
|
random_pk_bytes = random_sk.public_key.format(compressed=False)[1:33]
|
|
random_pk_hex = random_pk_bytes.hex()
|
|
random_sk_hex = random_sk.secret.hex() if hasattr(random_sk.secret, 'hex') else random_sk.secret
|
|
if isinstance(random_sk_hex, str):
|
|
random_sk_bytes = bytes.fromhex(random_sk_hex)
|
|
else:
|
|
random_sk_bytes = random_sk_hex
|
|
|
|
seal_json = json.dumps(seal, separators=(",", ":"), ensure_ascii=False)
|
|
encrypted_seal = nip44_encrypt(random_sk_bytes, receiver_pk, seal_json)
|
|
|
|
wrap_event = sign_event(
|
|
sk=random_sk_bytes,
|
|
pubkey=random_pk_hex,
|
|
kind=GIFT_WRAP_KIND,
|
|
content=encrypted_seal,
|
|
tags=[["p", receiver_pk]],
|
|
)
|
|
wrap_event["created_at"] = _jittered_now()
|
|
|
|
return GiftWrapResult(event=wrap_event, gift_wrap_pubkey=random_pk_hex)
|
|
|
|
|
|
def unwrap_gift_wrap(sk_bytes: bytes, gift_wrap_event: dict) -> UnwrappedDM | None:
|
|
try:
|
|
content = gift_wrap_event.get("content", "")
|
|
wrap_pubkey = gift_wrap_event.get("pubkey", "")
|
|
|
|
seal_json = nip44_decrypt(sk_bytes, wrap_pubkey, content)
|
|
seal = json.loads(seal_json)
|
|
|
|
if seal.get("kind") != SEAL_KIND:
|
|
return None
|
|
if seal.get("tags"):
|
|
return None
|
|
|
|
sender_pk = seal.get("pubkey", "")
|
|
encrypted_rumor = seal.get("content", "")
|
|
rumor_json = nip44_decrypt(sk_bytes, sender_pk, encrypted_rumor)
|
|
rumor = json.loads(rumor_json)
|
|
|
|
if rumor.get("pubkey") != sender_pk:
|
|
return None
|
|
|
|
return UnwrappedDM(
|
|
pubkey=sender_pk,
|
|
kind=rumor.get("kind", DM_CHAT_KIND),
|
|
content=rumor.get("content", ""),
|
|
tags=rumor.get("tags", []),
|
|
created_at=rumor.get("created_at", 0),
|
|
)
|
|
except Exception:
|
|
return None
|