1#![forbid(unsafe_code)]
8#![warn(missing_docs)]
9#![warn(rustdoc::bare_urls)]
10#![allow(clippy::mutable_key_type)] use 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#[derive(Debug, Clone)]
30pub struct NdbDatabase {
31 db: Ndb,
32}
33
34impl NdbDatabase {
35 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 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(¬e)?,
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}