fix(nip09): honour a tags in deletion requests
A kind-5 request could only name events by `e` tag. Worse, one that carried `a` tags and no `e` tags built a filter with an empty id list, which matched every event by that author and marked them all deleted. Handle `a` tags per NIP-09: parse `kind:pubkey:d` (the `d` value may contain ':'), require the pubkey to be the request author and the kind to be replaceable or addressable, and remove only versions up to the request's `created_at` so a later re-publication survives. An empty `d` addresses a replaceable kind. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Tbyw6FwjhEJg3gHfPHxWt (cherry picked from commit e695f9c66881d3d520d40f0cd3e9c95d9435938c)
This commit is contained in:
parent
d2350f907d
commit
fd19acff97
5 changed files with 191 additions and 52 deletions
|
|
@ -229,66 +229,61 @@ class NostrClientConnection:
|
|||
await self.websocket.send_text(json.dumps(data))
|
||||
|
||||
async def _handle_delete_event(self, event: NostrEvent):
|
||||
# NIP 09 - Handle both regular events (e tags) and parameterized replaceable events (a tags)
|
||||
"""NIP-09: `e` tags name events by id, `a` tags name replaceable or
|
||||
addressable events by `kind:pubkey:d`. Only the author's own events
|
||||
are touched; for `a` tags only versions up to the deletion request's
|
||||
`created_at` are removed, so a later re-publication survives."""
|
||||
ids_to_delete: list[str] = []
|
||||
|
||||
# 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]
|
||||
ids_to_delete += [e.id for e in events_to_delete if not e.is_delete_event]
|
||||
|
||||
for address in (t[1] for t in event.tags if t[0] == "a"):
|
||||
address_filter = self._address_filter(address, event.pubkey)
|
||||
if not address_filter:
|
||||
logger.debug(f"NIP-09: ignoring address '{address}'")
|
||||
continue
|
||||
# An addressable kind with an empty `d` names the version whose
|
||||
# `d` tag is empty or missing, which no tag join can express, so
|
||||
# fetch tags and filter here.
|
||||
empty_d = address_filter.is_addressable and not address_filter.d
|
||||
events_to_delete = await get_events(
|
||||
self.relay_id, address_filter, include_tags=empty_d
|
||||
)
|
||||
ids_to_delete += [
|
||||
e.id
|
||||
for e in events_to_delete
|
||||
if e.created_at <= event.created_at
|
||||
and (not empty_d or not e.has_tag_value_other_than("d", ""))
|
||||
]
|
||||
|
||||
# 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:
|
||||
# NOTE: Use "#d" alias, not "d" directly (Pydantic Field alias)
|
||||
nostr_filter = NostrFilter(
|
||||
authors=[addr_pubkey],
|
||||
kinds=[kind],
|
||||
**{"#d": [d_tag]}, # Use alias to set d field
|
||||
)
|
||||
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
|
||||
]
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Deletion request pubkey mismatch: {addr_pubkey} != {event.pubkey}"
|
||||
)
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid kind in address: {addr}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"Invalid address format (expected kind:pubkey:d-tag): {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))
|
||||
|
||||
@staticmethod
|
||||
def _address_filter(address: str, author: str) -> NostrFilter | None:
|
||||
# `kind:pubkey:d`; the `d` value may itself contain ':'.
|
||||
parts = address.split(":", 2)
|
||||
if len(parts) != 3 or not parts[0].isdigit():
|
||||
return None
|
||||
kind, pubkey, d_tag = int(parts[0]), parts[1], parts[2]
|
||||
if pubkey != author:
|
||||
return None
|
||||
# NIP-01: replaceable are 0, 3 and 10000-19999; addressable 30000-39999.
|
||||
replaceable = kind in (0, 3) or 10000 <= kind < 20000
|
||||
addressable = 30000 <= kind < 40000
|
||||
if not (replaceable or addressable):
|
||||
return None
|
||||
if d_tag:
|
||||
# `d` is a Pydantic alias field; it must be set through the alias.
|
||||
return NostrFilter(
|
||||
authors=[author], kinds=[kind], **{"#d": [d_tag]} # type: ignore[arg-type]
|
||||
)
|
||||
return NostrFilter(authors=[author], kinds=[kind])
|
||||
|
||||
async def _handle_request(
|
||||
self, subscription_id: str, nostr_filter: NostrFilter
|
||||
) -> list:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue