feat: persist identity

This commit is contained in:
kaichaosun 2026-02-27 14:32:47 +08:00
parent f4c08bd048
commit 37eb2749b2
No known key found for this signature in database
GPG Key ID: 223E0F992F4F03BF
2 changed files with 93 additions and 5 deletions

View File

@ -13,6 +13,8 @@ use safer_ffi::{
prelude::{c_slice, repr_c},
};
use storage::StorageConfig;
use crate::{
context::{Context, Introduction},
errors::ChatError,
@ -54,6 +56,26 @@ pub fn create_context(name: repr_c::String) -> repr_c::Box<ContextHandle> {
Box::new(ContextHandle(Context::new_with_name(&*name))).into()
}
/// Creates a new libchat Context with file-based persistent storage.
///
/// The identity will be loaded from storage if it exists, or created and saved if not.
///
/// # Parameters
/// - name: Friendly name for the identity (used if creating new identity)
/// - db_path: Path to the SQLite database file
///
/// # Returns
/// Opaque handle to the context. Must be freed with destroy_context()
#[ffi_export]
pub fn create_context_with_storage(
name: repr_c::String,
db_path: repr_c::String,
) -> repr_c::Box<ContextHandle> {
let config = StorageConfig::File(db_path.to_string());
let ctx = Context::open(&*name, config).expect("failed to open context with storage");
Box::new(ContextHandle(ctx)).into()
}
/// Returns the friendly name of the contexts installation.
///
#[ffi_export]

View File

@ -1,34 +1,73 @@
use std::rc::Rc;
use storage::StorageConfig;
use crate::{
conversation::{ConversationId, ConversationStore, Convo, Id},
errors::ChatError,
identity::Identity,
inbox::Inbox,
proto::{EncryptedPayload, EnvelopeV1, Message},
storage::{ChatStorage, StorageError},
types::{AddressedEnvelope, ContentData},
};
pub use crate::conversation::ConversationIdOwned;
pub use crate::inbox::Introduction;
/// Error type for Context operations.
#[derive(Debug, thiserror::Error)]
pub enum ContextError {
#[error("chat error: {0}")]
Chat(#[from] ChatError),
#[error("storage error: {0}")]
Storage(#[from] StorageError),
}
// This is the main entry point to the conversations api.
// Ctx manages lifetimes of objects to process and generate payloads.
pub struct Context {
_identity: Rc<Identity>,
store: ConversationStore,
inbox: Inbox,
storage: ChatStorage,
}
impl Context {
pub fn new_with_name(name: impl Into<String>) -> Self {
let identity = Rc::new(Identity::new(name));
let inbox = Inbox::new(Rc::clone(&identity)); //
Self {
/// Opens or creates a Context with the given storage configuration.
///
/// If an identity exists in storage, it will be restored.
/// Otherwise, a new identity will be created with the given name and saved.
pub fn open(name: impl Into<String>, config: StorageConfig) -> Result<Self, ContextError> {
let mut storage = ChatStorage::new(config)?;
let name = name.into();
// Load or create identity
let identity = if let Some(identity) = storage.load_identity()? {
identity
} else {
let identity = Identity::new(&name);
storage.save_identity(&identity)?;
identity
};
let identity = Rc::new(identity);
let inbox = Inbox::new(Rc::clone(&identity));
Ok(Self {
_identity: identity,
store: ConversationStore::new(),
inbox,
}
storage,
})
}
/// Creates a new in-memory Context (for testing).
///
/// Uses in-memory SQLite database. Each call creates a new isolated database.
pub fn new_with_name(name: impl Into<String>) -> Self {
Self::open(name, StorageConfig::InMemory).expect("in-memory storage should not fail")
}
pub fn installation_name(&self) -> &str {
@ -195,4 +234,31 @@ mod tests {
send_and_verify(&mut saro, &mut raya, &saro_convo_id, &content);
}
}
#[test]
fn identity_persistence() {
// Use file-based storage to test real persistence
let dir = tempfile::tempdir().unwrap();
let db_path = dir
.path()
.join("test_identity.db")
.to_string_lossy()
.to_string();
let config = StorageConfig::File(db_path);
// Create context - this should create and save a new identity
let ctx1 = Context::open("alice", config.clone()).unwrap();
let pubkey1 = ctx1._identity.public_key();
let name1 = ctx1.installation_name().to_string();
// Drop and reopen - should load the same identity
drop(ctx1);
let ctx2 = Context::open("alice", config).unwrap();
let pubkey2 = ctx2._identity.public_key();
let name2 = ctx2.installation_name().to_string();
// Identity should be the same
assert_eq!(pubkey1, pubkey2, "public key should persist");
assert_eq!(name1, name2, "name should persist");
}
}