mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-03-27 06:33:08 +00:00
wip
This commit is contained in:
parent
9a94f9a6d6
commit
6db3363aad
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -124,6 +124,7 @@ name = "client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libchat",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -12,8 +12,10 @@ mod types;
|
||||
mod utils;
|
||||
|
||||
pub use api::*;
|
||||
pub use context::{Context, Introduction};
|
||||
pub use context::{Context, ConversationIdOwned, Introduction};
|
||||
pub use errors::ChatError;
|
||||
pub use ::storage::StorageConfig;
|
||||
pub use types::{AddressedEnvelope, ContentData};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@ -8,3 +8,6 @@ crate-type = ["rlib"]
|
||||
|
||||
[dependencies]
|
||||
libchat = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@ -1,18 +1,95 @@
|
||||
use libchat::ChatError;
|
||||
use libchat::Context;
|
||||
use libchat::{
|
||||
AddressedEnvelope, ContentData, Context, ConversationIdOwned, Introduction, StorageConfig,
|
||||
};
|
||||
|
||||
pub struct ChatClient {
|
||||
use crate::{delivery::DeliveryService, errors::ClientError};
|
||||
|
||||
pub struct ChatClient<D: DeliveryService> {
|
||||
ctx: Context,
|
||||
delivery: D,
|
||||
}
|
||||
|
||||
impl ChatClient {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
impl<D: DeliveryService> ChatClient<D> {
|
||||
/// Create an in-memory, ephemeral client. Identity is lost on drop.
|
||||
pub fn new(name: impl Into<String>, delivery: D) -> Self {
|
||||
Self {
|
||||
ctx: Context::new_with_name(name),
|
||||
delivery,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_bundle(&mut self) -> Result<Vec<u8>, ChatError> {
|
||||
self.ctx.create_intro_bundle()
|
||||
/// Open or create a persistent client backed by `StorageConfig`.
|
||||
///
|
||||
/// If an identity already exists in storage it is loaded; otherwise a new
|
||||
/// one is created and saved.
|
||||
pub fn open(
|
||||
name: impl Into<String>,
|
||||
config: StorageConfig,
|
||||
delivery: D,
|
||||
) -> Result<Self, ClientError<D::Error>> {
|
||||
let ctx = Context::open(name, config)?;
|
||||
Ok(Self { ctx, delivery })
|
||||
}
|
||||
|
||||
/// Returns the installation name (identity label) of this client.
|
||||
pub fn installation_name(&self) -> &str {
|
||||
self.ctx.installation_name()
|
||||
}
|
||||
|
||||
/// Produce a serialised introduction bundle for sharing out-of-band.
|
||||
pub fn create_intro_bundle(&mut self) -> Result<Vec<u8>, ClientError<D::Error>> {
|
||||
self.ctx.create_intro_bundle().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Parse intro bundle bytes, initiate a private conversation, and deliver
|
||||
/// all outbound envelopes. Returns this side's conversation ID.
|
||||
pub fn create_conversation(
|
||||
&mut self,
|
||||
intro_bundle: &[u8],
|
||||
initial_content: &[u8],
|
||||
) -> Result<ConversationIdOwned, ClientError<D::Error>> {
|
||||
let intro = Introduction::try_from(intro_bundle)?;
|
||||
let (convo_id, envelopes) = self.ctx.create_private_convo(&intro, initial_content);
|
||||
self.dispatch_all(envelopes)?;
|
||||
Ok(convo_id)
|
||||
}
|
||||
|
||||
/// List all conversation IDs known to this client.
|
||||
pub fn list_conversations(&self) -> Result<Vec<ConversationIdOwned>, ClientError<D::Error>> {
|
||||
self.ctx.list_conversations().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Encrypt `content` and dispatch all outbound envelopes.
|
||||
pub fn send_message(
|
||||
&mut self,
|
||||
convo_id: &ConversationIdOwned,
|
||||
content: &[u8],
|
||||
) -> Result<(), ClientError<D::Error>> {
|
||||
let envelopes = self.ctx.send_content(convo_id.as_ref(), content)?;
|
||||
self.dispatch_all(envelopes)
|
||||
}
|
||||
|
||||
/// Decrypt an inbound payload. Returns `Some(ContentData)` for user
|
||||
/// content, `None` for protocol frames.
|
||||
pub fn receive(
|
||||
&mut self,
|
||||
payload: &[u8],
|
||||
) -> Result<Option<ContentData>, ClientError<D::Error>> {
|
||||
self.ctx.handle_payload(payload).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Access the delivery service (e.g. to pop inbound envelopes in tests).
|
||||
pub fn delivery_mut(&mut self) -> &mut D {
|
||||
&mut self.delivery
|
||||
}
|
||||
|
||||
fn dispatch_all(
|
||||
&mut self,
|
||||
envelopes: Vec<AddressedEnvelope>,
|
||||
) -> Result<(), ClientError<D::Error>> {
|
||||
for env in envelopes {
|
||||
self.delivery.deliver(env).map_err(ClientError::Delivery)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
24
crates/client/src/delivery.rs
Normal file
24
crates/client/src/delivery.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use libchat::AddressedEnvelope;
|
||||
|
||||
pub trait DeliveryService {
|
||||
type Error: std::fmt::Debug;
|
||||
fn deliver(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// In-memory delivery for tests. Envelopes are pushed to `inbox`; tests pop
|
||||
/// them and feed bytes to the peer's `receive()`.
|
||||
#[derive(Default)]
|
||||
pub struct InMemoryDelivery {
|
||||
pub inbox: VecDeque<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl DeliveryService for InMemoryDelivery {
|
||||
type Error = std::convert::Infallible;
|
||||
|
||||
fn deliver(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error> {
|
||||
self.inbox.push_back(envelope.data);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
15
crates/client/src/errors.rs
Normal file
15
crates/client/src/errors.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use libchat::ChatError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientError<D> {
|
||||
Chat(ChatError),
|
||||
/// Crypto state advanced but at least one envelope failed delivery.
|
||||
/// Caller decides whether to retry.
|
||||
Delivery(D),
|
||||
}
|
||||
|
||||
impl<D> From<ChatError> for ClientError<D> {
|
||||
fn from(e: ChatError) -> Self {
|
||||
Self::Chat(e)
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,10 @@
|
||||
mod client;
|
||||
mod delivery;
|
||||
mod errors;
|
||||
|
||||
pub use client::ChatClient;
|
||||
pub use delivery::{DeliveryService, InMemoryDelivery};
|
||||
pub use errors::ClientError;
|
||||
|
||||
// Re-export types callers need to interact with ChatClient
|
||||
pub use libchat::{ContentData, ConversationIdOwned, StorageConfig};
|
||||
|
||||
72
crates/client/tests/alice_and_bob.rs
Normal file
72
crates/client/tests/alice_and_bob.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use client::{ChatClient, ContentData, ConversationIdOwned, InMemoryDelivery};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn pop_and_receive(
|
||||
sender: &mut ChatClient<InMemoryDelivery>,
|
||||
receiver: &mut ChatClient<InMemoryDelivery>,
|
||||
) -> Option<ContentData> {
|
||||
let raw = sender.delivery_mut().inbox.pop_front().expect("expected envelope");
|
||||
receiver.receive(&raw).expect("receive failed")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alice_bob_message_exchange() {
|
||||
let mut alice = ChatClient::new("alice", InMemoryDelivery::default());
|
||||
let mut bob = ChatClient::new("bob", InMemoryDelivery::default());
|
||||
|
||||
// Exchange intro bundles out-of-band
|
||||
let bob_bundle = bob.create_intro_bundle().unwrap();
|
||||
|
||||
// Alice initiates conversation with Bob
|
||||
let alice_convo_id = alice
|
||||
.create_conversation(&bob_bundle, b"hello bob")
|
||||
.unwrap();
|
||||
|
||||
// Bob receives Alice's initial message
|
||||
let content = pop_and_receive(&mut alice, &mut bob).expect("expected content");
|
||||
assert_eq!(content.data, b"hello bob");
|
||||
assert!(content.is_new_convo);
|
||||
|
||||
let bob_convo_id: ConversationIdOwned = Arc::from(content.conversation_id.as_str());
|
||||
|
||||
// Bob replies
|
||||
bob.send_message(&bob_convo_id, b"hi alice").unwrap();
|
||||
let content = pop_and_receive(&mut bob, &mut alice).expect("expected content");
|
||||
assert_eq!(content.data, b"hi alice");
|
||||
assert!(!content.is_new_convo);
|
||||
|
||||
// Multiple back-and-forth rounds
|
||||
for i in 0u8..5 {
|
||||
let msg = format!("msg {i}");
|
||||
alice.send_message(&alice_convo_id, msg.as_bytes()).unwrap();
|
||||
let content = pop_and_receive(&mut alice, &mut bob).expect("expected content");
|
||||
assert_eq!(content.data, msg.as_bytes());
|
||||
|
||||
let reply = format!("reply {i}");
|
||||
bob.send_message(&bob_convo_id, reply.as_bytes()).unwrap();
|
||||
let content = pop_and_receive(&mut bob, &mut alice).expect("expected content");
|
||||
assert_eq!(content.data, reply.as_bytes());
|
||||
}
|
||||
|
||||
// Both sides have exactly one conversation
|
||||
assert_eq!(alice.list_conversations().unwrap().len(), 1);
|
||||
assert_eq!(bob.list_conversations().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_persistent_client() {
|
||||
use client::StorageConfig;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db_path = dir.path().join("test.db").to_string_lossy().to_string();
|
||||
let config = StorageConfig::File(db_path);
|
||||
|
||||
let client1 = ChatClient::open("alice", config.clone(), InMemoryDelivery::default()).unwrap();
|
||||
let name1 = client1.installation_name().to_string();
|
||||
drop(client1);
|
||||
|
||||
let client2 = ChatClient::open("alice", config, InMemoryDelivery::default()).unwrap();
|
||||
let name2 = client2.installation_name().to_string();
|
||||
|
||||
assert_eq!(name1, name2, "installation name should persist across restarts");
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user