mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-06-28 12:09:30 +00:00
Add 1:1 Chats using Groups (#139)
* Add Identified Trait for convo * Add PrivateV2Convo * Rename to DirectV1 * Rename ConvoTypeOwned variant * Update DirectV1 to support multiple members * Apply suggestion from @kaichaosun Co-authored-by: kaichao <kaichaosuna@gmail.com> --------- Co-authored-by: kaichao <kaichaosuna@gmail.com>
This commit is contained in:
parent
c5b264c827
commit
7612b233c9
@ -1,3 +1,4 @@
|
|||||||
|
mod direct_v1;
|
||||||
pub mod group_v1;
|
pub mod group_v1;
|
||||||
mod group_v2;
|
mod group_v2;
|
||||||
mod privatev1;
|
mod privatev1;
|
||||||
@ -6,6 +7,7 @@ pub use crate::errors::ChatError;
|
|||||||
use crate::outcomes::ConvoOutcome;
|
use crate::outcomes::ConvoOutcome;
|
||||||
use crate::proto::EncryptedPayload;
|
use crate::proto::EncryptedPayload;
|
||||||
use crate::service_context::{ExternalServices, ServiceContext};
|
use crate::service_context::{ExternalServices, ServiceContext};
|
||||||
|
pub use direct_v1::DirectV1Convo;
|
||||||
pub use group_v1::GroupV1Convo;
|
pub use group_v1::GroupV1Convo;
|
||||||
pub use group_v2::GroupV2Convo;
|
pub use group_v2::GroupV2Convo;
|
||||||
pub use privatev1::PrivateV1Convo;
|
pub use privatev1::PrivateV1Convo;
|
||||||
@ -15,7 +17,7 @@ pub type ConversationId = String;
|
|||||||
pub type ConversationIdRef<'a> = &'a str;
|
pub type ConversationIdRef<'a> = &'a str;
|
||||||
|
|
||||||
/// Behaviour shared by every conversation kind.
|
/// Behaviour shared by every conversation kind.
|
||||||
pub(crate) trait Convo<S: ExternalServices>: Identified {
|
pub(crate) trait Convo<S: ExternalServices>: Identified + Send {
|
||||||
fn send_content(&mut self, cx: &mut ServiceContext<S>, content: &[u8])
|
fn send_content(&mut self, cx: &mut ServiceContext<S>, content: &[u8])
|
||||||
-> Result<(), ChatError>;
|
-> Result<(), ChatError>;
|
||||||
|
|
||||||
|
|||||||
61
core/conversations/src/conversation/direct_v1.rs
Normal file
61
core/conversations/src/conversation/direct_v1.rs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
use chat_proto::logoschat::encryption::EncryptedPayload;
|
||||||
|
use shared_traits::IdentIdRef;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ChatError, ExternalServices,
|
||||||
|
conversation::{ConversationIdRef, Convo, GroupConvo, GroupV1Convo, Identified},
|
||||||
|
service_context::ServiceContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
type DelegateGroup = GroupV1Convo;
|
||||||
|
|
||||||
|
/// A Conversation between two participants.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DirectV1Convo {
|
||||||
|
inner_group: DelegateGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirectV1Convo {
|
||||||
|
// Constructor must accept multiple IdentId's
|
||||||
|
// While the conversation is limited to 2 participants, each participants may
|
||||||
|
// have multiple Installations.
|
||||||
|
pub fn new<S: ExternalServices>(
|
||||||
|
cx: &mut ServiceContext<S>,
|
||||||
|
members: &[IdentIdRef],
|
||||||
|
) -> Result<Self, ChatError> {
|
||||||
|
let mut inner_group = DelegateGroup::new(cx)?;
|
||||||
|
inner_group.add_member(cx, members)?;
|
||||||
|
Ok(Self { inner_group })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Identified for DirectV1Convo {
|
||||||
|
fn id(&self) -> ConversationIdRef<'_> {
|
||||||
|
self.inner_group.id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Convo<S> for DirectV1Convo
|
||||||
|
where
|
||||||
|
S: ExternalServices,
|
||||||
|
{
|
||||||
|
fn send_content(
|
||||||
|
&mut self,
|
||||||
|
cx: &mut ServiceContext<S>,
|
||||||
|
content: &[u8],
|
||||||
|
) -> Result<(), super::ChatError> {
|
||||||
|
self.inner_group.send_content(cx, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_frame(
|
||||||
|
&mut self,
|
||||||
|
cx: &mut ServiceContext<S>,
|
||||||
|
enc: EncryptedPayload,
|
||||||
|
) -> Result<crate::ConvoOutcome, ChatError> {
|
||||||
|
self.inner_group.handle_frame(cx, enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wakeup(&mut self, service_ctx: &mut ServiceContext<S>) -> Result<(), ChatError> {
|
||||||
|
self.inner_group.wakeup(service_ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use crate::causal_history::{CausalHistoryStore, MissingMessage};
|
use crate::causal_history::{CausalHistoryStore, MissingMessage};
|
||||||
use crate::conversation::{
|
use crate::conversation::{
|
||||||
ConversationIdRef, GroupV1Convo, GroupV2Convo, Identified, PrivateV1Convo,
|
ConversationIdRef, DirectV1Convo, GroupV1Convo, GroupV2Convo, Identified, PrivateV1Convo,
|
||||||
};
|
};
|
||||||
use crate::service_context::{ExternalServices, ServiceContext};
|
use crate::service_context::{ExternalServices, ServiceContext};
|
||||||
use crate::{DeliveryService, IdentityProvider, RegistrationService, WakeupService};
|
use crate::{DeliveryService, IdentityProvider, RegistrationService, WakeupService};
|
||||||
@ -16,6 +16,7 @@ use crypto::{Identity, PublicKey};
|
|||||||
use openmls::group::GroupId;
|
use openmls::group::GroupId;
|
||||||
use shared_traits::IdentIdRef;
|
use shared_traits::IdentIdRef;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Debug;
|
||||||
use storage::{ChatStore, ConversationKind, ConversationStore};
|
use storage::{ChatStore, ConversationKind, ConversationStore};
|
||||||
use tracing::{info, instrument};
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
@ -188,6 +189,14 @@ impl<'a, S: ExternalServices + 'static> Core<S> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
remote_bundle: &Introduction,
|
remote_bundle: &Introduction,
|
||||||
content: &[u8],
|
content: &[u8],
|
||||||
|
) -> Result<ConversationId, ChatError> {
|
||||||
|
self.create_private_convo_v1(remote_bundle, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_private_convo_v1(
|
||||||
|
&mut self,
|
||||||
|
remote_bundle: &Introduction,
|
||||||
|
content: &[u8],
|
||||||
) -> Result<ConversationId, ChatError> {
|
) -> Result<ConversationId, ChatError> {
|
||||||
let (mut convo, payloads) =
|
let (mut convo, payloads) =
|
||||||
self.inbox
|
self.inbox
|
||||||
@ -204,6 +213,17 @@ impl<'a, S: ExternalServices + 'static> Core<S> {
|
|||||||
Ok(convo_id)
|
Ok(convo_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_direct_convo_v1(
|
||||||
|
&mut self,
|
||||||
|
members: &[IdentIdRef],
|
||||||
|
) -> Result<ConversationId, ChatError> {
|
||||||
|
let convo = DirectV1Convo::new(&mut self.services, members)?;
|
||||||
|
let convo_id = convo.id().to_string();
|
||||||
|
self.register_convo(ConvoTypeOwned::Direct(Box::new(convo)))?;
|
||||||
|
|
||||||
|
Ok(convo_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_group_convo(
|
pub fn create_group_convo(
|
||||||
&mut self,
|
&mut self,
|
||||||
participants: &[IdentIdRef],
|
participants: &[IdentIdRef],
|
||||||
@ -266,6 +286,10 @@ impl<'a, S: ExternalServices + 'static> Core<S> {
|
|||||||
ConvoTypeOwned::Group(group_convo) => {
|
ConvoTypeOwned::Group(group_convo) => {
|
||||||
group_convo.add_member(&mut self.services, members)
|
group_convo.add_member(&mut self.services, members)
|
||||||
}
|
}
|
||||||
|
ConvoTypeOwned::Direct(convo) => Err(ChatError::UnsupportedFunction(
|
||||||
|
convo.id().into(),
|
||||||
|
"Add Member".into(),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut convo = self.load_group_convo(convo_id)?;
|
let mut convo = self.load_group_convo(convo_id)?;
|
||||||
@ -397,6 +421,7 @@ impl<'a, S: ExternalServices + 'static> Core<S> {
|
|||||||
};
|
};
|
||||||
let convo = match convo {
|
let convo = match convo {
|
||||||
ConvoTypeOwned::Group(c) => c.as_mut(),
|
ConvoTypeOwned::Group(c) => c.as_mut(),
|
||||||
|
ConvoTypeOwned::Direct(c) => c.as_mut(),
|
||||||
};
|
};
|
||||||
|
|
||||||
convo.wakeup(&mut self.services)
|
convo.wakeup(&mut self.services)
|
||||||
@ -466,15 +491,24 @@ impl<'a, S: ExternalServices + 'static> Core<S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum ConvoTypeOwned<S: ExternalServices> {
|
enum ConvoTypeOwned<S: ExternalServices> {
|
||||||
// Pairwise(Box<dyn BaseConvo<S>>),
|
Direct(Box<dyn Convo<S>>),
|
||||||
Group(Box<dyn GroupConvo<S>>),
|
Group(Box<dyn GroupConvo<S>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<S: ExternalServices> Debug for ConvoTypeOwned<S> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Direct(arg0) => f.debug_tuple("Pairwise").field(&arg0.id()).finish(),
|
||||||
|
Self::Group(arg0) => f.debug_tuple("Group").field(&arg0.id()).finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<S: ExternalServices> Identified for ConvoTypeOwned<S> {
|
impl<S: ExternalServices> Identified for ConvoTypeOwned<S> {
|
||||||
fn id(&self) -> ConversationIdRef<'_> {
|
fn id(&self) -> ConversationIdRef<'_> {
|
||||||
match self {
|
match self {
|
||||||
|
ConvoTypeOwned::Direct(convo) => convo.id(),
|
||||||
ConvoTypeOwned::Group(group_convo) => group_convo.id(),
|
ConvoTypeOwned::Group(group_convo) => group_convo.id(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -488,6 +522,7 @@ impl<S: ExternalServices> Convo<S> for ConvoTypeOwned<S> {
|
|||||||
) -> Result<(), ChatError> {
|
) -> Result<(), ChatError> {
|
||||||
match self {
|
match self {
|
||||||
ConvoTypeOwned::Group(group_convo) => group_convo.send_content(cx, content),
|
ConvoTypeOwned::Group(group_convo) => group_convo.send_content(cx, content),
|
||||||
|
ConvoTypeOwned::Direct(convo) => convo.send_content(cx, content),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -498,12 +533,14 @@ impl<S: ExternalServices> Convo<S> for ConvoTypeOwned<S> {
|
|||||||
) -> Result<ConvoOutcome, ChatError> {
|
) -> Result<ConvoOutcome, ChatError> {
|
||||||
match self {
|
match self {
|
||||||
ConvoTypeOwned::Group(group_convo) => group_convo.handle_frame(cx, enc),
|
ConvoTypeOwned::Group(group_convo) => group_convo.handle_frame(cx, enc),
|
||||||
|
ConvoTypeOwned::Direct(convo) => convo.handle_frame(cx, enc),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wakeup(&mut self, service_ctx: &mut ServiceContext<S>) -> Result<(), ChatError> {
|
fn wakeup(&mut self, service_ctx: &mut ServiceContext<S>) -> Result<(), ChatError> {
|
||||||
match self {
|
match self {
|
||||||
ConvoTypeOwned::Group(group_convo) => group_convo.wakeup(service_ctx),
|
ConvoTypeOwned::Group(group_convo) => group_convo.wakeup(service_ctx),
|
||||||
|
ConvoTypeOwned::Direct(convo) => convo.wakeup(service_ctx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ pub use thiserror::Error;
|
|||||||
|
|
||||||
use storage::StorageError;
|
use storage::StorageError;
|
||||||
|
|
||||||
|
use crate::ConversationId;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ChatError {
|
pub enum ChatError {
|
||||||
#[error("protocol error: {0:?}")]
|
#[error("protocol error: {0:?}")]
|
||||||
@ -42,6 +44,9 @@ pub enum ChatError {
|
|||||||
MlsError(#[from] MlsError),
|
MlsError(#[from] MlsError),
|
||||||
#[error("demls error: {0}")]
|
#[error("demls error: {0}")]
|
||||||
DeMlsError(#[from] ConversationError),
|
DeMlsError(#[from] ConversationError),
|
||||||
|
// Used when a core function is called with a convo_id which is unsupported
|
||||||
|
#[error("convo:{0} does not support {1}")]
|
||||||
|
UnsupportedFunction(ConversationId, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatError {
|
impl ChatError {
|
||||||
|
|||||||
@ -87,7 +87,7 @@ fn ctx_integration() {
|
|||||||
|
|
||||||
// Saro initiates conversation with Raya
|
// Saro initiates conversation with Raya
|
||||||
let mut content = vec![10];
|
let mut content = vec![10];
|
||||||
let saro_convo_id = saro.create_private_convo(&intro, &content).unwrap();
|
let saro_convo_id = saro.create_private_convo_v1(&intro, &content).unwrap();
|
||||||
|
|
||||||
// Raya receives the invite + initial message
|
// Raya receives the invite + initial message
|
||||||
let initial = recv_one(&mut raya);
|
let initial = recv_one(&mut raya);
|
||||||
@ -178,7 +178,7 @@ fn conversation_metadata_persistence() {
|
|||||||
|
|
||||||
let bundle = alice.create_intro_bundle().unwrap();
|
let bundle = alice.create_intro_bundle().unwrap();
|
||||||
let intro = Introduction::try_from(bundle.as_slice()).unwrap();
|
let intro = Introduction::try_from(bundle.as_slice()).unwrap();
|
||||||
bob.create_private_convo(&intro, b"hi").unwrap();
|
bob.create_private_convo_v1(&intro, b"hi").unwrap();
|
||||||
|
|
||||||
let result = recv_one(&mut alice);
|
let result = recv_one(&mut alice);
|
||||||
let PayloadOutcome::Inbox(io) = result else {
|
let PayloadOutcome::Inbox(io) = result else {
|
||||||
@ -219,7 +219,7 @@ fn conversation_full_flow() {
|
|||||||
|
|
||||||
let bundle = alice.create_intro_bundle().unwrap();
|
let bundle = alice.create_intro_bundle().unwrap();
|
||||||
let intro = Introduction::try_from(bundle.as_slice()).unwrap();
|
let intro = Introduction::try_from(bundle.as_slice()).unwrap();
|
||||||
let bob_convo_id = bob.create_private_convo(&intro, b"hello").unwrap();
|
let bob_convo_id = bob.create_private_convo_v1(&intro, b"hello").unwrap();
|
||||||
|
|
||||||
let result = recv_one(&mut alice);
|
let result = recv_one(&mut alice);
|
||||||
let PayloadOutcome::Inbox(io) = result else {
|
let PayloadOutcome::Inbox(io) = result else {
|
||||||
|
|||||||
43
core/integration_tests_core/tests/test_direct_v1.rs
Normal file
43
core/integration_tests_core/tests/test_direct_v1.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use integration_tests_core::TestHarness;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn happypath_roundtrip() {
|
||||||
|
let _ = tracing_subscriber::fmt()
|
||||||
|
.with_max_level(tracing::Level::INFO)
|
||||||
|
.with_test_writer()
|
||||||
|
.try_init();
|
||||||
|
|
||||||
|
const S_M1: &[u8] = b"Marco";
|
||||||
|
const R_M1: &[u8] = b"Polo";
|
||||||
|
|
||||||
|
// Initialize TestHarness with 2 clients
|
||||||
|
let mut harness = TestHarness::<2>::new(|_, _| {});
|
||||||
|
|
||||||
|
//Saro Create Convo
|
||||||
|
let particpant = harness.raya().addr();
|
||||||
|
let convo_id = harness
|
||||||
|
.saro()
|
||||||
|
.create_direct_convo_v1(&[&particpant])
|
||||||
|
.expect("saro create group");
|
||||||
|
|
||||||
|
// Carry the invite through (commit, WelcomeReady, routing to Raya's inbox,
|
||||||
|
// accept_welcome); settle until Raya has joined.
|
||||||
|
harness.process_until_label("Saro Send", |h| h.raya().convo_count() == 1);
|
||||||
|
|
||||||
|
// Saro sends a message; settle until Raya receives it.
|
||||||
|
info!(target: "chat", "Saro -> sending: {S_M1:?}");
|
||||||
|
harness
|
||||||
|
.saro()
|
||||||
|
.send_content(&convo_id, S_M1)
|
||||||
|
.expect("saro send");
|
||||||
|
|
||||||
|
harness.process_until(|h| h.raya().check(&convo_id, S_M1));
|
||||||
|
|
||||||
|
// Raya replies; settle until Saro receives it.
|
||||||
|
info!(target: "chat", "Raya -> sending:{R_M1:?}");
|
||||||
|
harness.raya().send_content(&convo_id, R_M1).unwrap();
|
||||||
|
harness.process_until(|h| h.saro().check(&convo_id, R_M1));
|
||||||
|
|
||||||
|
assert!(harness.saro().check(&convo_id, R_M1));
|
||||||
|
}
|
||||||
@ -168,7 +168,7 @@ where
|
|||||||
let intro = Introduction::try_from(intro_bundle)?;
|
let intro = Introduction::try_from(intro_bundle)?;
|
||||||
self.core
|
self.core
|
||||||
.lock()
|
.lock()
|
||||||
.create_private_convo(&intro, initial_content)
|
.create_private_convo_v1(&intro, initial_content)
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user