#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(rustdoc::bare_urls)]
use std::path::Path;
use std::sync::{Arc, Mutex};
use nrc_mls_storage::{Backend, NostrMlsStorageProvider};
use openmls_sqlite_storage::{Codec, SqliteStorageProvider};
use rusqlite::Connection;
use serde::de::DeserializeOwned;
use serde::Serialize;
mod db;
pub mod error;
mod groups;
mod messages;
mod migrations;
mod welcomes;
use self::error::Error;
type MlsStorage = SqliteStorageProvider<JsonCodec, Connection>;
#[derive(Default)]
pub struct JsonCodec;
impl Codec for JsonCodec {
type Error = serde_json::Error;
#[inline]
fn to_vec<T: Serialize>(value: &T) -> Result<Vec<u8>, Self::Error> {
serde_json::to_vec(value)
}
#[inline]
fn from_slice<T>(slice: &[u8]) -> Result<T, Self::Error>
where
T: DeserializeOwned,
{
serde_json::from_slice(slice)
}
}
pub struct NostrMlsSqliteStorage {
openmls_storage: MlsStorage,
db_connection: Arc<Mutex<Connection>>,
}
impl NostrMlsSqliteStorage {
pub fn new<P>(file_path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
if let Some(parent) = file_path.as_ref().parent() {
std::fs::create_dir_all(parent)?;
}
let mls_connection: Connection = Connection::open(&file_path)?;
mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
let mut openmls_storage: MlsStorage = SqliteStorageProvider::new(mls_connection);
openmls_storage.initialize()?;
let mut nostr_mls_connection = Connection::open(&file_path)?;
nostr_mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
migrations::run_migrations(&mut nostr_mls_connection)?;
Ok(Self {
openmls_storage,
db_connection: Arc::new(Mutex::new(nostr_mls_connection)),
})
}
#[cfg(test)]
pub fn new_in_memory() -> Result<Self, Error> {
let mls_connection = Connection::open_in_memory()?;
mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
let mut openmls_storage: MlsStorage = SqliteStorageProvider::new(mls_connection);
openmls_storage.initialize()?;
let mut nostr_mls_connection: Connection = Connection::open_in_memory()?;
nostr_mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
migrations::run_migrations(&mut nostr_mls_connection)?;
Ok(Self {
openmls_storage,
db_connection: Arc::new(Mutex::new(nostr_mls_connection)),
})
}
}
impl NostrMlsStorageProvider for NostrMlsSqliteStorage {
type OpenMlsStorageProvider = MlsStorage;
fn backend(&self) -> Backend {
Backend::SQLite
}
fn openmls_storage(&self) -> &Self::OpenMlsStorageProvider {
&self.openmls_storage
}
fn openmls_storage_mut(&mut self) -> &mut Self::OpenMlsStorageProvider {
&mut self.openmls_storage
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use openmls::group::GroupId;
use tempfile::tempdir;
use super::*;
#[test]
fn test_new_in_memory() {
let storage = NostrMlsSqliteStorage::new_in_memory();
assert!(storage.is_ok());
let storage = storage.unwrap();
assert_eq!(storage.backend(), Backend::SQLite);
}
#[test]
fn test_backend_type() {
let storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
assert_eq!(storage.backend(), Backend::SQLite);
assert!(storage.backend().is_persistent());
}
#[test]
fn test_file_based_storage() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("test_db.sqlite");
let storage = NostrMlsSqliteStorage::new(&db_path);
assert!(storage.is_ok());
assert!(db_path.exists());
let storage2 = NostrMlsSqliteStorage::new(&db_path);
assert!(storage2.is_ok());
drop(storage);
drop(storage2);
temp_dir.close().unwrap();
}
#[test]
fn test_openmls_storage_access() {
let storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
let _openmls_storage = storage.openmls_storage();
let mut mutable_storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
let _mutable_ref = mutable_storage.openmls_storage_mut();
}
#[test]
fn test_database_tables() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("migration_test.sqlite");
let storage = NostrMlsSqliteStorage::new(&db_path).unwrap();
{
let conn_guard = storage.db_connection.lock().unwrap();
let mut stmt = conn_guard
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
.unwrap();
let table_names: Vec<String> = stmt
.query_map([], |row| row.get(0))
.unwrap()
.map(|r| r.unwrap())
.collect();
assert!(table_names.contains(&"groups".to_string()));
assert!(table_names.contains(&"messages".to_string()));
assert!(table_names.contains(&"welcomes".to_string()));
assert!(table_names.contains(&"processed_messages".to_string()));
assert!(table_names.contains(&"processed_welcomes".to_string()));
assert!(table_names.contains(&"group_relays".to_string()));
assert!(table_names.contains(&"group_exporter_secrets".to_string()));
}
drop(storage);
temp_dir.close().unwrap();
}
#[test]
fn test_group_exporter_secrets() {
use nrc_mls_storage::groups::types::{Group, GroupExporterSecret, GroupState};
use nrc_mls_storage::groups::GroupStorage;
let storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
let mls_group_id = GroupId::from_slice(vec![1, 2, 3, 4].as_slice());
let group = Group {
mls_group_id: mls_group_id.clone(),
nostr_group_id: [0u8; 32],
name: "Test Group".to_string(),
description: "A test group for exporter secrets".to_string(),
admin_pubkeys: BTreeSet::new(),
last_message_id: None,
last_message_at: None,
epoch: 0,
state: GroupState::Active,
image_url: None,
image_key: None,
image_nonce: None,
};
storage.save_group(group.clone()).unwrap();
let secret_epoch_0 = GroupExporterSecret {
mls_group_id: mls_group_id.clone(),
epoch: 0,
secret: [0u8; 32],
};
let secret_epoch_1 = GroupExporterSecret {
mls_group_id: mls_group_id.clone(),
epoch: 1,
secret: [0u8; 32],
};
storage
.save_group_exporter_secret(secret_epoch_0.clone())
.unwrap();
storage
.save_group_exporter_secret(secret_epoch_1.clone())
.unwrap();
let retrieved_secret_0 = storage.get_group_exporter_secret(&mls_group_id, 0).unwrap();
assert!(retrieved_secret_0.is_some());
let retrieved_secret_0 = retrieved_secret_0.unwrap();
assert_eq!(retrieved_secret_0, secret_epoch_0);
let retrieved_secret_1 = storage.get_group_exporter_secret(&mls_group_id, 1).unwrap();
assert!(retrieved_secret_1.is_some());
let retrieved_secret_1 = retrieved_secret_1.unwrap();
assert_eq!(retrieved_secret_1, secret_epoch_1);
let non_existent_epoch = storage
.get_group_exporter_secret(&mls_group_id, 999)
.unwrap();
assert!(non_existent_epoch.is_none());
let non_existent_group_id = GroupId::from_slice(&[9, 9, 9, 9]);
let result = storage.get_group_exporter_secret(&non_existent_group_id, 0);
assert!(result.is_err());
let updated_secret_0 = GroupExporterSecret {
mls_group_id: mls_group_id.clone(),
epoch: 0,
secret: [0u8; 32],
};
storage
.save_group_exporter_secret(updated_secret_0.clone())
.unwrap();
let retrieved_updated_secret = storage
.get_group_exporter_secret(&mls_group_id, 0)
.unwrap()
.unwrap();
assert_eq!(retrieved_updated_secret, updated_secret_0);
let invalid_secret = GroupExporterSecret {
mls_group_id: non_existent_group_id.clone(),
epoch: 0,
secret: [0u8; 32],
};
let result = storage.save_group_exporter_secret(invalid_secret);
assert!(result.is_err());
}
}