nrc_mls_sqlite_storage/
lib.rs1#![forbid(unsafe_code)]
10#![warn(missing_docs)]
11#![warn(rustdoc::bare_urls)]
12
13use std::path::Path;
14use std::sync::{Arc, Mutex};
15
16use nrc_mls_storage::{Backend, NostrMlsStorageProvider};
17use openmls_sqlite_storage::{Codec, SqliteStorageProvider};
18use rusqlite::Connection;
19use serde::de::DeserializeOwned;
20use serde::Serialize;
21
22mod db;
23pub mod error;
24mod groups;
25mod messages;
26mod migrations;
27mod welcomes;
28
29use self::error::Error;
30
31type MlsStorage = SqliteStorageProvider<JsonCodec, Connection>;
33
34#[derive(Default)]
37pub struct JsonCodec;
38
39impl Codec for JsonCodec {
40 type Error = serde_json::Error;
41
42 #[inline]
43 fn to_vec<T: Serialize>(value: &T) -> Result<Vec<u8>, Self::Error> {
44 serde_json::to_vec(value)
45 }
46
47 #[inline]
48 fn from_slice<T>(slice: &[u8]) -> Result<T, Self::Error>
49 where
50 T: DeserializeOwned,
51 {
52 serde_json::from_slice(slice)
53 }
54}
55
56pub struct NostrMlsSqliteStorage {
61 openmls_storage: MlsStorage,
63 db_connection: Arc<Mutex<Connection>>,
65}
66
67impl NostrMlsSqliteStorage {
68 pub fn new<P>(file_path: P) -> Result<Self, Error>
78 where
79 P: AsRef<Path>,
80 {
81 if let Some(parent) = file_path.as_ref().parent() {
83 std::fs::create_dir_all(parent)?;
84 }
85
86 let mls_connection: Connection = Connection::open(&file_path)?;
88
89 mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
91
92 let mut openmls_storage: MlsStorage = SqliteStorageProvider::new(mls_connection);
94
95 openmls_storage.initialize()?;
97
98 let mut nostr_mls_connection = Connection::open(&file_path)?;
100
101 nostr_mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
103
104 migrations::run_migrations(&mut nostr_mls_connection)?;
106
107 Ok(Self {
108 openmls_storage,
109 db_connection: Arc::new(Mutex::new(nostr_mls_connection)),
110 })
111 }
112
113 #[cfg(test)]
119 pub fn new_in_memory() -> Result<Self, Error> {
120 let mls_connection = Connection::open_in_memory()?;
122
123 mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
125
126 let mut openmls_storage: MlsStorage = SqliteStorageProvider::new(mls_connection);
128
129 openmls_storage.initialize()?;
131
132 let mut nostr_mls_connection: Connection = Connection::open_in_memory()?;
136
137 nostr_mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
139
140 migrations::run_migrations(&mut nostr_mls_connection)?;
142
143 Ok(Self {
144 openmls_storage,
145 db_connection: Arc::new(Mutex::new(nostr_mls_connection)),
146 })
147 }
148}
149
150impl NostrMlsStorageProvider for NostrMlsSqliteStorage {
152 type OpenMlsStorageProvider = MlsStorage;
153
154 fn backend(&self) -> Backend {
160 Backend::SQLite
161 }
162
163 fn openmls_storage(&self) -> &Self::OpenMlsStorageProvider {
172 &self.openmls_storage
173 }
174
175 fn openmls_storage_mut(&mut self) -> &mut Self::OpenMlsStorageProvider {
184 &mut self.openmls_storage
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use std::collections::BTreeSet;
191
192 use openmls::group::GroupId;
193 use tempfile::tempdir;
194
195 use super::*;
196
197 #[test]
198 fn test_new_in_memory() {
199 let storage = NostrMlsSqliteStorage::new_in_memory();
200 assert!(storage.is_ok());
201 let storage = storage.unwrap();
202 assert_eq!(storage.backend(), Backend::SQLite);
203 }
204
205 #[test]
206 fn test_backend_type() {
207 let storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
208 assert_eq!(storage.backend(), Backend::SQLite);
209 assert!(storage.backend().is_persistent());
210 }
211
212 #[test]
213 fn test_file_based_storage() {
214 let temp_dir = tempdir().unwrap();
215 let db_path = temp_dir.path().join("test_db.sqlite");
216
217 let storage = NostrMlsSqliteStorage::new(&db_path);
219 assert!(storage.is_ok());
220
221 assert!(db_path.exists());
223
224 let storage2 = NostrMlsSqliteStorage::new(&db_path);
226 assert!(storage2.is_ok());
227
228 drop(storage);
230 drop(storage2);
231 temp_dir.close().unwrap();
232 }
233
234 #[test]
235 fn test_openmls_storage_access() {
236 let storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
237
238 let _openmls_storage = storage.openmls_storage();
240
241 let mut mutable_storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
243 let _mutable_ref = mutable_storage.openmls_storage_mut();
244 }
245
246 #[test]
247 fn test_database_tables() {
248 let temp_dir = tempdir().unwrap();
249 let db_path = temp_dir.path().join("migration_test.sqlite");
250
251 let storage = NostrMlsSqliteStorage::new(&db_path).unwrap();
253
254 {
256 let conn_guard = storage.db_connection.lock().unwrap();
257
258 let mut stmt = conn_guard
260 .prepare("SELECT name FROM sqlite_master WHERE type='table'")
261 .unwrap();
262 let table_names: Vec<String> = stmt
263 .query_map([], |row| row.get(0))
264 .unwrap()
265 .map(|r| r.unwrap())
266 .collect();
267
268 assert!(table_names.contains(&"groups".to_string()));
270 assert!(table_names.contains(&"messages".to_string()));
271 assert!(table_names.contains(&"welcomes".to_string()));
272 assert!(table_names.contains(&"processed_messages".to_string()));
273 assert!(table_names.contains(&"processed_welcomes".to_string()));
274 assert!(table_names.contains(&"group_relays".to_string()));
275 assert!(table_names.contains(&"group_exporter_secrets".to_string()));
276 } drop(storage);
280 temp_dir.close().unwrap();
281 }
282
283 #[test]
284 fn test_group_exporter_secrets() {
285 use nrc_mls_storage::groups::types::{Group, GroupExporterSecret, GroupState};
286 use nrc_mls_storage::groups::GroupStorage;
287
288 let storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
290
291 let mls_group_id = GroupId::from_slice(vec![1, 2, 3, 4].as_slice());
293 let group = Group {
294 mls_group_id: mls_group_id.clone(),
295 nostr_group_id: [0u8; 32],
296 name: "Test Group".to_string(),
297 description: "A test group for exporter secrets".to_string(),
298 admin_pubkeys: BTreeSet::new(),
299 last_message_id: None,
300 last_message_at: None,
301 epoch: 0,
302 state: GroupState::Active,
303 image_url: None,
304 image_key: None,
305 image_nonce: None,
306 };
307
308 storage.save_group(group.clone()).unwrap();
310
311 let secret_epoch_0 = GroupExporterSecret {
313 mls_group_id: mls_group_id.clone(),
314 epoch: 0,
315 secret: [0u8; 32],
316 };
317
318 let secret_epoch_1 = GroupExporterSecret {
319 mls_group_id: mls_group_id.clone(),
320 epoch: 1,
321 secret: [0u8; 32],
322 };
323
324 storage
326 .save_group_exporter_secret(secret_epoch_0.clone())
327 .unwrap();
328 storage
329 .save_group_exporter_secret(secret_epoch_1.clone())
330 .unwrap();
331
332 let retrieved_secret_0 = storage.get_group_exporter_secret(&mls_group_id, 0).unwrap();
334 assert!(retrieved_secret_0.is_some());
335 let retrieved_secret_0 = retrieved_secret_0.unwrap();
336 assert_eq!(retrieved_secret_0, secret_epoch_0);
337
338 let retrieved_secret_1 = storage.get_group_exporter_secret(&mls_group_id, 1).unwrap();
339 assert!(retrieved_secret_1.is_some());
340 let retrieved_secret_1 = retrieved_secret_1.unwrap();
341 assert_eq!(retrieved_secret_1, secret_epoch_1);
342
343 let non_existent_epoch = storage
345 .get_group_exporter_secret(&mls_group_id, 999)
346 .unwrap();
347 assert!(non_existent_epoch.is_none());
348
349 let non_existent_group_id = GroupId::from_slice(&[9, 9, 9, 9]);
351 let result = storage.get_group_exporter_secret(&non_existent_group_id, 0);
352 assert!(result.is_err());
353
354 let updated_secret_0 = GroupExporterSecret {
356 mls_group_id: mls_group_id.clone(),
357 epoch: 0,
358 secret: [0u8; 32],
359 };
360 storage
361 .save_group_exporter_secret(updated_secret_0.clone())
362 .unwrap();
363
364 let retrieved_updated_secret = storage
365 .get_group_exporter_secret(&mls_group_id, 0)
366 .unwrap()
367 .unwrap();
368 assert_eq!(retrieved_updated_secret, updated_secret_0);
369
370 let invalid_secret = GroupExporterSecret {
372 mls_group_id: non_existent_group_id.clone(),
373 epoch: 0,
374 secret: [0u8; 32],
375 };
376 let result = storage.save_group_exporter_secret(invalid_secret);
377 assert!(result.is_err());
378 }
379}