diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-03 14:50:22 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-03 15:18:23 +0000 |
| commit | 874a8abe1d076cfafd9baf919ec23d7d58200698 (patch) | |
| tree | dce0d0d36bddc496ff32f8555a8790d8dc7be7e4 /src/nostr/builder.rs | |
| parent | 9fd4350c57bbe986ebf65bf3ea4c996572e81884 (diff) | |
| parent | 92a9a3bfe0bc522e8ae411991a366a3a6310d525 (diff) | |
Merge relay.ngit.dev migration: bug fixes and migration tooling
This merge includes critical bug fixes and comprehensive migration tooling
developed during the relay.ngit.dev migration effort.
Bug Fixes:
- Fix git protocol error handling to return HTTP 200 with ERR pkt-line
- Fix naughty list false positives and DNS failure identification
- Fix database query filters in load_existing_events (remove .since())
- Fix OID fetch tracking to distinguish 0 OIDs from successful fetches
- Fix purgatory event source tracking for filtered expiry logging
- Implement OID retry logic for 'not our ref' errors
Migration Tools & Documentation:
- Complete 5-phase migration analysis pipeline with orchestration script
- Phase 1: Event fetching from source relay
- Phase 2: Git sync verification
- Phase 3: Categorization and relay comparison
- Phase 4: Log extraction (parse failures, purgatory expiry)
- Phase 5: Action classification for migration decisions
- Comprehensive migration guide with lessons learned
- Troubleshooting guide for permission and corruption issues
Configuration:
- Add NGIT_LOG_LEVEL configuration option
- Update git throttle limits to 60/minute
- Improve logging throughout for better observability
Diffstat (limited to 'src/nostr/builder.rs')
| -rw-r--r-- | src/nostr/builder.rs | 159 |
1 files changed, 141 insertions, 18 deletions
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index 34014db..3baa2ff 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs | |||
| @@ -98,6 +98,62 @@ impl Nip34WritePolicy { | |||
| 98 | self.ctx.set_local_relay(relay); | 98 | self.ctx.set_local_relay(relay); |
| 99 | } | 99 | } |
| 100 | 100 | ||
| 101 | /// Extract repository identifier from event's 'd' tag. | ||
| 102 | /// | ||
| 103 | /// Used for structured logging when parsing fails - we try to extract | ||
| 104 | /// the identifier even if full parsing failed. | ||
| 105 | fn extract_identifier_from_event(event: &Event) -> String { | ||
| 106 | use nostr_relay_builder::prelude::TagKind; | ||
| 107 | event | ||
| 108 | .tags | ||
| 109 | .iter() | ||
| 110 | .find(|t| t.kind() == TagKind::d()) | ||
| 111 | .and_then(|t| t.content()) | ||
| 112 | .map(|s| s.to_string()) | ||
| 113 | .unwrap_or_else(|| "unknown".to_string()) | ||
| 114 | } | ||
| 115 | |||
| 116 | /// Extract ALL repository identifiers from PR event's 'a' tags. | ||
| 117 | /// | ||
| 118 | /// PR events can reference multiple repositories via multiple 'a' tags | ||
| 119 | /// (e.g., when there are multiple maintainers). Each tag has format | ||
| 120 | /// `30617:<owner_pubkey>:<identifier>`. | ||
| 121 | /// | ||
| 122 | /// Returns a vector of unique identifiers, or `["unknown"]` if none found. | ||
| 123 | fn extract_repos_from_pr_event(event: &Event) -> Vec<String> { | ||
| 124 | let repos: Vec<String> = event | ||
| 125 | .tags | ||
| 126 | .iter() | ||
| 127 | .filter_map(|tag| { | ||
| 128 | let tag_vec = tag.clone().to_vec(); | ||
| 129 | if tag_vec.len() >= 2 && tag_vec[0] == "a" && tag_vec[1].starts_with("30617:") { | ||
| 130 | // Format: 30617:<owner_pubkey>:<identifier> | ||
| 131 | let parts: Vec<&str> = tag_vec[1].split(':').collect(); | ||
| 132 | if parts.len() >= 3 { | ||
| 133 | Some(parts[2].to_string()) | ||
| 134 | } else { | ||
| 135 | None | ||
| 136 | } | ||
| 137 | } else { | ||
| 138 | None | ||
| 139 | } | ||
| 140 | }) | ||
| 141 | .collect(); | ||
| 142 | |||
| 143 | // Deduplicate while preserving order | ||
| 144 | let mut seen = std::collections::HashSet::new(); | ||
| 145 | let unique_repos: Vec<String> = repos | ||
| 146 | .into_iter() | ||
| 147 | .filter(|r| seen.insert(r.clone())) | ||
| 148 | .collect(); | ||
| 149 | |||
| 150 | if unique_repos.is_empty() { | ||
| 151 | vec!["unknown".to_string()] | ||
| 152 | } else { | ||
| 153 | unique_repos | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 101 | /// Handle repository announcement event | 157 | /// Handle repository announcement event |
| 102 | async fn handle_announcement(&self, event: &Event) -> WritePolicyResult { | 158 | async fn handle_announcement(&self, event: &Event) -> WritePolicyResult { |
| 103 | let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); | 159 | let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); |
| @@ -129,10 +185,21 @@ impl Nip34WritePolicy { | |||
| 129 | WritePolicyResult::Accept | 185 | WritePolicyResult::Accept |
| 130 | } | 186 | } |
| 131 | Err(e) => { | 187 | Err(e) => { |
| 188 | let npub = event | ||
| 189 | .pubkey | ||
| 190 | .to_bech32() | ||
| 191 | .unwrap_or_else(|_| event.pubkey.to_hex()); | ||
| 192 | let event_id_short = &event.id.to_hex()[..12]; | ||
| 193 | // Try to extract repo identifier from 'd' tag even if parsing failed | ||
| 194 | let repo = Self::extract_identifier_from_event(event); | ||
| 195 | // Structured log for migration scripts | ||
| 132 | tracing::warn!( | 196 | tracing::warn!( |
| 133 | "Failed to parse repository announcement {}: {}", | 197 | "[PARSE_FAIL] kind={} event_id={}... reason=\"{}\" repo={} npub={}", |
| 134 | event_id_str, | 198 | event.kind.as_u16(), |
| 135 | e | 199 | event_id_short, |
| 200 | e, | ||
| 201 | repo, | ||
| 202 | npub | ||
| 136 | ); | 203 | ); |
| 137 | WritePolicyResult::reject(format!("Failed to parse announcement: {}", e)) | 204 | WritePolicyResult::reject(format!("Failed to parse announcement: {}", e)) |
| 138 | } | 205 | } |
| @@ -157,10 +224,21 @@ impl Nip34WritePolicy { | |||
| 157 | WritePolicyResult::Accept | 224 | WritePolicyResult::Accept |
| 158 | } | 225 | } |
| 159 | Err(e) => { | 226 | Err(e) => { |
| 227 | let npub = event | ||
| 228 | .pubkey | ||
| 229 | .to_bech32() | ||
| 230 | .unwrap_or_else(|_| event.pubkey.to_hex()); | ||
| 231 | let event_id_short = &event.id.to_hex()[..12]; | ||
| 232 | // Try to extract repo identifier from 'd' tag even if parsing failed | ||
| 233 | let repo = Self::extract_identifier_from_event(event); | ||
| 234 | // Structured log for migration scripts | ||
| 160 | tracing::warn!( | 235 | tracing::warn!( |
| 161 | "Failed to parse maintainer announcement {}: {}", | 236 | "[PARSE_FAIL] kind={} event_id={}... reason=\"{}\" repo={} npub={}", |
| 162 | event_id_str, | 237 | event.kind.as_u16(), |
| 163 | e | 238 | event_id_short, |
| 239 | e, | ||
| 240 | repo, | ||
| 241 | npub | ||
| 164 | ); | 242 | ); |
| 165 | WritePolicyResult::reject(format!("Failed to parse announcement: {}", e)) | 243 | WritePolicyResult::reject(format!("Failed to parse announcement: {}", e)) |
| 166 | } | 244 | } |
| @@ -183,8 +261,6 @@ impl Nip34WritePolicy { | |||
| 183 | /// * `event` - The state event to validate | 261 | /// * `event` - The state event to validate |
| 184 | /// * `is_synced` - True if this event came from proactive sync (vs user-submitted) | 262 | /// * `is_synced` - True if this event came from proactive sync (vs user-submitted) |
| 185 | async fn handle_state(&self, event: &Event, is_synced: bool) -> WritePolicyResult { | 263 | async fn handle_state(&self, event: &Event, is_synced: bool) -> WritePolicyResult { |
| 186 | let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); | ||
| 187 | |||
| 188 | match self.state_policy.validate(event) { | 264 | match self.state_policy.validate(event) { |
| 189 | StateResult::Accept => { | 265 | StateResult::Accept => { |
| 190 | // Process state alignment asynchronously | 266 | // Process state alignment asynchronously |
| @@ -195,7 +271,22 @@ impl Nip34WritePolicy { | |||
| 195 | { | 271 | { |
| 196 | Ok(poilicy_result) => poilicy_result, | 272 | Ok(poilicy_result) => poilicy_result, |
| 197 | Err(e) => { | 273 | Err(e) => { |
| 198 | tracing::warn!("Failed to process state event {}: {}", event_id_str, e); | 274 | let npub = event |
| 275 | .pubkey | ||
| 276 | .to_bech32() | ||
| 277 | .unwrap_or_else(|_| event.pubkey.to_hex()); | ||
| 278 | let event_id_short = &event.id.to_hex()[..12]; | ||
| 279 | // Try to extract repo identifier from 'd' tag even if parsing failed | ||
| 280 | let repo = Self::extract_identifier_from_event(event); | ||
| 281 | // Structured log for migration scripts | ||
| 282 | tracing::warn!( | ||
| 283 | "[PARSE_FAIL] kind={} event_id={}... reason=\"{}\" repo={} npub={}", | ||
| 284 | event.kind.as_u16(), | ||
| 285 | event_id_short, | ||
| 286 | e, | ||
| 287 | repo, | ||
| 288 | npub | ||
| 289 | ); | ||
| 199 | // reject if processing failed | 290 | // reject if processing failed |
| 200 | WritePolicyResult::Reject { | 291 | WritePolicyResult::Reject { |
| 201 | status: false, | 292 | status: false, |
| @@ -205,7 +296,22 @@ impl Nip34WritePolicy { | |||
| 205 | } | 296 | } |
| 206 | } | 297 | } |
| 207 | StateResult::Reject(reason) => { | 298 | StateResult::Reject(reason) => { |
| 208 | tracing::warn!("Rejected repository state {}: {}", event_id_str, reason); | 299 | let npub = event |
| 300 | .pubkey | ||
| 301 | .to_bech32() | ||
| 302 | .unwrap_or_else(|_| event.pubkey.to_hex()); | ||
| 303 | let event_id_short = &event.id.to_hex()[..12]; | ||
| 304 | // Try to extract repo identifier from 'd' tag even if parsing failed | ||
| 305 | let repo = Self::extract_identifier_from_event(event); | ||
| 306 | // Structured log for migration scripts | ||
| 307 | tracing::warn!( | ||
| 308 | "[PARSE_FAIL] kind={} event_id={}... reason=\"{}\" repo={} npub={}", | ||
| 309 | event.kind.as_u16(), | ||
| 310 | event_id_short, | ||
| 311 | reason, | ||
| 312 | repo, | ||
| 313 | npub | ||
| 314 | ); | ||
| 209 | WritePolicyResult::reject(reason) | 315 | WritePolicyResult::reject(reason) |
| 210 | } | 316 | } |
| 211 | } | 317 | } |
| @@ -303,9 +409,12 @@ impl Nip34WritePolicy { | |||
| 303 | ); | 409 | ); |
| 304 | 410 | ||
| 305 | // Add to purgatory | 411 | // Add to purgatory |
| 306 | self.ctx | 412 | self.ctx.purgatory.add_pr( |
| 307 | .purgatory | 413 | event.clone(), |
| 308 | .add_pr(event.clone(), event.id.to_hex(), commit.clone()); | 414 | event.id.to_hex(), |
| 415 | commit.clone(), | ||
| 416 | is_synced, | ||
| 417 | ); | ||
| 309 | 418 | ||
| 310 | WritePolicyResult::Reject { | 419 | WritePolicyResult::Reject { |
| 311 | status: true, // Client sees OK | 420 | status: true, // Client sees OK |
| @@ -323,11 +432,25 @@ impl Nip34WritePolicy { | |||
| 323 | } | 432 | } |
| 324 | Err(e) => { | 433 | Err(e) => { |
| 325 | // Error checking git data - reject event | 434 | // Error checking git data - reject event |
| 326 | tracing::warn!( | 435 | let npub = event |
| 327 | "Failed to check git data for PR event {}: {}", | 436 | .pubkey |
| 328 | event_id_str, | 437 | .to_bech32() |
| 329 | e | 438 | .unwrap_or_else(|_| event.pubkey.to_hex()); |
| 330 | ); | 439 | let event_id_short = &event.id.to_hex()[..12]; |
| 440 | // Extract ALL repo identifiers from 'a' tags for PR events | ||
| 441 | // (PR events can reference multiple repos when there are multiple maintainers) | ||
| 442 | let repos = Self::extract_repos_from_pr_event(event); | ||
| 443 | // Structured log for migration scripts - log once per repo | ||
| 444 | for repo in &repos { | ||
| 445 | tracing::warn!( | ||
| 446 | "[PARSE_FAIL] kind={} event_id={}... reason=\"git data check failed: {}\" repo={} npub={}", | ||
| 447 | event.kind.as_u16(), | ||
| 448 | event_id_short, | ||
| 449 | e, | ||
| 450 | repo, | ||
| 451 | npub | ||
| 452 | ); | ||
| 453 | } | ||
| 331 | WritePolicyResult::reject(format!("Failed to check git data: {}", e)) | 454 | WritePolicyResult::reject(format!("Failed to check git data: {}", e)) |
| 332 | } | 455 | } |
| 333 | } | 456 | } |