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/nostr_mirror.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/nostr_mirror.rs')
| -rw-r--r-- | src/nostr_mirror.rs | 139 |
1 files changed, 139 insertions, 0 deletions
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 @@ | |||
| 1 | use crate::db::MirrorDb; | ||
| 2 | use crate::discovery::DiscoveredRepo; | ||
| 3 | use crate::health::GraspServer; | ||
| 4 | use anyhow::Result; | ||
| 5 | use nostr::Kind; | ||
| 6 | use nostr_sdk::prelude::*; | ||
| 7 | |||
| 8 | pub struct NostrMirror { | ||
| 9 | client: nostr_sdk::Client, | ||
| 10 | } | ||
| 11 | |||
| 12 | impl NostrMirror { | ||
| 13 | pub fn new(client: nostr_sdk::Client) -> Self { | ||
| 14 | Self { client } | ||
| 15 | } | ||
| 16 | |||
| 17 | pub async fn forward_events_to_servers( | ||
| 18 | &self, | ||
| 19 | db: &MirrorDb, | ||
| 20 | events: &[Event], | ||
| 21 | servers: &[GraspServer], | ||
| 22 | ) -> Result<()> { | ||
| 23 | for event in events { | ||
| 24 | if db.have_seen_event(&event.id.to_hex()).await? { | ||
| 25 | continue; | ||
| 26 | } | ||
| 27 | |||
| 28 | for server in servers { | ||
| 29 | if !server.is_grasp_server() { | ||
| 30 | continue; | ||
| 31 | } | ||
| 32 | |||
| 33 | tracing::debug!( | ||
| 34 | event_id = %event.id.to_hex(), | ||
| 35 | kind = event.kind.as_u16(), | ||
| 36 | server = %server.domain, | ||
| 37 | "forwarding event" | ||
| 38 | ); | ||
| 39 | |||
| 40 | let url: RelayUrl = RelayUrl::parse(&server.relay_url)?; | ||
| 41 | let urls = vec![url]; | ||
| 42 | |||
| 43 | match self.client.send_event_to(urls, event.clone()).await { | ||
| 44 | Ok(_) => { | ||
| 45 | tracing::debug!( | ||
| 46 | event_id = %event.id.to_hex(), | ||
| 47 | server = %server.domain, | ||
| 48 | "event forwarded" | ||
| 49 | ); | ||
| 50 | } | ||
| 51 | Err(e) => { | ||
| 52 | tracing::warn!( | ||
| 53 | event_id = %event.id.to_hex(), | ||
| 54 | server = %server.domain, | ||
| 55 | error = %e, | ||
| 56 | "failed to forward event" | ||
| 57 | ); | ||
| 58 | } | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | let _ = db.record_event(&event.id.to_hex()).await; | ||
| 63 | } | ||
| 64 | |||
| 65 | Ok(()) | ||
| 66 | } | ||
| 67 | |||
| 68 | pub async fn forward_repo_events( | ||
| 69 | &self, | ||
| 70 | db: &MirrorDb, | ||
| 71 | repo: &DiscoveredRepo, | ||
| 72 | servers: &[GraspServer], | ||
| 73 | ) -> Result<()> { | ||
| 74 | let filters = vec![ | ||
| 75 | Filter::new() | ||
| 76 | .kind(Kind::Custom(30617)) | ||
| 77 | .author(repo.pubkey) | ||
| 78 | .identifier(&repo.identifier), | ||
| 79 | Filter::new() | ||
| 80 | .kind(Kind::Custom(30618)) | ||
| 81 | .author(repo.pubkey) | ||
| 82 | .identifier(&repo.identifier), | ||
| 83 | ]; | ||
| 84 | |||
| 85 | let mut all_events = Vec::new(); | ||
| 86 | for filter in filters { | ||
| 87 | let events = self | ||
| 88 | .client | ||
| 89 | .fetch_events(filter, std::time::Duration::from_secs(15)) | ||
| 90 | .await?; | ||
| 91 | all_events.extend(events); | ||
| 92 | } | ||
| 93 | |||
| 94 | if all_events.is_empty() { | ||
| 95 | tracing::debug!(identifier = %repo.identifier, "no events to forward"); | ||
| 96 | return Ok(()); | ||
| 97 | } | ||
| 98 | |||
| 99 | tracing::info!( | ||
| 100 | identifier = %repo.identifier, | ||
| 101 | count = all_events.len(), | ||
| 102 | "forwarding repo events" | ||
| 103 | ); | ||
| 104 | |||
| 105 | self.forward_events_to_servers(db, &all_events, servers).await | ||
| 106 | } | ||
| 107 | |||
| 108 | pub async fn sync_all_events( | ||
| 109 | &self, | ||
| 110 | db: &MirrorDb, | ||
| 111 | npubs: &[PublicKey], | ||
| 112 | servers: &[GraspServer], | ||
| 113 | ) -> Result<()> { | ||
| 114 | let git_kinds = [ | ||
| 115 | Kind::Custom(30617), | ||
| 116 | Kind::Custom(30618), | ||
| 117 | Kind::Custom(1631), | ||
| 118 | Kind::Custom(1642), | ||
| 119 | Kind::EventDeletion, | ||
| 120 | ]; | ||
| 121 | |||
| 122 | let mut all_events = Vec::new(); | ||
| 123 | |||
| 124 | for pk in npubs { | ||
| 125 | for kind in &git_kinds { | ||
| 126 | let filter = Filter::new().kind(*kind).author(*pk).limit(100); | ||
| 127 | let events = self | ||
| 128 | .client | ||
| 129 | .fetch_events(filter, std::time::Duration::from_secs(30)) | ||
| 130 | .await?; | ||
| 131 | all_events.extend(events); | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | tracing::info!(count = all_events.len(), "fetched events for forwarding"); | ||
| 136 | |||
| 137 | self.forward_events_to_servers(db, &all_events, servers).await | ||
| 138 | } | ||
| 139 | } | ||