diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-04 10:42:18 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-04 10:42:18 +0000 |
| commit | 9394657613014891ff91db6cd0a01b21bb257053 (patch) | |
| tree | e59ff64c5463039e4304928b3b24377e3e438822 /src/storage | |
| parent | 52bad9954cdddf55ab749fd0c6387edbc766632f (diff) | |
feat: implement NIP-01 compliant Nostr relay
- WebSocket-based relay using tokio-tungstenite
- Full NIP-01 protocol support (EVENT, REQ, CLOSE)
- Event validation (signature and ID)
- In-memory event storage
- Filter support (IDs, authors, kinds, since/until)
- Configuration via environment variables
- Nix flake for reproducible builds
- Test automation script
All 6 NIP-01 smoke tests passing (100%)
Diffstat (limited to 'src/storage')
| -rw-r--r-- | src/storage/mod.rs | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..2ec6d4e --- /dev/null +++ b/src/storage/mod.rs | |||
| @@ -0,0 +1,126 @@ | |||
| 1 | use anyhow::Result; | ||
| 2 | use nostr_sdk::Event; | ||
| 3 | use std::collections::HashMap; | ||
| 4 | use std::sync::Arc; | ||
| 5 | use tokio::sync::RwLock; | ||
| 6 | |||
| 7 | use crate::config::Config; | ||
| 8 | |||
| 9 | /// Simple in-memory storage for events | ||
| 10 | /// TODO: Persist to disk for production use | ||
| 11 | #[derive(Clone)] | ||
| 12 | pub struct Storage { | ||
| 13 | events: Arc<RwLock<HashMap<String, Event>>>, | ||
| 14 | data_path: String, | ||
| 15 | } | ||
| 16 | |||
| 17 | impl Storage { | ||
| 18 | pub fn new(config: &Config) -> Result<Self> { | ||
| 19 | // Create data directory if it doesn't exist | ||
| 20 | std::fs::create_dir_all(&config.relay_data_path)?; | ||
| 21 | |||
| 22 | Ok(Storage { | ||
| 23 | events: Arc::new(RwLock::new(HashMap::new())), | ||
| 24 | data_path: config.relay_data_path.clone(), | ||
| 25 | }) | ||
| 26 | } | ||
| 27 | |||
| 28 | pub async fn store_event(&self, event: Event) -> Result<()> { | ||
| 29 | let mut events = self.events.write().await; | ||
| 30 | events.insert(event.id.to_hex(), event); | ||
| 31 | Ok(()) | ||
| 32 | } | ||
| 33 | |||
| 34 | pub async fn get_event(&self, event_id: &str) -> Option<Event> { | ||
| 35 | let events = self.events.read().await; | ||
| 36 | events.get(event_id).cloned() | ||
| 37 | } | ||
| 38 | |||
| 39 | pub async fn query_events<F>(&self, filter: F) -> Vec<Event> | ||
| 40 | where | ||
| 41 | F: Fn(&Event) -> bool, | ||
| 42 | { | ||
| 43 | let events = self.events.read().await; | ||
| 44 | events.values().filter(|e| filter(e)).cloned().collect() | ||
| 45 | } | ||
| 46 | |||
| 47 | pub async fn count_events(&self) -> usize { | ||
| 48 | let events = self.events.read().await; | ||
| 49 | events.len() | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | #[cfg(test)] | ||
| 54 | mod tests { | ||
| 55 | use super::*; | ||
| 56 | use nostr_sdk::{EventBuilder, Keys, Kind}; | ||
| 57 | |||
| 58 | #[tokio::test] | ||
| 59 | async fn test_store_and_retrieve() { | ||
| 60 | let config = Config { | ||
| 61 | domain: "test".to_string(), | ||
| 62 | owner_npub: "npub1test".to_string(), | ||
| 63 | relay_name: "test".to_string(), | ||
| 64 | relay_description: "test".to_string(), | ||
| 65 | git_data_path: "./test_data/git".to_string(), | ||
| 66 | relay_data_path: "./test_data/relay".to_string(), | ||
| 67 | bind_address: "127.0.0.1:8080".to_string(), | ||
| 68 | }; | ||
| 69 | |||
| 70 | let storage = Storage::new(&config).unwrap(); | ||
| 71 | |||
| 72 | // Create a test event | ||
| 73 | let keys = Keys::generate(); | ||
| 74 | let event = EventBuilder::text_note("test content") | ||
| 75 | .sign_with_keys(&keys) | ||
| 76 | .unwrap(); | ||
| 77 | |||
| 78 | // Store it | ||
| 79 | storage.store_event(event.clone()).await.unwrap(); | ||
| 80 | |||
| 81 | // Retrieve it | ||
| 82 | let retrieved = storage.get_event(&event.id.to_hex()).await; | ||
| 83 | assert!(retrieved.is_some()); | ||
| 84 | assert_eq!(retrieved.unwrap().id, event.id); | ||
| 85 | |||
| 86 | // Count events | ||
| 87 | assert_eq!(storage.count_events().await, 1); | ||
| 88 | } | ||
| 89 | |||
| 90 | #[tokio::test] | ||
| 91 | async fn test_query_events() { | ||
| 92 | let config = Config { | ||
| 93 | domain: "test".to_string(), | ||
| 94 | owner_npub: "npub1test".to_string(), | ||
| 95 | relay_name: "test".to_string(), | ||
| 96 | relay_description: "test".to_string(), | ||
| 97 | git_data_path: "./test_data/git".to_string(), | ||
| 98 | relay_data_path: "./test_data/relay".to_string(), | ||
| 99 | bind_address: "127.0.0.1:8080".to_string(), | ||
| 100 | }; | ||
| 101 | |||
| 102 | let storage = Storage::new(&config).unwrap(); | ||
| 103 | |||
| 104 | // Create multiple events | ||
| 105 | let keys = Keys::generate(); | ||
| 106 | let event1 = EventBuilder::text_note("message 1") | ||
| 107 | .sign_with_keys(&keys) | ||
| 108 | .unwrap(); | ||
| 109 | let event2 = EventBuilder::text_note("message 2") | ||
| 110 | .sign_with_keys(&keys) | ||
| 111 | .unwrap(); | ||
| 112 | |||
| 113 | storage.store_event(event1.clone()).await.unwrap(); | ||
| 114 | storage.store_event(event2.clone()).await.unwrap(); | ||
| 115 | |||
| 116 | // Query all events | ||
| 117 | let all_events = storage.query_events(|_| true).await; | ||
| 118 | assert_eq!(all_events.len(), 2); | ||
| 119 | |||
| 120 | // Query by kind | ||
| 121 | let text_notes = storage | ||
| 122 | .query_events(|e| e.kind == Kind::TextNote) | ||
| 123 | .await; | ||
| 124 | assert_eq!(text_notes.len(), 2); | ||
| 125 | } | ||
| 126 | } | ||