1use std::collections::HashMap;
2use std::collections::{HashSet, VecDeque};
3use std::io::ErrorKind;
4use std::path::Path;
5use std::path::PathBuf;
6use std::sync::{
7 atomic::{AtomicU64, Ordering},
8 Arc,
9};
10use std::time::{Duration, Instant};
11
12use tokio::sync::{mpsc, Mutex, Semaphore};
13
14#[cfg(feature = "experimental-decentralized-pubsub")]
15use hashtree_network::{MeshEventStore, MeshRelayClient};
16use nostr::{ClientMessage as NostrClientMessage, JsonUtil, RelayMessage as NostrRelayMessage};
17use nostr::{Event, EventId, Filter as NostrFilter, SubscriptionId};
18
19use crate::socialgraph;
20
21const BLUETOOTH_EVENT_LOG_CAPACITY: usize = 100;
22const MAX_CONCURRENT_NOSTR_STORE_BLOCKING_TASKS: usize = 4;
23
24#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25pub struct BluetoothReceivedEventRecord {
26 pub event_id: String,
27 pub pubkey: String,
28 pub kind: u32,
29 pub created_at: u64,
30 pub received_at: u64,
31 pub peer_id: Option<String>,
32 pub cid_values: Vec<String>,
33}
34
35#[derive(Debug, Clone)]
36pub struct NostrRelayConfig {
37 pub spambox_db_max_bytes: u64,
38 pub max_query_limit: usize,
39 pub max_subs_per_client: usize,
40 pub max_filters_per_sub: usize,
41 pub spambox_max_events_per_min: u32,
42 pub spambox_max_reqs_per_min: u32,
43}
44
45impl Default for NostrRelayConfig {
46 fn default() -> Self {
47 Self {
48 spambox_db_max_bytes: 1024 * 1024 * 1024,
49 max_query_limit: 200,
50 max_subs_per_client: 64,
51 max_filters_per_sub: 32,
52 spambox_max_events_per_min: 120,
53 spambox_max_reqs_per_min: 120,
54 }
55 }
56}
57
58mod imp {
59 use super::*;
60 use anyhow::Result;
61
62 use crate::diagnostics::{
63 nostr_filter_summary, nostr_filters_summary, process_memory_snapshot,
64 trim_process_allocations,
65 };
66 use crate::socialgraph::{EventStorageClass, SocialGraphAccessControl, SocialGraphBackend};
67 use crate::storage::StorageRouter;
68 use hashtree_core::{nhash_decode, nhash_encode_full, Cid, NHashData};
69 use hashtree_nostr::{
70 is_parameterized_replaceable_kind, is_replaceable_kind, NostrEventStore, VerifiedEvent,
71 VerifiedStoredNostrEvent,
72 };
73 use tracing::{info, warn};
74
75 const NOSTR_INDEX_DIR: &str = "nostr-index";
76 const NOSTR_INDEX_LATEST_ROOT_FILE: &str = "latest-root.txt";
77 const NOSTR_INDEX_CHECKPOINT_ROOT_FILE: &str = "checkpoint-root.txt";
78
79 fn prefers_trusted_only(filter: &NostrFilter) -> bool {
80 let Some(kinds) = filter.kinds.as_ref() else {
81 return false;
82 };
83 if kinds.len() != 1 {
84 return false;
85 }
86
87 let kind = kinds.iter().next().expect("checked single kind").as_u16() as u32;
88 let has_authors = filter
89 .authors
90 .as_ref()
91 .is_some_and(|authors| !authors.is_empty());
92 if !has_authors {
93 return false;
94 }
95
96 if is_replaceable_kind(kind) {
97 return true;
98 }
99
100 if is_parameterized_replaceable_kind(kind) {
101 let d_tag = nostr::SingleLetterTag::lowercase(nostr::Alphabet::D);
102 return filter
103 .generic_tags
104 .get(&d_tag)
105 .is_some_and(|values| !values.is_empty());
106 }
107
108 false
109 }
110
111 struct NostrStore {
112 store: Arc<dyn SocialGraphBackend>,
113 blocking_permits: Arc<Semaphore>,
114 }
115
116 impl NostrStore {
117 fn new(store: Arc<dyn SocialGraphBackend>) -> Self {
118 Self {
119 store,
120 blocking_permits: Arc::new(Semaphore::new(
121 MAX_CONCURRENT_NOSTR_STORE_BLOCKING_TASKS,
122 )),
123 }
124 }
125
126 async fn ingest(&self, event: Event) -> Result<()> {
127 let store = Arc::clone(&self.store);
128 let _permit = self
129 .blocking_permits
130 .clone()
131 .acquire_owned()
132 .await
133 .map_err(|err| anyhow::anyhow!("trusted nostr store closed: {err}"))?;
134 tokio::task::spawn_blocking(move || {
135 crate::socialgraph::ingest_parsed_event(store.as_ref(), &event)
136 })
137 .await
138 .map_err(|err| anyhow::anyhow!("trusted nostr store ingest task failed: {err}"))?
139 }
140
141 async fn ingest_with_storage_class(
142 &self,
143 event: Event,
144 storage_class: EventStorageClass,
145 ) -> Result<()> {
146 let store = Arc::clone(&self.store);
147 let _permit = self
148 .blocking_permits
149 .clone()
150 .acquire_owned()
151 .await
152 .map_err(|err| anyhow::anyhow!("trusted nostr store closed: {err}"))?;
153 tokio::task::spawn_blocking(move || {
154 crate::socialgraph::ingest_parsed_event_with_storage_class(
155 store.as_ref(),
156 &event,
157 storage_class,
158 )
159 })
160 .await
161 .map_err(|err| anyhow::anyhow!("trusted nostr store ingest task failed: {err}"))?
162 }
163
164 async fn query(&self, filter: NostrFilter, limit: usize) -> Vec<Event> {
165 let store = Arc::clone(&self.store);
166 let filter_summary = nostr_filter_summary(&filter);
167 let memory_before = process_memory_snapshot();
168 let started = Instant::now();
169 let Ok(_permit) = self.blocking_permits.clone().acquire_owned().await else {
170 warn!("trusted nostr store query skipped: blocking semaphore closed");
171 return Vec::new();
172 };
173 let result = tokio::task::spawn_blocking(move || {
174 crate::socialgraph::query_events(store.as_ref(), &filter, limit)
175 })
176 .await;
177 match result {
178 Ok(events) => {
179 info!(
180 target: "hashtree_cli::nostr_relay::query",
181 limit,
182 events = events.len(),
183 elapsed_ms = started.elapsed().as_millis() as u64,
184 filter = %filter_summary,
185 memory_before = ?memory_before,
186 memory_after = ?process_memory_snapshot(),
187 "trusted nostr store query completed",
188 );
189 events
190 }
191 Err(err) => {
192 warn!("trusted nostr store query task failed: {}", err);
193 Vec::new()
194 }
195 }
196 }
197 }
198
199 struct HistoricalNostrIndex {
200 store: Arc<StorageRouter>,
201 latest_root_path: PathBuf,
202 checkpoint_root_path: PathBuf,
203 blocking_permits: Arc<Semaphore>,
204 }
205
206 impl HistoricalNostrIndex {
207 fn new(store: Arc<StorageRouter>, data_dir: PathBuf) -> Self {
208 let index_dir = data_dir.join(NOSTR_INDEX_DIR);
209 Self {
210 store,
211 latest_root_path: index_dir.join(NOSTR_INDEX_LATEST_ROOT_FILE),
212 checkpoint_root_path: index_dir.join(NOSTR_INDEX_CHECKPOINT_ROOT_FILE),
213 blocking_permits: Arc::new(Semaphore::new(
214 MAX_CONCURRENT_NOSTR_STORE_BLOCKING_TASKS,
215 )),
216 }
217 }
218
219 async fn ingest(&self, event: Event) -> Result<()> {
220 let root = self.load_existing_root().await?;
221 let store = Arc::clone(&self.store);
222 let Ok(_permit) = self.blocking_permits.clone().acquire_owned().await else {
223 anyhow::bail!("historical nostr index ingest skipped: blocking semaphore closed");
224 };
225 let next_root = tokio::task::spawn_blocking(move || {
226 let runtime = tokio::runtime::Builder::new_current_thread()
227 .enable_all()
228 .build()?;
229 runtime.block_on(async move {
230 let event_store = NostrEventStore::new(store);
231 let stored = VerifiedEvent::try_from(event)?
232 .to_stored_event()
233 .into_stored();
234 event_store
235 .build(root.as_ref(), vec![stored])
236 .await?
237 .or(root)
238 .ok_or_else(|| {
239 anyhow::anyhow!("historical nostr index ingest did not produce a root")
240 })
241 })
242 })
243 .await
244 .map_err(|err| anyhow::anyhow!("historical nostr index ingest task failed: {err}"))??;
245 self.persist_latest_root(&next_root).await
246 }
247
248 async fn query(&self, filter: &NostrFilter, limit: usize) -> Vec<Event> {
249 if limit == 0 {
250 return Vec::new();
251 }
252
253 let root = match self.load_existing_root().await {
254 Ok(Some(root)) => root,
255 Ok(None) => return Vec::new(),
256 Err(err) => {
257 warn!("historical nostr index root load failed: {}", err);
258 return Vec::new();
259 }
260 };
261
262 let filter_summary = nostr_filter_summary(filter);
263 let memory_before = process_memory_snapshot();
264 let started = Instant::now();
265 let store = Arc::clone(&self.store);
266 let filter = filter.clone();
267 let Ok(_permit) = self.blocking_permits.clone().acquire_owned().await else {
268 warn!("historical nostr index query skipped: blocking semaphore closed");
269 return Vec::new();
270 };
271 let result = tokio::task::spawn_blocking(move || {
272 let runtime = tokio::runtime::Builder::new_current_thread()
273 .enable_all()
274 .build()?;
275 let stored_events = runtime.block_on(async move {
276 let event_store = NostrEventStore::new(store);
277 event_store.query_events(Some(&root), &filter, limit).await
278 })?;
279 Ok::<_, anyhow::Error>(stored_events)
280 })
281 .await;
282 match result {
283 Ok(stored_events) => {
284 let stored_events = match stored_events {
285 Ok(stored_events) => stored_events,
286 Err(err) => {
287 warn!("historical nostr index query failed: {}", err);
288 return Vec::new();
289 }
290 };
291 let mut events = Vec::with_capacity(stored_events.len());
292 for stored in stored_events {
293 match VerifiedStoredNostrEvent::try_from(stored)
294 .and_then(|event| event.to_nostr_sdk_event())
295 .map(|event| event.into_event())
296 {
297 Ok(event) => events.push(event),
298 Err(err) => {
299 warn!("historical nostr index skipped invalid event: {}", err)
300 }
301 }
302 }
303 info!(
304 target: "hashtree_cli::nostr_relay::query",
305 limit,
306 events = events.len(),
307 elapsed_ms = started.elapsed().as_millis() as u64,
308 filter = %filter_summary,
309 memory_before = ?memory_before,
310 memory_after = ?process_memory_snapshot(),
311 "historical nostr index query completed",
312 );
313 events
314 }
315 Err(err) => {
316 warn!("historical nostr index query task failed: {}", err);
317 Vec::new()
318 }
319 }
320 }
321
322 async fn load_existing_root(&self) -> Result<Option<Cid>> {
323 if let Some(root) = load_nostr_index_root_file(&self.latest_root_path).await? {
324 return Ok(Some(root));
325 }
326 load_nostr_index_root_file(&self.checkpoint_root_path).await
327 }
328
329 async fn persist_latest_root(&self, root: &Cid) -> Result<()> {
330 if let Some(parent) = self.latest_root_path.parent() {
331 tokio::fs::create_dir_all(parent).await?;
332 }
333 tokio::fs::write(&self.latest_root_path, format!("{}\n", cid_to_nhash(root)?)).await?;
334 Ok(())
335 }
336 }
337
338 async fn load_nostr_index_root_file(path: &Path) -> Result<Option<Cid>> {
339 let root = match tokio::fs::read_to_string(path).await {
340 Ok(root) => root,
341 Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),
342 Err(err) => return Err(err.into()),
343 };
344 let trimmed = root.trim();
345 if trimmed.is_empty() {
346 return Ok(None);
347 }
348 parse_nostr_index_root(trimmed).map(Some)
349 }
350
351 fn parse_nostr_index_root(value: &str) -> Result<Cid> {
352 if value.starts_with("nhash1") {
353 let decoded = nhash_decode(value)?;
354 return Ok(Cid {
355 hash: decoded.hash,
356 key: decoded.decrypt_key,
357 });
358 }
359 Cid::parse(value).map_err(Into::into)
360 }
361
362 fn cid_to_nhash(cid: &Cid) -> Result<String> {
363 nhash_encode_full(&NHashData {
364 hash: cid.hash,
365 decrypt_key: cid.key,
366 })
367 .map_err(Into::into)
368 }
369
370 #[derive(Debug, Clone)]
371 struct ClientQuota {
372 last_reset: Instant,
373 spambox_events: u32,
374 reqs: u32,
375 }
376
377 impl ClientQuota {
378 fn new() -> Self {
379 Self {
380 last_reset: Instant::now(),
381 spambox_events: 0,
382 reqs: 0,
383 }
384 }
385
386 fn reset_if_needed(&mut self) {
387 if self.last_reset.elapsed() >= Duration::from_secs(60) {
388 self.last_reset = Instant::now();
389 self.spambox_events = 0;
390 self.reqs = 0;
391 }
392 }
393
394 fn allow_spambox_event(&mut self, limit: u32) -> bool {
395 self.reset_if_needed();
396 if self.spambox_events >= limit {
397 return false;
398 }
399 self.spambox_events += 1;
400 true
401 }
402
403 fn allow_req(&mut self, limit: u32) -> bool {
404 self.reset_if_needed();
405 if self.reqs >= limit {
406 return false;
407 }
408 self.reqs += 1;
409 true
410 }
411 }
412
413 struct ClientState {
414 sender: mpsc::UnboundedSender<String>,
415 pubkey: Option<String>,
416 quota: ClientQuota,
417 }
418
419 struct RecentEvents {
420 order: VecDeque<EventId>,
421 events: HashMap<EventId, Event>,
422 max_len: usize,
423 }
424
425 impl RecentEvents {
426 fn new(max_len: usize) -> Self {
427 Self {
428 order: VecDeque::new(),
429 events: HashMap::new(),
430 max_len: max_len.max(128),
431 }
432 }
433
434 fn insert(&mut self, event: Event) {
435 if self.events.contains_key(&event.id) {
436 return;
437 }
438 self.order.push_back(event.id);
439 self.events.insert(event.id, event);
440 while self.order.len() > self.max_len {
441 if let Some(oldest) = self.order.pop_front() {
442 self.events.remove(&oldest);
443 }
444 }
445 }
446
447 fn matching(&self, filter: &NostrFilter) -> Vec<Event> {
448 self.events
449 .values()
450 .filter(|event| filter.match_event(event, Default::default()))
451 .cloned()
452 .collect()
453 }
454 }
455
456 enum SpamboxStore {
457 Persistent(NostrStore),
458 Memory(MemorySpambox),
459 }
460
461 struct MemorySpambox {
462 events: Mutex<VecDeque<Event>>,
463 max_len: usize,
464 }
465
466 impl MemorySpambox {
467 fn new(max_len: usize) -> Self {
468 Self {
469 events: Mutex::new(VecDeque::new()),
470 max_len: max_len.max(128),
471 }
472 }
473
474 async fn ingest(&self, event: &Event) -> bool {
475 let mut events = self.events.lock().await;
476 events.push_back(event.clone());
477 while events.len() > self.max_len {
478 events.pop_front();
479 }
480 true
481 }
482 }
483
484 impl SpamboxStore {
485 async fn ingest(&self, event: &Event) -> bool {
486 match self {
487 SpamboxStore::Persistent(store) => store.ingest(event.clone()).await.is_ok(),
488 SpamboxStore::Memory(store) => store.ingest(event).await,
489 }
490 }
491 }
492
493 struct BluetoothEventLog {
494 path: PathBuf,
495 state: Mutex<BluetoothEventLogState>,
496 }
497
498 struct BluetoothEventLogState {
499 records: VecDeque<BluetoothReceivedEventRecord>,
500 event_ids: HashSet<String>,
501 }
502
503 impl BluetoothEventLog {
504 fn load(path: PathBuf) -> Self {
505 let records = std::fs::read_to_string(&path)
506 .ok()
507 .map(|serialized| {
508 serialized
509 .lines()
510 .filter_map(|line| {
511 serde_json::from_str::<BluetoothReceivedEventRecord>(line).ok()
512 })
513 .collect::<Vec<_>>()
514 })
515 .unwrap_or_default();
516 let mut trimmed = VecDeque::with_capacity(BLUETOOTH_EVENT_LOG_CAPACITY);
517 let start = records.len().saturating_sub(BLUETOOTH_EVENT_LOG_CAPACITY);
518 for record in records.into_iter().skip(start) {
519 trimmed.push_back(record);
520 }
521 let event_ids = trimmed
522 .iter()
523 .map(|record| record.event_id.clone())
524 .collect::<HashSet<_>>();
525
526 Self {
527 path,
528 state: Mutex::new(BluetoothEventLogState {
529 records: trimmed,
530 event_ids,
531 }),
532 }
533 }
534
535 async fn recent(&self, limit: usize) -> Vec<BluetoothReceivedEventRecord> {
536 let state = self.state.lock().await;
537 state
538 .records
539 .iter()
540 .rev()
541 .take(limit.max(1))
542 .cloned()
543 .collect()
544 }
545
546 async fn record(&self, event: &Event, peer_id: Option<String>) {
547 let record = BluetoothReceivedEventRecord {
548 event_id: event.id.to_hex(),
549 pubkey: event.pubkey.to_hex(),
550 kind: event.kind.as_u16() as u32,
551 created_at: event.created_at.as_secs(),
552 received_at: std::time::SystemTime::now()
553 .duration_since(std::time::UNIX_EPOCH)
554 .map(|value| value.as_secs())
555 .unwrap_or(0),
556 peer_id,
557 cid_values: cid_values_from_event(event),
558 };
559
560 let serialized = {
561 let mut state = self.state.lock().await;
562 if state.event_ids.contains(&record.event_id) {
563 return;
564 }
565
566 state.event_ids.insert(record.event_id.clone());
567 state.records.push_back(record);
568 while state.records.len() > BLUETOOTH_EVENT_LOG_CAPACITY {
569 if let Some(removed) = state.records.pop_front() {
570 state.event_ids.remove(&removed.event_id);
571 }
572 }
573
574 state
575 .records
576 .iter()
577 .filter_map(|entry| serde_json::to_string(entry).ok())
578 .collect::<Vec<_>>()
579 .join("\n")
580 };
581
582 if let Some(parent) = self.path.parent() {
583 let _ = std::fs::create_dir_all(parent);
584 }
585 let _ = std::fs::write(&self.path, serialized);
586 }
587 }
588
589 fn looks_like_cid_reference(value: &str) -> bool {
590 Cid::parse(value).is_ok() || nhash_decode(value).is_ok()
591 }
592
593 fn cid_values_from_event(event: &Event) -> Vec<String> {
594 let mut values = Vec::new();
595 let mut seen = HashSet::new();
596
597 for tag in event.tags.iter() {
598 let fields = tag.clone().to_vec();
599 if fields.first().is_some_and(|name| name == "cid") {
600 if let Some(value) = fields.get(1).filter(|value| !value.is_empty()) {
601 if seen.insert(value.clone()) {
602 values.push(value.clone());
603 }
604 }
605 continue;
606 }
607
608 for value in fields.into_iter().skip(1) {
609 if looks_like_cid_reference(&value) && seen.insert(value.clone()) {
610 values.push(value);
611 }
612 }
613 }
614
615 values
616 }
617
618 pub struct NostrRelay {
619 config: NostrRelayConfig,
620 trusted: NostrStore,
621 public_pubkeys: HashSet<String>,
622 spambox: Option<SpamboxStore>,
623 historical_index: Option<HistoricalNostrIndex>,
624 social_graph: Option<Arc<SocialGraphAccessControl>>,
625 clients: Mutex<HashMap<u64, ClientState>>,
626 subscriptions: Mutex<HashMap<u64, HashMap<SubscriptionId, Vec<NostrFilter>>>>,
627 recent_events: Mutex<RecentEvents>,
628 next_client_id: AtomicU64,
629 bluetooth_event_log: Arc<BluetoothEventLog>,
630 #[cfg(feature = "experimental-decentralized-pubsub")]
631 decentralized_pubsub_tx: std::sync::Mutex<Option<mpsc::UnboundedSender<Event>>>,
632 }
633
634 impl NostrRelay {
635 async fn collect_filter_events(
636 &self,
637 filter: &NostrFilter,
638 limit: usize,
639 seen: &mut HashSet<EventId>,
640 events: &mut Vec<Event>,
641 ) {
642 if limit == 0 {
643 return;
644 }
645
646 let mut added = 0usize;
647
648 if !prefers_trusted_only(filter) {
649 let recent = {
650 let cache = self.recent_events.lock().await;
651 cache.matching(filter)
652 };
653 for event in recent {
654 if seen.insert(event.id) {
655 events.push(event);
656 added += 1;
657 if added >= limit {
658 return;
659 }
660 }
661 }
662 }
663
664 for event in self.trusted.query(filter.clone(), limit).await {
665 if seen.insert(event.id) {
666 events.push(event);
667 added += 1;
668 if added >= limit {
669 return;
670 }
671 }
672 }
673
674 if let Some(index) = &self.historical_index {
675 let remaining = limit.saturating_sub(added);
676 for event in index.query(filter, remaining).await {
677 if seen.insert(event.id) {
678 events.push(event);
679 added += 1;
680 if added >= limit {
681 return;
682 }
683 }
684 }
685 }
686 }
687
688 async fn collect_filter_count(
689 &self,
690 filter: &NostrFilter,
691 limit: usize,
692 seen: &mut HashSet<EventId>,
693 ) {
694 if limit == 0 {
695 return;
696 }
697
698 let mut added = 0usize;
699
700 if !prefers_trusted_only(filter) {
701 let recent = {
702 let cache = self.recent_events.lock().await;
703 cache.matching(filter)
704 };
705 for event in recent {
706 if seen.insert(event.id) {
707 added += 1;
708 if added >= limit {
709 return;
710 }
711 }
712 }
713 }
714
715 for event in self.trusted.query(filter.clone(), limit).await {
716 if seen.insert(event.id) {
717 added += 1;
718 if added >= limit {
719 return;
720 }
721 }
722 }
723
724 if let Some(index) = &self.historical_index {
725 let remaining = limit.saturating_sub(added);
726 for event in index.query(filter, remaining).await {
727 if seen.insert(event.id) {
728 added += 1;
729 if added >= limit {
730 return;
731 }
732 }
733 }
734 }
735 }
736
737 pub fn new(
738 trusted_store: Arc<dyn SocialGraphBackend>,
739 data_dir: PathBuf,
740 public_pubkeys: HashSet<String>,
741 social_graph: Option<Arc<SocialGraphAccessControl>>,
742 config: NostrRelayConfig,
743 ) -> Result<Self> {
744 let spambox = if config.spambox_db_max_bytes == 0 {
745 Some(SpamboxStore::Memory(MemorySpambox::new(
746 config.max_query_limit * 2,
747 )))
748 } else {
749 let spam_dir = data_dir.join("socialgraph_spambox");
750 match socialgraph::open_social_graph_store_at_path(
751 &spam_dir,
752 Some(config.spambox_db_max_bytes),
753 ) {
754 Ok(store) => Some(SpamboxStore::Persistent(NostrStore::new(store))),
755 Err(err) => {
756 warn!(
757 "Failed to open social graph spambox (falling back to memory): {}",
758 err
759 );
760 Some(SpamboxStore::Memory(MemorySpambox::new(
761 config.max_query_limit * 2,
762 )))
763 }
764 }
765 };
766
767 let recent_size = config.max_query_limit.saturating_mul(2);
768 let bluetooth_event_log = Arc::new(BluetoothEventLog::load(
769 data_dir.join("bluetooth-events.jsonl"),
770 ));
771
772 Ok(Self {
773 config,
774 trusted: NostrStore::new(trusted_store),
775 public_pubkeys,
776 spambox,
777 historical_index: None,
778 social_graph,
779 clients: Mutex::new(HashMap::new()),
780 subscriptions: Mutex::new(HashMap::new()),
781 recent_events: Mutex::new(RecentEvents::new(recent_size)),
782 next_client_id: AtomicU64::new(1),
783 bluetooth_event_log,
784 #[cfg(feature = "experimental-decentralized-pubsub")]
785 decentralized_pubsub_tx: std::sync::Mutex::new(None),
786 })
787 }
788
789 pub fn with_historical_nostr_index(
790 mut self,
791 store: Arc<StorageRouter>,
792 data_dir: PathBuf,
793 ) -> Self {
794 self.historical_index = Some(HistoricalNostrIndex::new(store, data_dir));
795 self
796 }
797
798 pub fn next_client_id(&self) -> u64 {
799 self.next_client_id.fetch_add(1, Ordering::SeqCst)
800 }
801
802 #[cfg(feature = "experimental-decentralized-pubsub")]
803 pub fn set_decentralized_pubsub_sender(
804 &self,
805 sender: Option<mpsc::UnboundedSender<Event>>,
806 ) {
807 match self.decentralized_pubsub_tx.lock() {
808 Ok(mut slot) => {
809 *slot = sender;
810 }
811 Err(err) => {
812 warn!("nostr decentralized pubsub sender lock poisoned: {}", err);
813 }
814 }
815 }
816
817 #[cfg(feature = "experimental-decentralized-pubsub")]
818 fn enqueue_decentralized_pubsub_event(&self, event: &Event) {
819 let sender = match self.decentralized_pubsub_tx.lock() {
820 Ok(slot) => slot.clone(),
821 Err(err) => {
822 warn!("nostr decentralized pubsub sender lock poisoned: {}", err);
823 None
824 }
825 };
826
827 if let Some(sender) = sender {
828 if sender.send(event.clone()).is_err() {
829 warn!("nostr decentralized pubsub publisher is not running");
830 }
831 }
832 }
833
834 pub async fn ingest_trusted_event(&self, event: Event) -> Result<()> {
835 self.ingest_trusted_event_inner(event, true).await
836 }
837
838 pub async fn ingest_trusted_event_from_bluetooth(
839 &self,
840 event: Event,
841 peer_id: Option<String>,
842 ) -> Result<()> {
843 self.ingest_trusted_event_inner(event.clone(), true).await?;
844 self.bluetooth_event_log.record(&event, peer_id).await;
845 Ok(())
846 }
847
848 pub async fn ingest_trusted_event_silent(&self, event: Event) -> Result<()> {
849 self.ingest_trusted_event_inner(event, false).await
850 }
851
852 pub async fn ingest_peer_event_silent(&self, event: Event) -> Result<bool> {
853 event
854 .verify()
855 .map_err(|e| anyhow::anyhow!("invalid signature: {}", e))?;
856
857 if !self.is_trusted_event_for_client(None, &event).await {
858 return Ok(false);
859 }
860
861 let is_ephemeral = event.kind.is_ephemeral();
862 {
863 let mut recent = self.recent_events.lock().await;
864 recent.insert(event.clone());
865 }
866 if !is_ephemeral {
867 let storage_class = self.event_storage_class(&event);
868 self.trusted
869 .ingest_with_storage_class(event.clone(), storage_class)
870 .await?;
871 self.append_historical_index(&event).await;
872 }
873
874 Ok(true)
875 }
876
877 pub async fn bluetooth_received_events(
878 &self,
879 limit: usize,
880 ) -> Vec<BluetoothReceivedEventRecord> {
881 self.bluetooth_event_log.recent(limit).await
882 }
883
884 async fn ingest_trusted_event_inner(&self, event: Event, broadcast: bool) -> Result<()> {
885 event
886 .verify()
887 .map_err(|e| anyhow::anyhow!("invalid signature: {}", e))?;
888
889 let is_ephemeral = event.kind.is_ephemeral();
890 {
891 let mut recent = self.recent_events.lock().await;
892 recent.insert(event.clone());
893 }
894
895 if !is_ephemeral {
896 let storage_class = self.event_storage_class(&event);
897 self.trusted
898 .ingest_with_storage_class(event.clone(), storage_class)
899 .await?;
900 self.append_historical_index(&event).await;
901 }
902
903 if broadcast {
904 self.broadcast_event(&event).await;
905 }
906 Ok(())
907 }
908
909 pub async fn query_events(&self, filter: &NostrFilter, limit: usize) -> Vec<Event> {
910 let limit = limit.min(self.config.max_query_limit);
911 if limit == 0 {
912 return Vec::new();
913 }
914
915 let mut seen: HashSet<EventId> = HashSet::new();
916 let mut events = Vec::new();
917
918 if !prefers_trusted_only(filter) {
919 let recent = {
920 let cache = self.recent_events.lock().await;
921 cache.matching(filter)
922 };
923 for event in recent {
924 if seen.insert(event.id) {
925 events.push(event);
926 if events.len() >= limit {
927 return events;
928 }
929 }
930 }
931 }
932
933 for event in self.trusted.query(filter.clone(), limit).await {
934 if seen.insert(event.id) {
935 events.push(event);
936 if events.len() >= limit {
937 break;
938 }
939 }
940 }
941
942 if let Some(index) = &self.historical_index {
943 let remaining = limit.saturating_sub(events.len());
944 for event in index.query(filter, remaining).await {
945 if seen.insert(event.id) {
946 events.push(event);
947 if events.len() >= limit {
948 break;
949 }
950 }
951 }
952 }
953
954 events
955 }
956
957 pub async fn register_client(
958 &self,
959 client_id: u64,
960 sender: mpsc::UnboundedSender<String>,
961 pubkey: Option<String>,
962 ) {
963 let mut clients = self.clients.lock().await;
964 clients.insert(
965 client_id,
966 ClientState {
967 sender,
968 pubkey,
969 quota: ClientQuota::new(),
970 },
971 );
972 }
973
974 pub async fn unregister_client(&self, client_id: u64) {
975 let mut clients = self.clients.lock().await;
976 clients.remove(&client_id);
977 drop(clients);
978 let mut subs = self.subscriptions.lock().await;
979 subs.remove(&client_id);
980 }
981
982 pub async fn handle_client_message(&self, client_id: u64, msg: NostrClientMessage<'_>) {
983 match msg {
984 NostrClientMessage::Event(event) => {
985 self.handle_event(client_id, event.into_owned()).await;
986 }
987 NostrClientMessage::Req {
988 subscription_id,
989 filters,
990 } => {
991 self.handle_req(
992 client_id,
993 subscription_id.into_owned(),
994 filters
995 .into_iter()
996 .map(|filter| filter.into_owned())
997 .collect(),
998 )
999 .await;
1000 }
1001 NostrClientMessage::Count {
1002 subscription_id,
1003 filter,
1004 } => {
1005 self.handle_count(
1006 client_id,
1007 subscription_id.into_owned(),
1008 vec![filter.into_owned()],
1009 )
1010 .await;
1011 }
1012 NostrClientMessage::Close(subscription_id) => {
1013 self.handle_close(client_id, subscription_id.into_owned())
1014 .await;
1015 }
1016 NostrClientMessage::Auth(event) => {
1017 self.handle_auth(client_id, event.into_owned()).await;
1018 }
1019 NostrClientMessage::NegOpen { .. }
1020 | NostrClientMessage::NegMsg { .. }
1021 | NostrClientMessage::NegClose { .. } => {
1022 self.send_to_client(
1023 client_id,
1024 NostrRelayMessage::notice("negentropy not supported"),
1025 )
1026 .await;
1027 }
1028 }
1029 }
1030
1031 pub async fn register_subscription_query(
1032 &self,
1033 client_id: u64,
1034 subscription_id: SubscriptionId,
1035 mut filters: Vec<NostrFilter>,
1036 ) -> std::result::Result<Vec<Event>, &'static str> {
1037 if !self.allow_req(client_id).await {
1038 return Err("rate limited");
1039 }
1040
1041 if filters.len() > self.config.max_filters_per_sub {
1042 filters.truncate(self.config.max_filters_per_sub);
1043 }
1044
1045 {
1046 let mut subs = self.subscriptions.lock().await;
1047 let entry = subs.entry(client_id).or_default();
1048 if !entry.contains_key(&subscription_id)
1049 && entry.len() >= self.config.max_subs_per_client
1050 {
1051 return Err("too many subscriptions");
1052 }
1053 entry.insert(subscription_id.clone(), filters.clone());
1054 }
1055
1056 let mut seen: HashSet<EventId> = HashSet::new();
1057 let mut events = Vec::new();
1058 let memory_before = process_memory_snapshot();
1059 let started = Instant::now();
1060 let filter_summary = nostr_filters_summary(&filters);
1061 for filter in &filters {
1062 let remaining = self.config.max_query_limit.saturating_sub(events.len());
1063 if remaining == 0 {
1064 break;
1065 }
1066 let limit = filter
1067 .limit
1068 .unwrap_or(self.config.max_query_limit)
1069 .min(self.config.max_query_limit)
1070 .min(remaining);
1071 self.collect_filter_events(filter, limit, &mut seen, &mut events)
1072 .await;
1073 }
1074
1075 info!(
1076 target: "hashtree_cli::nostr_relay::query",
1077 client_id,
1078 subscription_id = %subscription_id,
1079 filters = filters.len(),
1080 events = events.len(),
1081 elapsed_ms = started.elapsed().as_millis() as u64,
1082 filter = %filter_summary,
1083 memory_before = ?memory_before,
1084 memory_after = ?process_memory_snapshot(),
1085 "nostr relay local subscription query completed",
1086 );
1087 Ok(events)
1088 }
1089
1090 async fn handle_auth(&self, client_id: u64, event: Event) {
1091 let ok = event.verify().is_ok();
1092 let message = if ok { "" } else { "invalid auth" };
1093 self.send_to_client(client_id, NostrRelayMessage::ok(event.id, ok, message))
1094 .await;
1095 }
1096
1097 async fn handle_close(&self, client_id: u64, subscription_id: SubscriptionId) {
1098 let mut subs = self.subscriptions.lock().await;
1099 if let Some(map) = subs.get_mut(&client_id) {
1100 map.remove(&subscription_id);
1101 }
1102 }
1103
1104 async fn handle_event(&self, client_id: u64, event: Event) {
1105 let ok = event.verify().is_ok();
1106 if !ok {
1107 self.send_to_client(
1108 client_id,
1109 NostrRelayMessage::ok(event.id, false, "invalid: signature"),
1110 )
1111 .await;
1112 return;
1113 }
1114
1115 let trusted = self.is_trusted_event(client_id, &event).await;
1116 if !trusted && !self.allow_spambox_event(client_id).await {
1117 self.send_to_client(
1118 client_id,
1119 NostrRelayMessage::ok(event.id, false, "rate limited"),
1120 )
1121 .await;
1122 return;
1123 }
1124
1125 let is_ephemeral = event.kind.is_ephemeral();
1126 if trusted {
1127 let mut recent = self.recent_events.lock().await;
1128 recent.insert(event.clone());
1129 }
1130 if !is_ephemeral {
1131 let stored = if trusted {
1132 let storage_class = self.event_storage_class(&event);
1133 let stored = self
1134 .trusted
1135 .ingest_with_storage_class(event.clone(), storage_class)
1136 .await
1137 .is_ok();
1138 if stored {
1139 self.append_historical_index(&event).await;
1140 }
1141 stored
1142 } else {
1143 match self.spambox.as_ref() {
1144 Some(spambox) => spambox.ingest(&event).await,
1145 None => false,
1146 }
1147 };
1148
1149 if !stored {
1150 let message = if trusted {
1151 "store failed"
1152 } else {
1153 "spambox full"
1154 };
1155 self.send_to_client(client_id, NostrRelayMessage::ok(event.id, false, message))
1156 .await;
1157 return;
1158 }
1159 }
1160
1161 let message = if trusted { "" } else { "spambox" };
1162 self.send_to_client(client_id, NostrRelayMessage::ok(event.id, true, message))
1163 .await;
1164
1165 if trusted {
1166 self.broadcast_event(&event).await;
1167 #[cfg(feature = "experimental-decentralized-pubsub")]
1168 self.enqueue_decentralized_pubsub_event(&event);
1169 }
1170 }
1171
1172 async fn handle_req(
1173 &self,
1174 client_id: u64,
1175 subscription_id: SubscriptionId,
1176 filters: Vec<NostrFilter>,
1177 ) {
1178 match self
1179 .register_subscription_query(client_id, subscription_id.clone(), filters)
1180 .await
1181 {
1182 Ok(events) => {
1183 for event in events {
1184 self.send_to_client(
1185 client_id,
1186 NostrRelayMessage::event(subscription_id.clone(), event),
1187 )
1188 .await;
1189 }
1190 trim_process_allocations();
1191
1192 self.send_to_client(client_id, NostrRelayMessage::eose(subscription_id))
1193 .await;
1194 }
1195 Err(message) => {
1196 self.send_to_client(
1197 client_id,
1198 NostrRelayMessage::closed(subscription_id, message),
1199 )
1200 .await;
1201 }
1202 }
1203 }
1204
1205 async fn handle_count(
1206 &self,
1207 client_id: u64,
1208 subscription_id: SubscriptionId,
1209 filters: Vec<NostrFilter>,
1210 ) {
1211 if !self.allow_req(client_id).await {
1212 self.send_to_client(
1213 client_id,
1214 NostrRelayMessage::closed(subscription_id, "rate limited"),
1215 )
1216 .await;
1217 return;
1218 }
1219
1220 let mut seen: HashSet<EventId> = HashSet::new();
1221 for filter in &filters {
1222 let limit = filter
1223 .limit
1224 .unwrap_or(self.config.max_query_limit)
1225 .min(self.config.max_query_limit);
1226 self.collect_filter_count(filter, limit, &mut seen).await;
1227 }
1228
1229 self.send_to_client(
1230 client_id,
1231 NostrRelayMessage::count(subscription_id, seen.len()),
1232 )
1233 .await;
1234 }
1235
1236 async fn is_trusted_event(&self, client_id: u64, event: &Event) -> bool {
1237 self.is_trusted_event_for_client(Some(client_id), event)
1238 .await
1239 }
1240
1241 async fn is_trusted_event_for_client(&self, client_id: Option<u64>, event: &Event) -> bool {
1242 let event_pubkey = event.pubkey.to_hex();
1243 let client_pubkey = {
1244 let clients = self.clients.lock().await;
1245 client_id.and_then(|client_id| {
1246 clients
1247 .get(&client_id)
1248 .and_then(|state| state.pubkey.clone())
1249 })
1250 };
1251 if let Some(pubkey) = client_pubkey {
1252 return pubkey == event_pubkey
1253 || self.social_graph.as_ref().is_some_and(|social_graph| {
1254 social_graph.check_write_access(&event_pubkey)
1255 });
1256 }
1257 if let Some(ref social_graph) = self.social_graph {
1258 return social_graph.check_write_access(&event_pubkey);
1259 }
1260 true
1261 }
1262
1263 async fn append_historical_index(&self, event: &Event) {
1264 if let Some(index) = &self.historical_index {
1265 if let Err(err) = index.ingest(event.clone()).await {
1266 warn!("historical nostr index ingest failed: {}", err);
1267 }
1268 }
1269 }
1270
1271 fn event_storage_class(&self, event: &Event) -> EventStorageClass {
1272 if self.public_pubkeys.contains(&event.pubkey.to_hex()) {
1273 EventStorageClass::Public
1274 } else {
1275 EventStorageClass::Ambient
1276 }
1277 }
1278
1279 async fn allow_spambox_event(&self, client_id: u64) -> bool {
1280 let mut clients = self.clients.lock().await;
1281 let Some(state) = clients.get_mut(&client_id) else {
1282 return false;
1283 };
1284 state
1285 .quota
1286 .allow_spambox_event(self.config.spambox_max_events_per_min)
1287 }
1288
1289 async fn allow_req(&self, client_id: u64) -> bool {
1290 let mut clients = self.clients.lock().await;
1291 let Some(state) = clients.get_mut(&client_id) else {
1292 return false;
1293 };
1294 state.quota.allow_req(self.config.spambox_max_reqs_per_min)
1295 }
1296
1297 async fn broadcast_event(&self, event: &Event) {
1298 let subscriptions = self.subscriptions.lock().await;
1299 let mut deliveries: Vec<(u64, SubscriptionId)> = Vec::new();
1300 for (client_id, subs) in subscriptions.iter() {
1301 for (sub_id, filters) in subs.iter() {
1302 if filters
1303 .iter()
1304 .any(|f| f.match_event(event, Default::default()))
1305 {
1306 deliveries.push((*client_id, sub_id.clone()));
1307 }
1308 }
1309 }
1310 drop(subscriptions);
1311
1312 for (client_id, sub_id) in deliveries {
1313 self.send_to_client(client_id, NostrRelayMessage::event(sub_id, event.clone()))
1314 .await;
1315 }
1316 }
1317
1318 async fn send_to_client(&self, client_id: u64, msg: NostrRelayMessage<'_>) {
1319 let sender = {
1320 let clients = self.clients.lock().await;
1321 clients.get(&client_id).map(|state| state.sender.clone())
1322 };
1323 if let Some(tx) = sender {
1324 let _ = tx.send(msg.as_json());
1325 }
1326 }
1327 }
1328}
1329
1330pub use imp::NostrRelay;
1331
1332#[cfg(feature = "experimental-decentralized-pubsub")]
1333#[async_trait::async_trait]
1334impl MeshEventStore for NostrRelay {
1335 async fn ingest_trusted_event(&self, event: Event) -> anyhow::Result<()> {
1336 NostrRelay::ingest_trusted_event(self, event).await
1337 }
1338
1339 async fn query_events(&self, filter: &NostrFilter, limit: usize) -> Vec<Event> {
1340 NostrRelay::query_events(self, filter, limit).await
1341 }
1342}
1343
1344#[cfg(feature = "experimental-decentralized-pubsub")]
1345#[async_trait::async_trait]
1346impl MeshRelayClient for NostrRelay {
1347 fn next_client_id(&self) -> u64 {
1348 NostrRelay::next_client_id(self)
1349 }
1350
1351 async fn register_client(
1352 &self,
1353 client_id: u64,
1354 sender: mpsc::UnboundedSender<String>,
1355 pubkey: Option<String>,
1356 ) {
1357 NostrRelay::register_client(self, client_id, sender, pubkey).await
1358 }
1359
1360 async fn unregister_client(&self, client_id: u64) {
1361 NostrRelay::unregister_client(self, client_id).await
1362 }
1363
1364 async fn handle_client_message(&self, client_id: u64, msg: NostrClientMessage<'static>) {
1365 NostrRelay::handle_client_message(self, client_id, msg).await
1366 }
1367
1368 async fn register_subscription_query(
1369 &self,
1370 client_id: u64,
1371 subscription_id: SubscriptionId,
1372 filters: Vec<NostrFilter>,
1373 ) -> std::result::Result<Vec<Event>, &'static str> {
1374 NostrRelay::register_subscription_query(self, client_id, subscription_id, filters).await
1375 }
1376
1377 async fn ingest_trusted_event_from_peer(
1378 &self,
1379 event: Event,
1380 peer_id: Option<String>,
1381 ) -> anyhow::Result<()> {
1382 NostrRelay::ingest_trusted_event_from_bluetooth(self, event, peer_id).await
1383 }
1384}
1385
1386#[cfg(test)]
1387#[path = "nostr_relay/tests.rs"]
1388mod tests;