upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/storage
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 10:42:18 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 10:42:18 +0000
commit9394657613014891ff91db6cd0a01b21bb257053 (patch)
treee59ff64c5463039e4304928b3b24377e3e438822 /src/storage
parent52bad9954cdddf55ab749fd0c6387edbc766632f (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.rs126
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 @@
1use anyhow::Result;
2use nostr_sdk::Event;
3use std::collections::HashMap;
4use std::sync::Arc;
5use tokio::sync::RwLock;
6
7use crate::config::Config;
8
9/// Simple in-memory storage for events
10/// TODO: Persist to disk for production use
11#[derive(Clone)]
12pub struct Storage {
13 events: Arc<RwLock<HashMap<String, Event>>>,
14 data_path: String,
15}
16
17impl 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)]
54mod 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}