mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
feat: cascade delete state events when announcements are deleted
State events (kind 30618) use 'd' tags to identify their repository, not 'a' tags referencing announcements. This meant they were not found by the existing cascade deletion logic which only searched for events with 'a' tags. This change adds special handling for state events after the graph-based deletion algorithm determines which announcements to delete: 1. After graph traversal identifies deleted announcements, extract all affected repository identifiers from the announcement addresses 2. For each identifier, query all announcements (kind 30617) and state events (kind 30618) with matching 'd' tag 3. Separate announcements into remaining (not being deleted) vs deleted 4. Build maintainer mapping from REMAINING announcements using collect_authorized_maintainers() from git::authorization 5. Delete state events where the author is either: - An owner whose announcement is being deleted (their repo is gone) - No longer listed as a maintainer in ANY remaining announcement This ensures state events are properly cleaned up when their authorizing announcement is removed, while preserving state events for maintainers who are still authorized through other announcements. The logic correctly handles multi-maintainer scenarios where: - Multiple maintainers publish announcements for the same repository - One maintainer deletes their announcement - State events from that maintainer should be deleted - State events from other still-authorized maintainers are preserved Implementation details: - Added query_announcements_by_identifier() helper function - Added query_state_events_by_identifier() helper function - Both use Filter with custom_tag(SingleLetterTag::lowercase(Alphabet::D)) - Integrated into determine_events_to_delete() as Step 6 - Comprehensive debug logging for troubleshooting
This commit is contained in:
@@ -5,11 +5,15 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
use nostr_relay_builder::prelude::{Event, EventId, Filter, Kind, PublicKey, ToBech32};
|
||||
use nostr_relay_builder::prelude::{
|
||||
Alphabet, Event, EventId, Filter, Kind, PublicKey, SingleLetterTag, ToBech32,
|
||||
};
|
||||
|
||||
use super::SharedDatabase;
|
||||
use crate::database::{DeletionMetadata, HoldingDatabase};
|
||||
use crate::git::archive::{archive_repository, create_archive_metadata};
|
||||
use crate::git::authorization::collect_authorized_maintainers;
|
||||
use crate::nostr::events::RepositoryAnnouncement;
|
||||
|
||||
/// Determine which events should be deleted using graph-based algorithm
|
||||
///
|
||||
@@ -187,8 +191,129 @@ pub async fn determine_events_to_delete(
|
||||
traversal_result.delete.len()
|
||||
);
|
||||
|
||||
// Return only events that should be deleted
|
||||
Ok(traversal_result.delete)
|
||||
// Step 6: Handle state event cascade deletion
|
||||
// State events (kind 30618) use 'd' tags, not 'a' tags, so they weren't found by
|
||||
// the graph-based deletion logic. We need to explicitly check them.
|
||||
let mut final_deleted_events = traversal_result.delete.clone();
|
||||
|
||||
// Identify which announcements are being deleted
|
||||
let deleted_announcement_ids: HashSet<EventId> = all_events
|
||||
.iter()
|
||||
.filter(|e| e.kind == Kind::from(30617) && final_deleted_events.contains(&e.id))
|
||||
.map(|e| e.id)
|
||||
.collect();
|
||||
|
||||
tracing::debug!(
|
||||
deleted_announcement_count = deleted_announcement_ids.len(),
|
||||
"Identified {} announcements being deleted for state event cascade",
|
||||
deleted_announcement_ids.len()
|
||||
);
|
||||
|
||||
// For each identifier being affected, handle state events
|
||||
for address in &announcement_addresses {
|
||||
let parts: Vec<&str> = address.split(':').collect();
|
||||
if parts.len() != 3 {
|
||||
continue; // Skip invalid addresses
|
||||
}
|
||||
let identifier = parts[2];
|
||||
|
||||
// Get ALL announcements for this identifier from database
|
||||
let all_announcements_events =
|
||||
query_announcements_by_identifier(database, identifier).await?;
|
||||
|
||||
tracing::debug!(
|
||||
identifier = %identifier,
|
||||
total_announcements = all_announcements_events.len(),
|
||||
"Queried announcements for identifier"
|
||||
);
|
||||
|
||||
// Convert to RepositoryAnnouncement structs
|
||||
let all_announcements: Vec<RepositoryAnnouncement> = all_announcements_events
|
||||
.iter()
|
||||
.filter_map(|e| RepositoryAnnouncement::from_event(e.clone()).ok())
|
||||
.collect();
|
||||
|
||||
// Separate into remaining vs deleted
|
||||
let remaining_announcements: Vec<RepositoryAnnouncement> = all_announcements
|
||||
.iter()
|
||||
.filter(|a| !deleted_announcement_ids.contains(&a.event.id))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let deleted_owner_pubkeys: HashSet<String> = all_announcements
|
||||
.iter()
|
||||
.filter(|a| deleted_announcement_ids.contains(&a.event.id))
|
||||
.map(|a| a.event.pubkey.to_hex())
|
||||
.collect();
|
||||
|
||||
tracing::debug!(
|
||||
identifier = %identifier,
|
||||
remaining_announcements = remaining_announcements.len(),
|
||||
deleted_owner_pubkeys = deleted_owner_pubkeys.len(),
|
||||
"Separated announcements into remaining and deleted"
|
||||
);
|
||||
|
||||
// Build maintainer mapping from remaining announcements
|
||||
let maintainer_map = collect_authorized_maintainers(&remaining_announcements);
|
||||
let all_authorized: HashSet<String> = maintainer_map
|
||||
.values()
|
||||
.flat_map(|v| v.iter().cloned())
|
||||
.collect();
|
||||
|
||||
tracing::debug!(
|
||||
identifier = %identifier,
|
||||
authorized_maintainers = all_authorized.len(),
|
||||
"Built maintainer mapping from remaining announcements"
|
||||
);
|
||||
|
||||
// Query all state events for this identifier
|
||||
let state_events = query_state_events_by_identifier(database, identifier).await?;
|
||||
|
||||
tracing::debug!(
|
||||
identifier = %identifier,
|
||||
state_events_count = state_events.len(),
|
||||
"Queried state events for identifier"
|
||||
);
|
||||
|
||||
// Delete state events where author is:
|
||||
// - One of the deleted owner pubkeys (their repo is being deleted), OR
|
||||
// - Not in any remaining maintainer list
|
||||
for state_event in state_events {
|
||||
let author_hex = state_event.pubkey.to_hex();
|
||||
|
||||
let should_delete = deleted_owner_pubkeys.contains(&author_hex)
|
||||
|| !all_authorized.contains(&author_hex);
|
||||
|
||||
if should_delete {
|
||||
final_deleted_events.insert(state_event.id);
|
||||
tracing::debug!(
|
||||
state_event_id = %state_event.id,
|
||||
author = %author_hex,
|
||||
identifier = %identifier,
|
||||
is_deleted_owner = deleted_owner_pubkeys.contains(&author_hex),
|
||||
is_unauthorized = !all_authorized.contains(&author_hex),
|
||||
"State event marked for deletion: author no longer authorized"
|
||||
);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
state_event_id = %state_event.id,
|
||||
author = %author_hex,
|
||||
identifier = %identifier,
|
||||
"State event kept: author still authorized"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
graph_deleted = traversal_result.delete.len(),
|
||||
state_events_deleted = final_deleted_events.len() - traversal_result.delete.len(),
|
||||
total_deleted = final_deleted_events.len(),
|
||||
"Cascade deletion complete including state events"
|
||||
);
|
||||
|
||||
// Return all events that should be deleted (including state events)
|
||||
Ok(final_deleted_events)
|
||||
}
|
||||
|
||||
/// Query all events that depend on the given event IDs
|
||||
@@ -408,6 +533,58 @@ fn has_event_tag(event: &Event, event_id: &EventId) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Query all repository announcements (kind 30617) with a specific identifier
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `database` - Database to query
|
||||
/// * `identifier` - Repository identifier to search for
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of announcement events with matching `d` tag
|
||||
async fn query_announcements_by_identifier(
|
||||
database: &SharedDatabase,
|
||||
identifier: &str,
|
||||
) -> Result<Vec<Event>, String> {
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::from(30617))
|
||||
.custom_tag(SingleLetterTag::lowercase(Alphabet::D), identifier);
|
||||
|
||||
let events: Vec<Event> = database
|
||||
.query(filter)
|
||||
.await
|
||||
.map_err(|e| format!("Database query failed: {}", e))?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
/// Query all repository state events (kind 30618) with a specific identifier
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `database` - Database to query
|
||||
/// * `identifier` - Repository identifier to search for
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of state events with matching `d` tag
|
||||
async fn query_state_events_by_identifier(
|
||||
database: &SharedDatabase,
|
||||
identifier: &str,
|
||||
) -> Result<Vec<Event>, String> {
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::from(30618))
|
||||
.custom_tag(SingleLetterTag::lowercase(Alphabet::D), identifier);
|
||||
|
||||
let events: Vec<Event> = database
|
||||
.query(filter)
|
||||
.await
|
||||
.map_err(|e| format!("Database query failed: {}", e))?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
/// Move events from main database to holding database
|
||||
///
|
||||
/// This is an atomic operation that:
|
||||
|
||||
Reference in New Issue
Block a user