From a94fbc1f616128c93539c71b003495e5f6291c69 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 26 Feb 2026 21:12:27 +0000 Subject: feat: request kind-5 deletions for state/announcement events by #e Add a non_proposal_event_ids parameter to get_fetch_filters and a corresponding field on FetchRequest. On each fetch, the event IDs of cached repo announcements and the state event are collected and used to build a dedicated kind-5 filter keyed on #e tags, as specified by NIP-09. The existing #a-tagged filter already covers addressable-event deletions; this new filter catches deletions from clients that follow NIP-09 strictly and do not embed a repo coordinate in their deletion event. --- src/lib/client.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/lib/client.rs b/src/lib/client.rs index e657f53..95dd78d 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs @@ -798,6 +798,9 @@ impl Connect for Client { ) .copied() .collect(); + // Only request non-proposal event deletions on the first loop iteration; + // cleared after first use so subsequent iterations don't re-request them. + let mut fresh_non_proposal_event_ids = request.non_proposal_event_ids.clone(); let mut report = FetchReport::default(); @@ -813,8 +816,13 @@ impl Connect for Client { let dim = Style::new().color256(247); loop { - let filters = - get_fetch_filters(&fresh_coordinates, &fresh_proposal_roots, &fresh_profiles); + let filters = get_fetch_filters( + &fresh_coordinates, + &fresh_proposal_roots, + &fresh_non_proposal_event_ids, + &fresh_profiles, + ); + fresh_non_proposal_event_ids = HashSet::new(); if let Some(pb) = &pb { pb.set_prefix( @@ -1727,6 +1735,8 @@ async fn create_relays_request( for filter in get_fetch_filters( &repo_coordinates_without_relays, &proposals, + &HashSet::new(), /* non_proposal_event_ids not yet computed; deletion events are not + * cached locally */ &missing_contributor_profiles .union( &profiles_to_fetch_from_user_relays @@ -1828,6 +1838,21 @@ async fn create_relays_request( } else { None }, + non_proposal_event_ids: { + let mut ids: HashSet = HashSet::new(); + // Include repo announcement event IDs so we can request kind-5 + // deletions for them by #e tag (NIP-09 style). + if let Some(repo_ref) = &repo_ref { + for event in repo_ref.events.values() { + ids.insert(event.id); + } + // Also include the state event ID if we have one. + if let Ok(existing_state) = get_state_from_cache(git_repo_path, repo_ref).await { + ids.insert(existing_state.event.id); + } + } + ids + }, proposals, contributors, missing_contributor_profiles, @@ -2075,6 +2100,7 @@ pub fn consolidate_fetch_reports(reports: Vec>) -> FetchRepo pub fn get_fetch_filters( repo_coordinates: &HashSet, proposal_ids: &HashSet, + non_proposal_event_ids: &HashSet, required_profiles: &HashSet, ) -> Vec { [ @@ -2124,6 +2150,19 @@ pub fn get_fetch_filters( ), ] }, + // Request kind-5 deletions for state events and repo announcements by + // their event ID (#e tag), as per NIP-09. The #a-tagged filter above + // covers addressable-event deletions; this covers the specific event IDs + // of the state and announcement events we already have cached. + if non_proposal_event_ids.is_empty() { + vec![] + } else { + vec![ + nostr::Filter::default() + .kind(Kind::EventDeletion) + .events(non_proposal_event_ids.clone()), + ] + }, if required_profiles.is_empty() { vec![] } else { @@ -2290,6 +2329,9 @@ pub struct FetchRequest { repo_coordinates_without_relays: Vec<(Nip19Coordinate, Option)>, state: Option<(Timestamp, EventId)>, proposals: HashSet, + /// Event IDs of non-proposal events (state events, repo announcements) for + /// which we should also request kind-5 deletion events by `#e` tag. + non_proposal_event_ids: HashSet, contributors: HashSet, missing_contributor_profiles: HashSet, existing_events: HashSet, -- cgit v1.2.3