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
This commit is contained in:
Patrick Mulligan 2025-11-16 22:18:02 +01:00
parent c4efb87b70
commit 8bfd792548

View file

@ -208,12 +208,46 @@ class NostrClientConnection:
await self.websocket.send_text(json.dumps(data)) await self.websocket.send_text(json.dumps(data))
async def _handle_delete_event(self, event: NostrEvent): async def _handle_delete_event(self, event: NostrEvent):
# NIP 09 # NIP 09 - Handle both regular events (e tags) and parameterized replaceable events (a tags)
nostr_filter = NostrFilter(authors=[event.pubkey])
nostr_filter.ids = [t[1] for t in event.tags if t[0] == "e"] # 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) 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] ids_to_delete.extend([e.id for e in events_to_delete if not e.is_delete_event])
await mark_events_deleted(self.relay_id, NostrFilter(ids=ids))
# 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( async def _handle_request(
self, subscription_id: str, nostr_filter: NostrFilter self, subscription_id: str, nostr_filter: NostrFilter