From 8816a192c95cf539b65975469a2d61aed46f0414 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 16:11:05 +0530 Subject: 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 --- src/nostr_mirror.rs | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/nostr_mirror.rs (limited to 'src/nostr_mirror.rs') diff --git a/src/nostr_mirror.rs b/src/nostr_mirror.rs new file mode 100644 index 0000000..76f66d0 --- /dev/null +++ b/src/nostr_mirror.rs @@ -0,0 +1,139 @@ +use crate::db::MirrorDb; +use crate::discovery::DiscoveredRepo; +use crate::health::GraspServer; +use anyhow::Result; +use nostr::Kind; +use nostr_sdk::prelude::*; + +pub struct NostrMirror { + client: nostr_sdk::Client, +} + +impl NostrMirror { + pub fn new(client: nostr_sdk::Client) -> Self { + Self { client } + } + + pub async fn forward_events_to_servers( + &self, + db: &MirrorDb, + events: &[Event], + servers: &[GraspServer], + ) -> Result<()> { + for event in events { + if db.have_seen_event(&event.id.to_hex()).await? { + continue; + } + + for server in servers { + if !server.is_grasp_server() { + continue; + } + + tracing::debug!( + event_id = %event.id.to_hex(), + kind = event.kind.as_u16(), + server = %server.domain, + "forwarding event" + ); + + let url: RelayUrl = RelayUrl::parse(&server.relay_url)?; + let urls = vec![url]; + + match self.client.send_event_to(urls, event.clone()).await { + Ok(_) => { + tracing::debug!( + event_id = %event.id.to_hex(), + server = %server.domain, + "event forwarded" + ); + } + Err(e) => { + tracing::warn!( + event_id = %event.id.to_hex(), + server = %server.domain, + error = %e, + "failed to forward event" + ); + } + } + } + + let _ = db.record_event(&event.id.to_hex()).await; + } + + Ok(()) + } + + pub async fn forward_repo_events( + &self, + db: &MirrorDb, + repo: &DiscoveredRepo, + servers: &[GraspServer], + ) -> Result<()> { + let filters = vec![ + Filter::new() + .kind(Kind::Custom(30617)) + .author(repo.pubkey) + .identifier(&repo.identifier), + Filter::new() + .kind(Kind::Custom(30618)) + .author(repo.pubkey) + .identifier(&repo.identifier), + ]; + + let mut all_events = Vec::new(); + for filter in filters { + let events = self + .client + .fetch_events(filter, std::time::Duration::from_secs(15)) + .await?; + all_events.extend(events); + } + + if all_events.is_empty() { + tracing::debug!(identifier = %repo.identifier, "no events to forward"); + return Ok(()); + } + + tracing::info!( + identifier = %repo.identifier, + count = all_events.len(), + "forwarding repo events" + ); + + self.forward_events_to_servers(db, &all_events, servers).await + } + + pub async fn sync_all_events( + &self, + db: &MirrorDb, + npubs: &[PublicKey], + servers: &[GraspServer], + ) -> Result<()> { + let git_kinds = [ + Kind::Custom(30617), + Kind::Custom(30618), + Kind::Custom(1631), + Kind::Custom(1642), + Kind::EventDeletion, + ]; + + let mut all_events = Vec::new(); + + for pk in npubs { + for kind in &git_kinds { + let filter = Filter::new().kind(*kind).author(*pk).limit(100); + let events = self + .client + .fetch_events(filter, std::time::Duration::from_secs(30)) + .await?; + all_events.extend(events); + } + } + + tracing::info!(count = all_events.len(), "fetched events for forwarding"); + + self.forward_events_to_servers(db, &all_events, servers).await + } +} -- cgit v1.2.3