mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-03-27 22:53:07 +00:00
feat: persist conversation store
This commit is contained in:
parent
3db9210ac3
commit
330059fd2d
@ -162,6 +162,12 @@ impl Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn add_convo(&mut self, convo: Box<dyn Convo>) -> ConversationIdOwned {
|
fn add_convo(&mut self, convo: Box<dyn Convo>) -> ConversationIdOwned {
|
||||||
|
// Persist conversation metadata to storage
|
||||||
|
let _ = self.storage.save_conversation(
|
||||||
|
convo.id(),
|
||||||
|
&convo.remote_id(),
|
||||||
|
convo.convo_type(),
|
||||||
|
);
|
||||||
self.store.insert_convo(convo)
|
self.store.insert_convo(convo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,4 +304,40 @@ mod tests {
|
|||||||
assert_eq!(content.data, b"hello after restart");
|
assert_eq!(content.data, b"hello after restart");
|
||||||
assert!(content.is_new_convo);
|
assert!(content.is_new_convo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conversation_metadata_persistence() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let db_path = dir
|
||||||
|
.path()
|
||||||
|
.join("test_convo_meta.db")
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
let config = StorageConfig::File(db_path);
|
||||||
|
|
||||||
|
// Create context, establish a conversation
|
||||||
|
let mut alice = Context::open("alice", config.clone()).unwrap();
|
||||||
|
let mut bob = Context::new_with_name("bob");
|
||||||
|
|
||||||
|
let bundle = alice.create_intro_bundle().unwrap();
|
||||||
|
let intro = Introduction::try_from(bundle.as_slice()).unwrap();
|
||||||
|
let (_, payloads) = bob.create_private_convo(&intro, b"hi");
|
||||||
|
|
||||||
|
let payload = payloads.first().unwrap();
|
||||||
|
let content = alice.handle_payload(&payload.data).unwrap().unwrap();
|
||||||
|
assert!(content.is_new_convo);
|
||||||
|
|
||||||
|
// Verify conversation metadata was persisted
|
||||||
|
let convos = alice.storage.load_conversations().unwrap();
|
||||||
|
assert_eq!(convos.len(), 1);
|
||||||
|
assert_eq!(convos[0].convo_type, "private_v1");
|
||||||
|
assert!(!convos[0].local_convo_id.is_empty());
|
||||||
|
assert!(!convos[0].remote_convo_id.is_empty());
|
||||||
|
|
||||||
|
// Drop and reopen - metadata should still be there
|
||||||
|
drop(alice);
|
||||||
|
let alice2 = Context::open("alice", config).unwrap();
|
||||||
|
let convos = alice2.storage.load_conversations().unwrap();
|
||||||
|
assert_eq!(convos.len(), 1, "conversation metadata should persist");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,9 @@ pub trait Convo: Id + Debug {
|
|||||||
) -> Result<Option<ContentData>, ChatError>;
|
) -> Result<Option<ContentData>, ChatError>;
|
||||||
|
|
||||||
fn remote_id(&self) -> String;
|
fn remote_id(&self) -> String;
|
||||||
|
|
||||||
|
/// Returns the conversation type identifier for storage.
|
||||||
|
fn convo_type(&self) -> &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConversationStore {
|
pub struct ConversationStore {
|
||||||
|
|||||||
@ -38,4 +38,8 @@ impl Convo for GroupTestConvo {
|
|||||||
fn remote_id(&self) -> String {
|
fn remote_id(&self) -> String {
|
||||||
self.id().to_string()
|
self.id().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convo_type(&self) -> &str {
|
||||||
|
"group_test"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -209,6 +209,10 @@ impl Convo for PrivateV1Convo {
|
|||||||
fn remote_id(&self) -> String {
|
fn remote_id(&self) -> String {
|
||||||
self.remote_convo_id.clone()
|
self.remote_convo_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convo_type(&self) -> &str {
|
||||||
|
"private_v1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for PrivateV1Convo {
|
impl Debug for PrivateV1Convo {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use storage::{RusqliteError, SqliteDb, StorageConfig, StorageError, params};
|
|||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use super::migrations;
|
use super::migrations;
|
||||||
use super::types::IdentityRecord;
|
use super::types::{ConversationRecord, IdentityRecord};
|
||||||
use crate::crypto::PrivateKey;
|
use crate::crypto::PrivateKey;
|
||||||
use crate::identity::Identity;
|
use crate::identity::Identity;
|
||||||
|
|
||||||
@ -110,6 +110,50 @@ impl ChatStorage {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Conversation Operations ====================
|
||||||
|
|
||||||
|
/// Saves conversation metadata.
|
||||||
|
pub fn save_conversation(
|
||||||
|
&mut self,
|
||||||
|
local_convo_id: &str,
|
||||||
|
remote_convo_id: &str,
|
||||||
|
convo_type: &str,
|
||||||
|
) -> Result<(), StorageError> {
|
||||||
|
self.db.connection().execute(
|
||||||
|
"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],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads all conversation records.
|
||||||
|
pub fn load_conversations(&self) -> Result<Vec<ConversationRecord>, StorageError> {
|
||||||
|
let mut stmt = self.db.connection().prepare(
|
||||||
|
"SELECT local_convo_id, remote_convo_id, convo_type FROM conversations",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let records = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
Ok(ConversationRecord {
|
||||||
|
local_convo_id: row.get(0)?,
|
||||||
|
remote_convo_id: row.get(1)?,
|
||||||
|
convo_type: row.get(2)?,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
Ok(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a conversation 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(())
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Identity Operations (continued) ====================
|
// ==================== Identity Operations (continued) ====================
|
||||||
|
|
||||||
/// Loads the identity if it exists.
|
/// Loads the identity if it exists.
|
||||||
@ -194,4 +238,32 @@ mod tests {
|
|||||||
storage.remove_ephemeral_key(&hex1).unwrap();
|
storage.remove_ephemeral_key(&hex1).unwrap();
|
||||||
assert!(storage.load_ephemeral_key(&hex1).unwrap().is_none());
|
assert!(storage.load_ephemeral_key(&hex1).unwrap().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_conversation_roundtrip() {
|
||||||
|
let mut storage = ChatStorage::new(StorageConfig::InMemory).unwrap();
|
||||||
|
|
||||||
|
// Initially empty
|
||||||
|
let convos = storage.load_conversations().unwrap();
|
||||||
|
assert!(convos.is_empty());
|
||||||
|
|
||||||
|
// Save conversations
|
||||||
|
storage
|
||||||
|
.save_conversation("local_1", "remote_1", "private_v1")
|
||||||
|
.unwrap();
|
||||||
|
storage
|
||||||
|
.save_conversation("local_2", "remote_2", "private_v1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let convos = storage.load_conversations().unwrap();
|
||||||
|
assert_eq!(convos.len(), 2);
|
||||||
|
|
||||||
|
// Remove one
|
||||||
|
storage.remove_conversation("local_1").unwrap();
|
||||||
|
let convos = storage.load_conversations().unwrap();
|
||||||
|
assert_eq!(convos.len(), 1);
|
||||||
|
assert_eq!(convos[0].local_convo_id, "local_2");
|
||||||
|
assert_eq!(convos[0].remote_convo_id, "remote_2");
|
||||||
|
assert_eq!(convos[0].convo_type, "private_v1");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,10 @@ pub fn get_migrations() -> Vec<(&'static str, &'static str)> {
|
|||||||
"002_ephemeral_keys",
|
"002_ephemeral_keys",
|
||||||
include_str!("migrations/002_ephemeral_keys.sql"),
|
include_str!("migrations/002_ephemeral_keys.sql"),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"003_conversations",
|
||||||
|
include_str!("migrations/003_conversations.sql"),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
-- Conversations metadata
|
||||||
|
-- Migration: 003_conversations
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS conversations (
|
||||||
|
local_convo_id TEXT PRIMARY KEY,
|
||||||
|
remote_convo_id TEXT NOT NULL,
|
||||||
|
convo_type TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
@ -22,6 +22,14 @@ impl From<IdentityRecord> for Identity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Record for storing conversation metadata.
|
||||||
|
#[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::*;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user