chore: further remove session

This commit is contained in:
kaichaosun 2026-02-05 14:12:21 +08:00
parent a14079807e
commit b2a1ca647b
No known key found for this signature in database
GPG Key ID: 223E0F992F4F03BF
11 changed files with 309 additions and 817 deletions

View File

@ -1,139 +0,0 @@
//! Example: Chat Session with Automatic Persistence
//!
//! This example demonstrates using ChatSession which automatically
//! persists all state changes to SQLite storage.
//!
//! Run with: cargo run -p logos-chat --example chat_session
use logos_chat::storage::ChatSession;
use tempfile::TempDir;
fn main() {
println!("=== Chat Session Example ===\n");
// Create temporary directories for databases
let alice_dir = TempDir::new().expect("Failed to create temp dir");
let bob_dir = TempDir::new().expect("Failed to create temp dir");
let alice_db = alice_dir.path().join("alice.db");
let bob_db = bob_dir.path().join("bob.db");
// =========================================
// Create sessions for Alice and Bob
// =========================================
println!("Step 1: Creating chat sessions...\n");
let mut alice = ChatSession::open_or_create(alice_db.to_str().unwrap(), "alice_secret_key")
.expect("Failed to create Alice's session");
println!(" Alice's session created");
println!(" Address: {}", &alice.local_address());
let mut bob = ChatSession::open_or_create(bob_db.to_str().unwrap(), "bob_secret_key")
.expect("Failed to create Bob's session");
println!(" Bob's session created");
println!(" Address: {}", &bob.local_address());
println!();
// =========================================
// Bob creates intro bundle
// =========================================
println!("Step 2: Bob creates introduction bundle...\n");
let bob_intro = bob
.create_intro_bundle()
.expect("Failed to create intro bundle");
println!(" Bob's intro bundle created");
println!(
" Installation key: {}",
&hex::encode(bob_intro.installation_key.as_bytes())
);
println!();
// =========================================
// Alice starts a chat with Bob
// =========================================
println!("Step 3: Alice starts a private chat with Bob...\n");
let (chat_id, envelopes) = alice
.start_private_chat(&bob_intro, "Hello Bob! 👋")
.expect("Failed to start chat");
println!(" Chat created: {}", chat_id);
println!(" Envelopes to deliver: {}", envelopes.len());
println!(" Chat automatically saved to storage!");
println!();
// =========================================
// Verify persistence by checking storage
// =========================================
println!("Step 4: Verifying persistence...\n");
// Check Alice's storage directly
let chat_record = alice
.storage()
.load_chat(&chat_id)
.expect("Failed to load chat");
if let Some(record) = chat_record {
println!(" Chat record found in storage:");
println!(" - ID: {}", record.chat_id);
println!(" - Type: {}", record.chat_type);
println!(" - Remote: {}", record.remote_address);
}
println!();
// =========================================
// Alice sends more messages
// =========================================
println!("Step 5: Alice sends more messages...\n");
let messages = [
"How are you?",
"Are you there?",
"Let me know when you're free!",
];
for msg in &messages {
let envelopes = alice
.send_message(&chat_id, msg.as_bytes())
.expect("Failed to send message");
println!(" Sent: \"{}\" ({} envelope(s))", msg, envelopes.len());
}
println!();
// =========================================
// Simulate app restart - reopen session
// =========================================
println!("Step 6: Simulating app restart...\n");
drop(alice); // Close Alice's session
println!(" Session closed. Reopening...\n");
let alice_restored = ChatSession::open(alice_db.to_str().unwrap(), "alice_secret_key")
.expect("Failed to reopen Alice's session");
println!(" Session restored!");
println!(" Address: {}", &alice_restored.local_address());
// Note: The chats list will be empty because we haven't implemented
// full chat restoration yet (which requires restoring ratchet states)
println!(" Chats in memory: {}", alice_restored.list_chats().len());
// But the chat metadata is persisted in storage
let stored_chats = alice_restored
.storage()
.list_chat_ids()
.expect("Failed to list chats");
println!(" Chats in storage: {:?}", stored_chats);
println!();
println!("=== Chat Session Example Complete ===\n");
println!("Key features demonstrated:");
println!(" ✓ Automatic identity persistence");
println!(" ✓ Automatic chat metadata persistence");
println!(" ✓ Session recovery after restart");
println!();
println!("TODO:");
println!(" - Full inbox key persistence");
println!(" - Ratchet state persistence (integration with double-ratchets storage)");
println!(" - Complete chat restoration on session open");
// Temp directories are automatically cleaned up when dropped
}

View File

@ -0,0 +1,135 @@
//! Example: Chat Flow with Automatic Persistence
//!
//! This example demonstrates the complete chat flow using ChatManager,
//! which automatically handles all storage operations.
//!
//! Run with: cargo run -p logos-chat --example chat_sesspersist_chat
use logos_chat::{ChatManager, StorageConfig};
use tempfile::TempDir;
fn main() {
println!("=== Chat Flow Example ===\n");
// Create temporary directories for databases
let alice_dir = TempDir::new().expect("Failed to create temp dir");
let bob_dir = TempDir::new().expect("Failed to create temp dir");
let alice_db = alice_dir.path().join("alice.db");
let bob_db = bob_dir.path().join("bob.db");
// =========================================
// Step 1: Create chat managers
// =========================================
println!("Step 1: Creating chat managers...\n");
// In production, use StorageConfig::Encrypted { path, key }
let mut alice = ChatManager::open(StorageConfig::File(alice_db.to_str().unwrap().to_string()))
.expect("Failed to create Alice's chat manager");
let mut bob = ChatManager::open(StorageConfig::File(bob_db.to_str().unwrap().to_string()))
.expect("Failed to create Bob's chat manager");
println!(" Alice's address: {}", alice.local_address());
println!(" Bob's address: {}", bob.local_address());
println!();
// =========================================
// Step 2: Bob creates intro bundle to share
// =========================================
println!("Step 2: Bob creates introduction bundle...\n");
let bob_intro = bob
.create_intro_bundle()
.expect("Failed to create intro bundle");
println!(" Bob shares his intro bundle with Alice");
println!(
" (Installation key: {})",
hex::encode(bob_intro.installation_key.as_bytes())
);
println!();
// =========================================
// Step 3: Alice starts a chat with Bob
// =========================================
println!("Step 3: Alice starts a private chat with Bob...\n");
let (chat_id, envelopes) = alice
.start_private_chat(&bob_intro, "Hello Bob! 👋")
.expect("Failed to start chat");
println!(" Chat created: {}", chat_id);
println!(" Envelopes to deliver: {}", envelopes.len());
println!(" (Chat automatically persisted to storage)");
println!();
// =========================================
// Step 4: Alice sends more messages
// =========================================
println!("Step 4: Alice sends more messages...\n");
let messages = ["How are you?", "Are you there?", ""];
for msg in &messages {
let envelopes = alice
.send_message(&chat_id, msg.as_bytes())
.expect("Failed to send message");
println!("\"{}\" ({} envelope)", msg, envelopes.len());
}
println!();
// =========================================
// Step 5: Verify persistence
// =========================================
println!("Step 5: Verifying persistence...\n");
println!(" Active chats in memory: {:?}", alice.list_chats());
println!(
" Chats persisted to storage: {:?}",
alice.list_stored_chats().unwrap()
);
println!();
// =========================================
// Step 6: Simulate app restart
// =========================================
println!("Step 6: Simulating app restart...\n");
let alice_address = alice.local_address();
drop(alice); // Close Alice's chat manager
println!(" Chat manager closed. Reopening...\n");
let alice_restored =
ChatManager::open(StorageConfig::File(alice_db.to_str().unwrap().to_string()))
.expect("Failed to reopen Alice's chat manager");
println!(" ✓ Chat manager restored!");
println!(
" ✓ Same address: {}",
alice_restored.local_address() == alice_address
);
println!(
" ✓ Stored chats: {:?}",
alice_restored.list_stored_chats().unwrap()
);
println!();
// =========================================
// Done!
// =========================================
println!("=== Example Complete ===\n");
println!("Key points:");
println!(" • ChatManager handles all storage internally");
println!(" • Identity is automatically created and persisted");
println!(" • Chats are automatically saved when created");
println!(" • State survives app restarts");
println!();
println!("For production, use encrypted storage:");
println!(" ChatManager::open(StorageConfig::Encrypted {{");
println!(" path: \"chat.db\".into(),");
println!(" key: \"user_encryption_key\".into(),");
println!(" }})");
// Temp directories are automatically cleaned up
}

View File

@ -1,145 +0,0 @@
//! Example: Persistent Chat with SQLite Storage
//!
//! This example demonstrates how to persist and restore chat state using
//! SQLite storage, so users can restart the app and continue their chats.
//!
//! Run with: cargo run -p logos-chat --example persistent_chat
use logos_chat::{
chat::ChatManager,
identity::Identity,
storage::{ChatStorage, ChatRecord},
};
use x25519_dalek::PublicKey;
fn main() {
println!("=== Persistent Chat Example ===\n");
// Use a temporary file for this example
let db_path = "/tmp/chat_example.db";
// Clean up from previous runs
let _ = std::fs::remove_file(db_path);
// =========================================
// Part 1: First Session - Create and save state
// =========================================
println!("--- Part 1: First Session ---\n");
{
// Open storage
let mut storage = ChatStorage::open(db_path)
.expect("Failed to open storage");
println!("1. Creating new identity...");
let alice = ChatManager::new();
let alice_address = alice.local_address();
println!(" Address: {}...{}", &alice_address[..8], &alice_address[alice_address.len()-8..]);
// Save identity to storage
// Note: In a real app, you'd access the identity from ChatManager
// For now, we'll create a separate identity to demonstrate storage
let identity = Identity::new();
storage.save_identity(&identity).expect("Failed to save identity");
println!(" Identity saved to database");
// Simulate creating some inbox keys
println!("\n2. Creating inbox keys...");
let secret1 = x25519_dalek::StaticSecret::random();
let pub1 = PublicKey::from(&secret1);
let pub1_hex = hex::encode(pub1.as_bytes());
storage.save_inbox_key(&pub1_hex, &secret1).expect("Failed to save inbox key");
println!(" Saved inbox key: {}...", &pub1_hex[..16]);
let secret2 = x25519_dalek::StaticSecret::random();
let pub2 = PublicKey::from(&secret2);
let pub2_hex = hex::encode(pub2.as_bytes());
storage.save_inbox_key(&pub2_hex, &secret2).expect("Failed to save inbox key");
println!(" Saved inbox key: {}...", &pub2_hex[..16]);
// Simulate creating some chats
println!("\n3. Creating chat records...");
let remote_key = PublicKey::from(&x25519_dalek::StaticSecret::random());
let chat1 = ChatRecord::new_private(
"chat_with_bob".to_string(),
remote_key,
"bob_delivery_addr".to_string(),
);
storage.save_chat(&chat1).expect("Failed to save chat");
println!(" Saved chat: {}", chat1.chat_id);
let remote_key2 = PublicKey::from(&x25519_dalek::StaticSecret::random());
let chat2 = ChatRecord::new_private(
"chat_with_carol".to_string(),
remote_key2,
"carol_delivery_addr".to_string(),
);
storage.save_chat(&chat2).expect("Failed to save chat");
println!(" Saved chat: {}", chat2.chat_id);
println!("\n First session complete. Closing database...");
}
// =========================================
// Part 2: Second Session - Restore state
// =========================================
println!("\n--- Part 2: Second Session (After Restart) ---\n");
{
// Reopen storage
let storage = ChatStorage::open(db_path)
.expect("Failed to open storage");
println!("1. Restoring identity...");
if let Some(identity) = storage.load_identity().expect("Failed to load identity") {
let address = identity.address();
println!(" Restored identity: {}...{}", &address[..8], &address[address.len()-8..]);
} else {
println!(" No identity found!");
}
println!("\n2. Restoring inbox keys...");
let inbox_keys = storage.load_all_inbox_keys().expect("Failed to load inbox keys");
println!(" Found {} inbox key(s)", inbox_keys.len());
for (pub_hex, _secret) in &inbox_keys {
println!(" - {}...", &pub_hex[..16]);
}
println!("\n3. Restoring chats...");
let chats = storage.load_all_chats().expect("Failed to load chats");
println!(" Found {} chat(s)", chats.len());
for chat in &chats {
println!(" - {} (type: {}, remote: {})",
chat.chat_id,
chat.chat_type,
chat.remote_address
);
}
// Demonstrate loading a specific chat
println!("\n4. Loading specific chat...");
if let Some(chat) = storage.load_chat("chat_with_bob").expect("Failed to load chat") {
println!(" Chat ID: {}", chat.chat_id);
println!(" Type: {}", chat.chat_type);
println!(" Remote Address: {}", chat.remote_address);
println!(" Created At: {}", chat.created_at);
}
// Demonstrate listing chat IDs
println!("\n5. Listing all chat IDs...");
let ids = storage.list_chat_ids().expect("Failed to list chat IDs");
for id in ids {
println!(" - {}", id);
}
}
// Cleanup
let _ = std::fs::remove_file(db_path);
println!("\n=== Persistent Chat Example Complete ===");
println!("\nNote: In a real application, you would:");
println!(" 1. Load identity on startup (or create new if none exists)");
println!(" 2. Restore inbox keys to handle incoming handshakes");
println!(" 3. Restore chat records and their associated ratchet states");
println!(" 4. Save state after each operation for durability");
}

View File

@ -1,23 +1,24 @@
//! Example: Ping-Pong Chat //! Example: Ping-Pong Chat
//! //!
//! This example demonstrates a back-and-forth conversation between two users. //! This example demonstrates a back-and-forth conversation between two users
//! Note: The handle_incoming implementation is currently stubbed, so this //! using in-memory storage (no persistence).
//! demonstrates the API flow rather than full encryption roundtrip.
//! //!
//! Run with: cargo run -p logos-chat --example ping_pong //! Run with: cargo run -p logos-chat --example ping_pong
use logos_chat::chat::ChatManager; use logos_chat::{ChatManager, StorageConfig};
fn main() { fn main() {
println!("=== Ping-Pong Chat Example ===\n"); println!("=== Ping-Pong Chat Example ===\n");
// Create two chat participants // Create two chat participants with in-memory storage
let mut alice = ChatManager::new(); let mut alice =
let mut bob = ChatManager::new(); ChatManager::open(StorageConfig::InMemory).expect("Failed to create Alice's chat manager");
let mut bob =
ChatManager::open(StorageConfig::InMemory).expect("Failed to create Bob's chat manager");
println!("Created participants:"); println!("Created participants:");
println!(" Alice: {}", &alice.local_address()); println!(" Alice: {}", alice.local_address());
println!(" Bob: {}", &bob.local_address()); println!(" Bob: {}", bob.local_address());
println!(); println!();
// Bob shares his intro bundle with Alice // Bob shares his intro bundle with Alice
@ -54,8 +55,8 @@ fn main() {
println!(); println!();
println!("Chat statistics:"); println!("Chat statistics:");
println!(" Alice's active chats: {}", alice.list_chats().len()); println!(" Alice's active chats: {:?}", alice.list_chats());
println!(" Bob's active chats: {}", bob.list_chats().len()); println!(" Bob's active chats: {:?}", bob.list_chats());
println!(); println!();
println!("=== Example Complete ==="); println!("=== Example Complete ===");

View File

@ -1,3 +1,8 @@
//! ChatManager with integrated SQLite persistence.
//!
//! This is the main entry point for the conversations API. It handles all
//! storage operations internally - users don't need to interact with storage directly.
use std::rc::Rc; use std::rc::Rc;
use crate::{ use crate::{
@ -5,63 +10,137 @@ use crate::{
errors::ChatError, errors::ChatError,
identity::Identity, identity::Identity,
inbox::{Inbox, Introduction}, inbox::{Inbox, Introduction},
storage::{ChatRecord, ChatStorage, StorageError},
types::{AddressedEnvelope, ContentData}, types::{AddressedEnvelope, ContentData},
}; };
/// Configuration for ChatManager storage.
pub enum StorageConfig {
/// In-memory storage (data lost on restart, useful for testing).
InMemory,
/// Unencrypted file storage (for development).
File(String),
/// Encrypted file storage (for production).
Encrypted { path: String, key: String },
}
/// Error type for ChatManager operations.
#[derive(Debug, thiserror::Error)]
pub enum ChatManagerError {
#[error("chat error: {0}")]
Chat(#[from] ChatError),
#[error("storage error: {0}")]
Storage(#[from] StorageError),
}
/// ChatManager is the main entry point for the conversations API. /// ChatManager is the main entry point for the conversations API.
/// It manages identity, inbox, and active chats.
/// ///
/// This is a pure Rust API - for FFI bindings, use `Context` which wraps this /// It manages identity, inbox, active chats, and automatically persists
/// with handle-based access. /// all state changes to SQLite storage.
///
/// # Example
///
/// ```ignore
/// // Create a new chat manager with encrypted storage
/// let mut chat = ChatManager::open(StorageConfig::Encrypted {
/// path: "chat.db".into(),
/// key: "my_secret_key".into(),
/// })?;
///
/// // Get your address to share with others
/// println!("My address: {}", chat.local_address());
///
/// // Create an intro bundle to share
/// let intro = chat.create_intro_bundle()?;
///
/// // Start a chat with someone
/// let (chat_id, envelopes) = chat.start_private_chat(&their_intro, "Hello!")?;
/// // Send envelopes over the network...
///
/// // Send more messages
/// let envelopes = chat.send_message(&chat_id, b"How are you?")?;
/// ```
pub struct ChatManager { pub struct ChatManager {
identity: Rc<Identity>, identity: Rc<Identity>,
store: ChatStore, store: ChatStore,
inbox: Inbox, inbox: Inbox,
storage: ChatStorage,
} }
impl ChatManager { impl ChatManager {
/// Create a new ChatManager with a fresh identity. /// Opens or creates a ChatManager with the given storage configuration.
pub fn new() -> Self { ///
let identity = Rc::new(Identity::new()); /// If an identity exists in storage, it will be restored.
let inbox = Inbox::new(Rc::clone(&identity)); /// Otherwise, a new identity will be created and saved.
Self { pub fn open(config: StorageConfig) -> Result<Self, ChatManagerError> {
identity, let mut storage = match config {
store: ChatStore::new(), StorageConfig::InMemory => ChatStorage::in_memory()?,
inbox, StorageConfig::File(path) => ChatStorage::open(&path)?,
} StorageConfig::Encrypted { path, key } => ChatStorage::new(&path, &key)?,
} };
// Load or create identity
let identity = if let Some(identity) = storage.load_identity()? {
identity
} else {
let identity = Identity::new();
storage.save_identity(&identity)?;
identity
};
/// Create a new ChatManager with an existing identity.
pub fn with_identity(identity: Identity) -> Self {
let identity = Rc::new(identity); let identity = Rc::new(identity);
let inbox = Inbox::new(Rc::clone(&identity)); let inbox = Inbox::new(Rc::clone(&identity));
Self {
// TODO: Restore inbox ephemeral keys from storage
// TODO: Restore active chats from storage
Ok(Self {
identity, identity,
store: ChatStore::new(), store: ChatStore::new(),
inbox, inbox,
} storage,
})
}
/// Creates a new in-memory ChatManager (for testing).
pub fn in_memory() -> Result<Self, ChatManagerError> {
Self::open(StorageConfig::InMemory)
} }
/// Get the local identity's public address. /// Get the local identity's public address.
///
/// This address can be shared with others so they can identify you.
pub fn local_address(&self) -> String { pub fn local_address(&self) -> String {
self.identity.address() self.identity.address()
} }
/// Create an introduction bundle that can be shared with others. /// Create an introduction bundle that can be shared with others.
/// They can use this to initiate a chat with you. ///
pub fn create_intro_bundle(&mut self) -> Result<Introduction, ChatError> { /// Others can use this bundle to initiate a chat with you.
/// Share it via QR code, link, or any other out-of-band method.
pub fn create_intro_bundle(&mut self) -> Result<Introduction, ChatManagerError> {
let pkb = self.inbox.create_bundle(); let pkb = self.inbox.create_bundle();
Ok(Introduction::from(pkb)) let intro = Introduction::from(pkb);
// Persist the ephemeral key
let public_key_hex = hex::encode(intro.ephemeral_key.as_bytes());
// TODO: Get the secret key from inbox and persist it
// self.storage.save_inbox_key(&public_key_hex, &secret)?;
let _ = public_key_hex; // Suppress unused warning for now
Ok(intro)
} }
/// Start a new private conversation with someone using their introduction bundle. /// Start a new private conversation with someone using their introduction bundle.
/// ///
/// Returns the chat ID and envelopes that must be delivered to the remote party. /// Returns the chat ID and envelopes that must be delivered to the remote party.
/// The chat state is automatically persisted.
pub fn start_private_chat( pub fn start_private_chat(
&mut self, &mut self,
remote_bundle: &Introduction, remote_bundle: &Introduction,
initial_message: &str, initial_message: &str,
) -> Result<(String, Vec<AddressedEnvelope>), ChatError> { ) -> Result<(String, Vec<AddressedEnvelope>), ChatManagerError> {
let (convo, payloads) = self let (convo, payloads) = self
.inbox .inbox
.invite_to_private_convo(remote_bundle, initial_message.to_string())?; .invite_to_private_convo(remote_bundle, initial_message.to_string())?;
@ -73,6 +152,15 @@ impl ChatManager {
.map(|p| p.to_envelope(chat_id.clone())) .map(|p| p.to_envelope(chat_id.clone()))
.collect(); .collect();
// Persist chat metadata
let chat_record = ChatRecord::new_private(
chat_id.clone(),
remote_bundle.installation_key,
"delivery_address".to_string(), // TODO: Get actual delivery address
);
self.storage.save_chat(&chat_record)?;
// Store in memory
self.store.insert_chat(convo); self.store.insert_chat(convo);
Ok((chat_id, envelopes)) Ok((chat_id, envelopes))
@ -81,11 +169,12 @@ impl ChatManager {
/// Send a message to an existing chat. /// Send a message to an existing chat.
/// ///
/// Returns envelopes that must be delivered to chat participants. /// Returns envelopes that must be delivered to chat participants.
/// The updated chat state is automatically persisted.
pub fn send_message( pub fn send_message(
&mut self, &mut self,
chat_id: &str, chat_id: &str,
content: &[u8], content: &[u8],
) -> Result<Vec<AddressedEnvelope>, ChatError> { ) -> Result<Vec<AddressedEnvelope>, ChatManagerError> {
let chat = self let chat = self
.store .store
.get_mut_chat(chat_id) .get_mut_chat(chat_id)
@ -93,6 +182,8 @@ impl ChatManager {
let payloads = chat.send_message(content)?; let payloads = chat.send_message(content)?;
// TODO: Persist updated ratchet state
Ok(payloads Ok(payloads
.into_iter() .into_iter()
.map(|p| p.to_envelope(chat.remote_id())) .map(|p| p.to_envelope(chat.remote_id()))
@ -102,11 +193,13 @@ impl ChatManager {
/// Handle an incoming payload from the network. /// Handle an incoming payload from the network.
/// ///
/// Returns the decrypted content if successful. /// Returns the decrypted content if successful.
pub fn handle_incoming(&mut self, _payload: &[u8]) -> Result<ContentData, ChatError> { /// Any new chats or state changes are automatically persisted.
pub fn handle_incoming(&mut self, _payload: &[u8]) -> Result<ContentData, ChatManagerError> {
// TODO: Implement proper payload handling // TODO: Implement proper payload handling
// 1. Determine if this is an inbox message or a chat message // 1. Determine if this is an inbox message or a chat message
// 2. Route to appropriate handler // 2. Route to appropriate handler
// 3. Return decrypted content // 3. Persist any state changes
// 4. Return decrypted content
Ok(ContentData { Ok(ContentData {
conversation_id: "convo_id".into(), conversation_id: "convo_id".into(),
data: vec![1, 2, 3, 4, 5, 6], data: vec![1, 2, 3, 4, 5, 6],
@ -118,20 +211,14 @@ impl ChatManager {
self.store.get_chat(chat_id) self.store.get_chat(chat_id)
} }
/// Get a mutable reference to an active chat.
pub fn get_chat_mut(&mut self, chat_id: &str) -> Option<&mut dyn Chat> {
self.store.get_mut_chat(chat_id)
}
/// List all active chat IDs. /// List all active chat IDs.
pub fn list_chats(&self) -> Vec<String> { pub fn list_chats(&self) -> Vec<String> {
self.store.chat_ids().map(|id| id.to_string()).collect() self.store.chat_ids().map(|id| id.to_string()).collect()
} }
}
impl Default for ChatManager { /// List all chat IDs from storage (includes chats not yet loaded into memory).
fn default() -> Self { pub fn list_stored_chats(&self) -> Result<Vec<String>, ChatManagerError> {
Self::new() Ok(self.storage.list_chat_ids()?)
} }
} }
@ -141,21 +228,32 @@ mod tests {
#[test] #[test]
fn test_create_chat_manager() { fn test_create_chat_manager() {
let manager = ChatManager::new(); let manager = ChatManager::in_memory().unwrap();
assert!(!manager.local_address().is_empty()); assert!(!manager.local_address().is_empty());
} }
#[test]
fn test_identity_persistence() {
let manager = ChatManager::in_memory().unwrap();
let address = manager.local_address();
// Identity should be persisted
let loaded = manager.storage.load_identity().unwrap();
assert!(loaded.is_some());
assert_eq!(loaded.unwrap().address(), address);
}
#[test] #[test]
fn test_create_intro_bundle() { fn test_create_intro_bundle() {
let mut manager = ChatManager::new(); let mut manager = ChatManager::in_memory().unwrap();
let bundle = manager.create_intro_bundle(); let bundle = manager.create_intro_bundle();
assert!(bundle.is_ok()); assert!(bundle.is_ok());
} }
#[test] #[test]
fn test_start_private_chat() { fn test_start_private_chat() {
let mut alice = ChatManager::new(); let mut alice = ChatManager::in_memory().unwrap();
let mut bob = ChatManager::new(); let mut bob = ChatManager::in_memory().unwrap();
// Bob creates an intro bundle // Bob creates an intro bundle
let bob_intro = bob.create_intro_bundle().unwrap(); let bob_intro = bob.create_intro_bundle().unwrap();
@ -167,5 +265,9 @@ mod tests {
let (chat_id, envelopes) = result.unwrap(); let (chat_id, envelopes) = result.unwrap();
assert!(!chat_id.is_empty()); assert!(!chat_id.is_empty());
assert!(!envelopes.is_empty()); assert!(!envelopes.is_empty());
// Chat should be persisted
let stored = alice.list_stored_chats().unwrap();
assert!(stored.contains(&chat_id));
} }
} }

View File

@ -1,11 +1,11 @@
//! FFI-oriented context that wraps ChatManager with handle-based access. //! FFI-oriented context that wraps ChatManager with handle-based access.
//! //!
//! For pure Rust usage, prefer using `ChatManager` directly from `chat.rs`. //! For pure Rust usage, prefer using `ChatManager` directly.
use std::collections::HashMap; use std::collections::HashMap;
use crate::{ use crate::{
chat::ChatManager, chat::{ChatManager, ChatManagerError, StorageConfig},
errors::ChatError, errors::ChatError,
types::{AddressedEnvelope, ContentData}, types::{AddressedEnvelope, ContentData},
}; };
@ -36,8 +36,10 @@ pub struct Context {
impl Context { impl Context {
pub fn new() -> Self { pub fn new() -> Self {
let manager = ChatManager::open(StorageConfig::InMemory)
.expect("Failed to create in-memory ChatManager");
Self { Self {
manager: ChatManager::new(), manager,
handle_to_chat_id: HashMap::new(), handle_to_chat_id: HashMap::new(),
chat_id_to_handle: HashMap::new(), chat_id_to_handle: HashMap::new(),
next_handle: INITIAL_CONVO_HANDLE, next_handle: INITIAL_CONVO_HANDLE,
@ -65,7 +67,7 @@ impl Context {
} }
/// Create an introduction bundle for sharing with other users. /// Create an introduction bundle for sharing with other users.
pub fn create_intro_bundle(&mut self) -> Result<Vec<u8>, ChatError> { pub fn create_intro_bundle(&mut self) -> Result<Vec<u8>, ChatManagerError> {
let intro = self.manager.create_intro_bundle()?; let intro = self.manager.create_intro_bundle()?;
Ok(intro.into()) Ok(intro.into())
} }
@ -92,7 +94,7 @@ impl Context {
&mut self, &mut self,
convo_handle: ConvoHandle, convo_handle: ConvoHandle,
content: &[u8], content: &[u8],
) -> Result<Vec<AddressedEnvelope>, ChatError> { ) -> Result<Vec<AddressedEnvelope>, ChatManagerError> {
let chat_id = self.resolve_handle(convo_handle)?; let chat_id = self.resolve_handle(convo_handle)?;
self.manager.send_message(&chat_id, content) self.manager.send_message(&chat_id, content)
} }
@ -131,11 +133,11 @@ impl Context {
} }
/// Resolve a handle to its chat ID. /// Resolve a handle to its chat ID.
fn resolve_handle(&self, handle: ConvoHandle) -> Result<String, ChatError> { fn resolve_handle(&self, handle: ConvoHandle) -> Result<String, ChatManagerError> {
self.handle_to_chat_id self.handle_to_chat_id
.get(&handle) .get(&handle)
.cloned() .cloned()
.ok_or_else(|| ChatError::NoConvo(handle)) .ok_or_else(|| ChatError::NoConvo(handle).into())
} }
} }

View File

@ -1,17 +1,21 @@
pub mod chat;
pub mod common; pub mod common;
pub mod dm; pub mod dm;
pub mod ffi; pub mod ffi;
pub mod group; pub mod group;
pub mod identity; pub mod identity;
pub mod inbox; pub mod inbox;
pub mod storage;
mod chat;
mod errors; mod errors;
mod proto; mod proto;
mod storage;
mod types; mod types;
mod utils; mod utils;
// Public API - this is what library users should use
pub use chat::{ChatManager, ChatManagerError, StorageConfig};
pub use inbox::Introduction;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@ -1,9 +1,6 @@
//! Chat-specific storage implementation. //! Chat-specific storage implementation.
use std::collections::HashMap;
use storage::{RusqliteError, SqliteDb, StorageError, params}; use storage::{RusqliteError, SqliteDb, StorageError, params};
use x25519_dalek::StaticSecret;
use super::types::{ChatRecord, IdentityRecord}; use super::types::{ChatRecord, IdentityRecord};
use crate::identity::Identity; use crate::identity::Identity;
@ -105,93 +102,6 @@ impl ChatStorage {
} }
} }
/// Checks if an identity exists.
pub fn has_identity(&self) -> Result<bool, StorageError> {
let count: i64 =
self.db
.connection()
.query_row("SELECT COUNT(*) FROM identity", [], |row| row.get(0))?;
Ok(count > 0)
}
// ==================== Inbox Key Operations ====================
/// Saves an inbox ephemeral key.
pub fn save_inbox_key(
&mut self,
public_key_hex: &str,
secret: &StaticSecret,
) -> Result<(), StorageError> {
self.db.connection().execute(
"INSERT OR REPLACE INTO inbox_keys (public_key_hex, secret_key) VALUES (?1, ?2)",
params![public_key_hex, secret.to_bytes().as_slice()],
)?;
Ok(())
}
/// Loads an inbox ephemeral key by its public key hex.
pub fn load_inbox_key(
&self,
public_key_hex: &str,
) -> Result<Option<StaticSecret>, StorageError> {
let mut stmt = self
.db
.connection()
.prepare("SELECT secret_key FROM inbox_keys WHERE public_key_hex = ?1")?;
let result = stmt.query_row(params![public_key_hex], |row| {
let secret_key: Vec<u8> = row.get(0)?;
Ok(secret_key)
});
match result {
Ok(secret_key) => {
let bytes: [u8; 32] = secret_key
.try_into()
.map_err(|_| StorageError::InvalidData("Invalid secret key length".into()))?;
Ok(Some(StaticSecret::from(bytes)))
}
Err(RusqliteError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Loads all inbox ephemeral keys.
pub fn load_all_inbox_keys(&self) -> Result<HashMap<String, StaticSecret>, StorageError> {
let mut stmt = self
.db
.connection()
.prepare("SELECT public_key_hex, secret_key FROM inbox_keys")?;
let rows = stmt.query_map([], |row| {
let public_key_hex: String = row.get(0)?;
let secret_key: Vec<u8> = row.get(1)?;
Ok((public_key_hex, secret_key))
})?;
let mut keys = HashMap::new();
for row in rows {
let (public_key_hex, secret_key) = row?;
let bytes: [u8; 32] = secret_key
.try_into()
.map_err(|_| StorageError::InvalidData("Invalid secret key length".into()))?;
keys.insert(public_key_hex, StaticSecret::from(bytes));
}
Ok(keys)
}
/// Deletes an inbox ephemeral key (after it's been used).
pub fn delete_inbox_key(&mut self, public_key_hex: &str) -> Result<(), StorageError> {
self.db.connection().execute(
"DELETE FROM inbox_keys WHERE public_key_hex = ?1",
params![public_key_hex],
)?;
Ok(())
}
// ==================== Chat Operations ====================
/// Saves a chat record. /// Saves a chat record.
pub fn save_chat(&mut self, chat: &ChatRecord) -> Result<(), StorageError> { pub fn save_chat(&mut self, chat: &ChatRecord) -> Result<(), StorageError> {
self.db.connection().execute( self.db.connection().execute(
@ -208,64 +118,6 @@ impl ChatStorage {
Ok(()) Ok(())
} }
/// Loads a chat record by ID.
pub fn load_chat(&self, chat_id: &str) -> Result<Option<ChatRecord>, StorageError> {
let mut stmt = self.db.connection().prepare(
"SELECT chat_id, chat_type, remote_public_key, remote_address, created_at
FROM chats WHERE chat_id = ?1",
)?;
let result = stmt.query_row(params![chat_id], |row| {
let remote_public_key: Option<Vec<u8>> = row.get(2)?;
Ok(ChatRecord {
chat_id: row.get(0)?,
chat_type: row.get(1)?,
remote_public_key: remote_public_key.and_then(|v| v.try_into().ok()),
remote_address: row.get(3)?,
created_at: row.get(4)?,
})
});
match result {
Ok(record) => Ok(Some(record)),
Err(RusqliteError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Loads all chat records.
pub fn load_all_chats(&self) -> Result<Vec<ChatRecord>, StorageError> {
let mut stmt = self.db.connection().prepare(
"SELECT chat_id, chat_type, remote_public_key, remote_address, created_at FROM chats",
)?;
let rows = stmt.query_map([], |row| {
let remote_public_key: Option<Vec<u8>> = row.get(2)?;
Ok(ChatRecord {
chat_id: row.get(0)?,
chat_type: row.get(1)?,
remote_public_key: remote_public_key.and_then(|v| v.try_into().ok()),
remote_address: row.get(3)?,
created_at: row.get(4)?,
})
})?;
let mut chats = Vec::new();
for row in rows {
chats.push(row?);
}
Ok(chats)
}
/// Deletes a chat record.
pub fn delete_chat(&mut self, chat_id: &str) -> Result<(), StorageError> {
self.db
.connection()
.execute("DELETE FROM chats WHERE chat_id = ?1", params![chat_id])?;
Ok(())
}
/// Lists all chat IDs. /// Lists all chat IDs.
pub fn list_chat_ids(&self) -> Result<Vec<String>, StorageError> { pub fn list_chat_ids(&self) -> Result<Vec<String>, StorageError> {
let mut stmt = self.db.connection().prepare("SELECT chat_id FROM chats")?; let mut stmt = self.db.connection().prepare("SELECT chat_id FROM chats")?;
@ -278,16 +130,6 @@ impl ChatStorage {
Ok(ids) Ok(ids)
} }
/// Checks if a chat exists.
pub fn chat_exists(&self, chat_id: &str) -> Result<bool, StorageError> {
let count: i64 = self.db.connection().query_row(
"SELECT COUNT(*) FROM chats WHERE chat_id = ?1",
params![chat_id],
|row| row.get(0),
)?;
Ok(count > 0)
}
} }
#[cfg(test)] #[cfg(test)]
@ -299,7 +141,6 @@ mod tests {
let mut storage = ChatStorage::in_memory().unwrap(); let mut storage = ChatStorage::in_memory().unwrap();
// Initially no identity // Initially no identity
assert!(!storage.has_identity().unwrap());
assert!(storage.load_identity().unwrap().is_none()); assert!(storage.load_identity().unwrap().is_none());
// Save identity // Save identity
@ -308,44 +149,16 @@ mod tests {
storage.save_identity(&identity).unwrap(); storage.save_identity(&identity).unwrap();
// Load identity // Load identity
assert!(storage.has_identity().unwrap());
let loaded = storage.load_identity().unwrap().unwrap(); let loaded = storage.load_identity().unwrap().unwrap();
assert_eq!(loaded.address(), address); assert_eq!(loaded.address(), address);
} }
#[test]
fn test_inbox_key_roundtrip() {
let mut storage = ChatStorage::in_memory().unwrap();
let secret = StaticSecret::random();
let public_key = x25519_dalek::PublicKey::from(&secret);
let public_key_hex = hex::encode(public_key.as_bytes());
// Save key
storage.save_inbox_key(&public_key_hex, &secret).unwrap();
// Load key
let loaded = storage.load_inbox_key(&public_key_hex).unwrap().unwrap();
assert_eq!(
x25519_dalek::PublicKey::from(&loaded).as_bytes(),
public_key.as_bytes()
);
// Load all keys
let all_keys = storage.load_all_inbox_keys().unwrap();
assert_eq!(all_keys.len(), 1);
assert!(all_keys.contains_key(&public_key_hex));
// Delete key
storage.delete_inbox_key(&public_key_hex).unwrap();
assert!(storage.load_inbox_key(&public_key_hex).unwrap().is_none());
}
#[test] #[test]
fn test_chat_roundtrip() { fn test_chat_roundtrip() {
let mut storage = ChatStorage::in_memory().unwrap(); let mut storage = ChatStorage::in_memory().unwrap();
let remote_key = x25519_dalek::PublicKey::from(&StaticSecret::random()); let secret = x25519_dalek::StaticSecret::random();
let remote_key = x25519_dalek::PublicKey::from(&secret);
let chat = ChatRecord::new_private( let chat = ChatRecord::new_private(
"chat_123".to_string(), "chat_123".to_string(),
remote_key, remote_key,
@ -355,18 +168,8 @@ mod tests {
// Save chat // Save chat
storage.save_chat(&chat).unwrap(); storage.save_chat(&chat).unwrap();
// Load chat
let loaded = storage.load_chat("chat_123").unwrap().unwrap();
assert_eq!(loaded.chat_id, "chat_123");
assert_eq!(loaded.chat_type, "private_v1");
assert_eq!(loaded.remote_address, "delivery_addr");
// List chats // List chats
let ids = storage.list_chat_ids().unwrap(); let ids = storage.list_chat_ids().unwrap();
assert_eq!(ids, vec!["chat_123"]); assert_eq!(ids, vec!["chat_123"]);
// Delete chat
storage.delete_chat("chat_123").unwrap();
assert!(!storage.chat_exists("chat_123").unwrap());
} }
} }

View File

@ -2,12 +2,13 @@
//! //!
//! This module provides storage implementations for the chat manager state, //! This module provides storage implementations for the chat manager state,
//! built on top of the shared `storage` crate. //! built on top of the shared `storage` crate.
//!
//! Note: This module is internal. Users should use `ChatManager` which
//! handles all storage operations automatically.
mod db; mod db;
mod session;
mod types; mod types;
pub use db::ChatStorage; pub(crate) use db::ChatStorage;
pub use session::{ChatSession, SessionError}; pub(crate) use storage::StorageError;
pub use storage::{SqliteDb, StorageConfig, StorageError}; pub(crate) use types::ChatRecord;
pub use types::{ChatRecord, IdentityRecord, InboxKeyRecord};

View File

@ -1,250 +0,0 @@
//! Session wrapper for automatic state persistence.
//!
//! Provides a `ChatSession` that wraps `ChatManager` and automatically
//! persists state changes to SQLite storage.
use crate::{
chat::ChatManager,
errors::ChatError,
identity::Identity,
inbox::Introduction,
storage::{ChatRecord, ChatStorage, StorageError},
types::{AddressedEnvelope, ContentData},
};
/// Error type for chat session operations.
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
#[error("chat error: {0}")]
Chat(#[from] ChatError),
#[error("storage error: {0}")]
Storage(#[from] StorageError),
#[error("session already exists for this identity")]
SessionExists,
}
/// A persistent chat session that automatically saves state to storage.
///
/// This wraps a `ChatManager` and ensures all state changes are persisted
/// to SQLite storage. When reopened, the session restores the previous state.
///
/// # Example
///
/// ```ignore
/// // Create a new session (or open existing)
/// let mut session = ChatSession::open_or_create("chat.db", "encryption_key")?;
///
/// // Create intro bundle (automatically persisted)
/// let intro = session.create_intro_bundle()?;
///
/// // Start a chat (automatically persisted)
/// let (chat_id, envelopes) = session.start_private_chat(&remote_intro, "Hello!")?;
///
/// // Later, reopen the session
/// let mut session = ChatSession::open("chat.db", "encryption_key")?;
/// // Previous identity and chats are restored
/// ```
pub struct ChatSession {
manager: ChatManager,
storage: ChatStorage,
}
impl ChatSession {
/// Opens an existing session from storage.
///
/// Returns an error if no identity exists in the storage.
pub fn open(path: &str, key: &str) -> Result<Self, SessionError> {
let storage = ChatStorage::new(path, key)?;
let identity = storage
.load_identity()?
.ok_or_else(|| SessionError::Storage(StorageError::NotFound("identity".into())))?;
let manager = ChatManager::with_identity(identity);
// TODO: Restore inbox ephemeral keys
// TODO: Restore active chats
Ok(Self { manager, storage })
}
/// Creates a new session with a fresh identity.
///
/// Returns an error if an identity already exists in the storage.
pub fn create(path: &str, key: &str) -> Result<Self, SessionError> {
let mut storage = ChatStorage::new(path, key)?;
if storage.has_identity()? {
return Err(SessionError::SessionExists);
}
let identity = Identity::new();
storage.save_identity(&identity)?;
let manager = ChatManager::with_identity(identity);
Ok(Self { manager, storage })
}
/// Opens an existing session or creates a new one if none exists.
pub fn open_or_create(path: &str, key: &str) -> Result<Self, SessionError> {
let mut storage = ChatStorage::new(path, key)?;
let identity = if let Some(identity) = storage.load_identity()? {
identity
} else {
let identity = Identity::new();
storage.save_identity(&identity)?;
identity
};
let manager = ChatManager::with_identity(identity);
// TODO: Restore inbox ephemeral keys and active chats
Ok(Self { manager, storage })
}
/// Creates an in-memory session (useful for testing).
pub fn in_memory() -> Result<Self, SessionError> {
let mut storage = ChatStorage::in_memory()?;
let identity = Identity::new();
storage.save_identity(&identity)?;
let manager = ChatManager::with_identity(identity);
Ok(Self { manager, storage })
}
/// Get the local identity's public address.
pub fn local_address(&self) -> String {
self.manager.local_address()
}
/// Create an introduction bundle that can be shared with others.
///
/// The ephemeral key is automatically persisted.
pub fn create_intro_bundle(&mut self) -> Result<Introduction, SessionError> {
let intro = self.manager.create_intro_bundle()?;
// Persist the ephemeral key
let _public_key_hex = hex::encode(intro.ephemeral_key.as_bytes());
// TODO: Get the secret key from inbox and persist it
// self.storage.save_inbox_key(&public_key_hex, &secret)?;
Ok(intro)
}
/// Start a new private conversation with someone using their introduction bundle.
///
/// The chat state is automatically persisted.
pub fn start_private_chat(
&mut self,
remote_bundle: &Introduction,
initial_message: &str,
) -> Result<(String, Vec<AddressedEnvelope>), SessionError> {
let (chat_id, envelopes) = self.manager.start_private_chat(remote_bundle, initial_message)?;
// Persist chat metadata
let chat_record = ChatRecord::new_private(
chat_id.clone(),
remote_bundle.installation_key,
"delivery_address".to_string(), // TODO: Get actual delivery address
);
self.storage.save_chat(&chat_record)?;
Ok((chat_id, envelopes))
}
/// Send a message to an existing chat.
///
/// The updated chat state is automatically persisted.
pub fn send_message(
&mut self,
chat_id: &str,
content: &[u8],
) -> Result<Vec<AddressedEnvelope>, SessionError> {
let envelopes = self.manager.send_message(chat_id, content)?;
// TODO: Persist updated ratchet state
Ok(envelopes)
}
/// Handle an incoming payload from the network.
pub fn handle_incoming(&mut self, payload: &[u8]) -> Result<ContentData, SessionError> {
let content = self.manager.handle_incoming(payload)?;
// TODO: Persist updated state
Ok(content)
}
/// List all active chat IDs.
pub fn list_chats(&self) -> Vec<String> {
self.manager.list_chats()
}
/// Get access to the underlying ChatManager.
pub fn manager(&self) -> &ChatManager {
&self.manager
}
/// Get mutable access to the underlying ChatManager.
pub fn manager_mut(&mut self) -> &mut ChatManager {
&mut self.manager
}
/// Get access to the underlying storage.
pub fn storage(&self) -> &ChatStorage {
&self.storage
}
/// Get mutable access to the underlying storage.
pub fn storage_mut(&mut self) -> &mut ChatStorage {
&mut self.storage
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_session() {
let session = ChatSession::in_memory().unwrap();
assert!(!session.local_address().is_empty());
}
#[test]
fn test_session_persistence() {
// Create a session with in-memory storage
let session = ChatSession::in_memory().unwrap();
let address = session.local_address();
// Verify identity was saved
let loaded_identity = session.storage.load_identity().unwrap();
assert!(loaded_identity.is_some());
assert_eq!(loaded_identity.unwrap().address(), address);
}
#[test]
fn test_start_chat_persists() {
let mut alice = ChatSession::in_memory().unwrap();
let mut bob = ChatSession::in_memory().unwrap();
// Bob creates intro bundle
let bob_intro = bob.create_intro_bundle().unwrap();
// Alice starts a chat
let (chat_id, _envelopes) = alice
.start_private_chat(&bob_intro, "Hello!")
.unwrap();
// Verify chat was persisted
let chat_record = alice.storage.load_chat(&chat_id).unwrap();
assert!(chat_record.is_some());
assert_eq!(chat_record.unwrap().chat_type, "private_v1");
}
}

View File

@ -26,28 +26,6 @@ impl From<IdentityRecord> for Identity {
} }
} }
/// Record for storing inbox ephemeral keys.
#[derive(Debug)]
pub struct InboxKeyRecord {
/// Hex-encoded public key (used as identifier).
pub public_key_hex: String,
/// The secret key bytes (32 bytes).
pub secret_key: [u8; 32],
}
impl InboxKeyRecord {
pub fn new(public_key_hex: String, secret: &StaticSecret) -> Self {
Self {
public_key_hex,
secret_key: secret.to_bytes(),
}
}
pub fn into_secret(self) -> StaticSecret {
StaticSecret::from(self.secret_key)
}
}
/// Record for storing chat metadata. /// Record for storing chat metadata.
/// Note: The actual double ratchet state is stored separately by the DR storage. /// Note: The actual double ratchet state is stored separately by the DR storage.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]