mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-05-21 17:39:28 +00:00
feat: abstract storage trait
This commit is contained in:
parent
d580c0ac75
commit
4cb2ec1608
@ -11,6 +11,9 @@ use crate::{
|
|||||||
inbox::Inbox,
|
inbox::Inbox,
|
||||||
proto::{EncryptedPayload, EnvelopeV1, Message},
|
proto::{EncryptedPayload, EnvelopeV1, Message},
|
||||||
storage::ChatStorage,
|
storage::ChatStorage,
|
||||||
|
store::{
|
||||||
|
ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, IdentityStore,
|
||||||
|
},
|
||||||
types::{AddressedEnvelope, ContentData},
|
types::{AddressedEnvelope, ContentData},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,11 +40,11 @@ impl Context {
|
|||||||
let name = name.into();
|
let name = name.into();
|
||||||
|
|
||||||
// Load or create identity
|
// Load or create identity
|
||||||
let identity = if let Some(identity) = storage.load_identity()? {
|
let identity = if let Some(identity) = IdentityStore::load_identity(&storage)? {
|
||||||
identity
|
identity
|
||||||
} else {
|
} else {
|
||||||
let identity = Identity::new(&name);
|
let identity = Identity::new(&name);
|
||||||
storage.save_identity(&identity)?;
|
IdentityStore::save_identity(&mut storage, &identity)?;
|
||||||
identity
|
identity
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -88,7 +91,7 @@ impl Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_conversations(&self) -> Result<Vec<ConversationIdOwned>, ChatError> {
|
pub fn list_conversations(&self) -> Result<Vec<ConversationIdOwned>, ChatError> {
|
||||||
let records = self.storage.load_conversations()?;
|
let records = ConversationStore::load_conversations(&self.storage)?;
|
||||||
Ok(records
|
Ok(records
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| Arc::from(r.local_convo_id.as_str()))
|
.map(|r| Arc::from(r.local_convo_id.as_str()))
|
||||||
@ -121,7 +124,9 @@ impl Context {
|
|||||||
let enc = EncryptedPayload::decode(env.payload)?;
|
let enc = EncryptedPayload::decode(env.payload)?;
|
||||||
match convo_id {
|
match convo_id {
|
||||||
c if c == self.inbox.id() => self.dispatch_to_inbox(enc),
|
c if c == self.inbox.id() => self.dispatch_to_inbox(enc),
|
||||||
c if self.storage.has_conversation(&c)? => self.dispatch_to_convo(&c, enc),
|
c if ConversationStore::has_conversation(&self.storage, &c)? => {
|
||||||
|
self.dispatch_to_convo(&c, enc)
|
||||||
|
}
|
||||||
_ => Ok(None),
|
_ => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,15 +138,13 @@ impl Context {
|
|||||||
) -> Result<Option<ContentData>, ChatError> {
|
) -> Result<Option<ContentData>, ChatError> {
|
||||||
// Look up the ephemeral key from storage
|
// Look up the ephemeral key from storage
|
||||||
let key_hex = Inbox::extract_ephemeral_key_hex(&enc_payload)?;
|
let key_hex = Inbox::extract_ephemeral_key_hex(&enc_payload)?;
|
||||||
let ephemeral_key = self
|
let ephemeral_key = EphemeralKeyStore::load_ephemeral_key(&self.storage, &key_hex)?
|
||||||
.storage
|
|
||||||
.load_ephemeral_key(&key_hex)?
|
|
||||||
.ok_or(ChatError::UnknownEphemeralKey())?;
|
.ok_or(ChatError::UnknownEphemeralKey())?;
|
||||||
|
|
||||||
let (convo, content) = self.inbox.handle_frame(&ephemeral_key, enc_payload)?;
|
let (convo, content) = self.inbox.handle_frame(&ephemeral_key, enc_payload)?;
|
||||||
|
|
||||||
// Remove consumed ephemeral key from storage
|
// Remove consumed ephemeral key from storage
|
||||||
self.storage.remove_ephemeral_key(&key_hex)?;
|
EphemeralKeyStore::remove_ephemeral_key(&mut self.storage, &key_hex)?;
|
||||||
|
|
||||||
self.persist_convo(convo.as_ref());
|
self.persist_convo(convo.as_ref());
|
||||||
Ok(content)
|
Ok(content)
|
||||||
@ -163,39 +166,38 @@ impl Context {
|
|||||||
|
|
||||||
pub fn create_intro_bundle(&mut self) -> Result<Vec<u8>, ChatError> {
|
pub fn create_intro_bundle(&mut self) -> Result<Vec<u8>, ChatError> {
|
||||||
let (intro, public_key_hex, private_key) = self.inbox.create_intro_bundle();
|
let (intro, public_key_hex, private_key) = self.inbox.create_intro_bundle();
|
||||||
self.storage
|
EphemeralKeyStore::save_ephemeral_key(&mut self.storage, &public_key_hex, &private_key)?;
|
||||||
.save_ephemeral_key(&public_key_hex, &private_key)?;
|
|
||||||
Ok(intro.into())
|
Ok(intro.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a conversation from DB by constructing it from metadata + ratchet state.
|
/// Loads a conversation from DB by constructing it from metadata + ratchet state.
|
||||||
fn load_convo(&self, convo_id: ConversationId) -> Result<PrivateV1Convo, ChatError> {
|
fn load_convo(&self, convo_id: ConversationId) -> Result<PrivateV1Convo, ChatError> {
|
||||||
let record = self
|
let meta = ConversationStore::load_conversation(&self.storage, convo_id)?
|
||||||
.storage
|
|
||||||
.load_conversation(convo_id)?
|
|
||||||
.ok_or_else(|| ChatError::NoConvo(convo_id.into()))?;
|
.ok_or_else(|| ChatError::NoConvo(convo_id.into()))?;
|
||||||
|
|
||||||
if record.convo_type != "private_v1" {
|
match meta.kind {
|
||||||
return Err(ChatError::BadBundleValue(format!(
|
ConversationKind::PrivateV1 => {
|
||||||
"unsupported conversation type: {}",
|
let dr_state: RatchetState = self.ratchet_storage.load(&meta.local_convo_id)?;
|
||||||
record.convo_type
|
|
||||||
)));
|
Ok(PrivateV1Convo::from_stored(
|
||||||
|
meta.local_convo_id,
|
||||||
|
meta.remote_convo_id,
|
||||||
|
dr_state,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
ConversationKind::Unknown(kind) => Err(ChatError::UnsupportedConvoType(kind)),
|
||||||
}
|
}
|
||||||
|
|
||||||
let dr_state: RatchetState = self.ratchet_storage.load(&record.local_convo_id)?;
|
|
||||||
|
|
||||||
Ok(PrivateV1Convo::from_stored(
|
|
||||||
record.local_convo_id,
|
|
||||||
record.remote_convo_id,
|
|
||||||
dr_state,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persists a conversation's metadata and ratchet state to DB.
|
/// Persists a conversation's metadata and ratchet state to DB.
|
||||||
fn persist_convo(&mut self, convo: &dyn Convo) -> ConversationIdOwned {
|
fn persist_convo(&mut self, convo: &dyn Convo) -> ConversationIdOwned {
|
||||||
let _ = self
|
let meta = ConversationMeta {
|
||||||
.storage
|
local_convo_id: convo.id().to_string(),
|
||||||
.save_conversation(convo.id(), &convo.remote_id(), convo.convo_type());
|
remote_convo_id: convo.remote_id(),
|
||||||
|
kind: ConversationKind::from_db(convo.convo_type()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = ConversationStore::save_conversation(&mut self.storage, &meta);
|
||||||
let _ = convo.save_ratchet_state(&mut self.ratchet_storage);
|
let _ = convo.save_ratchet_state(&mut self.ratchet_storage);
|
||||||
Arc::from(convo.id())
|
Arc::from(convo.id())
|
||||||
}
|
}
|
||||||
@ -328,13 +330,13 @@ mod tests {
|
|||||||
let content = alice.handle_payload(&payload.data).unwrap().unwrap();
|
let content = alice.handle_payload(&payload.data).unwrap().unwrap();
|
||||||
assert!(content.is_new_convo);
|
assert!(content.is_new_convo);
|
||||||
|
|
||||||
let convos = alice.storage.load_conversations().unwrap();
|
let convos = ConversationStore::load_conversations(&alice.storage).unwrap();
|
||||||
assert_eq!(convos.len(), 1);
|
assert_eq!(convos.len(), 1);
|
||||||
assert_eq!(convos[0].convo_type, "private_v1");
|
assert_eq!(convos[0].kind, ConversationKind::PrivateV1);
|
||||||
|
|
||||||
drop(alice);
|
drop(alice);
|
||||||
let alice2 = Context::open("alice", config).unwrap();
|
let alice2 = Context::open("alice", config).unwrap();
|
||||||
let convos = alice2.storage.load_conversations().unwrap();
|
let convos = ConversationStore::load_conversations(&alice2.storage).unwrap();
|
||||||
assert_eq!(convos.len(), 1, "conversation metadata should persist");
|
assert_eq!(convos.len(), 1, "conversation metadata should persist");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ mod identity;
|
|||||||
mod inbox;
|
mod inbox;
|
||||||
mod proto;
|
mod proto;
|
||||||
mod storage;
|
mod storage;
|
||||||
|
pub mod store;
|
||||||
mod types;
|
mod types;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,9 @@ use zeroize::Zeroize;
|
|||||||
use crate::{
|
use crate::{
|
||||||
identity::Identity,
|
identity::Identity,
|
||||||
storage::types::{ConversationRecord, IdentityRecord},
|
storage::types::{ConversationRecord, IdentityRecord},
|
||||||
|
store::{
|
||||||
|
ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, IdentityStore,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Chat-specific storage operations.
|
/// Chat-specific storage operations.
|
||||||
@ -204,6 +207,15 @@ impl ChatStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes a conversation record by its local ID.
|
||||||
|
pub fn remove_conversation(&mut self, local_convo_id: &str) -> Result<(), StorageError> {
|
||||||
|
self.db.connection().execute(
|
||||||
|
"DELETE FROM conversations WHERE local_convo_id = ?1",
|
||||||
|
params![local_convo_id],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Loads all conversation records.
|
/// Loads all conversation records.
|
||||||
pub fn load_conversations(&self) -> Result<Vec<ConversationRecord>, StorageError> {
|
pub fn load_conversations(&self) -> Result<Vec<ConversationRecord>, StorageError> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
@ -225,6 +237,79 @@ impl ChatStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IdentityStore for ChatStorage {
|
||||||
|
fn load_identity(&self) -> Result<Option<Identity>, StorageError> {
|
||||||
|
ChatStorage::load_identity(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_identity(&mut self, identity: &Identity) -> Result<(), StorageError> {
|
||||||
|
ChatStorage::save_identity(self, identity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EphemeralKeyStore for ChatStorage {
|
||||||
|
fn save_ephemeral_key(
|
||||||
|
&mut self,
|
||||||
|
public_key_hex: &str,
|
||||||
|
private_key: &PrivateKey,
|
||||||
|
) -> Result<(), StorageError> {
|
||||||
|
ChatStorage::save_ephemeral_key(self, public_key_hex, private_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_ephemeral_key(&self, public_key_hex: &str) -> Result<Option<PrivateKey>, StorageError> {
|
||||||
|
ChatStorage::load_ephemeral_key(self, public_key_hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_ephemeral_key(&mut self, public_key_hex: &str) -> Result<(), StorageError> {
|
||||||
|
ChatStorage::remove_ephemeral_key(self, public_key_hex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConversationStore for ChatStorage {
|
||||||
|
fn save_conversation(&mut self, meta: &ConversationMeta) -> Result<(), StorageError> {
|
||||||
|
ChatStorage::save_conversation(
|
||||||
|
self,
|
||||||
|
&meta.local_convo_id,
|
||||||
|
&meta.remote_convo_id,
|
||||||
|
meta.kind.as_db(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_conversation(
|
||||||
|
&self,
|
||||||
|
local_convo_id: &str,
|
||||||
|
) -> Result<Option<ConversationMeta>, StorageError> {
|
||||||
|
let record = ChatStorage::load_conversation(self, local_convo_id)?;
|
||||||
|
|
||||||
|
Ok(record.map(|record| ConversationMeta {
|
||||||
|
local_convo_id: record.local_convo_id,
|
||||||
|
remote_convo_id: record.remote_convo_id,
|
||||||
|
kind: ConversationKind::from_db(&record.convo_type),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_conversations(&self) -> Result<Vec<ConversationMeta>, StorageError> {
|
||||||
|
let records = ChatStorage::load_conversations(self)?;
|
||||||
|
|
||||||
|
Ok(records
|
||||||
|
.into_iter()
|
||||||
|
.map(|record| ConversationMeta {
|
||||||
|
local_convo_id: record.local_convo_id,
|
||||||
|
remote_convo_id: record.remote_convo_id,
|
||||||
|
kind: ConversationKind::from_db(&record.convo_type),
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_conversation(&self, local_convo_id: &str) -> Result<bool, StorageError> {
|
||||||
|
ChatStorage::has_conversation(self, local_convo_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_conversation(&mut self, local_convo_id: &str) -> Result<(), StorageError> {
|
||||||
|
ChatStorage::remove_conversation(self, local_convo_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
11
core/conversations/src/store.rs
Normal file
11
core/conversations/src/store.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
mod conversations;
|
||||||
|
mod ephemeral_keys;
|
||||||
|
mod identity;
|
||||||
|
|
||||||
|
pub use conversations::{ConversationKind, ConversationMeta, ConversationStore};
|
||||||
|
pub use ephemeral_keys::EphemeralKeyStore;
|
||||||
|
pub use identity::IdentityStore;
|
||||||
|
|
||||||
|
pub trait ChatStore: IdentityStore + EphemeralKeyStore + ConversationStore {}
|
||||||
|
|
||||||
|
impl<T> ChatStore for T where T: IdentityStore + EphemeralKeyStore + ConversationStore {}
|
||||||
45
core/conversations/src/store/conversations.rs
Normal file
45
core/conversations/src/store/conversations.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use storage::StorageError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ConversationKind {
|
||||||
|
PrivateV1,
|
||||||
|
Unknown(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConversationKind {
|
||||||
|
pub fn from_db(value: &str) -> Self {
|
||||||
|
match value {
|
||||||
|
"private_v1" => Self::PrivateV1,
|
||||||
|
other => Self::Unknown(other.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_db(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::PrivateV1 => "private_v1",
|
||||||
|
Self::Unknown(value) => value.as_str(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ConversationMeta {
|
||||||
|
pub local_convo_id: String,
|
||||||
|
pub remote_convo_id: String,
|
||||||
|
pub kind: ConversationKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ConversationStore {
|
||||||
|
fn save_conversation(&mut self, meta: &ConversationMeta) -> Result<(), StorageError>;
|
||||||
|
|
||||||
|
fn load_conversation(
|
||||||
|
&self,
|
||||||
|
local_convo_id: &str,
|
||||||
|
) -> Result<Option<ConversationMeta>, StorageError>;
|
||||||
|
|
||||||
|
fn remove_conversation(&mut self, local_convo_id: &str) -> Result<(), StorageError>;
|
||||||
|
|
||||||
|
fn load_conversations(&self) -> Result<Vec<ConversationMeta>, StorageError>;
|
||||||
|
|
||||||
|
fn has_conversation(&self, local_convo_id: &str) -> Result<bool, StorageError>;
|
||||||
|
}
|
||||||
14
core/conversations/src/store/ephemeral_keys.rs
Normal file
14
core/conversations/src/store/ephemeral_keys.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
use crypto::PrivateKey;
|
||||||
|
use storage::StorageError;
|
||||||
|
|
||||||
|
pub trait EphemeralKeyStore {
|
||||||
|
fn save_ephemeral_key(
|
||||||
|
&mut self,
|
||||||
|
public_key_hex: &str,
|
||||||
|
private_key: &PrivateKey,
|
||||||
|
) -> Result<(), StorageError>;
|
||||||
|
|
||||||
|
fn load_ephemeral_key(&self, public_key_hex: &str) -> Result<Option<PrivateKey>, StorageError>;
|
||||||
|
|
||||||
|
fn remove_ephemeral_key(&mut self, public_key_hex: &str) -> Result<(), StorageError>;
|
||||||
|
}
|
||||||
12
core/conversations/src/store/identity.rs
Normal file
12
core/conversations/src/store/identity.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
use storage::StorageError;
|
||||||
|
|
||||||
|
use crate::identity::Identity;
|
||||||
|
|
||||||
|
/// Persistence operations for installation identity data.
|
||||||
|
pub trait IdentityStore {
|
||||||
|
/// Loads the stored identity if one exists.
|
||||||
|
fn load_identity(&self) -> Result<Option<Identity>, StorageError>;
|
||||||
|
|
||||||
|
/// Persists the installation identity.
|
||||||
|
fn save_identity(&mut self, identity: &Identity) -> Result<(), StorageError>;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user