Generalize the AUTH-gated, recipient-only delivery rule from NIP-04 to also cover NIP-17 kind 1059 gift wraps. When the relay is configured to require AUTH for kind 1059, only the AUTH'd recipient named in the event's `p` tag receives it; otherwise gift wraps broadcast like any regular event. - relay/event.py: add `is_seal`, `is_gift_wrap`, `is_private_message` helpers (kinds 13, 1059) - relay/client_connection.py: rename `_is_direct_message_for_other` -> `_is_private_event_for_other`; key off `is_private_message` so the same gating applies to kinds 4 and 1059 - relay/relay.py: advertise NIPs 17, 44, 59 in NIP-11 supported_nips - README: document NIP-17/44/59 transport-level support - tests/test_nip17.py: unit tests for kind classification, AUTH-gated 1059 delivery (recipient vs non-recipient vs unauthenticated), and regression coverage for kind 4 gating NIP-44 (encryption) and NIP-59 (wrap/seal) are client-side concerns; the relay treats payloads as opaque ciphertext and stores kind 1059 like any regular event. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
3.7 KiB
Python
132 lines
3.7 KiB
Python
"""
|
|
Unit tests for NIP-17 (Private Direct Messages) handling.
|
|
|
|
Covers:
|
|
- kind 1059 (gift wrap) and kind 13 (seal) classification on NostrEvent
|
|
- the AUTH-gated private-recipient delivery rule in NostrClientConnection
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from ..relay.client_connection import NostrClientConnection
|
|
from ..relay.event import NostrEvent
|
|
from ..relay.relay import RelaySpec
|
|
|
|
RELAY_ID = "relay_nip17"
|
|
RECIPIENT = "1111111111111111111111111111111111111111111111111111111111111111"
|
|
OTHER = "2222222222222222222222222222222222222222222222222222222222222222"
|
|
EPHEMERAL = "3333333333333333333333333333333333333333333333333333333333333333"
|
|
SIG = "0" * 128
|
|
|
|
|
|
def _gift_wrap_for(recipient: str) -> NostrEvent:
|
|
"""Build a kind 1059 event addressed to recipient. Skips signature validity."""
|
|
return NostrEvent(
|
|
id="0" * 64,
|
|
relay_id=RELAY_ID,
|
|
publisher=EPHEMERAL,
|
|
pubkey=EPHEMERAL,
|
|
created_at=0,
|
|
kind=1059,
|
|
tags=[["p", recipient]],
|
|
content="ciphertext",
|
|
sig=SIG,
|
|
)
|
|
|
|
|
|
def test_kind_classification_helpers():
|
|
seal = NostrEvent(
|
|
id="0" * 64,
|
|
relay_id=RELAY_ID,
|
|
publisher=OTHER,
|
|
pubkey=OTHER,
|
|
created_at=0,
|
|
kind=13,
|
|
tags=[],
|
|
content="",
|
|
sig=SIG,
|
|
)
|
|
wrap = _gift_wrap_for(RECIPIENT)
|
|
|
|
assert seal.is_seal
|
|
assert not seal.is_gift_wrap
|
|
assert not seal.is_private_message # seals carry no recipient metadata
|
|
|
|
assert wrap.is_gift_wrap
|
|
assert not wrap.is_seal
|
|
assert wrap.is_private_message
|
|
assert not wrap.is_ephemeral_event # nostrmarket relies on storage
|
|
|
|
|
|
def _make_connection(relay_spec: RelaySpec) -> NostrClientConnection:
|
|
conn = NostrClientConnection(relay_id=RELAY_ID, websocket=MagicMock())
|
|
conn.get_client_config = lambda: relay_spec
|
|
return conn
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"force_auth,auth_pubkey,event_recipient,expected_filtered",
|
|
[
|
|
# AUTH not required for 1059 -> never filtered (matches NIP-04 default)
|
|
(False, None, RECIPIENT, False),
|
|
(False, RECIPIENT, RECIPIENT, False),
|
|
(False, OTHER, RECIPIENT, False),
|
|
# AUTH required for 1059 -> only the recipient gets it
|
|
(True, None, RECIPIENT, True),
|
|
(True, RECIPIENT, RECIPIENT, False),
|
|
(True, OTHER, RECIPIENT, True),
|
|
],
|
|
)
|
|
def test_gift_wrap_auth_gated_delivery(
|
|
force_auth, auth_pubkey, event_recipient, expected_filtered
|
|
):
|
|
spec = RelaySpec(forcedAuthEvents=[1059] if force_auth else [])
|
|
conn = _make_connection(spec)
|
|
conn.auth_pubkey = auth_pubkey
|
|
|
|
wrap = _gift_wrap_for(event_recipient)
|
|
assert conn._is_private_event_for_other(wrap) is expected_filtered
|
|
|
|
|
|
def test_kind_4_dm_still_gated_under_auth():
|
|
"""Regression: the NIP-04 gating behavior must remain identical."""
|
|
spec = RelaySpec(forcedAuthEvents=[4])
|
|
conn = _make_connection(spec)
|
|
conn.auth_pubkey = OTHER
|
|
|
|
dm = NostrEvent(
|
|
id="0" * 64,
|
|
relay_id=RELAY_ID,
|
|
publisher=RECIPIENT,
|
|
pubkey=RECIPIENT,
|
|
created_at=0,
|
|
kind=4,
|
|
tags=[["p", RECIPIENT]],
|
|
content="ciphertext",
|
|
sig=SIG,
|
|
)
|
|
assert conn._is_private_event_for_other(dm) is True
|
|
|
|
conn.auth_pubkey = RECIPIENT
|
|
assert conn._is_private_event_for_other(dm) is False
|
|
|
|
|
|
def test_non_private_kinds_never_filtered():
|
|
spec = RelaySpec(forcedAuthEvents=[1059, 4])
|
|
conn = _make_connection(spec)
|
|
conn.auth_pubkey = OTHER
|
|
|
|
note = NostrEvent(
|
|
id="0" * 64,
|
|
relay_id=RELAY_ID,
|
|
publisher=RECIPIENT,
|
|
pubkey=RECIPIENT,
|
|
created_at=0,
|
|
kind=1,
|
|
tags=[["p", RECIPIENT]],
|
|
content="hello",
|
|
sig=SIG,
|
|
)
|
|
assert conn._is_private_event_for_other(note) is False
|