mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-04-01 09:04:03 +00:00
chore: sqlite module
This commit is contained in:
parent
7b61afd7f8
commit
130578b956
@ -13,9 +13,12 @@ use safer_ffi::{
|
|||||||
prelude::{c_slice, repr_c},
|
prelude::{c_slice, repr_c},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use storage::StorageConfig;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
context::{Context, Introduction},
|
context::{Context, Introduction},
|
||||||
errors::ChatError,
|
errors::ChatError,
|
||||||
|
sqlite::ChatStorage,
|
||||||
types::ContentData,
|
types::ContentData,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,7 +45,7 @@ pub fn is_ok(error: i32) -> bool {
|
|||||||
/// Opaque wrapper for Context
|
/// Opaque wrapper for Context
|
||||||
#[derive_ReprC]
|
#[derive_ReprC]
|
||||||
#[repr(opaque)]
|
#[repr(opaque)]
|
||||||
pub struct ContextHandle(pub(crate) Context);
|
pub struct ContextHandle(pub(crate) Context<ChatStorage>);
|
||||||
|
|
||||||
/// Creates a new libchat Ctx
|
/// Creates a new libchat Ctx
|
||||||
///
|
///
|
||||||
@ -51,7 +54,9 @@ pub struct ContextHandle(pub(crate) Context);
|
|||||||
#[ffi_export]
|
#[ffi_export]
|
||||||
pub fn create_context(name: repr_c::String) -> repr_c::Box<ContextHandle> {
|
pub fn create_context(name: repr_c::String) -> repr_c::Box<ContextHandle> {
|
||||||
// Deference name to to `str` and then borrow to &str
|
// Deference name to to `str` and then borrow to &str
|
||||||
Box::new(ContextHandle(Context::new_with_name(&*name))).into()
|
let store =
|
||||||
|
ChatStorage::new(StorageConfig::InMemory).expect("in-memory storage should not fail");
|
||||||
|
Box::new(ContextHandle(Context::new_with_name(&*name, store))).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the friendly name of the contexts installation.
|
/// Returns the friendly name of the contexts installation.
|
||||||
|
|||||||
@ -10,8 +10,8 @@ use crate::{
|
|||||||
identity::Identity,
|
identity::Identity,
|
||||||
inbox::Inbox,
|
inbox::Inbox,
|
||||||
proto::{EncryptedPayload, EnvelopeV1, Message},
|
proto::{EncryptedPayload, EnvelopeV1, Message},
|
||||||
storage::ChatStorage,
|
sqlite::ChatStorage,
|
||||||
store::{ChatStore, ConversationKind, ConversationMeta},
|
store::{ChatStore, ConversationKind, ConversationMeta, IdentityStore},
|
||||||
types::{AddressedEnvelope, ContentData},
|
types::{AddressedEnvelope, ContentData},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -315,7 +315,7 @@ mod mock {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{context::mock::MockChatStore, store::ConversationStore};
|
use crate::{context::mock::MockChatStore, sqlite::ChatStorage, store::ConversationStore};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@ -402,11 +402,13 @@ mod tests {
|
|||||||
.to_string();
|
.to_string();
|
||||||
let config = StorageConfig::File(db_path);
|
let config = StorageConfig::File(db_path);
|
||||||
|
|
||||||
let mut ctx1 = Context::open("alice", config.clone(), MockChatStore::default()).unwrap();
|
let store1 = ChatStorage::new(config.clone()).unwrap();
|
||||||
|
let mut ctx1 = Context::open("alice", config.clone(), store1).unwrap();
|
||||||
let bundle1 = ctx1.create_intro_bundle().unwrap();
|
let bundle1 = ctx1.create_intro_bundle().unwrap();
|
||||||
|
|
||||||
drop(ctx1);
|
drop(ctx1);
|
||||||
let mut ctx2 = Context::open("alice", config.clone(), MockChatStore::default()).unwrap();
|
let store2 = ChatStorage::new(config.clone()).unwrap();
|
||||||
|
let mut ctx2 = Context::open("alice", config.clone(), store2).unwrap();
|
||||||
|
|
||||||
let intro = Introduction::try_from(bundle1.as_slice()).unwrap();
|
let intro = Introduction::try_from(bundle1.as_slice()).unwrap();
|
||||||
let mut bob = Context::new_with_name("bob", MockChatStore::default());
|
let mut bob = Context::new_with_name("bob", MockChatStore::default());
|
||||||
@ -431,7 +433,8 @@ mod tests {
|
|||||||
.to_string();
|
.to_string();
|
||||||
let config = StorageConfig::File(db_path);
|
let config = StorageConfig::File(db_path);
|
||||||
|
|
||||||
let mut alice = Context::open("alice", config.clone(), MockChatStore::default()).unwrap();
|
let store = ChatStorage::new(config.clone()).unwrap();
|
||||||
|
let mut alice = Context::open("alice", config.clone(), store).unwrap();
|
||||||
let mut bob = Context::new_with_name("bob", MockChatStore::default());
|
let mut bob = Context::new_with_name("bob", MockChatStore::default());
|
||||||
|
|
||||||
let bundle = alice.create_intro_bundle().unwrap();
|
let bundle = alice.create_intro_bundle().unwrap();
|
||||||
@ -447,7 +450,8 @@ mod tests {
|
|||||||
assert_eq!(convos[0].kind.as_str(), "private_v1");
|
assert_eq!(convos[0].kind.as_str(), "private_v1");
|
||||||
|
|
||||||
drop(alice);
|
drop(alice);
|
||||||
let alice2 = Context::open("alice", config, MockChatStore::default()).unwrap();
|
let store2 = ChatStorage::new(config.clone()).unwrap();
|
||||||
|
let alice2 = Context::open("alice", config, store2).unwrap();
|
||||||
let convos = alice2.storage.load_conversations().unwrap();
|
let convos = alice2.storage.load_conversations().unwrap();
|
||||||
assert_eq!(convos.len(), 1, "conversation metadata should persist");
|
assert_eq!(convos.len(), 1, "conversation metadata should persist");
|
||||||
}
|
}
|
||||||
@ -463,7 +467,8 @@ mod tests {
|
|||||||
let config = StorageConfig::File(db_path);
|
let config = StorageConfig::File(db_path);
|
||||||
|
|
||||||
// Alice and Bob establish a conversation
|
// Alice and Bob establish a conversation
|
||||||
let mut alice = Context::open("alice", config.clone(), MockChatStore::default()).unwrap();
|
let store = ChatStorage::new(config.clone()).unwrap();
|
||||||
|
let mut alice = Context::open("alice", config.clone(), store).unwrap();
|
||||||
let mut bob = Context::new_with_name("bob", MockChatStore::default());
|
let mut bob = Context::new_with_name("bob", MockChatStore::default());
|
||||||
|
|
||||||
let bundle = alice.create_intro_bundle().unwrap();
|
let bundle = alice.create_intro_bundle().unwrap();
|
||||||
@ -485,7 +490,8 @@ mod tests {
|
|||||||
|
|
||||||
// Drop Alice and reopen - conversation should survive
|
// Drop Alice and reopen - conversation should survive
|
||||||
drop(alice);
|
drop(alice);
|
||||||
let mut alice2 = Context::open("alice", config, MockChatStore::default()).unwrap();
|
let store2 = ChatStorage::new(config.clone()).unwrap();
|
||||||
|
let mut alice2 = Context::open("alice", config, store2).unwrap();
|
||||||
|
|
||||||
// Verify conversation was restored
|
// Verify conversation was restored
|
||||||
let convo_ids = alice2.list_conversations().unwrap();
|
let convo_ids = alice2.list_conversations().unwrap();
|
||||||
|
|||||||
@ -149,9 +149,7 @@ impl Inbox {
|
|||||||
|
|
||||||
/// Extracts the ephemeral key hex from an incoming encrypted payload
|
/// Extracts the ephemeral key hex from an incoming encrypted payload
|
||||||
/// so the caller can look it up from storage before calling handle_frame.
|
/// so the caller can look it up from storage before calling handle_frame.
|
||||||
pub fn extract_ephemeral_key_hex(
|
pub fn extract_ephemeral_key_hex(enc_payload: &EncryptedPayload) -> Result<String, ChatError> {
|
||||||
enc_payload: &EncryptedPayload,
|
|
||||||
) -> Result<String, ChatError> {
|
|
||||||
let Some(proto::Encryption::InboxHandshake(ref handshake)) = enc_payload.encryption else {
|
let Some(proto::Encryption::InboxHandshake(ref handshake)) = enc_payload.encryption else {
|
||||||
let got = format!("{:?}", enc_payload.encryption);
|
let got = format!("{:?}", enc_payload.encryption);
|
||||||
return Err(ChatError::ProtocolExpectation("inboxhandshake", got));
|
return Err(ChatError::ProtocolExpectation("inboxhandshake", got));
|
||||||
@ -241,7 +239,8 @@ impl Id for Inbox {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::storage::ChatStorage;
|
use crate::sqlite::ChatStorage;
|
||||||
|
use crate::store::EphemeralKeyStore;
|
||||||
use storage::StorageConfig;
|
use storage::StorageConfig;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -6,7 +6,7 @@ mod errors;
|
|||||||
mod identity;
|
mod identity;
|
||||||
mod inbox;
|
mod inbox;
|
||||||
mod proto;
|
mod proto;
|
||||||
mod storage;
|
mod sqlite;
|
||||||
mod store;
|
mod store;
|
||||||
mod types;
|
mod types;
|
||||||
mod utils;
|
mod utils;
|
||||||
@ -14,6 +14,8 @@ mod utils;
|
|||||||
pub use api::*;
|
pub use api::*;
|
||||||
pub use context::{Context, Introduction};
|
pub use context::{Context, Introduction};
|
||||||
pub use errors::ChatError;
|
pub use errors::ChatError;
|
||||||
|
pub use sqlite::ChatStorage;
|
||||||
|
pub use storage::StorageConfig;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
@ -9,7 +9,10 @@ use zeroize::Zeroize;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
identity::Identity,
|
identity::Identity,
|
||||||
storage::types::{ConversationRecord, IdentityRecord},
|
sqlite::types::IdentityRecord,
|
||||||
|
store::{
|
||||||
|
ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, IdentityStore,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Chat-specific storage operations.
|
/// Chat-specific storage operations.
|
||||||
@ -35,28 +38,14 @@ impl ChatStorage {
|
|||||||
Ok(Self { db })
|
Ok(Self { db })
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Identity Operations ====================
|
}
|
||||||
|
|
||||||
/// Saves the identity (secret key).
|
|
||||||
///
|
|
||||||
/// Note: The secret key bytes are explicitly zeroized after use to minimize
|
|
||||||
/// the time sensitive data remains in stack memory.
|
|
||||||
pub fn save_identity(&mut self, identity: &Identity) -> Result<(), StorageError> {
|
|
||||||
let mut secret_bytes = identity.secret().DANGER_to_bytes();
|
|
||||||
let result = self.db.connection().execute(
|
|
||||||
"INSERT OR REPLACE INTO identity (id, name, secret_key) VALUES (1, ?1, ?2)",
|
|
||||||
params![identity.get_name(), secret_bytes.as_slice()],
|
|
||||||
);
|
|
||||||
secret_bytes.zeroize();
|
|
||||||
result?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
impl IdentityStore for ChatStorage {
|
||||||
/// Loads the identity if it exists.
|
/// Loads the identity if it exists.
|
||||||
///
|
///
|
||||||
/// Note: Secret key bytes are zeroized after being copied into IdentityRecord,
|
/// Note: Secret key bytes are zeroized after being copied into IdentityRecord,
|
||||||
/// which handles its own zeroization via ZeroizeOnDrop.
|
/// which handles its own zeroization via ZeroizeOnDrop.
|
||||||
pub fn load_identity(&self) -> Result<Option<Identity>, StorageError> {
|
fn load_identity(&self) -> Result<Option<Identity>, StorageError> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.db
|
.db
|
||||||
.connection()
|
.connection()
|
||||||
@ -92,10 +81,25 @@ impl ChatStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Ephemeral Key Operations ====================
|
/// Saves the identity (secret key).
|
||||||
|
///
|
||||||
|
/// Note: The secret key bytes are explicitly zeroized after use to minimize
|
||||||
|
/// the time sensitive data remains in stack memory.
|
||||||
|
fn save_identity(&mut self, identity: &Identity) -> Result<(), StorageError> {
|
||||||
|
let mut secret_bytes = identity.secret().DANGER_to_bytes();
|
||||||
|
let result = self.db.connection().execute(
|
||||||
|
"INSERT OR REPLACE INTO identity (id, name, secret_key) VALUES (1, ?1, ?2)",
|
||||||
|
params![identity.get_name(), secret_bytes.as_slice()],
|
||||||
|
);
|
||||||
|
secret_bytes.zeroize();
|
||||||
|
result?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EphemeralKeyStore for ChatStorage {
|
||||||
/// Saves an ephemeral key pair to storage.
|
/// Saves an ephemeral key pair to storage.
|
||||||
pub fn save_ephemeral_key(
|
fn save_ephemeral_key(
|
||||||
&mut self,
|
&mut self,
|
||||||
public_key_hex: &str,
|
public_key_hex: &str,
|
||||||
private_key: &PrivateKey,
|
private_key: &PrivateKey,
|
||||||
@ -111,7 +115,7 @@ impl ChatStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a single ephemeral key by its public key hex.
|
/// Loads a single ephemeral key by its public key hex.
|
||||||
pub fn load_ephemeral_key(
|
fn load_ephemeral_key(
|
||||||
&self,
|
&self,
|
||||||
public_key_hex: &str,
|
public_key_hex: &str,
|
||||||
) -> Result<Option<PrivateKey>, StorageError> {
|
) -> Result<Option<PrivateKey>, StorageError> {
|
||||||
@ -146,43 +150,54 @@ impl ChatStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Removes an ephemeral key from storage.
|
/// Removes an ephemeral key from storage.
|
||||||
pub fn remove_ephemeral_key(&mut self, public_key_hex: &str) -> Result<(), StorageError> {
|
fn remove_ephemeral_key(&mut self, public_key_hex: &str) -> Result<(), StorageError> {
|
||||||
self.db.connection().execute(
|
self.db.connection().execute(
|
||||||
"DELETE FROM ephemeral_keys WHERE public_key_hex = ?1",
|
"DELETE FROM ephemeral_keys WHERE public_key_hex = ?1",
|
||||||
params![public_key_hex],
|
params![public_key_hex],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Conversation Operations ====================
|
impl ConversationStore for ChatStorage {
|
||||||
|
|
||||||
/// Saves conversation metadata.
|
/// Saves conversation metadata.
|
||||||
pub fn save_conversation(
|
fn save_conversation(&mut self, meta: &ConversationMeta) -> Result<(), StorageError> {
|
||||||
&mut self,
|
|
||||||
local_convo_id: &str,
|
|
||||||
remote_convo_id: &str,
|
|
||||||
convo_type: &str,
|
|
||||||
) -> Result<(), StorageError> {
|
|
||||||
self.db.connection().execute(
|
self.db.connection().execute(
|
||||||
"INSERT OR REPLACE INTO conversations (local_convo_id, remote_convo_id, convo_type) VALUES (?1, ?2, ?3)",
|
"INSERT OR REPLACE INTO conversations (local_convo_id, remote_convo_id, convo_type) VALUES (?1, ?2, ?3)",
|
||||||
params![local_convo_id, remote_convo_id, convo_type],
|
params![meta.local_convo_id, meta.remote_convo_id, meta.kind.as_str()],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a conversation exists by its local ID.
|
/// Loads a single conversation record by its local ID.
|
||||||
pub fn has_conversation(&self, local_convo_id: &str) -> Result<bool, StorageError> {
|
fn load_conversation(
|
||||||
let exists: bool = self.db.connection().query_row(
|
&self,
|
||||||
"SELECT EXISTS(SELECT 1 FROM conversations WHERE local_convo_id = ?1)",
|
local_convo_id: &str,
|
||||||
params![local_convo_id],
|
) -> Result<Option<ConversationMeta>, StorageError> {
|
||||||
|row| row.get(0),
|
let mut stmt = self.db.connection().prepare(
|
||||||
|
"SELECT local_convo_id, remote_convo_id, convo_type FROM conversations WHERE local_convo_id = ?1",
|
||||||
)?;
|
)?;
|
||||||
Ok(exists)
|
|
||||||
|
let result = stmt.query_row(params![local_convo_id], |row| {
|
||||||
|
let local_convo_id: String = row.get(0)?;
|
||||||
|
let remote_convo_id: String = row.get(1)?;
|
||||||
|
let convo_type: String = row.get(2)?;
|
||||||
|
Ok(ConversationMeta {
|
||||||
|
local_convo_id,
|
||||||
|
remote_convo_id,
|
||||||
|
kind: ConversationKind::from(convo_type.as_str()),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(meta) => Ok(Some(meta)),
|
||||||
|
Err(RusqliteError::QueryReturnedNoRows) => Ok(None),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes a conversation by its local ID.
|
/// Removes a conversation by its local ID.
|
||||||
#[allow(dead_code)]
|
fn remove_conversation(&mut self, local_convo_id: &str) -> Result<(), StorageError> {
|
||||||
pub fn remove_conversation(&mut self, local_convo_id: &str) -> Result<(), StorageError> {
|
|
||||||
self.db.connection().execute(
|
self.db.connection().execute(
|
||||||
"DELETE FROM conversations WHERE local_convo_id = ?1",
|
"DELETE FROM conversations WHERE local_convo_id = ?1",
|
||||||
params![local_convo_id],
|
params![local_convo_id],
|
||||||
@ -190,32 +205,8 @@ impl ChatStorage {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a single conversation record by its local ID.
|
|
||||||
pub fn load_conversation(
|
|
||||||
&self,
|
|
||||||
local_convo_id: &str,
|
|
||||||
) -> Result<Option<ConversationRecord>, StorageError> {
|
|
||||||
let mut stmt = self.db.connection().prepare(
|
|
||||||
"SELECT local_convo_id, remote_convo_id, convo_type FROM conversations WHERE local_convo_id = ?1",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let result = stmt.query_row(params![local_convo_id], |row| {
|
|
||||||
Ok(ConversationRecord {
|
|
||||||
local_convo_id: row.get(0)?,
|
|
||||||
remote_convo_id: row.get(1)?,
|
|
||||||
convo_type: row.get(2)?,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(record) => Ok(Some(record)),
|
|
||||||
Err(RusqliteError::QueryReturnedNoRows) => Ok(None),
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads all conversation records.
|
/// Loads all conversation records.
|
||||||
pub fn load_conversations(&self) -> Result<Vec<ConversationRecord>, StorageError> {
|
fn load_conversations(&self) -> Result<Vec<ConversationMeta>, StorageError> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.db
|
.db
|
||||||
.connection()
|
.connection()
|
||||||
@ -223,16 +214,29 @@ impl ChatStorage {
|
|||||||
|
|
||||||
let records = stmt
|
let records = stmt
|
||||||
.query_map([], |row| {
|
.query_map([], |row| {
|
||||||
Ok(ConversationRecord {
|
let local_convo_id: String = row.get(0)?;
|
||||||
local_convo_id: row.get(0)?,
|
let remote_convo_id: String = row.get(1)?;
|
||||||
remote_convo_id: row.get(1)?,
|
let convo_type: String = row.get(2)?;
|
||||||
convo_type: row.get(2)?,
|
Ok(ConversationMeta {
|
||||||
|
local_convo_id,
|
||||||
|
remote_convo_id,
|
||||||
|
kind: ConversationKind::from(convo_type.as_str()),
|
||||||
})
|
})
|
||||||
})?
|
})?
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
Ok(records)
|
Ok(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if a conversation exists by its local ID.
|
||||||
|
fn has_conversation(&self, local_convo_id: &str) -> Result<bool, StorageError> {
|
||||||
|
let exists: bool = self.db.connection().query_row(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM conversations WHERE local_convo_id = ?1)",
|
||||||
|
params![local_convo_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
Ok(exists)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -287,10 +291,18 @@ mod tests {
|
|||||||
|
|
||||||
// Save conversations
|
// Save conversations
|
||||||
storage
|
storage
|
||||||
.save_conversation("local_1", "remote_1", "private_v1")
|
.save_conversation(&ConversationMeta {
|
||||||
|
local_convo_id: "local_1".into(),
|
||||||
|
remote_convo_id: "remote_1".into(),
|
||||||
|
kind: ConversationKind::PrivateV1,
|
||||||
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
storage
|
storage
|
||||||
.save_conversation("local_2", "remote_2", "private_v1")
|
.save_conversation(&ConversationMeta {
|
||||||
|
local_convo_id: "local_2".into(),
|
||||||
|
remote_convo_id: "remote_2".into(),
|
||||||
|
kind: ConversationKind::PrivateV1,
|
||||||
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let convos = storage.load_conversations().unwrap();
|
let convos = storage.load_conversations().unwrap();
|
||||||
@ -302,6 +314,6 @@ mod tests {
|
|||||||
assert_eq!(convos.len(), 1);
|
assert_eq!(convos.len(), 1);
|
||||||
assert_eq!(convos[0].local_convo_id, "local_2");
|
assert_eq!(convos[0].local_convo_id, "local_2");
|
||||||
assert_eq!(convos[0].remote_convo_id, "remote_2");
|
assert_eq!(convos[0].remote_convo_id, "remote_2");
|
||||||
assert_eq!(convos[0].convo_type, "private_v1");
|
assert_eq!(convos[0].kind.as_str(), "private_v1");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -22,13 +22,6 @@ impl From<IdentityRecord> for Identity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ConversationRecord {
|
|
||||||
pub local_convo_id: String,
|
|
||||||
pub remote_convo_id: String,
|
|
||||||
pub convo_type: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -1,14 +1,18 @@
|
|||||||
use libchat::ChatError;
|
use libchat::ChatError;
|
||||||
|
use libchat::ChatStorage;
|
||||||
use libchat::Context;
|
use libchat::Context;
|
||||||
|
use libchat::StorageConfig;
|
||||||
|
|
||||||
pub struct ChatClient {
|
pub struct ChatClient {
|
||||||
ctx: Context,
|
ctx: Context<ChatStorage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatClient {
|
impl ChatClient {
|
||||||
pub fn new(name: impl Into<String>) -> Self {
|
pub fn new(name: impl Into<String>) -> Self {
|
||||||
|
let store =
|
||||||
|
ChatStorage::new(StorageConfig::InMemory).expect("in-memory storage should not fail");
|
||||||
Self {
|
Self {
|
||||||
ctx: Context::new_with_name(name),
|
ctx: Context::new_with_name(name, store),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user