Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutable Metadata group name Part 2 #644

Merged
merged 11 commits into from
Apr 19, 2024
26 changes: 26 additions & 0 deletions bindings_ffi/src/mls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,32 @@ impl FfiGroup {
Ok(())
}

pub async fn update_group_name(&self, group_name: String) -> Result<(), GenericError> {
let group = MlsGroup::new(
self.inner_client.as_ref(),
self.group_id.clone(),
self.created_at_ns,
self.added_by_address.clone(),
);

group.update_group_name(group_name).await?;

Ok(())
}

pub fn group_name(&self) -> Result<String, GenericError> {
let group = MlsGroup::new(
self.inner_client.as_ref(),
self.group_id.clone(),
self.created_at_ns,
self.added_by_address.clone(),
);

let group_name = group.group_name()?;

Ok(group_name)
}

pub async fn stream(
&self,
message_callback: Box<dyn FfiMessageCallback>,
Expand Down
17 changes: 17 additions & 0 deletions xmtp_mls/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,20 @@ const NANOSECONDS_IN_HOUR: i64 = 3_600_000_000_000;
pub const UPDATE_INSTALLATIONS_INTERVAL_NS: i64 = NANOSECONDS_IN_HOUR / 2; // 30 min

pub const MAX_GROUP_SIZE: u8 = 250;

/// MLS Extension Types
///
/// Copied from draft-ietf-mls-protocol-16:
///
/// | Value | Name | Message(s) | Recommended | Reference |
/// |:-----------------|:-------------------------|:-----------|:------------|:----------|
/// | 0x0000 | RESERVED | N/A | N/A | RFC XXXX |
/// | 0x0001 | application_id | LN | Y | RFC XXXX |
/// | 0x0002 | ratchet_tree | GI | Y | RFC XXXX |
/// | 0x0003 | required_capabilities | GC | Y | RFC XXXX |
/// | 0x0004 | external_pub | GI | Y | RFC XXXX |
/// | 0x0005 | external_senders | GC | Y | RFC XXXX |
/// | 0xff00 - 0xffff | Reserved for Private Use | N/A | N/A | RFC XXXX |
pub const MUTABLE_METADATA_EXTENSION_ID: u16 = 0xff00;

pub const DEFAULT_GROUP_NAME: &str = "New Group";
22 changes: 15 additions & 7 deletions xmtp_mls/src/groups/group_mutable_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

use xmtp_proto::xmtp::mls::message_contents::GroupMutableMetadataV1 as GroupMutableMetadataProto;

use crate::configuration::MUTABLE_METADATA_EXTENSION_ID;

#[derive(Debug, Error)]
pub enum GroupMutableMetadataError {
#[error("serialization: {0}")]
Expand All @@ -24,9 +26,7 @@

impl GroupMutableMetadata {
pub fn new(group_name: String) -> Self {
Self {
group_name
}
Self { group_name }
}
}

Expand All @@ -36,7 +36,7 @@
fn try_from(value: GroupMutableMetadata) -> Result<Self, Self::Error> {
let mut buf = Vec::new();
let proto_val = GroupMutableMetadataProto {
group_name: value.group_name.clone()
group_name: value.group_name.clone(),
};
proto_val.encode(&mut buf)?;

Expand All @@ -49,28 +49,36 @@

fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
let proto_val = GroupMutableMetadataProto::decode(value.as_slice())?;
Self::from_proto(proto_val)
Self::try_from(proto_val)
}
}

impl TryFrom<GroupMutableMetadataProto> for GroupMutableMetadata {
type Error = GroupMutableMetadataError;

fn try_from(value: GroupMutableMetadataProto) -> Result<Self, Self::Error> {
Self::new(value.group_name.clone())
Ok(Self::new(value.group_name.clone()))
}
}

pub fn extract_group_mutable_metadata(
group: &OpenMlsGroup,
) -> Result<GroupMutableMetadata, GroupMutableMetadataError> {
todo!()
let extensions = group.export_group_context().extensions();
for extension in extensions.iter() {
if let Extension::Unknown(MUTABLE_METADATA_EXTENSION_ID, UnknownExtension(meta_data)) =
extension
{
return GroupMutableMetadata::try_from(meta_data);
}
}
Err(GroupMutableMetadataError::MissingExtension)
}

#[cfg(test)]
mod tests {

use super::*;

Check warning on line 81 in xmtp_mls/src/groups/group_mutable_metadata.rs

View workflow job for this annotation

GitHub Actions / Test

unused import: `super::*`
#[test]
fn test_preconfigured_mutable_metadata() {
// TODO add test here
Expand Down
4 changes: 1 addition & 3 deletions xmtp_mls/src/groups/intents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use openmls::prelude::{
tls_codec::{Error as TlsCodecError, Serialize},
MlsMessageOut,
};
use prost::{DecodeError, Message};
use prost::{bytes::Bytes, DecodeError, Message};
use thiserror::Error;

use xmtp_proto::xmtp::mls::database::{
Expand Down Expand Up @@ -251,8 +251,6 @@ impl From<UpdateMetadataIntentData> for Vec<u8> {
}
}

use prost::bytes::Bytes;

impl TryFrom<Vec<u8>> for UpdateMetadataIntentData {
type Error = IntentError;

Expand Down
163 changes: 149 additions & 14 deletions xmtp_mls/src/groups/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod group_metadata;
pub mod group_mutable_metadata;
mod group_permissions;
mod intents;
mod members;
Expand All @@ -9,13 +10,18 @@ pub mod validated_commit;

use intents::SendMessageIntentData;
use openmls::{
credentials::BasicCredential,
credentials::{BasicCredential, CredentialType},
error::LibraryError,
extensions::{Extension, Extensions, Metadata},
group::{MlsGroupCreateConfig, MlsGroupJoinConfig},
extensions::{
Extension, ExtensionType, Extensions, Metadata, RequiredCapabilitiesExtension,
UnknownExtension,
},
group::{CreateGroupContextExtProposalError, MlsGroupCreateConfig, MlsGroupJoinConfig},
messages::proposals::ProposalType,
prelude::{
BasicCredentialError, CredentialWithKey, CryptoConfig, Error as TlsCodecError, GroupId,
MlsGroup as OpenMlsGroup, StagedWelcome, Welcome as MlsWelcome, WireFormatPolicy,
BasicCredentialError, Capabilities, CredentialWithKey, CryptoConfig,
Error as TlsCodecError, GroupId, MlsGroup as OpenMlsGroup, StagedWelcome,
Welcome as MlsWelcome, WireFormatPolicy,
},
};
use openmls_traits::OpenMlsProvider;
Expand All @@ -25,17 +31,27 @@ use thiserror::Error;
use xmtp_cryptography::signature::is_valid_ed25519_public_key;
use xmtp_proto::{
api_client::XmtpMlsClient,
xmtp::mls::api::v1::{
group_message::{Version as GroupMessageVersion, V1 as GroupMessageV1},
GroupMessage,
xmtp::mls::{
api::v1::{
group_message::{Version as GroupMessageVersion, V1 as GroupMessageV1},
GroupMessage,
},
message_contents::{
plaintext_envelope::{Content, V1},
GroupMutableMetadataV1, PlaintextEnvelope,
},
},
xmtp::mls::message_contents::plaintext_envelope::{Content, V1},
xmtp::mls::message_contents::PlaintextEnvelope,
};

use self::group_metadata::extract_group_metadata;
pub use self::group_permissions::PreconfiguredPolicies;
pub use self::intents::{AddressesOrInstallationIds, IntentError};
use self::{
group_metadata::extract_group_metadata,
group_mutable_metadata::{
extract_group_mutable_metadata, GroupMutableMetadata, GroupMutableMetadataError,
},
intents::UpdateMetadataIntentData,
};
use self::{
group_metadata::{ConversationType, GroupMetadata, GroupMetadataError},
group_permissions::PolicySet,
Expand All @@ -45,7 +61,9 @@ use self::{

use crate::{
client::{deserialize_welcome, ClientError, MessageProcessingError},
configuration::{CIPHERSUITE, MAX_GROUP_SIZE},
configuration::{
CIPHERSUITE, DEFAULT_GROUP_NAME, MAX_GROUP_SIZE, MUTABLE_METADATA_EXTENSION_ID,
},
hpke::{decrypt_welcome, HpkeError},
identity::{Identity, IdentityError},
retry::RetryableError,
Expand Down Expand Up @@ -113,6 +131,8 @@ pub enum GroupError {
CommitValidation(#[from] CommitValidationError),
#[error("Metadata error {0}")]
GroupMetadata(#[from] GroupMetadataError),
#[error("Mutable Metadata error {0}")]
GroupMutableMetadata(#[from] GroupMutableMetadataError),
#[error("Errors occurred during sync {0:?}")]
Sync(Vec<GroupError>),
#[error("Hpke error: {0}")]
Expand All @@ -121,6 +141,8 @@ pub enum GroupError {
Identity(#[from] IdentityError),
#[error("serialization error: {0}")]
EncodeError(#[from] prost::EncodeError),
#[error("create group context proposal error: {0}")]
CreateGroupContextExtProposalError(#[from] CreateGroupContextExtProposalError<StorageError>),
#[error("Credential error")]
CredentialError(#[from] BasicCredentialError),
#[error("LeafNode error")]
Expand Down Expand Up @@ -202,7 +224,8 @@ where
&client.identity,
permissions.unwrap_or_default().to_policy_set(),
)?;
let group_config = build_group_config(protected_metadata)?;
let mutable_metadata = build_mutable_metadata_extension(DEFAULT_GROUP_NAME.to_string())?;
let group_config = build_group_config(protected_metadata, mutable_metadata)?;

let mut mls_group = OpenMlsGroup::new(
&provider,
Expand Down Expand Up @@ -420,6 +443,25 @@ where
self.sync_until_intent_resolved(conn, intent.id).await
}

pub async fn update_group_name(&self, group_name: String) -> Result<(), GroupError> {
let conn = &mut self.client.store.conn()?;
let intent_data: Vec<u8> = UpdateMetadataIntentData::new(group_name).into();
let intent = conn.insert_group_intent(NewGroupIntent::new(
IntentKind::MetadataUpdate,
self.group_id.clone(),
intent_data,
))?;

self.sync_until_intent_resolved(conn, intent.id).await
}

// Query the database for stored messages. Optionally filtered by time, kind, delivery_status
// and limit
pub fn group_name(&self) -> Result<String, GroupError> {
let mutable_metadata = self.mutable_metadata()?;
Ok(mutable_metadata.group_name)
}

// Used in tests
#[allow(dead_code)]
pub(crate) async fn remove_members_by_installation_id(
Expand Down Expand Up @@ -462,6 +504,14 @@ where

Ok(extract_group_metadata(&mls_group)?)
}

pub fn mutable_metadata(&self) -> Result<GroupMutableMetadata, GroupError> {
let conn = &self.client.store.conn()?;
let provider = XmtpOpenMlsProvider::new(conn);
let mls_group = self.load_mls_group(&provider)?;

Ok(extract_group_mutable_metadata(&mls_group)?)
}
}

fn extract_message_v1(message: GroupMessage) -> Result<GroupMessageV1, MessageProcessingError> {
Expand Down Expand Up @@ -507,13 +557,55 @@ fn build_protected_metadata_extension(
Ok(Extension::ImmutableMetadata(protected_metadata))
}

pub fn build_mutable_metadata_extension(group_name: String) -> Result<Extension, GroupError> {
let mutable_metadata: GroupMutableMetadataV1 = GroupMutableMetadataV1 { group_name };

let unknown_gc_extension = UnknownExtension(mutable_metadata.encode_to_vec());

Ok(Extension::Unknown(
MUTABLE_METADATA_EXTENSION_ID,
unknown_gc_extension,
))
}

fn build_group_config(
protected_metadata_extension: Extension,
mutable_metadata_extension: Extension,
) -> Result<MlsGroupCreateConfig, GroupError> {
let extensions = Extensions::single(protected_metadata_extension);
let required_extension_types = &[
ExtensionType::Unknown(MUTABLE_METADATA_EXTENSION_ID),
ExtensionType::ImmutableMetadata,
ExtensionType::LastResort,
ExtensionType::ApplicationId,
];

let required_proposal_types = &[ProposalType::GroupContextExtensions];

let capabilities = Capabilities::new(
None,
None,
Some(required_extension_types),
Some(required_proposal_types),
None,
);
let credentials = &[CredentialType::Basic];

let required_capabilities =
Extension::RequiredCapabilities(RequiredCapabilitiesExtension::new(
required_extension_types,
required_proposal_types,
credentials,
));

let extensions = Extensions::from_vec(vec![
protected_metadata_extension,
mutable_metadata_extension,
required_capabilities,
])?;

Ok(MlsGroupCreateConfig::builder()
.with_group_context_extensions(extensions)?
.capabilities(capabilities)
.crypto_config(CryptoConfig::with_default_version(CIPHERSUITE))
.wire_format_policy(WireFormatPolicy::default())
.max_past_epochs(3) // Trying with 3 max past epochs for now
Expand Down Expand Up @@ -1005,6 +1097,49 @@ mod tests {
.is_err(),);
}

#[tokio::test]
async fn test_group_mutable_data() {
let amal = ClientBuilder::new_test_client(&generate_local_wallet()).await;
let bola = ClientBuilder::new_test_client(&generate_local_wallet()).await;

// Create a group and verify it has the default group name
let policies = Some(PreconfiguredPolicies::GroupCreatorIsAdmin);
let amal_group: MlsGroup<_> = amal.create_group(policies).unwrap();
amal_group.sync().await.unwrap();

let group_mutable_metadata = amal_group.mutable_metadata().unwrap();
assert!(group_mutable_metadata.group_name.eq("New Group"));

// Add bola to the group
amal_group
.add_members(vec![bola.account_address()])
.await
.unwrap();
bola.sync_welcomes().await.unwrap();
let bola_groups = bola.find_groups(None, None, None, None).unwrap();
assert_eq!(bola_groups.len(), 1);
let bola_group = bola_groups.first().unwrap();
bola_group.sync().await.unwrap();
let group_mutable_metadata = bola_group.mutable_metadata().unwrap();
assert!(group_mutable_metadata.group_name.eq("New Group"));

// Update group name
amal_group
.update_group_name("New Group Name 1".to_string())
.await
.unwrap();

// Verify amal group sees update
amal_group.sync().await.unwrap();
let amal_group_name: String = amal_group.mutable_metadata().expect("msg").group_name;
assert_eq!(amal_group_name, "New Group Name 1");

// Verify bola group sees update
bola_group.sync().await.unwrap();
let bola_group_name: String = bola_group.mutable_metadata().expect("msg").group_name;
assert_eq!(bola_group_name, "New Group Name 1");
}

#[tokio::test]
async fn test_staged_welcome() {
// Create Clients
Expand Down
Loading
Loading