diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-31 09:18:21 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-31 10:49:09 +0000 |
| commit | 768fe91caa676e4501aa26e14e01ca47f3ea4ca1 (patch) | |
| tree | e697becb6b2253909d399073f5c2bd2d571fcf5e /src/nostr/policy/state.rs | |
| parent | 3d6901831904141166d9ed8f47813c45cba109b6 (diff) | |
purgatory: fix pr event recieve code
Diffstat (limited to 'src/nostr/policy/state.rs')
| -rw-r--r-- | src/nostr/policy/state.rs | 187 |
1 files changed, 2 insertions, 185 deletions
diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs index 13f2549..1203890 100644 --- a/src/nostr/policy/state.rs +++ b/src/nostr/policy/state.rs | |||
| @@ -6,15 +6,12 @@ use nostr_relay_builder::builder::WritePolicyResult; | |||
| 6 | /// | 6 | /// |
| 7 | /// Handles validation of NIP-34 repository state events (kind 30618) | 7 | /// Handles validation of NIP-34 repository state events (kind 30618) |
| 8 | /// and aligns git refs with authorized state according to GRASP-01. | 8 | /// and aligns git refs with authorized state according to GRASP-01. |
| 9 | use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag}; | 9 | use nostr_relay_builder::prelude::Event; |
| 10 | 10 | ||
| 11 | use super::PolicyContext; | 11 | use super::PolicyContext; |
| 12 | use crate::git::authorization::{collect_authorized_maintainers, fetch_repository_data}; | 12 | use crate::git::authorization::{collect_authorized_maintainers, fetch_repository_data}; |
| 13 | use crate::git::{self}; | 13 | use crate::git::{self}; |
| 14 | use crate::nostr::events::{ | 14 | use crate::nostr::events::{validate_state, RepositoryAnnouncement, RepositoryState}; |
| 15 | validate_state, RepositoryAnnouncement, RepositoryState, KIND_REPOSITORY_ANNOUNCEMENT, | ||
| 16 | KIND_REPOSITORY_STATE, | ||
| 17 | }; | ||
| 18 | 15 | ||
| 19 | /// Result of aligning a repository with authorized state | 16 | /// Result of aligning a repository with authorized state |
| 20 | #[derive(Debug, Default)] | 17 | #[derive(Debug, Default)] |
| @@ -168,186 +165,6 @@ impl StatePolicy { | |||
| 168 | } | 165 | } |
| 169 | } | 166 | } |
| 170 | 167 | ||
| 171 | /// Check if any git repositories exist for the given identifier | ||
| 172 | /// | ||
| 173 | /// Scans the git_data_path for any directories matching the pattern: | ||
| 174 | /// `<any-npub>/<identifier>.git` | ||
| 175 | /// | ||
| 176 | /// This is used to distinguish "no git data yet" from "not authorized". | ||
| 177 | fn has_git_data_for_identifier(&self, identifier: &str) -> bool { | ||
| 178 | let git_data_path = &self.ctx.git_data_path; | ||
| 179 | |||
| 180 | // Check if git_data_path exists | ||
| 181 | if !git_data_path.exists() { | ||
| 182 | return false; | ||
| 183 | } | ||
| 184 | |||
| 185 | // Scan for any npub directories | ||
| 186 | let read_dir = match std::fs::read_dir(git_data_path) { | ||
| 187 | Ok(dir) => dir, | ||
| 188 | Err(_) => return false, | ||
| 189 | }; | ||
| 190 | |||
| 191 | for entry in read_dir.flatten() { | ||
| 192 | if let Ok(file_type) = entry.file_type() { | ||
| 193 | if file_type.is_dir() { | ||
| 194 | // Check if <npub>/<identifier>.git exists | ||
| 195 | let repo_path = entry.path().join(format!("{}.git", identifier)); | ||
| 196 | if repo_path.exists() { | ||
| 197 | return true; | ||
| 198 | } | ||
| 199 | } | ||
| 200 | } | ||
| 201 | } | ||
| 202 | |||
| 203 | false | ||
| 204 | } | ||
| 205 | |||
| 206 | /// Check if this state event is the latest for its identifier among authorized authors | ||
| 207 | /// | ||
| 208 | /// A state is considered "latest" if no other state event in the database | ||
| 209 | /// from an authorized author has a newer timestamp. | ||
| 210 | async fn is_latest_state_for_identifier( | ||
| 211 | &self, | ||
| 212 | state: &RepositoryState, | ||
| 213 | authorized_pubkeys: &[PublicKey], | ||
| 214 | ) -> Result<bool, String> { | ||
| 215 | let filter = Filter::new() | ||
| 216 | .kind(Kind::from(KIND_REPOSITORY_STATE)) | ||
| 217 | .custom_tag( | ||
| 218 | SingleLetterTag::lowercase(Alphabet::D), | ||
| 219 | state.identifier.clone(), | ||
| 220 | ); | ||
| 221 | |||
| 222 | match self.ctx.database.query(filter).await { | ||
| 223 | Ok(events) => { | ||
| 224 | for event in events { | ||
| 225 | // Skip comparing to self (same event ID) | ||
| 226 | if event.id == state.event.id { | ||
| 227 | continue; | ||
| 228 | } | ||
| 229 | // Only consider events from authorized authors for this announcement | ||
| 230 | if !authorized_pubkeys.contains(&event.pubkey) { | ||
| 231 | continue; | ||
| 232 | } | ||
| 233 | // If any existing event from an authorized author is newer, this is not the latest | ||
| 234 | if event.created_at > state.event.created_at { | ||
| 235 | tracing::debug!( | ||
| 236 | "State {} is not latest: found newer state {} from {} (ts {} > {})", | ||
| 237 | state.event.id.to_hex(), | ||
| 238 | event.id.to_hex(), | ||
| 239 | event.pubkey.to_hex(), | ||
| 240 | event.created_at.as_secs(), | ||
| 241 | state.event.created_at.as_secs() | ||
| 242 | ); | ||
| 243 | return Ok(false); | ||
| 244 | } | ||
| 245 | } | ||
| 246 | Ok(true) | ||
| 247 | } | ||
| 248 | Err(e) => Err(format!("Database query failed: {}", e)), | ||
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 252 | /// Find all repository announcements where the given pubkey is authorized | ||
| 253 | async fn find_authorized_announcements( | ||
| 254 | &self, | ||
| 255 | identifier: &str, | ||
| 256 | state_author: &PublicKey, | ||
| 257 | ) -> Result<Vec<RepositoryAnnouncement>, String> { | ||
| 258 | let filter = Filter::new() | ||
| 259 | .kind(Kind::from(KIND_REPOSITORY_ANNOUNCEMENT)) | ||
| 260 | .custom_tag( | ||
| 261 | SingleLetterTag::lowercase(Alphabet::D), | ||
| 262 | identifier.to_string(), | ||
| 263 | ); | ||
| 264 | |||
| 265 | match self.ctx.database.query(filter).await { | ||
| 266 | Ok(events) => { | ||
| 267 | let mut authorized = Vec::new(); | ||
| 268 | let state_author_hex = state_author.to_hex(); | ||
| 269 | |||
| 270 | for event in events { | ||
| 271 | if let Ok(announcement) = RepositoryAnnouncement::from_event(event.clone()) { | ||
| 272 | // Check if state author is authorized for this announcement | ||
| 273 | let is_owner = event.pubkey == *state_author; | ||
| 274 | let is_maintainer = announcement.maintainers.contains(&state_author_hex); | ||
| 275 | |||
| 276 | if is_owner || is_maintainer { | ||
| 277 | tracing::debug!( | ||
| 278 | "Found authorized announcement for {}: owner={}, maintainer={}", | ||
| 279 | identifier, | ||
| 280 | if is_owner { | ||
| 281 | event.pubkey.to_hex() | ||
| 282 | } else { | ||
| 283 | "n/a".to_string() | ||
| 284 | }, | ||
| 285 | is_maintainer | ||
| 286 | ); | ||
| 287 | authorized.push(announcement); | ||
| 288 | } | ||
| 289 | } | ||
| 290 | } | ||
| 291 | Ok(authorized) | ||
| 292 | } | ||
| 293 | Err(e) => Err(format!("Database query failed: {}", e)), | ||
| 294 | } | ||
| 295 | } | ||
| 296 | |||
| 297 | /// Identify all owner repositories for which this state event is the latest authorized state | ||
| 298 | async fn identify_owner_repositories( | ||
| 299 | &self, | ||
| 300 | state: &RepositoryState, | ||
| 301 | ) -> Result<Vec<(RepositoryAnnouncement, std::path::PathBuf)>, String> { | ||
| 302 | // Find all announcements where state author is authorized | ||
| 303 | let announcements = self | ||
| 304 | .find_authorized_announcements(&state.identifier, &state.event.pubkey) | ||
| 305 | .await?; | ||
| 306 | |||
| 307 | if announcements.is_empty() { | ||
| 308 | tracing::debug!( | ||
| 309 | "No authorized announcements found for state {} by {}", | ||
| 310 | state.identifier, | ||
| 311 | state.event.pubkey.to_hex() | ||
| 312 | ); | ||
| 313 | return Ok(Vec::new()); | ||
| 314 | } | ||
| 315 | |||
| 316 | let mut owner_repos = Vec::new(); | ||
| 317 | |||
| 318 | for announcement in announcements { | ||
| 319 | // Build the list of authorized pubkeys for this specific announcement | ||
| 320 | let mut authorized_pubkeys = vec![announcement.event.pubkey]; | ||
| 321 | for maintainer_hex in &announcement.maintainers { | ||
| 322 | if let Ok(pk) = PublicKey::from_hex(maintainer_hex) { | ||
| 323 | authorized_pubkeys.push(pk); | ||
| 324 | } | ||
| 325 | } | ||
| 326 | |||
| 327 | // Check if this is the latest state event for THIS announcement's context | ||
| 328 | if !self | ||
| 329 | .is_latest_state_for_identifier(state, &authorized_pubkeys) | ||
| 330 | .await? | ||
| 331 | { | ||
| 332 | tracing::debug!( | ||
| 333 | "Skipping {} in {}'s repo - not the latest state event for this context", | ||
| 334 | state.identifier, | ||
| 335 | announcement.event.pubkey.to_hex() | ||
| 336 | ); | ||
| 337 | continue; | ||
| 338 | } | ||
| 339 | |||
| 340 | // Build repository path: <git_data_path>/<owner_npub>/<identifier>.git | ||
| 341 | let repo_path = self | ||
| 342 | .ctx | ||
| 343 | .git_data_path | ||
| 344 | .join(announcement.repo_path().clone()); | ||
| 345 | owner_repos.push((announcement, repo_path)); | ||
| 346 | } | ||
| 347 | |||
| 348 | Ok(owner_repos) | ||
| 349 | } | ||
| 350 | |||
| 351 | /// Align a repository's refs with the authorized state | 168 | /// Align a repository's refs with the authorized state |
| 352 | /// | 169 | /// |
| 353 | /// This function: | 170 | /// This function: |