diff options
| author | Your Name <you@example.com> | 2026-05-26 16:11:05 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-26 16:11:05 +0530 |
| commit | 8816a192c95cf539b65975469a2d61aed46f0414 (patch) | |
| tree | 7590318244a56fabbfa6919ef6d0fab5be529134 /src/discovery.rs | |
feat: initial implementation of grasp-mirror daemon
GRASP mirror daemon that discovers repos from watched npubs and mirrors
git data + Nostr events across all known GRASP servers for redundancy.
Features:
- Configurable npub watch list via .env (MIRROR_NPUBS)
- TOML config for GRASP server list, index relays, storage paths
- NIP-11 verification of GRASP servers on startup
- Discovery of repos via kind:30617 announcements on index relays
- Git mirroring (bare clone + push --mirror) to missing GRASP servers
- Nostr event forwarding to all GRASP server embedded relays
- SQLite state tracking for sync status and event dedup
- Optional signing key for updating announcements with new clone URLs
- CLI subcommands: daemon, status, verify, mirror-once
Architecture:
config.rs - TOML + .env config loading
db.rs - SQLite state tracking
health.rs - NIP-11 GRASP server verification
discovery.rs - Relay subscription, kind:30617 parsing
git_mirror.rs - Bare clone + push to GRASP servers
nostr_mirror.rs - Event forwarding to all GRASP relays
signing.rs - Optional announcement updates
main.rs - CLI entry point, daemon loop
Diffstat (limited to 'src/discovery.rs')
| -rw-r--r-- | src/discovery.rs | 142 |
1 files changed, 142 insertions, 0 deletions
diff --git a/src/discovery.rs b/src/discovery.rs new file mode 100644 index 0000000..59b9f2c --- /dev/null +++ b/src/discovery.rs | |||
| @@ -0,0 +1,142 @@ | |||
| 1 | use crate::db::MirrorDb; | ||
| 2 | use crate::health::GraspServer; | ||
| 3 | use anyhow::{Context, Result}; | ||
| 4 | use nostr::Kind; | ||
| 5 | use nostr_sdk::prelude::*; | ||
| 6 | use std::collections::HashMap; | ||
| 7 | |||
| 8 | #[derive(Debug, Clone)] | ||
| 9 | pub struct DiscoveredRepo { | ||
| 10 | pub pubkey: PublicKey, | ||
| 11 | pub identifier: String, | ||
| 12 | pub event_id: EventId, | ||
| 13 | pub clone_urls: Vec<String>, | ||
| 14 | pub relay_urls: Vec<String>, | ||
| 15 | } | ||
| 16 | |||
| 17 | pub async fn discover_repos_from_relays( | ||
| 18 | client: &nostr_sdk::Client, | ||
| 19 | npubs: &[PublicKey], | ||
| 20 | relay_urls: &[String], | ||
| 21 | ) -> Result<Vec<DiscoveredRepo>> { | ||
| 22 | for url in relay_urls { | ||
| 23 | if let Err(e) = client.add_relay(url).await { | ||
| 24 | tracing::warn!(relay = %url, error = %e, "failed to add relay"); | ||
| 25 | continue; | ||
| 26 | } | ||
| 27 | } | ||
| 28 | client.connect().await; | ||
| 29 | |||
| 30 | let mut repos = Vec::new(); | ||
| 31 | |||
| 32 | for pk in npubs { | ||
| 33 | let filter = Filter::new() | ||
| 34 | .kind(Kind::Custom(30617)) | ||
| 35 | .author(*pk) | ||
| 36 | .limit(50); | ||
| 37 | |||
| 38 | let events = client | ||
| 39 | .fetch_events(filter, std::time::Duration::from_secs(30)) | ||
| 40 | .await | ||
| 41 | .context("failed to fetch events from relays")?; | ||
| 42 | |||
| 43 | for event in events.into_iter() { | ||
| 44 | if let Some(repo) = parse_announcement(&event) { | ||
| 45 | repos.push(repo); | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | tracing::info!(count = repos.len(), "discovered repos from relays"); | ||
| 51 | Ok(repos) | ||
| 52 | } | ||
| 53 | |||
| 54 | fn parse_announcement(event: &Event) -> Option<DiscoveredRepo> { | ||
| 55 | let mut identifier = None; | ||
| 56 | let mut clone_urls = Vec::new(); | ||
| 57 | let mut relay_urls = Vec::new(); | ||
| 58 | |||
| 59 | for tag in event.tags.iter() { | ||
| 60 | let tag_vec = tag.clone().to_vec(); | ||
| 61 | if tag_vec.len() < 2 { | ||
| 62 | continue; | ||
| 63 | } | ||
| 64 | match tag_vec[0].as_str() { | ||
| 65 | "d" => identifier = Some(tag_vec[1].clone()), | ||
| 66 | "clone" => clone_urls.push(tag_vec[1].clone()), | ||
| 67 | "relays" => relay_urls.push(tag_vec[1].clone()), | ||
| 68 | _ => {} | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | let identifier = identifier?; | ||
| 73 | if clone_urls.is_empty() { | ||
| 74 | tracing::warn!( | ||
| 75 | identifier = %identifier, | ||
| 76 | "repo announcement has no clone URLs" | ||
| 77 | ); | ||
| 78 | } | ||
| 79 | |||
| 80 | Some(DiscoveredRepo { | ||
| 81 | pubkey: event.pubkey, | ||
| 82 | identifier, | ||
| 83 | event_id: event.id, | ||
| 84 | clone_urls, | ||
| 85 | relay_urls, | ||
| 86 | }) | ||
| 87 | } | ||
| 88 | |||
| 89 | pub async fn persist_discovered_repos( | ||
| 90 | db: &MirrorDb, | ||
| 91 | repos: &[DiscoveredRepo], | ||
| 92 | ) -> Result<HashMap<String, i64>> { | ||
| 93 | let mut repo_ids = HashMap::new(); | ||
| 94 | for repo in repos { | ||
| 95 | let pk_hex = repo.pubkey.to_hex(); | ||
| 96 | match db | ||
| 97 | .upsert_repo(&pk_hex, &repo.identifier, &repo.event_id.to_hex()) | ||
| 98 | .await | ||
| 99 | { | ||
| 100 | Ok(id) => { | ||
| 101 | tracing::debug!( | ||
| 102 | identifier = %repo.identifier, | ||
| 103 | repo_id = id, | ||
| 104 | "persisted repo" | ||
| 105 | ); | ||
| 106 | repo_ids.insert(format!("{}:{}", pk_hex, repo.identifier), id); | ||
| 107 | } | ||
| 108 | Err(e) => { | ||
| 109 | tracing::error!( | ||
| 110 | identifier = %repo.identifier, | ||
| 111 | error = %e, | ||
| 112 | "failed to persist repo" | ||
| 113 | ); | ||
| 114 | } | ||
| 115 | } | ||
| 116 | } | ||
| 117 | Ok(repo_ids) | ||
| 118 | } | ||
| 119 | |||
| 120 | pub fn identify_missing_servers( | ||
| 121 | repo: &DiscoveredRepo, | ||
| 122 | servers: &HashMap<String, GraspServer>, | ||
| 123 | ) -> Vec<GraspServer> { | ||
| 124 | let mut missing = Vec::new(); | ||
| 125 | |||
| 126 | for (_domain, server) in servers { | ||
| 127 | if !server.is_grasp_server() { | ||
| 128 | continue; | ||
| 129 | } | ||
| 130 | |||
| 131 | let has_clone = repo | ||
| 132 | .clone_urls | ||
| 133 | .iter() | ||
| 134 | .any(|url| url.contains(&server.domain)); | ||
| 135 | |||
| 136 | if !has_clone { | ||
| 137 | missing.push(server.clone()); | ||
| 138 | } | ||
| 139 | } | ||
| 140 | |||
| 141 | missing | ||
| 142 | } | ||