diff --git a/conversations/src/chat.rs b/conversations/src/chat.rs new file mode 100644 index 0000000..dd4458f --- /dev/null +++ b/conversations/src/chat.rs @@ -0,0 +1,171 @@ +use std::rc::Rc; + +use crate::{ + common::{Chat, ChatStore, HasChatId}, + errors::ChatError, + identity::Identity, + inbox::{Inbox, Introduction}, + types::{AddressedEnvelope, ContentData}, +}; + +/// 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 +/// with handle-based access. +pub struct ChatManager { + identity: Rc, + store: ChatStore, + inbox: Inbox, +} + +impl ChatManager { + /// Create a new ChatManager with a fresh identity. + pub fn new() -> Self { + let identity = Rc::new(Identity::new()); + let inbox = Inbox::new(Rc::clone(&identity)); + Self { + identity, + store: ChatStore::new(), + inbox, + } + } + + /// Create a new ChatManager with an existing identity. + pub fn with_identity(identity: Identity) -> Self { + let identity = Rc::new(identity); + let inbox = Inbox::new(Rc::clone(&identity)); + Self { + identity, + store: ChatStore::new(), + inbox, + } + } + + /// Get the local identity's public address. + pub fn local_address(&self) -> String { + self.identity.address() + } + + /// 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 { + let pkb = self.inbox.create_bundle(); + Ok(Introduction::from(pkb)) + } + + /// 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. + pub fn start_private_chat( + &mut self, + remote_bundle: &Introduction, + initial_message: &str, + ) -> Result<(String, Vec), ChatError> { + let (convo, payloads) = self + .inbox + .invite_to_private_convo(remote_bundle, initial_message.to_string())?; + + let chat_id = convo.id().to_string(); + + let envelopes: Vec = payloads + .into_iter() + .map(|p| p.to_envelope(chat_id.clone())) + .collect(); + + self.store.insert_chat(convo); + + Ok((chat_id, envelopes)) + } + + /// Send a message to an existing chat. + /// + /// Returns envelopes that must be delivered to chat participants. + pub fn send_message( + &mut self, + chat_id: &str, + content: &[u8], + ) -> Result, ChatError> { + let chat = self + .store + .get_mut_chat(chat_id) + .ok_or_else(|| ChatError::NoChatId(chat_id.to_string()))?; + + let payloads = chat.send_message(content)?; + + Ok(payloads + .into_iter() + .map(|p| p.to_envelope(chat.remote_id())) + .collect()) + } + + /// Handle an incoming payload from the network. + /// + /// Returns the decrypted content if successful. + pub fn handle_incoming(&mut self, _payload: &[u8]) -> Result { + // TODO: Implement proper payload handling + // 1. Determine if this is an inbox message or a chat message + // 2. Route to appropriate handler + // 3. Return decrypted content + Ok(ContentData { + conversation_id: "convo_id".into(), + data: vec![1, 2, 3, 4, 5, 6], + }) + } + + /// Get a reference to an active chat. + pub fn get_chat(&self, chat_id: &str) -> Option<&dyn Chat> { + 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. + pub fn list_chats(&self) -> Vec { + self.store.chat_ids().map(|id| id.to_string()).collect() + } +} + +impl Default for ChatManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_chat_manager() { + let manager = ChatManager::new(); + assert!(!manager.local_address().is_empty()); + } + + #[test] + fn test_create_intro_bundle() { + let mut manager = ChatManager::new(); + let bundle = manager.create_intro_bundle(); + assert!(bundle.is_ok()); + } + + #[test] + fn test_start_private_chat() { + let mut alice = ChatManager::new(); + let mut bob = ChatManager::new(); + + // Bob creates an intro bundle + let bob_intro = bob.create_intro_bundle().unwrap(); + + // Alice starts a chat with Bob + let result = alice.start_private_chat(&bob_intro, "Hello Bob!"); + assert!(result.is_ok()); + + let (chat_id, envelopes) = result.unwrap(); + assert!(!chat_id.is_empty()); + assert!(!envelopes.is_empty()); + } +} diff --git a/conversations/src/context.rs b/conversations/src/context.rs index 6e0f041..f401c37 100644 --- a/conversations/src/context.rs +++ b/conversations/src/context.rs @@ -1,113 +1,146 @@ -use std::{collections::HashMap, rc::Rc, sync::Arc}; +//! FFI-oriented context that wraps ChatManager with handle-based access. +//! +//! For pure Rust usage, prefer using `ChatManager` directly from `chat.rs`. + +use std::collections::HashMap; use crate::{ - common::{Chat, ChatStore, HasChatId}, + chat::ChatManager, errors::ChatError, - identity::Identity, - inbox::Inbox, types::{AddressedEnvelope, ContentData}, }; pub use crate::inbox::Introduction; -//Offset handles to make debuging easier +// Offset handles to make debugging easier const INITIAL_CONVO_HANDLE: u32 = 0xF5000001; -/// Used to identify a conversation on the othersize of the FFI. +/// Used to identify a conversation across the FFI boundary. +/// This is an opaque integer handle that maps to an internal ChatId string. pub type ConvoHandle = u32; -// This is the main entry point to the conversations api. -// Ctx manages lifetimes of objects to process and generate payloads. +/// Context is the FFI-oriented wrapper around ChatManager. +/// +/// It provides handle-based access to chats, suitable for FFI consumers +/// that can't work with Rust strings directly. +/// +/// For pure Rust usage, prefer using `ChatManager` directly. pub struct Context { - _identity: Rc, - store: ChatStore, - inbox: Inbox, - convo_handle_map: HashMap>, - next_convo_handle: ConvoHandle, + manager: ChatManager, + /// Maps FFI handles to internal chat IDs + handle_to_chat_id: HashMap, + /// Maps chat IDs back to FFI handles for lookup + chat_id_to_handle: HashMap, + next_handle: ConvoHandle, } impl Context { pub fn new() -> Self { - let identity = Rc::new(Identity::new()); - let inbox = Inbox::new(Rc::clone(&identity)); // Self { - _identity: identity, - store: ChatStore::new(), - inbox, - convo_handle_map: HashMap::new(), - next_convo_handle: INITIAL_CONVO_HANDLE, + manager: ChatManager::new(), + handle_to_chat_id: HashMap::new(), + chat_id_to_handle: HashMap::new(), + next_handle: INITIAL_CONVO_HANDLE, } } + /// Create a Context wrapping an existing ChatManager. + pub fn with_manager(manager: ChatManager) -> Self { + Self { + manager, + handle_to_chat_id: HashMap::new(), + chat_id_to_handle: HashMap::new(), + next_handle: INITIAL_CONVO_HANDLE, + } + } + + /// Access the underlying ChatManager for direct Rust API usage. + pub fn manager(&self) -> &ChatManager { + &self.manager + } + + /// Access the underlying ChatManager mutably. + pub fn manager_mut(&mut self) -> &mut ChatManager { + &mut self.manager + } + + /// Create an introduction bundle for sharing with other users. + pub fn create_intro_bundle(&mut self) -> Result, ChatError> { + let intro = self.manager.create_intro_bundle()?; + Ok(intro.into()) + } + + /// Create a new private conversation using a remote party's introduction bundle. + /// + /// Returns an FFI handle and addressed envelopes to be delivered. pub fn create_private_convo( &mut self, remote_bundle: &Introduction, content: String, ) -> (ConvoHandle, Vec) { - let (convo, payloads) = self - .inbox - .invite_to_private_convo(remote_bundle, content) + let (chat_id, envelopes) = self + .manager + .start_private_chat(remote_bundle, &content) .unwrap_or_else(|_| todo!("Log/Surface Error")); - let payload_bytes = payloads - .into_iter() - .map(|p| p.to_envelope(convo.id().to_string())) - .collect(); - - let convo_handle = self.add_convo(convo); - (convo_handle, payload_bytes) + let handle = self.register_chat_id(chat_id); + (handle, envelopes) } + /// Send content to an existing conversation identified by handle. pub fn send_content( &mut self, convo_handle: ConvoHandle, content: &[u8], ) -> Result, ChatError> { - // Lookup convo from handle - let convo = self.get_convo_mut(convo_handle)?; - - // Generate encrypted payloads - let payloads = convo.send_message(content)?; - - // Attach conversation_ids to Envelopes - Ok(payloads - .into_iter() - .map(|p| p.to_envelope(convo.remote_id())) - .collect()) + let chat_id = self.resolve_handle(convo_handle)?; + self.manager.send_message(&chat_id, content) } - pub fn handle_payload(&mut self, _payload: &[u8]) -> Option { - // !TODO Replace Mock - Some(ContentData { - conversation_id: "convo_id".into(), - data: vec![1, 2, 3, 4, 5, 6], - }) + /// Handle an incoming payload. + pub fn handle_payload(&mut self, payload: &[u8]) -> Option { + self.manager.handle_incoming(payload).ok() } - pub fn create_intro_bundle(&mut self) -> Result, ChatError> { - let pkb = self.inbox.create_bundle(); - Ok(Introduction::from(pkb).into()) + /// Get the chat ID for a given handle. + pub fn get_chat_id(&self, handle: ConvoHandle) -> Option<&str> { + self.handle_to_chat_id.get(&handle).map(|s| s.as_str()) } - fn add_convo(&mut self, convo: impl Chat + HasChatId + 'static) -> ConvoHandle { - let handle = self.next_convo_handle; - self.next_convo_handle += 1; - let convo_id = self.store.insert_chat(convo); - self.convo_handle_map.insert(handle, convo_id); + /// Get the handle for a given chat ID. + pub fn get_handle(&self, chat_id: &str) -> Option { + self.chat_id_to_handle.get(chat_id).copied() + } + + // --- Internal helpers --- + + /// Register a chat ID and return its FFI handle. + fn register_chat_id(&mut self, chat_id: String) -> ConvoHandle { + // Check if already registered + if let Some(&handle) = self.chat_id_to_handle.get(&chat_id) { + return handle; + } + + let handle = self.next_handle; + self.next_handle += 1; + + self.handle_to_chat_id.insert(handle, chat_id.clone()); + self.chat_id_to_handle.insert(chat_id, handle); handle } - // Returns a mutable reference to a Convo for a given ConvoHandle - fn get_convo_mut(&mut self, handle: ConvoHandle) -> Result<&mut dyn Chat, ChatError> { - let convo_id = self - .convo_handle_map + /// Resolve a handle to its chat ID. + fn resolve_handle(&self, handle: ConvoHandle) -> Result { + self.handle_to_chat_id .get(&handle) - .ok_or_else(|| ChatError::NoConvo(handle))? - .clone(); - - self.store - .get_mut_chat(&convo_id) + .cloned() .ok_or_else(|| ChatError::NoConvo(handle)) } } + +impl Default for Context { + fn default() -> Self { + Self::new() + } +} diff --git a/conversations/src/errors.rs b/conversations/src/errors.rs index 06cd563..27cc64a 100644 --- a/conversations/src/errors.rs +++ b/conversations/src/errors.rs @@ -20,6 +20,8 @@ pub enum ChatError { BadParsing(&'static str), #[error("convo with handle: {0} was not found")] NoConvo(u32), + #[error("chat with id '{0}' was not found")] + NoChatId(String), } #[derive(Error, Debug)] diff --git a/conversations/src/ffi/api.rs b/conversations/src/ffi/api.rs index a3a8fd5..5a6a97f 100644 --- a/conversations/src/ffi/api.rs +++ b/conversations/src/ffi/api.rs @@ -12,7 +12,7 @@ pub enum ErrorCode { UnknownError = -6, } -use crate::context::{Context, ConvoHandle, Introduction}; +use super::context::{Context, ConvoHandle, Introduction}; /// Opaque wrapper for Context #[derive_ReprC] diff --git a/conversations/src/ffi/context.rs b/conversations/src/ffi/context.rs new file mode 100644 index 0000000..f401c37 --- /dev/null +++ b/conversations/src/ffi/context.rs @@ -0,0 +1,146 @@ +//! FFI-oriented context that wraps ChatManager with handle-based access. +//! +//! For pure Rust usage, prefer using `ChatManager` directly from `chat.rs`. + +use std::collections::HashMap; + +use crate::{ + chat::ChatManager, + errors::ChatError, + types::{AddressedEnvelope, ContentData}, +}; + +pub use crate::inbox::Introduction; + +// Offset handles to make debugging easier +const INITIAL_CONVO_HANDLE: u32 = 0xF5000001; + +/// Used to identify a conversation across the FFI boundary. +/// This is an opaque integer handle that maps to an internal ChatId string. +pub type ConvoHandle = u32; + +/// Context is the FFI-oriented wrapper around ChatManager. +/// +/// It provides handle-based access to chats, suitable for FFI consumers +/// that can't work with Rust strings directly. +/// +/// For pure Rust usage, prefer using `ChatManager` directly. +pub struct Context { + manager: ChatManager, + /// Maps FFI handles to internal chat IDs + handle_to_chat_id: HashMap, + /// Maps chat IDs back to FFI handles for lookup + chat_id_to_handle: HashMap, + next_handle: ConvoHandle, +} + +impl Context { + pub fn new() -> Self { + Self { + manager: ChatManager::new(), + handle_to_chat_id: HashMap::new(), + chat_id_to_handle: HashMap::new(), + next_handle: INITIAL_CONVO_HANDLE, + } + } + + /// Create a Context wrapping an existing ChatManager. + pub fn with_manager(manager: ChatManager) -> Self { + Self { + manager, + handle_to_chat_id: HashMap::new(), + chat_id_to_handle: HashMap::new(), + next_handle: INITIAL_CONVO_HANDLE, + } + } + + /// Access the underlying ChatManager for direct Rust API usage. + pub fn manager(&self) -> &ChatManager { + &self.manager + } + + /// Access the underlying ChatManager mutably. + pub fn manager_mut(&mut self) -> &mut ChatManager { + &mut self.manager + } + + /// Create an introduction bundle for sharing with other users. + pub fn create_intro_bundle(&mut self) -> Result, ChatError> { + let intro = self.manager.create_intro_bundle()?; + Ok(intro.into()) + } + + /// Create a new private conversation using a remote party's introduction bundle. + /// + /// Returns an FFI handle and addressed envelopes to be delivered. + pub fn create_private_convo( + &mut self, + remote_bundle: &Introduction, + content: String, + ) -> (ConvoHandle, Vec) { + let (chat_id, envelopes) = self + .manager + .start_private_chat(remote_bundle, &content) + .unwrap_or_else(|_| todo!("Log/Surface Error")); + + let handle = self.register_chat_id(chat_id); + (handle, envelopes) + } + + /// Send content to an existing conversation identified by handle. + pub fn send_content( + &mut self, + convo_handle: ConvoHandle, + content: &[u8], + ) -> Result, ChatError> { + let chat_id = self.resolve_handle(convo_handle)?; + self.manager.send_message(&chat_id, content) + } + + /// Handle an incoming payload. + pub fn handle_payload(&mut self, payload: &[u8]) -> Option { + self.manager.handle_incoming(payload).ok() + } + + /// Get the chat ID for a given handle. + pub fn get_chat_id(&self, handle: ConvoHandle) -> Option<&str> { + self.handle_to_chat_id.get(&handle).map(|s| s.as_str()) + } + + /// Get the handle for a given chat ID. + pub fn get_handle(&self, chat_id: &str) -> Option { + self.chat_id_to_handle.get(chat_id).copied() + } + + // --- Internal helpers --- + + /// Register a chat ID and return its FFI handle. + fn register_chat_id(&mut self, chat_id: String) -> ConvoHandle { + // Check if already registered + if let Some(&handle) = self.chat_id_to_handle.get(&chat_id) { + return handle; + } + + let handle = self.next_handle; + self.next_handle += 1; + + self.handle_to_chat_id.insert(handle, chat_id.clone()); + self.chat_id_to_handle.insert(chat_id, handle); + + handle + } + + /// Resolve a handle to its chat ID. + fn resolve_handle(&self, handle: ConvoHandle) -> Result { + self.handle_to_chat_id + .get(&handle) + .cloned() + .ok_or_else(|| ChatError::NoConvo(handle)) + } +} + +impl Default for Context { + fn default() -> Self { + Self::new() + } +} diff --git a/conversations/src/ffi/mod.rs b/conversations/src/ffi/mod.rs index e5fdf85..96b95ea 100644 --- a/conversations/src/ffi/mod.rs +++ b/conversations/src/ffi/mod.rs @@ -1 +1,4 @@ pub mod api; +mod context; + +pub use context::{Context, ConvoHandle, Introduction}; diff --git a/conversations/src/inbox/inbox.rs b/conversations/src/inbox/inbox.rs index b3a8d1b..2155933 100644 --- a/conversations/src/inbox/inbox.rs +++ b/conversations/src/inbox/inbox.rs @@ -8,7 +8,6 @@ use std::rc::Rc; use crypto::{PrekeyBundle, SecretKey}; use crate::common::{Chat, ChatId, HasChatId, InboundMessageHandler}; -use crate::context::Introduction; use crate::dm::privatev1::PrivateV1Convo; use crate::errors::ChatError; use crate::identity::Identity; @@ -17,6 +16,8 @@ use crate::inbox::handshake::InboxHandshake; use crate::proto::{self, CopyBytes}; use crate::types::{AddressedEncryptedPayload, ContentData}; +use super::Introduction; + /// Compute the deterministic Delivery_address for an installation fn delivery_address_for_installation(_: PublicKey) -> String { // TODO: Implement Delivery Address diff --git a/conversations/src/lib.rs b/conversations/src/lib.rs index c40aaa0..a4109a2 100644 --- a/conversations/src/lib.rs +++ b/conversations/src/lib.rs @@ -1,10 +1,10 @@ +pub mod chat; pub mod common; pub mod dm; pub mod ffi; pub mod group; pub mod inbox; -mod context; mod errors; mod identity; mod proto;