mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-04-01 00:53:13 +00:00
feat: abstract storage trait
This commit is contained in:
parent
d580c0ac75
commit
4cb2ec1608
@ -11,6 +11,9 @@ use crate::{
|
||||
inbox::Inbox,
|
||||
proto::{EncryptedPayload, EnvelopeV1, Message},
|
||||
storage::ChatStorage,
|
||||
store::{
|
||||
ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, IdentityStore,
|
||||
},
|
||||
types::{AddressedEnvelope, ContentData},
|
||||
};
|
||||
|
||||
@ -37,11 +40,11 @@ impl Context {
|
||||
let name = name.into();
|
||||
|
||||
// Load or create identity
|
||||
let identity = if let Some(identity) = storage.load_identity()? {
|
||||
let identity = if let Some(identity) = IdentityStore::load_identity(&storage)? {
|
||||
identity
|
||||
} else {
|
||||
let identity = Identity::new(&name);
|
||||
storage.save_identity(&identity)?;
|
||||
IdentityStore::save_identity(&mut storage, &identity)?;
|
||||
identity
|
||||
};
|
||||
|
||||
@ -88,7 +91,7 @@ impl Context {
|
||||
}
|
||||
|
||||
pub fn list_conversations(&self) -> Result<Vec<ConversationIdOwned>, ChatError> {
|
||||
let records = self.storage.load_conversations()?;
|
||||
let records = ConversationStore::load_conversations(&self.storage)?;
|
||||
Ok(records
|
||||
.into_iter()
|
||||
.map(|r| Arc::from(r.local_convo_id.as_str()))
|
||||
@ -121,7 +124,9 @@ impl Context {
|
||||
let enc = EncryptedPayload::decode(env.payload)?;
|
||||
match convo_id {
|
||||
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),
|
||||
}
|
||||
}
|
||||
@ -133,15 +138,13 @@ impl Context {
|
||||
) -> Result<Option<ContentData>, ChatError> {
|
||||
// Look up the ephemeral key from storage
|
||||
let key_hex = Inbox::extract_ephemeral_key_hex(&enc_payload)?;
|
||||
let ephemeral_key = self
|
||||
.storage
|
||||
.load_ephemeral_key(&key_hex)?
|
||||
let ephemeral_key = EphemeralKeyStore::load_ephemeral_key(&self.storage, &key_hex)?
|
||||
.ok_or(ChatError::UnknownEphemeralKey())?;
|
||||
|
||||
let (convo, content) = self.inbox.handle_frame(&ephemeral_key, enc_payload)?;
|
||||
|
||||
// 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());
|
||||
Ok(content)
|
||||
@ -163,39 +166,38 @@ impl Context {
|
||||
|
||||
pub fn create_intro_bundle(&mut self) -> Result<Vec<u8>, ChatError> {
|
||||
let (intro, public_key_hex, private_key) = self.inbox.create_intro_bundle();
|
||||
self.storage
|
||||
.save_ephemeral_key(&public_key_hex, &private_key)?;
|
||||
EphemeralKeyStore::save_ephemeral_key(&mut self.storage, &public_key_hex, &private_key)?;
|
||||
Ok(intro.into())
|
||||
}
|
||||
|
||||
/// Loads a conversation from DB by constructing it from metadata + ratchet state.
|
||||
fn load_convo(&self, convo_id: ConversationId) -> Result<PrivateV1Convo, ChatError> {
|
||||
let record = self
|
||||
.storage
|
||||
.load_conversation(convo_id)?
|
||||
let meta = ConversationStore::load_conversation(&self.storage, convo_id)?
|
||||
.ok_or_else(|| ChatError::NoConvo(convo_id.into()))?;
|
||||
|
||||
if record.convo_type != "private_v1" {
|
||||
return Err(ChatError::BadBundleValue(format!(
|
||||
"unsupported conversation type: {}",
|
||||
record.convo_type
|
||||
)));
|
||||
match meta.kind {
|
||||
ConversationKind::PrivateV1 => {
|
||||
let dr_state: RatchetState = self.ratchet_storage.load(&meta.local_convo_id)?;
|
||||
|
||||
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.
|
||||
fn persist_convo(&mut self, convo: &dyn Convo) -> ConversationIdOwned {
|
||||
let _ = self
|
||||
.storage
|
||||
.save_conversation(convo.id(), &convo.remote_id(), convo.convo_type());
|
||||
let meta = ConversationMeta {
|
||||
local_convo_id: convo.id().to_string(),
|
||||
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);
|
||||
Arc::from(convo.id())
|
||||
}
|
||||
@ -328,13 +330,13 @@ mod tests {
|
||||
let content = alice.handle_payload(&payload.data).unwrap().unwrap();
|
||||
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[0].convo_type, "private_v1");
|
||||
assert_eq!(convos[0].kind, ConversationKind::PrivateV1);
|
||||
|
||||
drop(alice);
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ mod identity;
|
||||
mod inbox;
|
||||
mod proto;
|
||||
mod storage;
|
||||
pub mod store;
|
||||
mod types;
|
||||
mod utils;
|
||||
|
||||
|
||||
@ -10,6 +10,9 @@ use zeroize::Zeroize;
|
||||
use crate::{
|
||||
identity::Identity,
|
||||
storage::types::{ConversationRecord, IdentityRecord},
|
||||
store::{
|
||||
ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, IdentityStore,
|
||||
},
|
||||
};
|
||||
|
||||
/// 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.
|
||||
pub fn load_conversations(&self) -> Result<Vec<ConversationRecord>, StorageError> {
|
||||
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)]
|
||||
mod tests {
|
||||
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