mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-03-27 22:53:07 +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"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libchat",
|
"libchat",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -12,8 +12,10 @@ mod types;
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use api::*;
|
pub use api::*;
|
||||||
pub use context::{Context, Introduction};
|
pub use context::{Context, ConversationIdOwned, Introduction};
|
||||||
pub use errors::ChatError;
|
pub use errors::ChatError;
|
||||||
|
pub use ::storage::StorageConfig;
|
||||||
|
pub use types::{AddressedEnvelope, ContentData};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
@ -8,3 +8,6 @@ crate-type = ["rlib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libchat = { workspace = true }
|
libchat = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|||||||
@ -1,18 +1,95 @@
|
|||||||
use libchat::ChatError;
|
use libchat::{
|
||||||
use libchat::Context;
|
AddressedEnvelope, ContentData, Context, ConversationIdOwned, Introduction, StorageConfig,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct ChatClient {
|
use crate::{delivery::DeliveryService, errors::ClientError};
|
||||||
|
|
||||||
|
pub struct ChatClient<D: DeliveryService> {
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
|
delivery: D,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatClient {
|
impl<D: DeliveryService> ChatClient<D> {
|
||||||
pub fn new(name: impl Into<String>) -> Self {
|
/// Create an in-memory, ephemeral client. Identity is lost on drop.
|
||||||
|
pub fn new(name: impl Into<String>, delivery: D) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ctx: Context::new_with_name(name),
|
ctx: Context::new_with_name(name),
|
||||||
|
delivery,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_bundle(&mut self) -> Result<Vec<u8>, ChatError> {
|
/// Open or create a persistent client backed by `StorageConfig`.
|
||||||
self.ctx.create_intro_bundle()
|
///
|
||||||
|
/// 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 client;
|
||||||
|
mod delivery;
|
||||||
|
mod errors;
|
||||||
|
|
||||||
pub use client::ChatClient;
|
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