upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/discovery.rs
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-26 16:11:05 +0530
committerYour Name <you@example.com>2026-05-26 16:11:05 +0530
commit8816a192c95cf539b65975469a2d61aed46f0414 (patch)
tree7590318244a56fabbfa6919ef6d0fab5be529134 /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.rs142
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 @@
1use crate::db::MirrorDb;
2use crate::health::GraspServer;
3use anyhow::{Context, Result};
4use nostr::Kind;
5use nostr_sdk::prelude::*;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
9pub 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
17pub 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
54fn 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
89pub 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
120pub 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}