diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-24 08:02:12 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-24 11:54:18 +0000 |
| commit | 70d0197e85ae4ef85202781f6d2dc9e76bd508b3 (patch) | |
| tree | 45efb6565e81ba755acc5955e68d5b7119d1e122 /src/nostr | |
| parent | f8c3e3920ed2a1bdaab30be912276993449a5476 (diff) | |
feat(purgatory): add broken purgatory implementation
Diffstat (limited to 'src/nostr')
| -rw-r--r-- | src/nostr/builder.rs | 116 | ||||
| -rw-r--r-- | src/nostr/policy/mod.rs | 5 | ||||
| -rw-r--r-- | src/nostr/policy/pr_event.rs | 149 | ||||
| -rw-r--r-- | src/nostr/policy/state.rs | 55 |
4 files changed, 310 insertions, 15 deletions
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index 8dd6291..2b4d524 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs | |||
| @@ -13,7 +13,7 @@ use nostr_relay_builder::prelude::*; | |||
| 13 | 13 | ||
| 14 | use crate::config::{Config, DatabaseBackend}; | 14 | use crate::config::{Config, DatabaseBackend}; |
| 15 | use crate::nostr::events::{ | 15 | use crate::nostr::events::{ |
| 16 | RepositoryAnnouncement, RepositoryState, KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, | 16 | RepositoryAnnouncement, KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, |
| 17 | KIND_REPOSITORY_STATE, KIND_USER_GRASP_LIST, | 17 | KIND_REPOSITORY_STATE, KIND_USER_GRASP_LIST, |
| 18 | }; | 18 | }; |
| 19 | use crate::nostr::policy::{ | 19 | use crate::nostr::policy::{ |
| @@ -57,8 +57,9 @@ impl Nip34WritePolicy { | |||
| 57 | domain: impl Into<String>, | 57 | domain: impl Into<String>, |
| 58 | database: SharedDatabase, | 58 | database: SharedDatabase, |
| 59 | git_data_path: impl Into<std::path::PathBuf>, | 59 | git_data_path: impl Into<std::path::PathBuf>, |
| 60 | purgatory: std::sync::Arc<crate::purgatory::Purgatory>, | ||
| 60 | ) -> Self { | 61 | ) -> Self { |
| 61 | let ctx = PolicyContext::new(domain, database, git_data_path); | 62 | let ctx = PolicyContext::new(domain, database, git_data_path, purgatory); |
| 62 | Self { | 63 | Self { |
| 63 | announcement_policy: AnnouncementPolicy::new(ctx.clone()), | 64 | announcement_policy: AnnouncementPolicy::new(ctx.clone()), |
| 64 | state_policy: StatePolicy::new(ctx.clone()), | 65 | state_policy: StatePolicy::new(ctx.clone()), |
| @@ -143,21 +144,50 @@ impl Nip34WritePolicy { | |||
| 143 | 144 | ||
| 144 | match self.state_policy.validate(event) { | 145 | match self.state_policy.validate(event) { |
| 145 | StateResult::Accept => { | 146 | StateResult::Accept => { |
| 146 | // Parse state to get HEAD and branch info | 147 | // Parse state to get identifier for purgatory message |
| 147 | match RepositoryState::from_event(event.clone()) { | 148 | let identifier = event |
| 148 | Ok(_state) => { | 149 | .tags |
| 149 | // Process state alignment asynchronously | 150 | .iter() |
| 150 | if let Err(e) = self.state_policy.process_state_event(event).await { | 151 | .find_map(|tag| { |
| 151 | tracing::warn!("Failed to process state event {}: {}", event_id_str, e); | 152 | let tag_vec = tag.clone().to_vec(); |
| 153 | if tag_vec.len() >= 2 && tag_vec[0] == "d" { | ||
| 154 | Some(tag_vec[1].clone()) | ||
| 155 | } else { | ||
| 156 | None | ||
| 152 | } | 157 | } |
| 158 | }) | ||
| 159 | .unwrap_or_else(|| "unknown".to_string()); | ||
| 153 | 160 | ||
| 154 | tracing::debug!("Accepted repository state: {}", event_id_str); | 161 | // Process state alignment asynchronously |
| 162 | match self.state_policy.process_state_event(event).await { | ||
| 163 | Ok(0) => { | ||
| 164 | // No repos aligned - event was added to purgatory | ||
| 165 | tracing::info!( | ||
| 166 | "State event {} added to purgatory: waiting for git data for identifier {}", | ||
| 167 | event_id_str, | ||
| 168 | identifier | ||
| 169 | ); | ||
| 170 | WritePolicyResult::Reject { | ||
| 171 | status: true, // Client sees OK | ||
| 172 | message: format!( | ||
| 173 | "purgatory: state event stored, waiting for git push for {}", | ||
| 174 | identifier | ||
| 175 | ) | ||
| 176 | .into(), | ||
| 177 | } | ||
| 178 | } | ||
| 179 | Ok(count) => { | ||
| 180 | // Successfully aligned repos | ||
| 181 | tracing::debug!( | ||
| 182 | "Accepted repository state {}: aligned {} repo(s)", | ||
| 183 | event_id_str, | ||
| 184 | count | ||
| 185 | ); | ||
| 155 | WritePolicyResult::Accept | 186 | WritePolicyResult::Accept |
| 156 | } | 187 | } |
| 157 | Err(e) => { | 188 | Err(e) => { |
| 158 | tracing::warn!("Failed to parse repository state {}: {}", event_id_str, e); | 189 | tracing::warn!("Failed to process state event {}: {}", event_id_str, e); |
| 159 | // Still accept the event even if we can't parse it | 190 | // Still accept the event even if processing failed |
| 160 | // The validation passed, so it's structurally valid | ||
| 161 | WritePolicyResult::Accept | 191 | WritePolicyResult::Accept |
| 162 | } | 192 | } |
| 163 | } | 193 | } |
| @@ -173,6 +203,58 @@ impl Nip34WritePolicy { | |||
| 173 | async fn handle_pr_event(&self, event: &Event) -> WritePolicyResult { | 203 | async fn handle_pr_event(&self, event: &Event) -> WritePolicyResult { |
| 174 | let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); | 204 | let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); |
| 175 | 205 | ||
| 206 | // Check if git data exists (checks placeholders and commit existence) | ||
| 207 | match self.pr_event_policy.check_git_data_exists(event).await { | ||
| 208 | Ok(false) => { | ||
| 209 | // No git data exists - add to purgatory | ||
| 210 | let commit = event | ||
| 211 | .tags | ||
| 212 | .iter() | ||
| 213 | .find_map(|tag| { | ||
| 214 | let tag_vec = tag.clone().to_vec(); | ||
| 215 | if tag_vec.len() >= 2 && tag_vec[0] == "c" { | ||
| 216 | Some(tag_vec[1].clone()) | ||
| 217 | } else { | ||
| 218 | None | ||
| 219 | } | ||
| 220 | }) | ||
| 221 | .unwrap_or_else(|| "unknown".to_string()); | ||
| 222 | |||
| 223 | tracing::info!( | ||
| 224 | "PR event {} added to purgatory: waiting for git push with commit {}", | ||
| 225 | event_id_str, | ||
| 226 | commit | ||
| 227 | ); | ||
| 228 | |||
| 229 | // Add to purgatory | ||
| 230 | self.ctx | ||
| 231 | .purgatory | ||
| 232 | .add_pr(event.clone(), event.id.to_hex(), commit.clone()); | ||
| 233 | |||
| 234 | return WritePolicyResult::Reject { | ||
| 235 | status: true, // Client sees OK | ||
| 236 | message: format!( | ||
| 237 | "purgatory: PR event stored, waiting for git push with commit {}", | ||
| 238 | commit | ||
| 239 | ) | ||
| 240 | .into(), | ||
| 241 | }; | ||
| 242 | } | ||
| 243 | Ok(true) => { | ||
| 244 | // Git data exists - proceed with normal validation | ||
| 245 | tracing::debug!("Git data exists for PR event {}", event_id_str); | ||
| 246 | } | ||
| 247 | Err(e) => { | ||
| 248 | // Error checking git data - reject event | ||
| 249 | tracing::warn!( | ||
| 250 | "Failed to check git data for PR event {}: {}", | ||
| 251 | event_id_str, | ||
| 252 | e | ||
| 253 | ); | ||
| 254 | return WritePolicyResult::reject(format!("Failed to check git data: {}", e)); | ||
| 255 | } | ||
| 256 | } | ||
| 257 | |||
| 176 | // Validate refs/nostr refs for this PR event | 258 | // Validate refs/nostr refs for this PR event |
| 177 | // This deletes any refs/nostr/<event-id> that points to wrong commit | 259 | // This deletes any refs/nostr/<event-id> that points to wrong commit |
| 178 | if let Err(e) = self.pr_event_policy.validate_nostr_ref(event).await { | 260 | if let Err(e) = self.pr_event_policy.validate_nostr_ref(event).await { |
| @@ -289,7 +371,10 @@ pub struct RelayWithDatabase { | |||
| 289 | /// Returns a `RelayWithDatabase` struct containing: | 371 | /// Returns a `RelayWithDatabase` struct containing: |
| 290 | /// - The `LocalRelay` for handling WebSocket connections | 372 | /// - The `LocalRelay` for handling WebSocket connections |
| 291 | /// - The `SharedDatabase` for direct database queries (e.g., push authorization) | 373 | /// - The `SharedDatabase` for direct database queries (e.g., push authorization) |
| 292 | pub async fn create_relay(config: &Config) -> Result<RelayWithDatabase> { | 374 | pub async fn create_relay( |
| 375 | config: &Config, | ||
| 376 | purgatory: Arc<crate::purgatory::Purgatory>, | ||
| 377 | ) -> Result<RelayWithDatabase> { | ||
| 293 | tracing::info!("Configuring nostr relay with GRASP-01 validation..."); | 378 | tracing::info!("Configuring nostr relay with GRASP-01 validation..."); |
| 294 | 379 | ||
| 295 | // Determine database path | 380 | // Determine database path |
| @@ -337,7 +422,10 @@ pub async fn create_relay(config: &Config) -> Result<RelayWithDatabase> { | |||
| 337 | // Build relay with GRASP-01 validation | 422 | // Build relay with GRASP-01 validation |
| 338 | // Clone Arc for the write policy so both relay and policy can access the database | 423 | // Clone Arc for the write policy so both relay and policy can access the database |
| 339 | let git_data_path = config.effective_git_data_path(); | 424 | let git_data_path = config.effective_git_data_path(); |
| 340 | let write_policy = Nip34WritePolicy::new(&config.domain, database.clone(), &git_data_path); | 425 | |
| 426 | // Create write policy with purgatory integration | ||
| 427 | let write_policy = | ||
| 428 | Nip34WritePolicy::new(&config.domain, database.clone(), &git_data_path, purgatory); | ||
| 341 | 429 | ||
| 342 | let relay = LocalRelayBuilder::default() | 430 | let relay = LocalRelayBuilder::default() |
| 343 | .database(database.clone()) | 431 | .database(database.clone()) |
diff --git a/src/nostr/policy/mod.rs b/src/nostr/policy/mod.rs index 19db5f6..2a446fe 100644 --- a/src/nostr/policy/mod.rs +++ b/src/nostr/policy/mod.rs | |||
| @@ -16,6 +16,8 @@ pub use related::{ReferenceResult, RelatedEventPolicy}; | |||
| 16 | pub use state::{AlignmentResult, StatePolicy, StateResult}; | 16 | pub use state::{AlignmentResult, StatePolicy, StateResult}; |
| 17 | 17 | ||
| 18 | use super::SharedDatabase; | 18 | use super::SharedDatabase; |
| 19 | use crate::purgatory::Purgatory; | ||
| 20 | use std::sync::Arc; | ||
| 19 | 21 | ||
| 20 | /// Shared context for all sub-policies | 22 | /// Shared context for all sub-policies |
| 21 | #[derive(Clone)] | 23 | #[derive(Clone)] |
| @@ -23,6 +25,7 @@ pub struct PolicyContext { | |||
| 23 | pub domain: String, | 25 | pub domain: String, |
| 24 | pub database: SharedDatabase, | 26 | pub database: SharedDatabase, |
| 25 | pub git_data_path: std::path::PathBuf, | 27 | pub git_data_path: std::path::PathBuf, |
| 28 | pub purgatory: Arc<Purgatory>, | ||
| 26 | } | 29 | } |
| 27 | 30 | ||
| 28 | impl PolicyContext { | 31 | impl PolicyContext { |
| @@ -30,11 +33,13 @@ impl PolicyContext { | |||
| 30 | domain: impl Into<String>, | 33 | domain: impl Into<String>, |
| 31 | database: SharedDatabase, | 34 | database: SharedDatabase, |
| 32 | git_data_path: impl Into<std::path::PathBuf>, | 35 | git_data_path: impl Into<std::path::PathBuf>, |
| 36 | purgatory: Arc<Purgatory>, | ||
| 33 | ) -> Self { | 37 | ) -> Self { |
| 34 | Self { | 38 | Self { |
| 35 | domain: domain.into(), | 39 | domain: domain.into(), |
| 36 | database, | 40 | database, |
| 37 | git_data_path: git_data_path.into(), | 41 | git_data_path: git_data_path.into(), |
| 42 | purgatory, | ||
| 38 | } | 43 | } |
| 39 | } | 44 | } |
| 40 | } | 45 | } |
diff --git a/src/nostr/policy/pr_event.rs b/src/nostr/policy/pr_event.rs index 53da369..c7602b0 100644 --- a/src/nostr/policy/pr_event.rs +++ b/src/nostr/policy/pr_event.rs | |||
| @@ -19,6 +19,155 @@ impl PrEventPolicy { | |||
| 19 | Self { ctx } | 19 | Self { ctx } |
| 20 | } | 20 | } |
| 21 | 21 | ||
| 22 | /// Check if git data exists for a PR event | ||
| 23 | /// | ||
| 24 | /// This checks: | ||
| 25 | /// 1. If a placeholder exists (git-data-first scenario) | ||
| 26 | /// 2. If the commit exists in any relevant repository | ||
| 27 | /// | ||
| 28 | /// # Returns | ||
| 29 | /// - `Ok(true)` if git data ready (either placeholder found or commit exists) | ||
| 30 | /// - `Ok(false)` if git data missing (should add to purgatory) | ||
| 31 | /// - `Err(msg)` on errors | ||
| 32 | pub async fn check_git_data_exists(&self, event: &Event) -> Result<bool, String> { | ||
| 33 | let event_id = event.id.to_hex(); | ||
| 34 | |||
| 35 | // Extract the `c` tag (commit hash) from the PR event | ||
| 36 | let commit = event.tags.iter().find_map(|tag| { | ||
| 37 | let tag_vec = tag.clone().to_vec(); | ||
| 38 | if tag_vec.len() >= 2 && tag_vec[0] == "c" { | ||
| 39 | Some(tag_vec[1].clone()) | ||
| 40 | } else { | ||
| 41 | None | ||
| 42 | } | ||
| 43 | }); | ||
| 44 | |||
| 45 | let commit = match commit { | ||
| 46 | Some(c) => c, | ||
| 47 | None => { | ||
| 48 | return Err(format!("PR event {} has no 'c' tag", event_id)); | ||
| 49 | } | ||
| 50 | }; | ||
| 51 | |||
| 52 | // Check for placeholder first (git-data-first scenario) | ||
| 53 | if let Some(placeholder_commit) = self.ctx.purgatory.find_pr_placeholder(&event_id) { | ||
| 54 | if placeholder_commit == commit { | ||
| 55 | // Perfect match - git data arrived first with matching commit | ||
| 56 | tracing::debug!( | ||
| 57 | "Found matching placeholder for PR event {} with commit {}", | ||
| 58 | event_id, | ||
| 59 | commit | ||
| 60 | ); | ||
| 61 | // Remove placeholder - event processing will continue normally | ||
| 62 | self.ctx.purgatory.remove_pr(&event_id); | ||
| 63 | return Ok(true); | ||
| 64 | } else { | ||
| 65 | // Placeholder has different commit - incoming event supersedes | ||
| 66 | tracing::info!( | ||
| 67 | "PR event {} supersedes placeholder: event expects commit {}, placeholder has {}", | ||
| 68 | event_id, | ||
| 69 | commit, | ||
| 70 | placeholder_commit | ||
| 71 | ); | ||
| 72 | // Remove placeholder with old commit data | ||
| 73 | self.ctx.purgatory.remove_pr(&event_id); | ||
| 74 | // TODO: Also remove git data (refs/nostr/<event-id>) - Phase 5 | ||
| 75 | // Fall through to check if new commit exists | ||
| 76 | } | ||
| 77 | } | ||
| 78 | |||
| 79 | // Check if commit exists in any repository referenced by this PR | ||
| 80 | // Extract ALL `a` tags (repository references) from the PR event | ||
| 81 | let repo_refs: Vec<String> = event | ||
| 82 | .tags | ||
| 83 | .iter() | ||
| 84 | .filter_map(|tag| { | ||
| 85 | let tag_vec = tag.clone().to_vec(); | ||
| 86 | if tag_vec.len() >= 2 && tag_vec[0] == "a" && tag_vec[1].starts_with("30617:") { | ||
| 87 | Some(tag_vec[1].clone()) | ||
| 88 | } else { | ||
| 89 | None | ||
| 90 | } | ||
| 91 | }) | ||
| 92 | .collect(); | ||
| 93 | |||
| 94 | if repo_refs.is_empty() { | ||
| 95 | // No repo references - cannot check git data | ||
| 96 | // This is unusual but let it through (other validation will catch issues) | ||
| 97 | return Ok(true); | ||
| 98 | } | ||
| 99 | |||
| 100 | // Check each repository to see if commit exists | ||
| 101 | for repo_ref in repo_refs { | ||
| 102 | // Parse the repo reference: 30617:<pubkey>:<identifier> | ||
| 103 | let parts: Vec<&str> = repo_ref.split(':').collect(); | ||
| 104 | if parts.len() < 3 { | ||
| 105 | continue; | ||
| 106 | } | ||
| 107 | |||
| 108 | let repo_pubkey = match PublicKey::from_hex(parts[1]) { | ||
| 109 | Ok(pk) => pk, | ||
| 110 | Err(_) => continue, | ||
| 111 | }; | ||
| 112 | let identifier = parts[2]; | ||
| 113 | |||
| 114 | // Look up repository announcement to get the npub for path | ||
| 115 | let filter = Filter::new() | ||
| 116 | .kind(Kind::from(KIND_REPOSITORY_ANNOUNCEMENT)) | ||
| 117 | .author(repo_pubkey) | ||
| 118 | .custom_tag( | ||
| 119 | SingleLetterTag::lowercase(Alphabet::D), | ||
| 120 | identifier.to_string(), | ||
| 121 | ); | ||
| 122 | |||
| 123 | let announcements: Vec<Event> = match self.ctx.database.query(filter).await { | ||
| 124 | Ok(events) => events.into_iter().collect(), | ||
| 125 | Err(e) => { | ||
| 126 | tracing::warn!( | ||
| 127 | "Failed to query for repository announcement for PR {}: {}", | ||
| 128 | event_id, | ||
| 129 | e | ||
| 130 | ); | ||
| 131 | continue; | ||
| 132 | } | ||
| 133 | }; | ||
| 134 | |||
| 135 | if announcements.is_empty() { | ||
| 136 | continue; | ||
| 137 | } | ||
| 138 | |||
| 139 | // Check each matching announcement | ||
| 140 | for announcement_event in announcements { | ||
| 141 | let announcement = match RepositoryAnnouncement::from_event(announcement_event) { | ||
| 142 | Ok(a) => a, | ||
| 143 | Err(_) => continue, | ||
| 144 | }; | ||
| 145 | |||
| 146 | // Build repository path | ||
| 147 | let repo_path = self.ctx.git_data_path.join(announcement.repo_path()); | ||
| 148 | |||
| 149 | // Check if commit exists | ||
| 150 | if git::commit_exists(&repo_path, &commit) { | ||
| 151 | tracing::debug!( | ||
| 152 | "Found commit {} for PR event {} in repository {}", | ||
| 153 | commit, | ||
| 154 | event_id, | ||
| 155 | repo_path.display() | ||
| 156 | ); | ||
| 157 | return Ok(true); | ||
| 158 | } | ||
| 159 | } | ||
| 160 | } | ||
| 161 | |||
| 162 | // No git data found - should add to purgatory | ||
| 163 | tracing::debug!( | ||
| 164 | "No git data found for PR event {} with commit {}", | ||
| 165 | event_id, | ||
| 166 | commit | ||
| 167 | ); | ||
| 168 | Ok(false) | ||
| 169 | } | ||
| 170 | |||
| 22 | /// Validate refs/nostr/<event-id> ref against a PR or PR Update event's `c` tag | 171 | /// Validate refs/nostr/<event-id> ref against a PR or PR Update event's `c` tag |
| 23 | /// | 172 | /// |
| 24 | /// When a PR event (kind 1618) or PR Update event (kind 1619) is received, | 173 | /// When a PR event (kind 1618) or PR Update event (kind 1619) is received, |
diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs index 43349e2..5e749ed 100644 --- a/src/nostr/policy/state.rs +++ b/src/nostr/policy/state.rs | |||
| @@ -66,6 +66,24 @@ impl StatePolicy { | |||
| 66 | let state = RepositoryState::from_event(event.clone()) | 66 | let state = RepositoryState::from_event(event.clone()) |
| 67 | .map_err(|e| format!("Failed to parse state: {}", e))?; | 67 | .map_err(|e| format!("Failed to parse state: {}", e))?; |
| 68 | 68 | ||
| 69 | // Check if ANY git repositories exist for this identifier (regardless of authorization) | ||
| 70 | // This helps us distinguish "no git data yet" from "not authorized" or "not latest" | ||
| 71 | let has_any_git_data = self.has_git_data_for_identifier(&state.identifier); | ||
| 72 | |||
| 73 | if !has_any_git_data { | ||
| 74 | // No git data exists yet - add to purgatory | ||
| 75 | tracing::debug!( | ||
| 76 | "No git data found for identifier {}, adding state event {} to purgatory", | ||
| 77 | state.identifier, | ||
| 78 | event.id.to_hex() | ||
| 79 | ); | ||
| 80 | self.ctx | ||
| 81 | .purgatory | ||
| 82 | .add_state(event.clone(), state.identifier.clone(), event.pubkey); | ||
| 83 | // Return 0 repos aligned, but this is not an error | ||
| 84 | return Ok(0); | ||
| 85 | } | ||
| 86 | |||
| 69 | // Identify owner repositories for which this is the latest authorized state | 87 | // Identify owner repositories for which this is the latest authorized state |
| 70 | let owner_repos = self.identify_owner_repositories(&state).await?; | 88 | let owner_repos = self.identify_owner_repositories(&state).await?; |
| 71 | let repo_count = owner_repos.len(); | 89 | let repo_count = owner_repos.len(); |
| @@ -97,13 +115,48 @@ impl StatePolicy { | |||
| 97 | ); | 115 | ); |
| 98 | } else { | 116 | } else { |
| 99 | tracing::debug!( | 117 | tracing::debug!( |
| 100 | "No owner repos to align for state - git data not available yet or not latest" | 118 | "No owner repos to align for state - git data exists but author not authorized or not latest" |
| 101 | ); | 119 | ); |
| 102 | } | 120 | } |
| 103 | 121 | ||
| 104 | Ok(total_aligned) | 122 | Ok(total_aligned) |
| 105 | } | 123 | } |
| 106 | 124 | ||
| 125 | /// Check if any git repositories exist for the given identifier | ||
| 126 | /// | ||
| 127 | /// Scans the git_data_path for any directories matching the pattern: | ||
| 128 | /// `<any-npub>/<identifier>.git` | ||
| 129 | /// | ||
| 130 | /// This is used to distinguish "no git data yet" from "not authorized". | ||
| 131 | fn has_git_data_for_identifier(&self, identifier: &str) -> bool { | ||
| 132 | let git_data_path = &self.ctx.git_data_path; | ||
| 133 | |||
| 134 | // Check if git_data_path exists | ||
| 135 | if !git_data_path.exists() { | ||
| 136 | return false; | ||
| 137 | } | ||
| 138 | |||
| 139 | // Scan for any npub directories | ||
| 140 | let read_dir = match std::fs::read_dir(git_data_path) { | ||
| 141 | Ok(dir) => dir, | ||
| 142 | Err(_) => return false, | ||
| 143 | }; | ||
| 144 | |||
| 145 | for entry in read_dir.flatten() { | ||
| 146 | if let Ok(file_type) = entry.file_type() { | ||
| 147 | if file_type.is_dir() { | ||
| 148 | // Check if <npub>/<identifier>.git exists | ||
| 149 | let repo_path = entry.path().join(format!("{}.git", identifier)); | ||
| 150 | if repo_path.exists() { | ||
| 151 | return true; | ||
| 152 | } | ||
| 153 | } | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | false | ||
| 158 | } | ||
| 159 | |||
| 107 | /// Check if this state event is the latest for its identifier among authorized authors | 160 | /// Check if this state event is the latest for its identifier among authorized authors |
| 108 | /// | 161 | /// |
| 109 | /// A state is considered "latest" if no other state event in the database | 162 | /// A state is considered "latest" if no other state event in the database |