upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/nostr
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-19 11:55:32 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-19 15:43:29 +0000
commitfa065ad128882755f2a988d6203b59a2ab5e38ff (patch)
treee8326de70a6e6ea56b5bf4250e0a00a3cda4afed /src/nostr
parent98c6fa4bfa897ff0b8f9c95ea698d4d065b5e9f3 (diff)
add landing page and nostr-relay-builder relay on same port
Diffstat (limited to 'src/nostr')
-rw-r--r--src/nostr/builder.rs121
-rw-r--r--src/nostr/mod.rs2
-rw-r--r--src/nostr/relay.rs340
3 files changed, 122 insertions, 341 deletions
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
new file mode 100644
index 0000000..cd1f4d2
--- /dev/null
+++ b/src/nostr/builder.rs
@@ -0,0 +1,121 @@
1/// Nostr Relay Builder Configuration
2///
3/// This module integrates nostr-relay-builder with NIP-34 validation logic
4/// preserved from the original implementation.
5use std::net::SocketAddr;
6use std::path::Path;
7
8use nostr::nips::nip19::ToBech32;
9use nostr_relay_builder::prelude::*;
10
11use crate::config::Config;
12use crate::nostr::events::{
13 validate_announcement, validate_state, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE,
14};
15
16/// NIP-34 Write Policy
17///
18/// Validates repository announcement and state events according to GRASP-01 spec.
19/// Preserves all original validation logic from src/nostr/events.rs.
20#[derive(Debug, Clone)]
21pub struct Nip34WritePolicy {
22 domain: String,
23}
24
25impl Nip34WritePolicy {
26 pub fn new(domain: impl Into<String>) -> Self {
27 Self {
28 domain: domain.into(),
29 }
30 }
31}
32
33impl WritePolicy for Nip34WritePolicy {
34 fn admit_event<'a>(
35 &'a self,
36 event: &'a nostr_relay_builder::prelude::Event,
37 _addr: &'a SocketAddr,
38 ) -> BoxedFuture<'a, PolicyResult> {
39 Box::pin(async move {
40 match event.kind.as_u16() {
41 KIND_REPOSITORY_ANNOUNCEMENT => match validate_announcement(event, &self.domain) {
42 Ok(_) => {
43 tracing::debug!(
44 "Accepted repository announcement: {}",
45 event
46 .id
47 .to_bech32()
48 .unwrap_or_else(|_| "invalid".to_string())
49 );
50 PolicyResult::Accept
51 }
52 Err(e) => {
53 tracing::warn!(
54 "Rejected repository announcement {}: {}",
55 event
56 .id
57 .to_bech32()
58 .unwrap_or_else(|_| "invalid".to_string()),
59 e
60 );
61 PolicyResult::Reject(e.to_string())
62 }
63 },
64 KIND_REPOSITORY_STATE => match validate_state(event) {
65 Ok(_) => {
66 tracing::debug!(
67 "Accepted repository state: {}",
68 event
69 .id
70 .to_bech32()
71 .unwrap_or_else(|_| "invalid".to_string())
72 );
73 PolicyResult::Accept
74 }
75 Err(e) => {
76 tracing::warn!(
77 "Rejected repository state {}: {}",
78 event
79 .id
80 .to_bech32()
81 .unwrap_or_else(|_| "invalid".to_string()),
82 e
83 );
84 PolicyResult::Reject(e.to_string())
85 }
86 },
87 // Accept all other event kinds without validation
88 _ => PolicyResult::Accept,
89 }
90 })
91 }
92}
93
94/// Create a configured LocalRelay with NIP-34 validation
95pub fn create_relay(config: &Config) -> Result<LocalRelay> {
96 tracing::info!("Configuring nostr relay...");
97
98 // Determine database path
99 let db_path = Path::new(&config.relay_data_path);
100
101 // Create database - using in-memory for now, can switch to persistent later
102 // TODO: Add configuration for NostrDB or LMDB backends
103 let database = MemoryDatabase::with_opts(MemoryDatabaseOptions {
104 events: true,
105 max_events: Some(100_000),
106 });
107
108 tracing::info!("Using in-memory database (path: {})", db_path.display());
109
110 // Build relay with NIP-34 validation
111 let builder = RelayBuilder::default()
112 .database(database)
113 .write_policy(Nip34WritePolicy::new(&config.domain));
114
115 tracing::info!(
116 "Relay configured with NIP-34 validation for domain: {}",
117 config.domain
118 );
119
120 Ok(LocalRelay::new(builder))
121}
diff --git a/src/nostr/mod.rs b/src/nostr/mod.rs
index b485b91..2bf0346 100644
--- a/src/nostr/mod.rs
+++ b/src/nostr/mod.rs
@@ -1,2 +1,2 @@
1pub mod builder;
1pub mod events; 2pub mod events;
2pub mod relay;
diff --git a/src/nostr/relay.rs b/src/nostr/relay.rs
deleted file mode 100644
index 1033b5b..0000000
--- a/src/nostr/relay.rs
+++ /dev/null
@@ -1,340 +0,0 @@
1use anyhow::Result;
2use futures_util::{SinkExt, StreamExt};
3use nostr_sdk::{Event, EventId, Filter, Kind};
4use serde_json::{json, Value};
5use std::collections::HashMap;
6use std::net::SocketAddr;
7use std::sync::Arc;
8use tokio::net::{TcpListener, TcpStream};
9use tokio::sync::RwLock;
10use tokio_tungstenite::{accept_async, tungstenite::Message};
11use tracing::{debug, error, info, warn};
12
13use crate::config::Config;
14use crate::nostr::events::{validate_announcement, validate_state, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE};
15use crate::storage::Storage;
16
17type Subscriptions = Arc<RwLock<HashMap<String, Vec<Filter>>>>;
18
19pub struct RelayServer {
20 config: Config,
21 storage: Storage,
22}
23
24impl RelayServer {
25 pub fn new(config: Config, storage: Storage) -> Result<Self> {
26 Ok(RelayServer { config, storage })
27 }
28
29 pub async fn run(self) -> Result<()> {
30 let addr: SocketAddr = self.config.bind_address.parse()?;
31 let listener = TcpListener::bind(&addr).await?;
32
33 info!("✅ Nostr relay listening on ws://{}", addr);
34 info!("📡 Ready to accept connections...");
35
36 loop {
37 match listener.accept().await {
38 Ok((stream, peer_addr)) => {
39 debug!("New connection from: {}", peer_addr);
40 let storage = self.storage.clone();
41 tokio::spawn(async move {
42 if let Err(e) = handle_connection(stream, storage).await {
43 error!("Error handling connection from {}: {}", peer_addr, e);
44 }
45 });
46 }
47 Err(e) => {
48 error!("Error accepting connection: {}", e);
49 }
50 }
51 }
52 }
53}
54
55async fn handle_connection(stream: TcpStream, storage: Storage) -> Result<()> {
56 let ws_stream = accept_async(stream).await?;
57 let (mut ws_sender, mut ws_receiver) = ws_stream.split();
58
59 let subscriptions: Subscriptions = Arc::new(RwLock::new(HashMap::new()));
60
61 while let Some(msg) = ws_receiver.next().await {
62 match msg {
63 Ok(Message::Text(text)) => {
64 debug!("Received message: {}", text);
65
66 match handle_message(&text, &storage, &subscriptions).await {
67 Ok(responses) => {
68 for response in responses {
69 let response_text = serde_json::to_string(&response)?;
70 debug!("Sending response: {}", response_text);
71 ws_sender.send(Message::Text(response_text)).await?;
72 }
73 }
74 Err(e) => {
75 warn!("Error handling message: {}", e);
76 let notice = json!(["NOTICE", format!("Error: {}", e)]);
77 ws_sender.send(Message::Text(notice.to_string())).await?;
78 }
79 }
80 }
81 Ok(Message::Close(_)) => {
82 debug!("Client closed connection");
83 break;
84 }
85 Ok(Message::Ping(data)) => {
86 ws_sender.send(Message::Pong(data)).await?;
87 }
88 Ok(_) => {
89 // Ignore other message types
90 }
91 Err(e) => {
92 error!("WebSocket error: {}", e);
93 break;
94 }
95 }
96 }
97
98 Ok(())
99}
100
101async fn handle_message(
102 text: &str,
103 storage: &Storage,
104 subscriptions: &Subscriptions,
105) -> Result<Vec<Value>> {
106 let msg: Value = serde_json::from_str(text)?;
107
108 if let Some(arr) = msg.as_array() {
109 if arr.is_empty() {
110 return Ok(vec![json!(["NOTICE", "Empty message"])]);
111 }
112
113 let msg_type = arr[0].as_str().unwrap_or("");
114
115 match msg_type {
116 "EVENT" => handle_event(arr, storage).await,
117 "REQ" => handle_req(arr, storage, subscriptions).await,
118 "CLOSE" => handle_close(arr, subscriptions).await,
119 _ => Ok(vec![json!(["NOTICE", format!("Unknown message type: {}", msg_type)])]),
120 }
121 } else {
122 Ok(vec![json!(["NOTICE", "Invalid message format"])])
123 }
124}
125
126async fn handle_event(arr: &[Value], storage: &Storage) -> Result<Vec<Value>> {
127 if arr.len() < 2 {
128 return Ok(vec![json!(["NOTICE", "EVENT message requires event object"])]);
129 }
130
131 let event: Event = serde_json::from_value(arr[1].clone())?;
132 let event_id = event.id;
133
134 // Verify event (signature and ID)
135 if event.verify().is_err() {
136 return Ok(vec![json!(["OK", event_id.to_hex(), false, "invalid: signature or ID verification failed"])]);
137 }
138
139 // Check if event already exists
140 if storage.get_event(&event_id.to_hex()).await.is_some() {
141 return Ok(vec![json!(["OK", event_id.to_hex(), true, "duplicate: event already exists"])]);
142 }
143
144 // Validate repository announcements (kind 30617)
145 if event.kind == Kind::from(KIND_REPOSITORY_ANNOUNCEMENT) {
146 // Get domain from storage config
147 let domain = storage.get_domain();
148
149 match validate_announcement(&event, &domain) {
150 Ok(()) => {
151 info!("✅ Valid repository announcement: {} ({})", event_id, event.kind);
152 }
153 Err(e) => {
154 warn!("❌ Invalid repository announcement: {}", e);
155 return Ok(vec![json!(["OK", event_id.to_hex(), false, format!("invalid: {}", e)])]);
156 }
157 }
158 }
159
160 // Validate repository state announcements (kind 30618)
161 if event.kind == Kind::from(KIND_REPOSITORY_STATE) {
162 match validate_state(&event) {
163 Ok(()) => {
164 info!("✅ Valid repository state: {} ({})", event_id, event.kind);
165 }
166 Err(e) => {
167 warn!("❌ Invalid repository state: {}", e);
168 return Ok(vec![json!(["OK", event_id.to_hex(), false, format!("invalid: {}", e)])]);
169 }
170 }
171 }
172
173 // Store the event
174 storage.store_event(event.clone()).await?;
175
176 info!("✅ Stored event: {} (kind: {})", event_id, event.kind);
177
178 Ok(vec![json!(["OK", event_id.to_hex(), true, ""])])
179}
180
181async fn handle_req(
182 arr: &[Value],
183 storage: &Storage,
184 subscriptions: &Subscriptions,
185) -> Result<Vec<Value>> {
186 if arr.len() < 2 {
187 return Ok(vec![json!(["NOTICE", "REQ message requires subscription ID"])]);
188 }
189
190 let sub_id = arr[1].as_str().ok_or_else(|| anyhow::anyhow!("Invalid subscription ID"))?;
191
192 // Parse filters
193 let mut filters = Vec::new();
194 for filter_value in &arr[2..] {
195 let filter: Filter = serde_json::from_value(filter_value.clone())?;
196 filters.push(filter.clone());
197 }
198
199 // Store subscription
200 {
201 let mut subs = subscriptions.write().await;
202 subs.insert(sub_id.to_string(), filters.clone());
203 }
204
205 debug!("Created subscription: {} with {} filters", sub_id, filters.len());
206
207 // Query and send matching events
208 let mut responses = Vec::new();
209
210 for filter in filters {
211 let events = storage.query_events(|event| {
212 matches_filter(event, &filter)
213 }).await;
214
215 for event in events {
216 responses.push(json!(["EVENT", sub_id, event]));
217 }
218 }
219
220 // Send EOSE (End of Stored Events)
221 responses.push(json!(["EOSE", sub_id]));
222
223 debug!("Subscription {} returned {} events", sub_id, responses.len() - 1);
224
225 Ok(responses)
226}
227
228async fn handle_close(arr: &[Value], subscriptions: &Subscriptions) -> Result<Vec<Value>> {
229 if arr.len() < 2 {
230 return Ok(vec![json!(["NOTICE", "CLOSE message requires subscription ID"])]);
231 }
232
233 let sub_id = arr[1].as_str().ok_or_else(|| anyhow::anyhow!("Invalid subscription ID"))?;
234
235 {
236 let mut subs = subscriptions.write().await;
237 subs.remove(sub_id);
238 }
239
240 debug!("Closed subscription: {}", sub_id);
241
242 Ok(vec![])
243}
244
245fn matches_filter(event: &Event, filter: &Filter) -> bool {
246 // Check IDs
247 if let Some(ref ids) = filter.ids {
248 if !ids.is_empty() && !ids.contains(&event.id) {
249 return false;
250 }
251 }
252
253 // Check authors
254 if let Some(ref authors) = filter.authors {
255 if !authors.is_empty() && !authors.contains(&event.pubkey) {
256 return false;
257 }
258 }
259
260 // Check kinds
261 if let Some(ref kinds) = filter.kinds {
262 if !kinds.is_empty() && !kinds.contains(&event.kind) {
263 return false;
264 }
265 }
266
267 // Check since
268 if let Some(since) = filter.since {
269 if event.created_at < since {
270 return false;
271 }
272 }
273
274 // Check until
275 if let Some(until) = filter.until {
276 if event.created_at > until {
277 return false;
278 }
279 }
280
281 // TODO: Check tags (#e, #p, etc.)
282
283 true
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use nostr_sdk::{EventBuilder, Keys, Kind};
290
291 #[test]
292 fn test_matches_filter_by_id() {
293 let keys = Keys::generate();
294 let event = EventBuilder::text_note("test")
295 .sign_with_keys(&keys)
296 .unwrap();
297
298 // Filter matching the event ID
299 let filter = Filter::new().id(event.id);
300 assert!(matches_filter(&event, &filter));
301
302 // Filter not matching
303 let other_id = EventId::all_zeros();
304 let filter = Filter::new().id(other_id);
305 assert!(!matches_filter(&event, &filter));
306 }
307
308 #[test]
309 fn test_matches_filter_by_author() {
310 let keys = Keys::generate();
311 let event = EventBuilder::text_note("test")
312 .sign_with_keys(&keys)
313 .unwrap();
314
315 // Filter matching the author
316 let filter = Filter::new().author(keys.public_key());
317 assert!(matches_filter(&event, &filter));
318
319 // Filter not matching
320 let other_keys = Keys::generate();
321 let filter = Filter::new().author(other_keys.public_key());
322 assert!(!matches_filter(&event, &filter));
323 }
324
325 #[test]
326 fn test_matches_filter_by_kind() {
327 let keys = Keys::generate();
328 let event = EventBuilder::text_note("test")
329 .sign_with_keys(&keys)
330 .unwrap();
331
332 // Filter matching the kind
333 let filter = Filter::new().kind(Kind::TextNote);
334 assert!(matches_filter(&event, &filter));
335
336 // Filter not matching
337 let filter = Filter::new().kind(Kind::Metadata);
338 assert!(!matches_filter(&event, &filter));
339 }
340}