upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/nostr/policy/deletion.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/nostr/policy/deletion.rs')
-rw-r--r--src/nostr/policy/deletion.rs138
1 files changed, 89 insertions, 49 deletions
diff --git a/src/nostr/policy/deletion.rs b/src/nostr/policy/deletion.rs
index 69a5758..01241c9 100644
--- a/src/nostr/policy/deletion.rs
+++ b/src/nostr/policy/deletion.rs
@@ -1,7 +1,7 @@
1/// Deletion Policy - NIP-09 event deletion request handling 1/// Deletion Policy - NIP-09 event deletion request handling
2/// 2///
3/// Handles kind 5 (EventDeletion) events that request removal of repository 3/// Handles kind 5 (EventDeletion) events that request removal of purgatory entries
4/// announcements (kind 30617) from purgatory. 4/// for repository announcements (kind 30617) and state events (kind 30618).
5/// 5///
6/// ## NIP-09 Rules Enforced 6/// ## NIP-09 Rules Enforced
7/// 7///
@@ -13,9 +13,9 @@
13/// 13///
14/// ## Purgatory Interaction 14/// ## Purgatory Interaction
15/// 15///
16/// When a valid deletion request targets a kind 30617 announcement that is currently 16/// - Kind 30617 (announcement) in purgatory: entry removed, bare repo deleted from disk
17/// in purgatory (not yet promoted to the database), the purgatory entry is removed 17/// - Kind 30618 (state event) in purgatory: matching state event(s) removed by event ID
18/// and the bare repository is deleted from disk. 18/// or by (author, identifier) coordinate
19use nostr_relay_builder::prelude::{Event, WritePolicyResult}; 19use nostr_relay_builder::prelude::{Event, WritePolicyResult};
20 20
21use super::PolicyContext; 21use super::PolicyContext;
@@ -48,13 +48,13 @@ impl DeletionPolicy {
48 WritePolicyResult::Accept 48 WritePolicyResult::Accept
49 } 49 }
50 50
51 /// Remove any purgatory announcements targeted by this deletion event. 51 /// Remove any purgatory entries targeted by this deletion event.
52 /// 52 ///
53 /// Handles both reference styles from NIP-09: 53 /// Handles both reference styles from NIP-09:
54 /// - `e` tags: event ID references — match against purgatory entry event IDs 54 /// - `e` tags: event ID references — match against announcement or state event IDs
55 /// - `a` tags: addressable coordinate references — `30617:<pubkey>:<identifier>` 55 /// - `a` tags: addressable coordinate references — `30617:…` or `30618:…`
56 /// 56 ///
57 /// Only removes entries where the purgatory entry's owner matches the deletion 57 /// Only removes entries where the purgatory entry's author matches the deletion
58 /// event's pubkey (enforces author-only deletion). 58 /// event's pubkey (enforces author-only deletion).
59 fn remove_purgatory_targets(&self, event: &Event) { 59 fn remove_purgatory_targets(&self, event: &Event) {
60 let author = &event.pubkey; 60 let author = &event.pubkey;
@@ -81,17 +81,19 @@ impl DeletionPolicy {
81 } 81 }
82 } 82 }
83 83
84 /// Remove a purgatory announcement matched by event ID. 84 /// Remove a purgatory entry (announcement or state event) matched by event ID.
85 /// 85 ///
86 /// Scans all purgatory announcements owned by `author` and removes the one 86 /// Checks announcements first (kind 30617), then state events (kind 30618).
87 /// whose event ID hex matches `target_id_hex`. 87 /// Only removes entries whose author matches `author`.
88 fn remove_by_event_id(&self, author: &nostr_relay_builder::prelude::PublicKey, target_id_hex: &str, _deletion_created_at: u64) { 88 fn remove_by_event_id(
89 // Scan announcements owned by this author for a matching event ID 89 &self,
90 // We use get_announcements_by_identifier would require knowing the identifier, 90 author: &nostr_relay_builder::prelude::PublicKey,
91 // so instead we iterate via find_announcement after collecting all entries. 91 target_id_hex: &str,
92 _deletion_created_at: u64,
93 ) {
94 // --- Check announcements (kind 30617) ---
92 // The DashMap doesn't expose a direct "find by event ID" method, so we use 95 // The DashMap doesn't expose a direct "find by event ID" method, so we use
93 // the announcements_for_sync snapshot to get all (repo_id, _) pairs and then 96 // the announcements_for_sync snapshot to enumerate all (repo_id, _) pairs.
94 // look up each one.
95 let all = self.ctx.purgatory.announcements_for_sync(); 97 let all = self.ctx.purgatory.announcements_for_sync();
96 for (repo_id, _) in all { 98 for (repo_id, _) in all {
97 // repo_id format: "30617:{pubkey_hex}:{identifier}" 99 // repo_id format: "30617:{pubkey_hex}:{identifier}"
@@ -102,7 +104,6 @@ impl DeletionPolicy {
102 let entry_pubkey_hex = parts[1]; 104 let entry_pubkey_hex = parts[1];
103 let identifier = parts[2]; 105 let identifier = parts[2];
104 106
105 // Only check entries owned by the deletion event author
106 if entry_pubkey_hex != author.to_hex() { 107 if entry_pubkey_hex != author.to_hex() {
107 continue; 108 continue;
108 } 109 }
@@ -116,18 +117,37 @@ impl DeletionPolicy {
116 "Deletion request: removing purgatory announcement by event ID" 117 "Deletion request: removing purgatory announcement by event ID"
117 ); 118 );
118 self.evict_purgatory_entry(author, identifier); 119 self.evict_purgatory_entry(author, identifier);
119 return; // event IDs are unique, no need to continue 120 return; // event IDs are unique
121 }
122 }
123 }
124
125 // --- Check state events (kind 30618) ---
126 // State events are keyed by identifier; scan all identifiers for a match.
127 let state_identifiers = self.ctx.purgatory.get_all_identifiers();
128 for identifier in state_identifiers {
129 let entries = self.ctx.purgatory.find_state(&identifier);
130 for entry in entries {
131 if entry.author == *author && entry.event.id.to_hex() == target_id_hex {
132 tracing::info!(
133 event_id = %target_id_hex,
134 identifier = %identifier,
135 author = %author.to_hex(),
136 "Deletion request: removing purgatory state event by event ID"
137 );
138 self.ctx.purgatory.remove_state_event(&identifier, &entry.event.id);
139 return; // event IDs are unique
120 } 140 }
121 } 141 }
122 } 142 }
123 } 143 }
124 144
125 /// Remove a purgatory announcement matched by addressable coordinate. 145 /// Remove a purgatory entry matched by addressable coordinate.
146 ///
147 /// The coordinate format is `<kind>:<pubkey>:<d-identifier>`.
148 /// Handles kind 30617 (announcements) and kind 30618 (state events).
126 /// 149 ///
127 /// The coordinate format is `<kind>:<pubkey>:<d-identifier>`. Only kind 30617 150 /// Per NIP-09, all versions up to `deletion_created_at` are considered deleted.
128 /// coordinates are relevant here. Per NIP-09, all versions up to `deletion_created_at`
129 /// are considered deleted — since purgatory entries are always a single event per
130 /// (owner, identifier), we delete if the entry's `created_at` ≤ `deletion_created_at`.
131 fn remove_by_coordinate( 151 fn remove_by_coordinate(
132 &self, 152 &self,
133 author: &nostr_relay_builder::prelude::PublicKey, 153 author: &nostr_relay_builder::prelude::PublicKey,
@@ -144,11 +164,6 @@ impl DeletionPolicy {
144 let coord_pubkey_hex = parts[1]; 164 let coord_pubkey_hex = parts[1];
145 let identifier = parts[2]; 165 let identifier = parts[2];
146 166
147 // Only handle kind 30617 (GitRepoAnnouncement)
148 if kind_str != "30617" {
149 return;
150 }
151
152 // The coordinate pubkey must match the deletion event author 167 // The coordinate pubkey must match the deletion event author
153 if coord_pubkey_hex != author.to_hex() { 168 if coord_pubkey_hex != author.to_hex() {
154 tracing::debug!( 169 tracing::debug!(
@@ -159,25 +174,50 @@ impl DeletionPolicy {
159 return; 174 return;
160 } 175 }
161 176
162 if let Some(entry) = self.ctx.purgatory.find_announcement(author, identifier) { 177 match kind_str {
163 // Per NIP-09: delete all versions up to deletion_created_at 178 "30617" => {
164 if entry.event.created_at.as_secs() <= deletion_created_at { 179 // Announcement purgatory entry
165 tracing::info!( 180 if let Some(entry) = self.ctx.purgatory.find_announcement(author, identifier) {
166 identifier = %identifier, 181 if entry.event.created_at.as_secs() <= deletion_created_at {
167 author = %author.to_hex(), 182 tracing::info!(
168 entry_created_at = entry.event.created_at.as_secs(), 183 identifier = %identifier,
169 deletion_created_at = %deletion_created_at, 184 author = %author.to_hex(),
170 "Deletion request: removing purgatory announcement by coordinate" 185 "Deletion request: removing purgatory announcement by coordinate"
171 ); 186 );
172 self.evict_purgatory_entry(author, identifier); 187 self.evict_purgatory_entry(author, identifier);
173 } else { 188 } else {
174 tracing::debug!( 189 tracing::debug!(
175 identifier = %identifier, 190 identifier = %identifier,
176 author = %author.to_hex(), 191 author = %author.to_hex(),
177 entry_created_at = entry.event.created_at.as_secs(), 192 "Ignoring deletion: purgatory announcement is newer than deletion request"
178 deletion_created_at = %deletion_created_at, 193 );
179 "Ignoring deletion: purgatory entry is newer than deletion request" 194 }
180 ); 195 }
196 }
197 "30618" => {
198 // State event purgatory entries for this (author, identifier).
199 // Remove all entries authored by `author` with created_at ≤ deletion_created_at.
200 let entries = self.ctx.purgatory.find_state(identifier);
201 let mut removed = 0usize;
202 for entry in entries {
203 if entry.author == *author
204 && entry.event.created_at.as_secs() <= deletion_created_at
205 {
206 self.ctx.purgatory.remove_state_event(identifier, &entry.event.id);
207 removed += 1;
208 }
209 }
210 if removed > 0 {
211 tracing::info!(
212 identifier = %identifier,
213 author = %author.to_hex(),
214 removed = %removed,
215 "Deletion request: removed purgatory state event(s) by coordinate"
216 );
217 }
218 }
219 _ => {
220 // Other kinds not handled
181 } 221 }
182 } 222 }
183 } 223 }