diff --git a/README.md b/README.md index d5f7ade..21ac93f 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,6 @@ - [x] Regular Events - [x] Replaceable Events - [x] Ephemeral Events -- [x] **NIP-17**: Private Direct Messages - - `kind: 1059` gift wraps stored and broadcast like regular events - - if `AUTH` enabled for `kind: 1059`: deliver only to the recipient - named in the `p` tag (same gating as NIP-04) - - encryption (NIP-44) and wrapping (NIP-59) are client-side concerns; - the relay handles transport only - [x] **NIP-20**: Command Results - todo: use correct prefixes - [x] **NIP-22**: Event created_at Limits @@ -54,12 +48,8 @@ - todo - [x] **NIP-42**: Authentication of clients to relays - todo: use correct prefix -- [x] **NIP-44**: Encrypted Payloads (Versioned) - - relay treats payloads as opaque; encryption is client-side - [ ] **NIP-50**: Search Capability - todo -- [x] **NIP-59**: Gift Wrap - - `kind: 13` (seal) and `kind: 1059` (gift wrap) accepted; unwrapping is client-side ## Create Relay diff --git a/relay/client_connection.py b/relay/client_connection.py index b9c9708..8b5fdfd 100644 --- a/relay/client_connection.py +++ b/relay/client_connection.py @@ -69,7 +69,7 @@ class NostrClientConnection: self.event_validator.get_client_config = get_client_config async def notify_event(self, event: NostrEvent) -> bool: - if self._is_private_event_for_other(event): + if self._is_direct_message_for_other(event): return False for nostr_filter in self.filters: @@ -83,14 +83,13 @@ class NostrClientConnection: ) return False - def _is_private_event_for_other(self, event: NostrEvent) -> bool: + def _is_direct_message_for_other(self, event: NostrEvent) -> bool: """ - p-tagged events that carry a single intended recipient (NIP-04 kind 4 - direct messages and NIP-17 kind 1059 gift wraps) should not be - broadcast to arbitrary subscribers when the relay enforces AUTH for - that kind. Deliver only to the AUTH'd recipient named in a `p` tag. + Direct messages are not inteded to be boradcast (even if encrypted). + If the server requires AUTH for kind '4' then direct message will be + sent only to the intended client. """ - if not event.is_private_message: + if not event.is_direct_message: return False if not self.config.event_requires_auth(event.kind): return False @@ -318,7 +317,7 @@ class NostrClientConnection: nostr_filter.enforce_limit(self.config.limit_per_filter) self.filters.append(nostr_filter) events = await get_events(self.relay_id, nostr_filter) - events = [e for e in events if not self._is_private_event_for_other(e)] + events = [e for e in events if not self._is_direct_message_for_other(e)] serialized_events = [ event.serialize_response(subscription_id) for event in events ] diff --git a/relay/event.py b/relay/event.py index b6343e0..8cee1b9 100644 --- a/relay/event.py +++ b/relay/event.py @@ -64,21 +64,6 @@ class NostrEvent(BaseModel): def is_delete_event(self) -> bool: return self.kind == 5 - @property - def is_seal(self) -> bool: - return self.kind == 13 - - @property - def is_gift_wrap(self) -> bool: - return self.kind == 1059 - - @property - def is_private_message(self) -> bool: - # Kinds whose payload addresses a single recipient via a `p` tag and is - # not meant to be broadcast to other subscribers when AUTH is enforced. - # NIP-04 (kind 4) and NIP-17 (kind 1059 gift wrap). - return self.is_direct_message or self.is_gift_wrap - @property def is_regular_event(self) -> bool: return self.kind >= 1000 and self.kind < 10000 diff --git a/relay/relay.py b/relay/relay.py index 29e0121..80677c7 100644 --- a/relay/relay.py +++ b/relay/relay.py @@ -116,7 +116,7 @@ class NostrRelay(BaseModel): ) -> dict: return { "contact": "https://t.me/lnbits", - "supported_nips": [1, 2, 4, 9, 11, 15, 16, 17, 20, 22, 28, 42, 44, 59], + "supported_nips": [1, 2, 4, 9, 11, 15, 16, 20, 22, 28, 42], "software": "LNbits", "version": "", } diff --git a/tests/test_nip17.py b/tests/test_nip17.py deleted file mode 100644 index c53070b..0000000 --- a/tests/test_nip17.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -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