Skip to main content

hashtree_cli/
nostr_relay.rs

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;