From afbb75d294f9132fe80e503a57e7d57e1a413839 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Fri, 26 Jun 2026 18:28:31 -0700 Subject: [PATCH] Add group description --- Cargo.lock | 12 +- core/conversations/src/conversation.rs | 6 + .../src/conversation/group_cent_info.rs | 371 ++++++++++++++++++ .../src/conversation/group_v1.rs | 5 + .../src/conversation/group_v2.rs | 6 +- .../src/conversation/mls_extensions.rs | 104 +++++ core/conversations/src/core.rs | 27 ++ core/conversations/src/inbox_v2.rs | 49 ++- core/conversations/src/types.rs | 17 + .../tests/test_group_cent_info.rs | 66 ++++ 10 files changed, 654 insertions(+), 9 deletions(-) create mode 100644 core/conversations/src/conversation/group_cent_info.rs create mode 100644 core/conversations/src/conversation/mls_extensions.rs create mode 100644 core/integration_tests_core/tests/test_group_cent_info.rs diff --git a/Cargo.lock b/Cargo.lock index 4566a19..a20fcd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2104,7 +2104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3707,7 +3707,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4993,7 +4993,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5051,7 +5051,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5661,7 +5661,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6359,7 +6359,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/core/conversations/src/conversation.rs b/core/conversations/src/conversation.rs index 6f17892..c7f75dc 100644 --- a/core/conversations/src/conversation.rs +++ b/core/conversations/src/conversation.rs @@ -1,13 +1,17 @@ mod direct_v1; +mod group_cent_info; pub mod group_v1; mod group_v2; +pub mod mls_extensions; mod privatev1; pub use crate::errors::ChatError; use crate::outcomes::ConvoOutcome; use crate::proto::EncryptedPayload; use crate::service_context::{ExternalServices, ServiceContext}; +use crate::types::ConvoMetadata; pub use direct_v1::DirectV1Convo; +pub use group_cent_info::GroupCentInfoConvo; pub use group_v1::GroupV1Convo; pub use group_v2::GroupV2Convo; pub use privatev1::PrivateV1Convo; @@ -42,6 +46,8 @@ pub(crate) trait GroupConvo: Convo + std::fmt::Debug + S cx: &mut ServiceContext, members: &[IdentIdRef], ) -> Result<(), ChatError>; + + fn metadata(&self) -> ConvoMetadata; } pub(crate) trait Identified { diff --git a/core/conversations/src/conversation/group_cent_info.rs b/core/conversations/src/conversation/group_cent_info.rs new file mode 100644 index 0000000..7a068a6 --- /dev/null +++ b/core/conversations/src/conversation/group_cent_info.rs @@ -0,0 +1,371 @@ +/// GroupV1 is a conversationType which provides effecient handling of multiple participants +/// Properties: +/// - Harvest Now Decrypt Later (HNDL) protection provided by XWING +/// - Multiple +use blake2::{Blake2b, Digest, digest::consts::U6}; +use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload}; +use chat_proto::logoschat::reliability::ReliablePayload; +use openmls::prelude::tls_codec::Deserialize; +use openmls::prelude::*; +use prost::Message as _; +use shared_traits::IdentIdRef; +use std::collections::VecDeque; +use tracing::debug; + +use crate::account_directory::{AccountDirectory, resolve_device_ids}; +use crate::conversation::ConversationIdRef; +use crate::inbox_v2::MlsProvider; +use crate::service_context::{ExternalServices, ServiceContext}; +use crate::types::ConvoMetadata; +use crate::utils::{blake2b_hex, hash_size}; +use crate::{ + DeliveryService, IdentityProvider, + conversation::{ChatError, Convo, GroupConvo, Identified}, + outcomes::{Content, ConvoOutcome}, + service_traits::KeyPackageProvider, + types::AddressedEncryptedPayload, +}; + +use super::mls_extensions::{ + ConvoMetaInfo, GROUP_METADATA_EXTENSION_TYPE, capabilities_with_group_metadata, +}; + +const OUTBOUND_HASH_CACHE_SIZE: usize = 25; + +pub struct GroupCentInfoConvo { + mls_group: MlsGroup, + convo_id: String, + // Cache outbound message Id's to filter out re-entrant messages + outbound_msgs: VecDeque, +} + +impl std::fmt::Debug for GroupCentInfoConvo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GroupCentInfoConvo") + .field("convo_id", &self.convo_id) + .field("mls_epoch", &self.mls_group.epoch()) + .finish_non_exhaustive() + } +} + +impl GroupCentInfoConvo { + // Create a new conversation with the creator as the only participant. + pub fn new( + cx: &mut ServiceContext, + name: &str, + desc: &str, + ) -> Result { + let config = Self::mls_create_config(cx, name, desc); + let mls_group = MlsGroup::new( + &cx.mls_provider, + &cx.mls_identity, + &config, + cx.mls_identity.get_credential(), + ) + .unwrap(); + let convo_id = hex::encode(mls_group.group_id().as_slice()); + Self::subscribe(&mut cx.ds, &convo_id)?; + + Ok(Self { + mls_group, + convo_id, + outbound_msgs: VecDeque::new(), + }) + } + + // Constructs a new conversation upon receiving a MlsWelcome message. + pub fn new_from_welcome( + cx: &mut ServiceContext, + welcome: Welcome, + ) -> Result { + let mls_group = + StagedWelcome::build_from_welcome(&cx.mls_provider, &Self::mls_join_config(), welcome) + .unwrap() + .build() + .unwrap() + .into_group(&cx.mls_provider) + .unwrap(); + + let convo_id = hex::encode(mls_group.group_id().as_slice()); + Self::subscribe(&mut cx.ds, &convo_id)?; + + Ok(Self { + mls_group, + convo_id, + outbound_msgs: VecDeque::new(), + }) + } + + // Configure the delivery service to listen for the required delivery addresses. + fn subscribe(ds: &mut impl DeliveryService, convo_id: &str) -> Result<(), ChatError> { + ds.subscribe(&Self::delivery_address_from_id(convo_id)) + .map_err(ChatError::generic)?; + + Ok(()) + } + + fn mls_create_config( + cx: &mut ServiceContext, + name: &str, + desc: &str, + ) -> MlsGroupCreateConfig { + let meta = ConvoMetaInfo::new(name, cx.mls_identity.id(), desc); + + let extensions = Extensions::from_vec(vec![Extension::Unknown( + GROUP_METADATA_EXTENSION_TYPE, + UnknownExtension(meta.to_extension_bytes()), + )]) + .expect("failed to create extensions"); + + MlsGroupCreateConfig::builder() + .ciphersuite(cx.mls_provider.crypto().supported_ciphersuites()[0]) + .capabilities(capabilities_with_group_metadata()) + .use_ratchet_tree_extension(true) // Embed the ratchet tree in the Welcome so joiners can build the group + .with_group_context_extensions(extensions) + .build() + } + + fn mls_join_config() -> MlsGroupJoinConfig { + MlsGroupJoinConfig::builder().build() + } + + fn delivery_address_from_id(convo_id: &str) -> String { + let hash = Blake2b::::new() + .chain_update("delivery_addr|") + .chain_update(convo_id) + .finalize(); + hex::encode(hash) + } + + fn delivery_address(&self) -> String { + Self::delivery_address_from_id(&self.convo_id) + } + + /// Resolve an account to a KeyPackage for *every* device it authorizes. + /// + /// First resolves the account to its device ids through the account + /// directory ([`resolve_device_ids`]), then fetches each device's + /// KeyPackage. When the account never published a bundle, resolution falls + /// back to a single device id equal to the account id — the pre-directory + /// behaviour — so single-device accounts are unaffected. + fn key_packages_for_account( + &self, + ident: IdentIdRef, + provider: &impl MlsProvider, + registry: &(impl KeyPackageProvider + AccountDirectory), + ) -> Result, ChatError> { + let device_ids = + resolve_device_ids(registry, ident).map_err(|e| ChatError::Generic(e.to_string()))?; + + let mut keypackages = Vec::with_capacity(device_ids.len()); + for device_id in &device_ids { + let retrieved = registry + .retrieve(device_id) + .map_err(|e| ChatError::Generic(e.to_string()))?; + let Some(keypkg_bytes) = retrieved else { + return Err(ChatError::Protocol(format!( + "no keypackage for device {device_id} of account {ident}" + ))); + }; + + let key_package_in = KeyPackageIn::tls_deserialize(&mut keypkg_bytes.as_slice())?; + let keypkg = key_package_in.validate(provider.crypto(), ProtocolVersion::Mls10)?; //TODO: P3 - Hardcoded Protocol Version + keypackages.push(keypkg); + } + Ok(keypackages) + } + + fn send_message( + &mut self, + content: &[u8], + cx: &mut ServiceContext, + ) -> Result<(), ChatError> { + let sender_id = cx.mls_identity.id().as_str(); + let reliable = cx.causal.on_send(&self.convo_id, sender_id, content); + let wire = reliable.encode_to_vec(); + + let mls_message_out = self + .mls_group + .create_message(&cx.mls_provider, &cx.mls_identity, &wire) + .unwrap(); + + let msg_bytes = mls_message_out.to_bytes().unwrap(); + self.send_payload(cx, msg_bytes) + } + + // Publish outboubound payloads to the DeliveryService + fn send_payload( + &mut self, + cx: &mut ServiceContext, + msg_bytes: Vec, + ) -> Result<(), ChatError> { + // Hash and Cache to detect inbound messages + let msg_hash = blake2b_hex::(&[&msg_bytes]); + self.outbound_msgs.push_back(msg_hash); + if self.outbound_msgs.len() > OUTBOUND_HASH_CACHE_SIZE { + let _ = self.outbound_msgs.remove(0); + } + + // Wrap in Payload frames + let aep = AddressedEncryptedPayload { + delivery_address: self.delivery_address(), + data: EncryptedPayload { + encryption: Some(encrypted_payload::Encryption::Plaintext(Plaintext { + payload: msg_bytes.into(), + })), + }, + }; + let env = aep.into_envelope(self.convo_id.clone()); + + // Send via DS + cx.ds + .publish(env) + .map_err(|e| ChatError::Delivery(e.to_string())) + } +} + +impl Identified for GroupCentInfoConvo { + fn id(&self) -> ConversationIdRef<'_> { + &self.convo_id + } +} + +impl Convo for GroupCentInfoConvo { + fn send_content( + &mut self, + cx: &mut ServiceContext, + content: &[u8], + ) -> Result<(), ChatError> { + self.send_message(content, cx) + } + + fn handle_frame( + &mut self, + cx: &mut ServiceContext, + encoded_payload: EncryptedPayload, + ) -> Result { + let bytes = match encoded_payload.encryption { + Some(encrypted_payload::Encryption::Plaintext(pt)) => pt.payload, + _ => { + return Err(ChatError::ProtocolExpectation( + "None", + "Some(Encryption::Plaintext)".into(), + )); + } + }; + + // Bail early if we sent this message + let msg_hash = blake2b_hex::(&[bytes.as_ref()]); + if self.outbound_msgs.contains(&msg_hash) { + debug!("Dropping message, sent from self"); + return Ok(ConvoOutcome::empty(self.convo_id.to_string())); + } + + let mls_message: MlsMessageIn = + MlsMessageIn::tls_deserialize_exact_bytes(&bytes).map_err(ChatError::generic)?; + + let protocol_message: ProtocolMessage = mls_message + .try_into_protocol_message() + .map_err(ChatError::generic)?; + + if protocol_message.epoch() < self.mls_group.epoch() { + // TODO: (P1) Add logging for messages arriving from past epoch. + return Ok(ConvoOutcome::empty(self.id().to_string())); + } + + let processed = self + .mls_group + .process_message(&cx.mls_provider, protocol_message) + .map_err(ChatError::generic)?; + + let cred_bytes = processed.credential().serialized_content().to_vec(); + + let content = match processed.into_content() { + ProcessedMessageContent::ApplicationMessage(msg) => { + let reliable = ReliablePayload::decode(msg.into_bytes().as_slice())?; + cx.causal.on_receive(&self.convo_id, &reliable); + Some(Content { + bytes: reliable.content.to_vec(), + encoded_credential: cred_bytes, + }) + } + ProcessedMessageContent::StagedCommitMessage(commit) => { + self.mls_group + .merge_staged_commit(&cx.mls_provider, *commit) + .map_err(ChatError::generic)?; + None + } + _ => { + // TODO: (P2) Log unknown message type + None + } + }; + Ok(ConvoOutcome { + convo_id: self.id().to_string(), + content, + }) + } + + fn wakeup(&mut self, _: &mut ServiceContext) -> Result<(), ChatError> { + Ok(()) + } +} + +impl GroupConvo for GroupCentInfoConvo { + // add_members returns: + // commit — the Commit message Alice broadcasts to all members + // welcome — the Welcome message sent privately to each new joiner + // _group_info — used for external joins; ignore for now + fn add_member( + &mut self, + cx: &mut ServiceContext, + members: &[IdentIdRef], + ) -> Result<(), ChatError> { + if members.len() > 50 { + // This is a temporary limit that originates from the the De-MLS epoch time. + return Err(ChatError::Protocol( + "Cannot add more than 50 Members at a time".into(), + )); + } + + // Resolve each account to a KeyPackage per authorized device and flatten + // them into one list — every device of every invitee becomes an MLS + // leaf, so all of a user's installations join the group. + let mut keypkgs = Vec::with_capacity(members.len()); + for ident in members { + keypkgs.extend(self.key_packages_for_account(ident, &cx.mls_provider, &cx.registry)?); + } + + let (commit, welcome, _group_info) = self + .mls_group + .add_members( + &cx.mls_provider, + &cx.mls_identity, + keypkgs.iter().as_slice(), + ) + .unwrap(); + + self.mls_group + .merge_pending_commit(&cx.mls_provider) + .unwrap(); + + for ident in members { + crate::inbox_v2::invite_user_group_cent_info(&mut cx.ds, ident, &welcome)?; + } + + self.send_payload(cx, commit.to_bytes()?) + } + + fn metadata(&self) -> ConvoMetadata { + let res = self.mls_group.extensions().iter().find_map(|ext| { + if let Extension::Unknown(ext_type, UnknownExtension(bytes)) = ext { + if *ext_type == GROUP_METADATA_EXTENSION_TYPE { + return ConvoMetaInfo::from_extension_bytes(bytes).ok(); + }; + } + None + }); + + res.map(Into::into).unwrap_or_else(ConvoMetadata::empty) + } +} diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index b562a4a..aec40ca 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -17,6 +17,7 @@ use crate::conversation::ConversationIdRef; use crate::inbox_v2::MlsProvider; use crate::service_context::{ExternalServices, ServiceContext}; +use crate::types::ConvoMetadata; use crate::utils::{blake2b_hex, hash_size}; use crate::{ DeliveryService, IdentityProvider, @@ -353,4 +354,8 @@ impl GroupConvo for GroupV1Convo { self.send_payload(cx, commit.to_bytes()?) } + + fn metadata(&self) -> ConvoMetadata { + ConvoMetadata::empty() + } } diff --git a/core/conversations/src/conversation/group_v2.rs b/core/conversations/src/conversation/group_v2.rs index 1532145..fb155af 100644 --- a/core/conversations/src/conversation/group_v2.rs +++ b/core/conversations/src/conversation/group_v2.rs @@ -2,7 +2,7 @@ // DeMLS and Libchat have different execution models, trait definitions and ownership/lifetimes of objects. // The easies path is to do a Spike to see what it would take, gather the friction points and then iterate. -use crate::types::AddressedEncryptedPayload; +use crate::types::{AddressedEncryptedPayload, ConvoMetadata}; use crate::{Content, WakeupService}; use alloy::signers::local::PrivateKeySigner; use blake2::{Blake2b, Digest, digest::consts::U6}; @@ -312,6 +312,10 @@ where Ok(()) } + fn metadata(&self) -> ConvoMetadata { + ConvoMetadata::empty() + } + // fn conversation_state(&self) -> Result { // Ok(self // .conversation diff --git a/core/conversations/src/conversation/mls_extensions.rs b/core/conversations/src/conversation/mls_extensions.rs new file mode 100644 index 0000000..83ecce2 --- /dev/null +++ b/core/conversations/src/conversation/mls_extensions.rs @@ -0,0 +1,104 @@ +use openmls::{ + extensions::ExtensionType, + prelude::{ + Capabilities, + tls_codec::{Deserialize, Error as TlsError, Serialize, Size, VLByteSlice, VLBytes}, + }, +}; +use shared_traits::{IdentId, IdentIdRef}; +use std::io::{Read, Write}; + +use crate::types::ConvoMetadata; + +pub const GROUP_METADATA_EXTENSION_TYPE: u16 = 0xFF01; + +pub fn capabilities_with_group_metadata() -> Capabilities { + Capabilities::new( + None, // default protocol versions + None, // default ciphersuites + Some(&[ExtensionType::Unknown(GROUP_METADATA_EXTENSION_TYPE)]), + None, // default proposal types + None, // default credential types + ) +} + +#[derive(Debug, Clone)] +pub struct ConvoMetaInfo { + version: u16, + name: String, + owner: IdentId, + desc: String, +} + +impl ConvoMetaInfo { + pub fn new(name: impl Into, owner: IdentIdRef, desc: impl Into) -> Self { + Self { + version: 1, + name: name.into(), + owner: owner.clone(), + desc: desc.into(), + } + } + + pub fn to_extension_bytes(&self) -> Vec { + // TLS presentation-language encoding — matches the wire format used by + // the rest of the MLS stack, so no extra serializer is pulled in. + self.tls_serialize_detached().expect("serialization failed") + } + + pub fn from_extension_bytes(bytes: &[u8]) -> Result { + Self::tls_deserialize(&mut &bytes[..]) + } +} + +// Each field is encoded as a variable-length opaque (`opaque `); `IdentId` +// and `String` aren't `tls_codec` types, so we encode/decode their UTF-8 bytes. +impl Size for ConvoMetaInfo { + fn tls_serialized_len(&self) -> usize { + self.version.tls_serialized_len() + + VLByteSlice(self.name.as_bytes()).tls_serialized_len() + + VLByteSlice(self.owner.as_str().as_bytes()).tls_serialized_len() + + VLByteSlice(self.desc.as_bytes()).tls_serialized_len() + } +} + +impl Serialize for ConvoMetaInfo { + fn tls_serialize(&self, writer: &mut W) -> Result { + let mut written = self.version.tls_serialize(writer)?; + written += VLByteSlice(self.name.as_bytes()).tls_serialize(writer)?; + written += VLByteSlice(self.owner.as_str().as_bytes()).tls_serialize(writer)?; + written += VLByteSlice(self.desc.as_bytes()).tls_serialize(writer)?; + Ok(written) + } +} + +impl Deserialize for ConvoMetaInfo { + fn tls_deserialize(bytes: &mut R) -> Result { + let version = u16::tls_deserialize(bytes)?; + let name = vl_string(bytes)?; + let owner = IdentId::new(vl_string(bytes)?); + let desc = vl_string(bytes)?; + Ok(Self { + version, + name, + owner, + desc, + }) + } +} + +fn vl_string(bytes: &mut R) -> Result { + let raw = VLBytes::tls_deserialize(bytes)?; + String::from_utf8(raw.into()) + .map_err(|_| TlsError::DecodingError("invalid utf-8 in ConvoMetaInfo".into())) +} + +impl From for ConvoMetadata { + fn from(value: ConvoMetaInfo) -> Self { + Self { + owner: value.owner.to_string(), + name: value.name, + desc: value.desc, + } + } +} diff --git a/core/conversations/src/core.rs b/core/conversations/src/core.rs index 7f2d5e4..091015c 100644 --- a/core/conversations/src/core.rs +++ b/core/conversations/src/core.rs @@ -1,8 +1,10 @@ use crate::causal_history::{CausalHistoryStore, MissingMessage}; +use crate::conversation::GroupCentInfoConvo; use crate::conversation::{ ConversationIdRef, DirectV1Convo, GroupV1Convo, GroupV2Convo, Identified, PrivateV1Convo, }; use crate::service_context::{ExternalServices, ServiceContext}; +use crate::types::ConvoMetadata; use crate::{DeliveryService, IdentityProvider, RegistrationService, WakeupService}; use crate::{ conversation::{Convo, GroupConvo}, @@ -277,6 +279,24 @@ impl<'a, S: ExternalServices + 'static> Core { Ok(convo_id) } + pub fn create_group_cent_info( + &mut self, + participants: &[IdentIdRef], + name: &str, + desc: &str, + ) -> Result { + // TODO: (P1) Ensure errors are handled properly. This is a high chance for + // desynchronized state: MlsGroup persistence, conversation persistence, and + // invite delivery all happen separately. + let mut convo = GroupCentInfoConvo::new(&mut self.services, name, desc)?; + convo.add_member(&mut self.services, participants)?; + let convo_id = convo.id().to_string(); + + self.register_convo(ConvoTypeOwned::Group(Box::new(convo)))?; + + Ok(convo_id) + } + /// Add members to an existing group conversation. pub fn group_add_member( &mut self, @@ -496,6 +516,13 @@ impl<'a, S: ExternalServices + 'static> Core { .load_conversation(convo_id)? .ok_or_else(|| ChatError::NoConvo(convo_id.into())) } + + pub fn convo_metadata(&self, convo_id: ConversationIdRef) -> ConvoMetadata { + match self.cached_convos.get(convo_id) { + Some(ConvoTypeOwned::Group(group_convo)) => group_convo.metadata(), + _ => ConvoMetadata::empty(), + } + } } enum ConvoTypeOwned { diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index e52605f..504a285 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -17,10 +17,12 @@ pub(crate) use mls_provider::MlsEphemeralPqProvider; use crate::ChatError; use crate::DeliveryService; use crate::RegistrationService; +use crate::conversation::GroupCentInfoConvo; use crate::conversation::GroupConvo; use crate::conversation::GroupV1Convo; use crate::conversation::GroupV2Convo; use crate::conversation::Identified as _; +use crate::conversation::mls_extensions::GROUP_METADATA_EXTENSION_TYPE; use crate::service_context::{ExternalServices, ServiceContext}; use crate::utils::{blake2b_hex, hash_size}; use crate::{ @@ -75,6 +77,30 @@ pub fn invite_user_v2( .map_err(ChatError::generic) } +pub fn invite_user_group_cent_info( + ds: &mut DS, + ident_id: IdentIdRef, + welcome: &MlsMessageOut, +) -> Result<(), ChatError> { + let frame = InboxV2Frame { + payload: Some(InviteType::GroupV3(welcome.to_bytes()?)), + }; + + let envelope = EnvelopeV1 { + conversation_hint: conversation_id_for(ident_id), + salt: 0, + payload: frame.encode_to_vec().into(), + }; + + let outbound_msg = AddressedEnvelope { + delivery_address: delivery_address_for(ident_id), + data: envelope.encode_to_vec(), + }; + + ds.publish(outbound_msg).map_err(ChatError::generic)?; + Ok(()) +} + /// An PQ focused Conversation initializer. /// InboxV2 Incorporates an Account based identity system to support PQ based conversation protocols /// such as MLS. @@ -144,6 +170,20 @@ impl InboxV2 { let convo = GroupV2Convo::new_from_welcome(service_ctx, &mw)?; Ok(Some(Box::new(convo))) } + InviteType::GroupV3(welcome_bytes) => { + let (msg_in, _remaining) = + MlsMessageIn::tls_deserialize_bytes(welcome_bytes.as_slice())?; + + let MlsMessageBodyIn::Welcome(welcome) = msg_in.extract() else { + return Err(ChatError::ProtocolExpectation( + "something else", + "Welcome".into(), + )); + }; + + let convo = GroupCentInfoConvo::new_from_welcome(service_ctx, welcome)?; + Ok(Some(Box::new(convo))) + } } } @@ -189,7 +229,10 @@ impl InboxV2 { ) -> Result { let capabilities = Capabilities::builder() .ciphersuites(vec![CIPHER_SUITE]) - .extensions(vec![ExtensionType::ApplicationId]) + .extensions(vec![ + ExtensionType::ApplicationId, + ExtensionType::Unknown(GROUP_METADATA_EXTENSION_TYPE), + ]) .build(); let a = KeyPackage::builder() .leaf_node_capabilities(capabilities) @@ -279,7 +322,7 @@ impl InboxV2 { #[derive(Clone, PartialEq, Message)] pub struct InboxV2Frame { - #[prost(oneof = "InviteType", tags = "1, 2")] + #[prost(oneof = "InviteType", tags = "1, 2, 3")] pub payload: Option, } @@ -289,6 +332,8 @@ pub enum InviteType { GroupV1(GroupV1HeavyInvite), #[prost(bytes, tag = "2")] GroupV2(Vec), + #[prost(bytes, tag = "3")] + GroupV3(Vec), } #[derive(Clone, PartialEq, Message)] diff --git a/core/conversations/src/types.rs b/core/conversations/src/types.rs index 1433425..a092b3b 100644 --- a/core/conversations/src/types.rs +++ b/core/conversations/src/types.rs @@ -66,3 +66,20 @@ impl AddressedEncryptedPayload { ) } } + +#[derive(Debug)] +pub struct ConvoMetadata { + pub owner: String, + pub name: String, + pub desc: String, +} + +impl ConvoMetadata { + pub fn empty() -> Self { + Self { + owner: String::new(), + name: String::new(), + desc: String::new(), + } + } +} diff --git a/core/integration_tests_core/tests/test_group_cent_info.rs b/core/integration_tests_core/tests/test_group_cent_info.rs new file mode 100644 index 0000000..a40ecc0 --- /dev/null +++ b/core/integration_tests_core/tests/test_group_cent_info.rs @@ -0,0 +1,66 @@ +use integration_tests_core::TestHarness; +use std::time::Duration; + +#[test] +fn dev() { + let _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_test_writer() + .try_init(); + + let mut harness = TestHarness::<3>::new(|_, _| {}); + + let raya_id = harness.raya().ident_id().clone(); + let pax_id = harness.pax().ident_id().clone(); + + const M_R1: &[u8; 12] = b"Hi From Raya"; + const M_P1: &[u8; 13] = b"Hey it's Pax!"; + + // Step: Saro Create Convo with Raya + + let convo_id = harness + .saro() + .create_group_cent_info( + &[&raya_id], + "Saro<>Raya", + "this is a DM Between Raya and Saro", + ) + .expect("Saro invite Raya "); + harness.process_until(|h| h.raya().list_conversations().unwrap().len() == 1); + + // Step: Raya Send Content + + println!("{:?}", harness.raya().convo_metadata(&convo_id)); + + harness + .raya() + .send_content(&convo_id, M_R1) + .expect("Raya send Msg"); + + harness.process_until(|h| h.saro().received_messages().len() == 1); + + // Step: Saro add Pax + + harness + .saro() + .group_add_member(&convo_id, &[&pax_id]) + .expect("Saro invite pax"); + harness.process_until(|h| h.pax().list_conversations().unwrap().len() == 1); + + // Step: Pax send Content + + harness + .pax() + .send_content(&convo_id, M_P1) + .expect("Pax send"); + harness.process(Duration::from_millis(500)); + + assert!(harness.saro().check(&convo_id, M_R1)); + assert!(harness.saro().check(&convo_id, M_P1)); + + assert!(!harness.raya().check(&convo_id, M_R1)); + assert!(harness.raya().check(&convo_id, M_P1)); + + assert!(!harness.pax().check(&convo_id, M_R1)); + assert!(!harness.pax().check(&convo_id, M_P1)); +}