nostr_ndb/
lib.rs

1// Copyright (c) 2022-2023 Yuki Kishimoto
2// Copyright (c) 2023-2025 Rust Nostr Developers
3// Distributed under the MIT software license
4
5//! [`nostrdb`](https://github.com/damus-io/nostrdb) storage backend for Nostr apps
6
7#![forbid(unsafe_code)]
8#![warn(missing_docs)]
9#![warn(rustdoc::bare_urls)]
10#![allow(clippy::mutable_key_type)] // TODO: remove when possible. Needed to suppress false positive for async_trait
11
12use std::borrow::Cow;
13use std::ops::{Deref, DerefMut};
14
15pub extern crate nostr;
16pub extern crate nostr_database as database;
17pub extern crate nostrdb;
18
19use nostr_database::prelude::*;
20use nostrdb::{
21    Config, Filter as NdbFilter, IngestMetadata, Ndb, NdbStrVariant, Note, QueryResult, Transaction,
22};
23
24const MAX_RESULTS: i32 = 10_000;
25
26// Wrap `Ndb` into `NdbDatabase` because only traits defined in the current crate can be implemented for types defined outside the crate!
27
28/// [`nostrdb`](https://github.com/damus-io/nostrdb) backend
29#[derive(Debug, Clone)]
30pub struct NdbDatabase {
31    db: Ndb,
32}
33
34impl NdbDatabase {
35    /// Open nostrdb
36    pub fn open<P>(path: P) -> Result<Self, DatabaseError>
37    where
38        P: AsRef<str>,
39    {
40        let path: &str = path.as_ref();
41        let config = Config::new();
42
43        Ok(Self {
44            db: Ndb::new(path, &config).map_err(DatabaseError::backend)?,
45        })
46    }
47}
48
49impl Deref for NdbDatabase {
50    type Target = Ndb;
51
52    fn deref(&self) -> &Self::Target {
53        &self.db
54    }
55}
56
57impl DerefMut for NdbDatabase {
58    fn deref_mut(&mut self) -> &mut Self::Target {
59        &mut self.db
60    }
61}
62
63impl From<Ndb> for NdbDatabase {
64    fn from(db: Ndb) -> Self {
65        Self { db }
66    }
67}
68
69impl NostrDatabase for NdbDatabase {
70    fn backend(&self) -> Backend {
71        Backend::LMDB
72    }
73
74    fn save_event<'a>(
75        &'a self,
76        event: &'a Event,
77    ) -> BoxedFuture<'a, Result<SaveEventStatus, DatabaseError>> {
78        Box::pin(async move {
79            let msg = RelayMessage::Event {
80                subscription_id: Cow::Owned(SubscriptionId::new("ndb")),
81                event: Cow::Borrowed(event),
82            };
83            let json: String = msg.as_json();
84            self.db
85                .process_event_with(&json, IngestMetadata::new())
86                .map_err(DatabaseError::backend)?;
87            // TODO: shouldn't return a success since we don't know if the ingestion was successful or not.
88            Ok(SaveEventStatus::Success)
89        })
90    }
91
92    fn check_id<'a>(
93        &'a self,
94        event_id: &'a EventId,
95    ) -> BoxedFuture<'a, Result<DatabaseEventStatus, DatabaseError>> {
96        Box::pin(async move {
97            let txn = Transaction::new(&self.db).map_err(DatabaseError::backend)?;
98            let res = self.db.get_note_by_id(&txn, event_id.as_bytes());
99            Ok(if res.is_ok() {
100                DatabaseEventStatus::Saved
101            } else {
102                DatabaseEventStatus::NotExistent
103            })
104        })
105    }
106
107    fn event_by_id<'a>(
108        &'a self,
109        event_id: &'a EventId,
110    ) -> BoxedFuture<'a, Result<Option<Event>, DatabaseError>> {
111        Box::pin(async move {
112            let txn: Transaction = Transaction::new(&self.db).map_err(DatabaseError::backend)?;
113            let res: Result<Note, nostrdb::Error> =
114                self.db.get_note_by_id(&txn, event_id.as_bytes());
115            match res {
116                Ok(note) => Ok(Some(ndb_note_to_event(note)?.into_owned())),
117                Err(nostrdb::Error::NotFound) => Ok(None),
118                Err(e) => Err(DatabaseError::backend(e)),
119            }
120        })
121    }
122
123    fn count(&self, filter: Filter) -> BoxedFuture<Result<usize, DatabaseError>> {
124        Box::pin(async move {
125            let txn: Transaction = Transaction::new(&self.db).map_err(DatabaseError::backend)?;
126            let res: Vec<QueryResult> = ndb_query(&self.db, &txn, &filter)?;
127            Ok(res.len())
128        })
129    }
130
131    fn query(&self, filter: Filter) -> BoxedFuture<Result<Events, DatabaseError>> {
132        Box::pin(async move {
133            let txn: Transaction = Transaction::new(&self.db).map_err(DatabaseError::backend)?;
134            let mut events: Events = Events::new(&filter);
135            let res: Vec<QueryResult> = ndb_query(&self.db, &txn, &filter)?;
136            events.extend(
137                res.into_iter()
138                    .filter_map(|r| ndb_note_to_event(r.note).ok())
139                    .map(|e| e.into_owned()),
140            );
141            Ok(events)
142        })
143    }
144
145    fn negentropy_items(
146        &self,
147        filter: Filter,
148    ) -> BoxedFuture<Result<Vec<(EventId, Timestamp)>, DatabaseError>> {
149        Box::pin(async move {
150            let txn: Transaction = Transaction::new(&self.db).map_err(DatabaseError::backend)?;
151            let res: Vec<QueryResult> = ndb_query(&self.db, &txn, &filter)?;
152            Ok(res
153                .into_iter()
154                .map(|r| ndb_note_to_neg_item(r.note))
155                .collect())
156        })
157    }
158
159    fn delete(&self, _filter: Filter) -> BoxedFuture<Result<(), DatabaseError>> {
160        Box::pin(async move { Err(DatabaseError::NotSupported) })
161    }
162
163    #[inline]
164    fn wipe(&self) -> BoxedFuture<Result<(), DatabaseError>> {
165        Box::pin(async move { Err(DatabaseError::NotSupported) })
166    }
167}
168
169fn ndb_query<'a>(
170    db: &Ndb,
171    txn: &'a Transaction,
172    filter: &Filter,
173) -> Result<Vec<QueryResult<'a>>, DatabaseError> {
174    let filter: nostrdb::Filter = ndb_filter_conversion(filter);
175    db.query(txn, &[filter], MAX_RESULTS)
176        .map_err(DatabaseError::backend)
177}
178
179fn ndb_filter_conversion(f: &Filter) -> nostrdb::Filter {
180    let mut filter = NdbFilter::new();
181
182    if let Some(ids) = &f.ids {
183        if !ids.is_empty() {
184            filter = filter.ids(ids.iter().map(|p| p.as_bytes()));
185        }
186    }
187
188    if let Some(authors) = &f.authors {
189        if !authors.is_empty() {
190            filter = filter.authors(authors.iter().map(|p| p.as_bytes()));
191        }
192    }
193
194    if let Some(kinds) = &f.kinds {
195        if !kinds.is_empty() {
196            filter = filter.kinds(kinds.iter().map(|p| p.as_u16() as u64));
197        }
198    }
199
200    if !f.generic_tags.is_empty() {
201        for (single_letter, set) in f.generic_tags.iter() {
202            filter = filter.tags(set.iter().map(|s| s.as_str()), single_letter.as_char());
203        }
204    }
205
206    if let Some(since) = f.since {
207        filter = filter.since(since.as_secs());
208    }
209
210    if let Some(until) = f.until {
211        filter = filter.until(until.as_secs());
212    }
213
214    if let Some(limit) = f.limit {
215        filter = filter.limit(limit as u64);
216    }
217
218    filter.build()
219}
220
221fn ndb_note_to_event(note: Note) -> Result<EventBorrow, DatabaseError> {
222    Ok(EventBorrow {
223        id: note.id(),
224        pubkey: note.pubkey(),
225        created_at: Timestamp::from(note.created_at()),
226        kind: note.kind().try_into().map_err(DatabaseError::backend)?,
227        tags: ndb_note_to_tags(&note)?,
228        content: note.content(),
229        sig: note.sig(),
230    })
231}
232
233fn ndb_note_to_tags<'a>(note: &Note<'a>) -> Result<Vec<CowTag<'a>>, DatabaseError> {
234    let ndb_tags = note.tags();
235    let mut tags: Vec<CowTag<'a>> = Vec::with_capacity(ndb_tags.count() as usize);
236    for tag in ndb_tags.iter() {
237        let tag_str: Vec<Cow<'a, str>> = tag
238            .into_iter()
239            .map(|s| match s.variant() {
240                NdbStrVariant::Id(id) => Cow::Owned(hex::encode(id)),
241                NdbStrVariant::Str(s) => Cow::Borrowed(s),
242            })
243            .collect();
244        let tag = CowTag::parse(tag_str).map_err(DatabaseError::backend)?;
245        tags.push(tag);
246    }
247    Ok(tags)
248}
249
250fn ndb_note_to_neg_item(note: Note) -> (EventId, Timestamp) {
251    let id = EventId::from_byte_array(*note.id());
252    let created_at = Timestamp::from_secs(note.created_at());
253    (id, created_at)
254}