use std::sync::Arc;
use anyhow::Result;
#[cfg(feature = "e2e-encryption")]
use cryptostore::CryptostoreData;
use helpers::{BorrowedSqlType, SqlType};
use matrix_sdk_base::store::StoreConfig;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_store_encryption::StoreCipher;
mod helpers;
pub use helpers::SupportedDatabase;
use matrix_sdk_base::{deserialized_responses::MemberEvent, MinimalRoomMemberEvent, RoomInfo};
use ruma::{
events::{
presence::PresenceEvent,
receipt::Receipt,
room::member::{StrippedRoomMemberEvent, SyncRoomMemberEvent},
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
AnySyncStateEvent,
},
serde::Raw,
};
use sqlx::{
database::HasArguments, migrate::Migrate, types::Json, ColumnIndex, Database, Executor,
IntoArguments, Pool, Transaction,
};
#[cfg(feature = "e2e-encryption")]
mod cryptostore;
mod statestore;
#[allow(single_use_lifetimes)]
#[derive(Debug)]
pub struct StateStore<DB: SupportedDatabase> {
db: Arc<Pool<DB>>,
#[cfg(feature = "e2e-encryption")]
cryptostore: Option<CryptostoreData>,
}
#[allow(single_use_lifetimes)]
impl<DB: SupportedDatabase> StateStore<DB> {
pub async fn new(db: &Arc<Pool<DB>>) -> Result<Self>
where
<DB as Database>::Connection: Migrate,
{
let db = Arc::clone(db);
let migrator = DB::get_migrator();
migrator.run(&*db).await?;
#[cfg(not(feature = "e2e-encryption"))]
{
Ok(Self { db })
}
#[cfg(feature = "e2e-encryption")]
{
Ok(Self {
db,
cryptostore: None,
})
}
}
#[cfg(feature = "e2e-encryption")]
pub(crate) fn ensure_e2e(&self) -> Result<&CryptostoreData> {
self.cryptostore
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Not unlocked"))
}
#[cfg(feature = "e2e-encryption")]
pub async fn unlock(&mut self) -> Result<()>
where
for<'a> <DB as HasArguments<'a>>::Arguments: IntoArguments<'a, DB>,
for<'c> &'c mut <DB as sqlx::Database>::Connection: Executor<'c, Database = DB>,
for<'c, 'a> &'a mut Transaction<'c, DB>: Executor<'a, Database = DB>,
for<'a> &'a [u8]: BorrowedSqlType<'a, DB>,
for<'a> &'a str: BorrowedSqlType<'a, DB>,
Vec<u8>: SqlType<DB>,
String: SqlType<DB>,
bool: SqlType<DB>,
Vec<u8>: SqlType<DB>,
Option<String>: SqlType<DB>,
Json<Raw<AnyGlobalAccountDataEvent>>: SqlType<DB>,
Json<Raw<PresenceEvent>>: SqlType<DB>,
Json<SyncRoomMemberEvent>: SqlType<DB>,
Json<MinimalRoomMemberEvent>: SqlType<DB>,
Json<Raw<AnySyncStateEvent>>: SqlType<DB>,
Json<Raw<AnyRoomAccountDataEvent>>: SqlType<DB>,
Json<RoomInfo>: SqlType<DB>,
Json<Receipt>: SqlType<DB>,
Json<Raw<AnyStrippedStateEvent>>: SqlType<DB>,
Json<StrippedRoomMemberEvent>: SqlType<DB>,
Json<MemberEvent>: SqlType<DB>,
for<'a> &'a str: ColumnIndex<<DB as Database>::Row>,
{
self.cryptostore = Some(CryptostoreData::new_unencrypted());
self.load_tracked_users().await?;
Ok(())
}
#[cfg(feature = "e2e-encryption")]
pub async fn unlock_with_passphrase(&mut self, passphrase: &str) -> Result<()>
where
for<'a> <DB as HasArguments<'a>>::Arguments: IntoArguments<'a, DB>,
for<'c> &'c mut <DB as sqlx::Database>::Connection: Executor<'c, Database = DB>,
for<'c, 'a> &'a mut Transaction<'c, DB>: Executor<'a, Database = DB>,
for<'a> &'a [u8]: BorrowedSqlType<'a, DB>,
for<'a> &'a str: BorrowedSqlType<'a, DB>,
Vec<u8>: SqlType<DB>,
String: SqlType<DB>,
bool: SqlType<DB>,
Vec<u8>: SqlType<DB>,
Option<String>: SqlType<DB>,
Json<Raw<AnyGlobalAccountDataEvent>>: SqlType<DB>,
Json<Raw<PresenceEvent>>: SqlType<DB>,
Json<SyncRoomMemberEvent>: SqlType<DB>,
Json<MinimalRoomMemberEvent>: SqlType<DB>,
Json<Raw<AnySyncStateEvent>>: SqlType<DB>,
Json<Raw<AnyRoomAccountDataEvent>>: SqlType<DB>,
Json<RoomInfo>: SqlType<DB>,
Json<Receipt>: SqlType<DB>,
Json<Raw<AnyStrippedStateEvent>>: SqlType<DB>,
Json<StrippedRoomMemberEvent>: SqlType<DB>,
Json<MemberEvent>: SqlType<DB>,
for<'a> &'a str: ColumnIndex<<DB as Database>::Row>,
{
let cipher_export = self.get_kv(b"cipher").await?;
if let Some(cipher) = cipher_export {
self.cryptostore = Some(CryptostoreData::new(StoreCipher::import(
passphrase, &cipher,
)?));
} else {
let cipher = StoreCipher::new()?;
self.insert_kv(b"cipher", &cipher.export(passphrase)?)
.await?;
self.cryptostore = Some(CryptostoreData::new(cipher));
}
self.load_tracked_users().await?;
Ok(())
}
}
pub async fn store_config<DB: SupportedDatabase>(
db: &Arc<Pool<DB>>,
passphrase: Option<&str>,
) -> Result<StoreConfig>
where
<DB as Database>::Connection: Migrate,
for<'a> <DB as HasArguments<'a>>::Arguments: IntoArguments<'a, DB>,
for<'c> &'c mut <DB as sqlx::Database>::Connection: Executor<'c, Database = DB>,
for<'c, 'a> &'a mut Transaction<'c, DB>: Executor<'a, Database = DB>,
for<'a> &'a [u8]: BorrowedSqlType<'a, DB>,
for<'a> &'a str: BorrowedSqlType<'a, DB>,
Vec<u8>: SqlType<DB>,
String: SqlType<DB>,
bool: SqlType<DB>,
Vec<u8>: SqlType<DB>,
Option<String>: SqlType<DB>,
Json<Raw<AnyGlobalAccountDataEvent>>: SqlType<DB>,
Json<Raw<PresenceEvent>>: SqlType<DB>,
Json<SyncRoomMemberEvent>: SqlType<DB>,
Json<MinimalRoomMemberEvent>: SqlType<DB>,
Json<Raw<AnySyncStateEvent>>: SqlType<DB>,
Json<Raw<AnyRoomAccountDataEvent>>: SqlType<DB>,
Json<RoomInfo>: SqlType<DB>,
Json<Receipt>: SqlType<DB>,
Json<Raw<AnyStrippedStateEvent>>: SqlType<DB>,
Json<StrippedRoomMemberEvent>: SqlType<DB>,
Json<MemberEvent>: SqlType<DB>,
for<'a> &'a str: ColumnIndex<<DB as Database>::Row>,
{
#[cfg(not(feature = "e2e-encryption"))]
{
let _ = passphrase;
let state_store = StateStore::new(db).await?;
Ok(StoreConfig::new().state_store(Box::new(state_store)))
}
#[cfg(feature = "e2e-encryption")]
{
let state_store = StateStore::new(db).await?;
let mut crypto_store = StateStore::new(db).await?;
if let Some(passphrase) = passphrase {
crypto_store.unlock_with_passphrase(passphrase).await?;
} else {
crypto_store.unlock().await?;
}
Ok(StoreConfig::new()
.state_store(Box::new(state_store))
.crypto_store(Box::new(crypto_store)))
}
}