From 8bfd79254853904fe797c49d3bef56615dae4d2c Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Sun, 16 Nov 2025 22:18:02 +0100 Subject: [PATCH] Add NIP-09 support for parameterized replaceable events (NIP-33) Extended NIP-09 deletion event handling to support both regular events and parameterized replaceable events (NIP-33). **Previous behavior:** - Only handled 'e' tags (regular event IDs) - Did not support 'a' tags for addressable/replaceable events **New behavior:** - Handles both 'e' tags (event IDs) and 'a' tags (event addresses) - Parses 'a' tag format: kind:pubkey:d-identifier - Validates deletion author matches event address pubkey (NIP-09 requirement) - Creates appropriate filters for each deletion type **Implementation:** - Added parsing for 'a' tag event addresses - Extract kind, pubkey, and d-tag from address format - Build NostrFilter with authors, kinds, and d-tag parameters - Collect all event IDs to delete from both 'e' and 'a' tags - Mark matching events as deleted in single operation This enables proper deletion of parameterized replaceable events like calendar events (kind 31922-31924), long-form content (kind 30023), and other addressable event kinds. Implements NIP-09: https://github.com/nostr-protocol/nips/blob/master/09.md Supports NIP-33: https://github.com/nostr-protocol/nips/blob/master/33.md --- relay/client_connection.py | 46 +++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/relay/client_connection.py b/relay/client_connection.py index 77a25b9..ed9a84b 100644 --- a/relay/client_connection.py +++ b/relay/client_connection.py @@ -208,12 +208,46 @@ class NostrClientConnection: await self.websocket.send_text(json.dumps(data)) async def _handle_delete_event(self, event: NostrEvent): - # NIP 09 - nostr_filter = NostrFilter(authors=[event.pubkey]) - nostr_filter.ids = [t[1] for t in event.tags if t[0] == "e"] - events_to_delete = await get_events(self.relay_id, nostr_filter, False) - ids = [e.id for e in events_to_delete if not e.is_delete_event] - await mark_events_deleted(self.relay_id, NostrFilter(ids=ids)) + # NIP 09 - Handle both regular events (e tags) and parameterized replaceable events (a tags) + + # Get event IDs from 'e' tags (for regular events) + event_ids = [t[1] for t in event.tags if t[0] == "e"] + + # Get event addresses from 'a' tags (for parameterized replaceable events) + event_addresses = [t[1] for t in event.tags if t[0] == "a"] + + ids_to_delete = [] + + # Handle regular event deletions (e tags) + if event_ids: + nostr_filter = NostrFilter(authors=[event.pubkey], ids=event_ids) + events_to_delete = await get_events(self.relay_id, nostr_filter, False) + ids_to_delete.extend([e.id for e in events_to_delete if not e.is_delete_event]) + + # Handle parameterized replaceable event deletions (a tags) + if event_addresses: + for addr in event_addresses: + # Parse address format: kind:pubkey:d-tag + parts = addr.split(":") + if len(parts) == 3: + kind_str, addr_pubkey, d_tag = parts + try: + kind = int(kind_str) + # Only delete if the address pubkey matches the deletion event author + if addr_pubkey == event.pubkey: + nostr_filter = NostrFilter( + authors=[addr_pubkey], + kinds=[kind], + d=[d_tag] + ) + events_to_delete = await get_events(self.relay_id, nostr_filter, False) + ids_to_delete.extend([e.id for e in events_to_delete if not e.is_delete_event]) + except ValueError: + logger.warning(f"Invalid kind in address: {addr}") + + # Only mark events as deleted if we found specific IDs + if ids_to_delete: + await mark_events_deleted(self.relay_id, NostrFilter(ids=ids_to_delete)) async def _handle_request( self, subscription_id: str, nostr_filter: NostrFilter