ForcePilot/backend/package/yuxi/channel/extensions/nostr/gift_wrap.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

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