diff options
Diffstat (limited to 'src/nostr/policy/announcement.rs')
| -rw-r--r-- | src/nostr/policy/announcement.rs | 117 |
1 files changed, 111 insertions, 6 deletions
diff --git a/src/nostr/policy/announcement.rs b/src/nostr/policy/announcement.rs index 15a6e58..1118497 100644 --- a/src/nostr/policy/announcement.rs +++ b/src/nostr/policy/announcement.rs | |||
| @@ -3,6 +3,7 @@ | |||
| 3 | /// Handles validation of NIP-34 repository announcements (kind 30617) | 3 | /// Handles validation of NIP-34 repository announcements (kind 30617) |
| 4 | /// according to GRASP-01 specification. | 4 | /// according to GRASP-01 specification. |
| 5 | use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag}; | 5 | use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag}; |
| 6 | use std::collections::HashSet; | ||
| 6 | 7 | ||
| 7 | use super::PolicyContext; | 8 | use super::PolicyContext; |
| 8 | use crate::config::Config; | 9 | use crate::config::Config; |
| @@ -11,12 +12,14 @@ use crate::nostr::events::{validate_announcement, RepositoryAnnouncement}; | |||
| 11 | /// Result of announcement policy evaluation | 12 | /// Result of announcement policy evaluation |
| 12 | #[derive(Debug, Clone, PartialEq)] | 13 | #[derive(Debug, Clone, PartialEq)] |
| 13 | pub enum AnnouncementResult { | 14 | pub enum AnnouncementResult { |
| 14 | /// Accept: Event lists our service (GRASP-01 compliant) | 15 | /// Accept: Event lists our service (GRASP-01 compliant) - replacement announcement |
| 15 | Accept, | 16 | Accept, |
| 16 | /// Accept as maintainer: Event accepted via maintainer exception (multi-maintainer) | 17 | /// Accept as maintainer: Event accepted via maintainer exception (multi-maintainer) |
| 17 | AcceptMaintainer, | 18 | AcceptMaintainer, |
| 18 | /// Accept as archive: Event accepted via GRASP-05 archive whitelist (read-only) | 19 | /// Accept as archive: Event accepted via GRASP-05 archive whitelist (read-only) |
| 19 | AcceptArchive, | 20 | AcceptArchive, |
| 21 | /// Accept to purgatory: New announcement, waiting for git data | ||
| 22 | AcceptPurgatory, | ||
| 20 | /// Reject: Event fails validation with reason | 23 | /// Reject: Event fails validation with reason |
| 21 | Reject(String), | 24 | Reject(String), |
| 22 | } | 25 | } |
| @@ -35,10 +38,12 @@ impl AnnouncementPolicy { | |||
| 35 | 38 | ||
| 36 | /// Validate a repository announcement event | 39 | /// Validate a repository announcement event |
| 37 | /// | 40 | /// |
| 38 | /// Returns `Accept` if the announcement lists the service properly, | 41 | /// Returns: |
| 39 | /// `AcceptMaintainer` if accepted via maintainer exception, | 42 | /// - `Accept` if this is a replacement announcement (active announcement exists) |
| 40 | /// `AcceptArchive` if accepted via GRASP-05 archive config, | 43 | /// - `AcceptPurgatory` if this is a new announcement (no active announcement exists) |
| 41 | /// or `Reject` with reason. | 44 | /// - `AcceptMaintainer` if accepted via maintainer exception |
| 45 | /// - `AcceptArchive` if accepted via GRASP-05 archive config | ||
| 46 | /// - `Reject` with reason if validation fails | ||
| 42 | pub async fn validate(&self, event: &Event) -> AnnouncementResult { | 47 | pub async fn validate(&self, event: &Event) -> AnnouncementResult { |
| 43 | // First, try validation (GRASP-01 + GRASP-05) | 48 | // First, try validation (GRASP-01 + GRASP-05) |
| 44 | let validation_result = validate_announcement(event, &self.config); | 49 | let validation_result = validate_announcement(event, &self.config); |
| @@ -67,11 +72,111 @@ impl AnnouncementPolicy { | |||
| 67 | Err(_) => AnnouncementResult::Reject(reason), | 72 | Err(_) => AnnouncementResult::Reject(reason), |
| 68 | } | 73 | } |
| 69 | } | 74 | } |
| 70 | // Accept, AcceptArchive, or AcceptMaintainer - return as-is | 75 | AnnouncementResult::Accept | AnnouncementResult::AcceptArchive => { |
| 76 | // Parse announcement to check for existing active announcement | ||
| 77 | match RepositoryAnnouncement::from_event(event.clone()) { | ||
| 78 | Ok(announcement) => { | ||
| 79 | // Check if there's already an active announcement for this (pubkey, identifier) | ||
| 80 | match self | ||
| 81 | .has_active_announcement(&event.pubkey, &announcement.identifier) | ||
| 82 | .await | ||
| 83 | { | ||
| 84 | Ok(true) => { | ||
| 85 | // Replacement announcement - accept immediately | ||
| 86 | tracing::debug!( | ||
| 87 | identifier = %announcement.identifier, | ||
| 88 | "Replacement announcement - accepting immediately" | ||
| 89 | ); | ||
| 90 | validation_result | ||
| 91 | } | ||
| 92 | Ok(false) => { | ||
| 93 | // New announcement - route to purgatory | ||
| 94 | tracing::debug!( | ||
| 95 | identifier = %announcement.identifier, | ||
| 96 | "New announcement - routing to purgatory" | ||
| 97 | ); | ||
| 98 | AnnouncementResult::AcceptPurgatory | ||
| 99 | } | ||
| 100 | Err(e) => { | ||
| 101 | tracing::warn!( | ||
| 102 | error = %e, | ||
| 103 | "Failed to check for existing announcement - rejecting" | ||
| 104 | ); | ||
| 105 | AnnouncementResult::Reject(format!( | ||
| 106 | "Database error checking existing announcement: {}", | ||
| 107 | e | ||
| 108 | )) | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | Err(e) => AnnouncementResult::Reject(format!( | ||
| 113 | "Failed to parse announcement: {}", | ||
| 114 | e | ||
| 115 | )), | ||
| 116 | } | ||
| 117 | } | ||
| 118 | // AcceptPurgatory shouldn't come from validate_announcement, but handle it | ||
| 71 | result => result, | 119 | result => result, |
| 72 | } | 120 | } |
| 73 | } | 121 | } |
| 74 | 122 | ||
| 123 | /// Check if there's an active announcement in the database for this (pubkey, identifier) | ||
| 124 | async fn has_active_announcement( | ||
| 125 | &self, | ||
| 126 | pubkey: &PublicKey, | ||
| 127 | identifier: &str, | ||
| 128 | ) -> Result<bool, String> { | ||
| 129 | let filter = Filter::new() | ||
| 130 | .kind(Kind::GitRepoAnnouncement) | ||
| 131 | .author(*pubkey) | ||
| 132 | .custom_tag( | ||
| 133 | SingleLetterTag::lowercase(Alphabet::D), | ||
| 134 | identifier.to_string(), | ||
| 135 | ); | ||
| 136 | |||
| 137 | let events: Vec<Event> = match self.ctx.database.query(filter).await { | ||
| 138 | Ok(events) => events.into_iter().collect(), | ||
| 139 | Err(e) => return Err(format!("Database query failed: {}", e)), | ||
| 140 | }; | ||
| 141 | |||
| 142 | Ok(!events.is_empty()) | ||
| 143 | } | ||
| 144 | |||
| 145 | /// Add an announcement to purgatory | ||
| 146 | /// | ||
| 147 | /// Creates the bare repository and stores the announcement in purgatory | ||
| 148 | /// until git data arrives. | ||
| 149 | pub fn add_to_purgatory(&self, event: &Event) -> Result<(), String> { | ||
| 150 | let announcement = RepositoryAnnouncement::from_event(event.clone()) | ||
| 151 | .map_err(|e| format!("Failed to parse announcement: {}", e))?; | ||
| 152 | |||
| 153 | // Create bare repository | ||
| 154 | self.ensure_bare_repository(&announcement)?; | ||
| 155 | |||
| 156 | // Build repo path | ||
| 157 | let repo_path = self.ctx.git_data_path.join(announcement.repo_path()); | ||
| 158 | |||
| 159 | // Extract relays from announcement | ||
| 160 | let relays: HashSet<String> = announcement.relays.iter().cloned().collect(); | ||
| 161 | |||
| 162 | // Add to purgatory | ||
| 163 | self.ctx.purgatory.add_announcement( | ||
| 164 | event.clone(), | ||
| 165 | announcement.identifier.clone(), | ||
| 166 | event.pubkey, | ||
| 167 | repo_path, | ||
| 168 | relays, | ||
| 169 | ); | ||
| 170 | |||
| 171 | tracing::info!( | ||
| 172 | identifier = %announcement.identifier, | ||
| 173 | event_id = %event.id, | ||
| 174 | "Added announcement to purgatory" | ||
| 175 | ); | ||
| 176 | |||
| 177 | Ok(()) | ||
| 178 | } | ||
| 179 | |||
| 75 | /// Create a bare git repository if it doesn't exist | 180 | /// Create a bare git repository if it doesn't exist |
| 76 | /// Path format: <git_data_path>/<npub>/<identifier>.git | 181 | /// Path format: <git_data_path>/<npub>/<identifier>.git |
| 77 | pub fn ensure_bare_repository( | 182 | pub fn ensure_bare_repository( |