1use tls_codec::Serialize;
9use xmtp_configuration::ApplicationComponentDefinition;
10use xmtp_proto::types::ConversationType;
11use xmtp_proto::xmtp::mls::message_contents::{
12 ComponentMetadata, ComponentPermissions, ComponentType,
13 MembershipPolicy as MembershipPolicyProto, MetadataPolicy as MetadataPolicyProto,
14 PermissionsUpdatePolicy as PermissionsUpdatePolicyProto, PolicySet as PolicySetProto,
15 membership_policy::{BasePolicy as MembershipBasePolicy, Kind as MembershipPolicyKind},
16 metadata_policy::{
17 AndCondition as MetadataAndCondition, AnyCondition as MetadataAnyCondition,
18 Kind as MetadataPolicyKind, MetadataBasePolicy,
19 },
20 permissions_update_policy::{Kind as PermissionsPolicyKind, PermissionsBasePolicy},
21};
22
23use crate::{
24 app_data::{
25 component_id::ComponentId,
26 component_registry::{ComponentRegistry, ComponentRegistryError, new_component_metadata},
27 migration::MigrationError,
28 },
29 group_mutable_metadata::MetadataField,
30 inbox_id::InboxId,
31 tls_set::TlsSetDelta,
32};
33
34pub fn synthesize_registry_from_policy_set(
57 policy_set: &PolicySetProto,
58) -> Result<ComponentRegistry, MigrationError> {
59 build_registry(policy_set, true, true)
64}
65
66pub(crate) fn build_registry(
67 policy_set: &PolicySetProto,
68 include_dm_members: bool,
69 include_oneshot_message: bool,
70) -> Result<ComponentRegistry, MigrationError> {
71 let mut registry = ComponentRegistry::new();
72
73 let add_member = policy_set
75 .add_member_policy
76 .as_ref()
77 .ok_or(MigrationError::MissingPolicyField("add_member_policy"))?;
78 let remove_member = policy_set
79 .remove_member_policy
80 .as_ref()
81 .ok_or(MigrationError::MissingPolicyField("remove_member_policy"))?;
82 let add_admin = policy_set
83 .add_admin_policy
84 .as_ref()
85 .ok_or(MigrationError::MissingPolicyField("add_admin_policy"))?;
86 let remove_admin = policy_set
87 .remove_admin_policy
88 .as_ref()
89 .ok_or(MigrationError::MissingPolicyField("remove_admin_policy"))?;
90 policy_set
91 .update_permissions_policy
92 .as_ref()
93 .ok_or(MigrationError::MissingPolicyField(
94 "update_permissions_policy",
95 ))?;
96
97 let known: std::collections::HashSet<&'static str> = metadata_field_registry_mapping()
101 .iter()
102 .map(|(f, _, _)| f.as_str())
103 .collect();
104 for key in policy_set.update_metadata_policy.keys() {
105 if !known.contains(key.as_str()) {
106 return Err(MigrationError::UnknownMetadataField(key.clone()));
107 }
108 }
109
110 for (field, component_id, component_type) in metadata_field_registry_mapping() {
126 let default_base = if matches!(field, MetadataField::MinimumSupportedProtocolVersion) {
127 MetadataBasePolicy::AllowIfSuperAdmin
128 } else {
129 MetadataBasePolicy::AllowIfAdmin
130 };
131 let policy = policy_set
132 .update_metadata_policy
133 .get(field.as_str())
134 .cloned()
135 .unwrap_or_else(|| metadata_policy(default_base));
136
137 registry.set(
138 *component_id,
139 new_component_metadata(
140 ComponentPermissions {
141 insert_policy: Some(policy.clone()),
142 update_policy: Some(policy),
143 delete_policy: Some(metadata_policy(MetadataBasePolicy::AllowIfSuperAdmin)),
144 },
145 *component_type,
146 ),
147 )?;
148 }
149
150 let super_admin = metadata_policy(MetadataBasePolicy::AllowIfSuperAdmin);
163 registry.set(
164 ComponentId::COMMIT_LOG_SIGNER,
165 new_component_metadata(
166 ComponentPermissions {
167 insert_policy: Some(super_admin.clone()),
168 update_policy: Some(super_admin.clone()),
169 delete_policy: Some(super_admin.clone()),
170 },
171 ComponentType::Bytes,
172 ),
173 )?;
174
175 let admin_policy = admin_list_policy_to_metadata_policy(add_admin)?;
177 let remove_admin_policy = admin_list_policy_to_metadata_policy(remove_admin)?;
178 registry.set(
179 ComponentId::ADMIN_LIST,
180 new_component_metadata(
181 ComponentPermissions {
182 insert_policy: Some(admin_policy.clone()),
183 update_policy: Some(admin_policy),
184 delete_policy: Some(remove_admin_policy),
185 },
186 ComponentType::TlsSetInboxId,
187 ),
188 )?;
189
190 registry.set(
192 ComponentId::GROUP_MEMBERSHIP,
193 new_component_metadata(
194 ComponentPermissions {
195 insert_policy: Some(membership_policy_to_metadata_policy(add_member)?),
196 update_policy: Some(metadata_policy(MetadataBasePolicy::Allow)),
197 delete_policy: Some(membership_policy_to_metadata_policy(remove_member)?),
198 },
199 ComponentType::TlsMapInboxIdBytes,
200 ),
201 )?;
202
203 let immutable_permissions = ComponentPermissions {
210 insert_policy: Some(metadata_policy(MetadataBasePolicy::AllowIfSuperAdmin)),
211 update_policy: Some(metadata_policy(MetadataBasePolicy::Deny)),
212 delete_policy: Some(metadata_policy(MetadataBasePolicy::Deny)),
213 };
214 for id in [
215 ComponentId::CONVERSATION_TYPE,
216 ComponentId::CREATOR_INBOX_ID,
217 ] {
218 registry.set(
219 id,
220 new_component_metadata(immutable_permissions.clone(), ComponentType::Bytes),
221 )?;
222 }
223 if include_oneshot_message {
224 registry.set(
225 ComponentId::ONESHOT_MESSAGE,
226 new_component_metadata(immutable_permissions.clone(), ComponentType::Bytes),
227 )?;
228 }
229 if include_dm_members {
230 registry.set(
231 ComponentId::DM_MEMBERS,
232 new_component_metadata(immutable_permissions, ComponentType::TlsSetInboxId),
233 )?;
234 }
235
236 Ok(registry)
237}
238
239pub(crate) fn metadata_field_registry_mapping()
256-> &'static [(MetadataField, ComponentId, ComponentType)] {
257 &[
258 (
259 MetadataField::GroupName,
260 ComponentId::GROUP_NAME,
261 ComponentType::String,
262 ),
263 (
264 MetadataField::Description,
265 ComponentId::GROUP_DESCRIPTION,
266 ComponentType::String,
267 ),
268 (
269 MetadataField::GroupImageUrlSquare,
270 ComponentId::GROUP_IMAGE_URL,
271 ComponentType::String,
272 ),
273 (
274 MetadataField::MessageDisappearFromNS,
275 ComponentId::MESSAGE_DISAPPEAR_FROM_NS,
276 ComponentType::Bytes,
277 ),
278 (
279 MetadataField::MessageDisappearInNS,
280 ComponentId::MESSAGE_DISAPPEAR_IN_NS,
281 ComponentType::Bytes,
282 ),
283 (
284 MetadataField::AppData,
285 ComponentId::APP_DATA,
286 ComponentType::String,
287 ),
288 (
289 MetadataField::MinimumSupportedProtocolVersion,
290 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION,
291 ComponentType::String,
292 ),
293 ]
294}
295
296pub(crate) fn metadata_policy(base: MetadataBasePolicy) -> MetadataPolicyProto {
297 MetadataPolicyProto {
298 kind: Some(MetadataPolicyKind::Base(base as i32)),
299 }
300}
301
302pub(crate) fn admin_list_policy_to_metadata_policy(
309 p: &PermissionsUpdatePolicyProto,
310) -> Result<MetadataPolicyProto, MigrationError> {
311 match &p.kind {
312 Some(PermissionsPolicyKind::Base(base)) => match PermissionsBasePolicy::try_from(*base) {
313 Ok(PermissionsBasePolicy::AllowIfAdmin) => {
314 Ok(metadata_policy(MetadataBasePolicy::AllowIfAdmin))
315 }
316 Ok(PermissionsBasePolicy::AllowIfSuperAdmin) => {
317 Ok(metadata_policy(MetadataBasePolicy::AllowIfSuperAdmin))
318 }
319 Ok(PermissionsBasePolicy::Deny) => Ok(metadata_policy(MetadataBasePolicy::Deny)),
320 _ => Err(MigrationError::NonConstrainedAdminPolicy(Some(*base))),
321 },
322 Some(PermissionsPolicyKind::AndCondition(_))
323 | Some(PermissionsPolicyKind::AnyCondition(_))
324 | None => Err(MigrationError::NonConstrainedAdminPolicy(None)),
325 }
326}
327
328pub(crate) fn membership_policy_to_metadata_policy(
335 p: &MembershipPolicyProto,
336) -> Result<MetadataPolicyProto, MigrationError> {
337 match &p.kind {
338 Some(MembershipPolicyKind::Base(base)) => {
339 let mapped = match MembershipBasePolicy::try_from(*base) {
340 Ok(MembershipBasePolicy::Allow) => MetadataBasePolicy::Allow,
341 Ok(MembershipBasePolicy::Deny) => MetadataBasePolicy::Deny,
342 Ok(MembershipBasePolicy::AllowIfAdminOrSuperAdmin) => {
343 MetadataBasePolicy::AllowIfAdmin
344 }
345 Ok(MembershipBasePolicy::AllowIfSuperAdmin) => {
346 MetadataBasePolicy::AllowIfSuperAdmin
347 }
348 _ => return Err(MigrationError::UnknownMembershipPolicy(Some(*base))),
349 };
350 Ok(metadata_policy(mapped))
351 }
352 Some(MembershipPolicyKind::AndCondition(condition)) if !condition.policies.is_empty() => {
353 Ok(MetadataPolicyProto {
354 kind: Some(MetadataPolicyKind::AndCondition(MetadataAndCondition {
355 policies: condition
356 .policies
357 .iter()
358 .map(membership_policy_to_metadata_policy)
359 .collect::<Result<_, _>>()?,
360 })),
361 })
362 }
363 Some(MembershipPolicyKind::AnyCondition(condition)) if !condition.policies.is_empty() => {
364 Ok(MetadataPolicyProto {
365 kind: Some(MetadataPolicyKind::AnyCondition(MetadataAnyCondition {
366 policies: condition
367 .policies
368 .iter()
369 .map(membership_policy_to_metadata_policy)
370 .collect::<Result<_, _>>()?,
371 })),
372 })
373 }
374 _ => Err(MigrationError::UnknownMembershipPolicy(None)),
375 }
376}
377
378pub fn encode_metadata_attribute_value(
386 component_id: ComponentId,
387 legacy_value: &str,
388) -> Result<Vec<u8>, MigrationError> {
389 match component_id {
390 ComponentId::COMMIT_LOG_SIGNER => {
391 let raw = hex::decode(legacy_value).map_err(|error| {
392 MigrationError::InvalidCommitLogSignerHex {
393 reason: error.to_string(),
394 }
395 })?;
396 let expected = xmtp_cryptography::configuration::ED25519_KEY_LENGTH;
397 if raw.len() != expected {
398 return Err(MigrationError::InvalidCommitLogSignerLength {
399 expected,
400 actual: raw.len(),
401 });
402 }
403 Ok(raw)
404 }
405 ComponentId::MESSAGE_DISAPPEAR_FROM_NS => {
406 parse_disappearing_i64("messageDisappearFromNS", legacy_value)
407 }
408 ComponentId::MESSAGE_DISAPPEAR_IN_NS => {
409 parse_disappearing_i64("messageDisappearInNS", legacy_value)
410 }
411 _ => Ok(legacy_value.as_bytes().to_vec()),
412 }
413}
414
415pub(crate) fn parse_disappearing_i64(
416 field: &'static str,
417 legacy_value: &str,
418) -> Result<Vec<u8>, MigrationError> {
419 let n: i64 = legacy_value
420 .parse()
421 .map_err(
422 |err: std::num::ParseIntError| MigrationError::InvalidDisappearingTimestamp {
423 field,
424 value: legacy_value.to_string(),
425 reason: err.to_string(),
426 },
427 )?;
428 Ok(n.to_be_bytes().to_vec())
429}
430
431pub(crate) fn encode_inbox_id_set(inbox_ids_hex: &[String]) -> Result<Vec<u8>, MigrationError> {
441 let ids: Vec<InboxId> = inbox_ids_hex
442 .iter()
443 .map(|s| InboxId::from_hex(s))
444 .collect::<Result<Vec<_>, _>>()?;
445 let mut sorted_ids: Vec<InboxId> = ids;
450 sorted_ids.sort();
451 sorted_ids.dedup();
452 let mut delta: TlsSetDelta<InboxId> = TlsSetDelta::new();
453 for id in sorted_ids {
454 delta = delta.insert(id);
455 }
456 Ok(delta.tls_serialize_detached()?)
457}
458
459pub(crate) fn encode_dm_members(
464 dm: &crate::group_metadata::DmMembers<xmtp_id::InboxId>,
465) -> Result<Vec<u8>, MigrationError> {
466 let one_str: &str = dm.member_one_inbox_id.as_ref();
472 let two_str: &str = dm.member_two_inbox_id.as_ref();
473 let one = InboxId::from_hex(one_str)?;
474 let two = InboxId::from_hex(two_str)?;
475 if one == two {
476 return Err(MigrationError::DmMembersSelfReference(format!(
480 "{} (member_one={one_str}, member_two={two_str})",
481 one.to_hex(),
482 )));
483 }
484 let (a, b) = if one <= two { (one, two) } else { (two, one) };
486 let delta = TlsSetDelta::<InboxId>::new().insert(a).insert(b);
487 Ok(delta.tls_serialize_detached()?)
488}
489
490pub(crate) fn encode_conversation_type(value: i32) -> Vec<u8> {
494 value.to_be_bytes().to_vec()
495}
496
497#[cfg(test)]
507pub(crate) fn decode_conversation_type(bytes: &[u8]) -> Result<i32, MigrationError> {
508 let arr: [u8; 4] = bytes
509 .try_into()
510 .map_err(|_| MigrationError::ConversationTypePayloadLength(bytes.len()))?;
511 Ok(i32::from_be_bytes(arr))
512}
513
514pub fn catalogue_registry_entries(
529 catalogue: &[ApplicationComponentDefinition],
530 conversation_type: ConversationType,
531) -> impl Iterator<Item = (ComponentId, ComponentMetadata)> + '_ {
532 let mut seen = std::collections::HashSet::new();
533 catalogue
534 .iter()
535 .filter(move |definition| seen.insert(definition.component_id))
536 .filter(|definition| definition.validate().is_ok())
537 .filter(move |definition| match conversation_type {
538 ConversationType::Dm => definition.in_dms,
539 ConversationType::Sync => false,
540 ConversationType::Group | ConversationType::Oneshot => definition.in_groups,
541 })
542 .map(|definition| {
543 (
544 ComponentId::new(definition.component_id),
545 ComponentMetadata {
546 permissions: Some(definition.permissions.clone().into()),
547 component_type: definition.component_type,
548 external_committer_permissions: None,
549 },
550 )
551 })
552 .filter(|(_, metadata)| is_known_metadata(metadata))
553}
554
555fn is_known_metadata(metadata: &ComponentMetadata) -> bool {
558 fn is_known(policy: &MetadataPolicyProto) -> bool {
559 match &policy.kind {
560 Some(MetadataPolicyKind::Base(base)) => MetadataBasePolicy::try_from(*base)
561 .is_ok_and(|base| base != MetadataBasePolicy::Unspecified),
562 Some(MetadataPolicyKind::AndCondition(MetadataAndCondition { policies }))
563 | Some(MetadataPolicyKind::AnyCondition(MetadataAnyCondition { policies })) => {
564 !policies.is_empty() && policies.iter().all(is_known)
565 }
566 None => false,
567 }
568 }
569 ComponentType::try_from(metadata.component_type)
570 .is_ok_and(|component_type| component_type != ComponentType::Unspecified)
571 && metadata.permissions.as_ref().is_some_and(|permissions| {
572 [
573 &permissions.insert_policy,
574 &permissions.update_policy,
575 &permissions.delete_policy,
576 ]
577 .into_iter()
578 .all(|policy| policy.as_ref().is_some_and(is_known))
579 })
580}
581
582fn register_configured_fields(
588 registry: &mut ComponentRegistry,
589 conversation_type: ConversationType,
590 catalogue: &[ApplicationComponentDefinition],
591) -> Result<(), MigrationError> {
592 if conversation_type == ConversationType::Sync {
593 return Ok(());
594 }
595 let self_owned = metadata_policy(MetadataBasePolicy::AllowIfSelfOrNonMember);
596 registry.set(
597 ComponentId::USER_DISPLAY_NAME,
598 new_component_metadata(
599 ComponentPermissions {
600 insert_policy: Some(self_owned.clone()),
601 update_policy: Some(self_owned.clone()),
602 delete_policy: Some(self_owned),
603 },
604 ComponentType::TlsMapInboxIdString,
605 ),
606 )?;
607 if conversation_type != ConversationType::Dm {
608 let image_url = registry.get(&ComponentId::GROUP_IMAGE_URL)?.ok_or(
609 ComponentRegistryError::NotFound(ComponentId::GROUP_IMAGE_URL),
610 )?;
611 registry.set(
612 ComponentId::GROUP_IMAGE,
613 ComponentMetadata {
614 component_type: ComponentType::Bytes as i32,
615 ..image_url
616 },
617 )?;
618 }
619 catalogue_registry_entries(catalogue, conversation_type)
620 .try_for_each(|(id, metadata)| registry.set(id, metadata))?;
621 Ok(())
622}
623
624pub enum InitialGroupKind<'a> {
626 Group {
628 conversation_type: xmtp_proto::types::ConversationType,
629 oneshot_message: Option<&'a xmtp_proto::xmtp::mls::message_contents::OneshotMessage>,
630 },
631 Dm { target_inbox_id: &'a str },
633 RestoredDm {
635 member_one_inbox_id: &'a str,
636 member_two_inbox_id: &'a str,
637 },
638}
639
640pub fn initial_dictionary(
646 kind: InitialGroupKind<'_>,
647 policy_set: &PolicySetProto,
648 opts: &crate::group::GroupMetadataOptions,
649 creator_inbox_id: &str,
650 commit_log_signer: Option<&[u8]>,
651 catalogue: &[ApplicationComponentDefinition],
652) -> Result<openmls::extensions::AppDataDictionary, MigrationError> {
653 use crate::tls_set::TlsSet;
654 use openmls::extensions::AppDataDictionary;
655 use prost::Message as _;
656 use std::collections::BTreeMap;
657 use xmtp_proto::xmtp::mls::message_contents::{
658 GroupMembershipEntry,
659 group_membership_entry::{V1, Version},
660 };
661
662 let creator = InboxId::from_hex(creator_inbox_id)?;
663 let (conversation_type, dm_members, oneshot_message) = match kind {
664 InitialGroupKind::Group {
665 conversation_type,
666 oneshot_message,
667 } => (conversation_type, None, oneshot_message),
668 InitialGroupKind::Dm { target_inbox_id } => {
669 let target = InboxId::from_hex(target_inbox_id)?;
670 if target == creator {
671 return Err(MigrationError::DmMembersSelfReference(creator.to_hex()));
672 }
673 (ConversationType::Dm, Some((creator, target)), None)
674 }
675 InitialGroupKind::RestoredDm {
676 member_one_inbox_id,
677 member_two_inbox_id,
678 } => {
679 let first = InboxId::from_hex(member_one_inbox_id)?;
680 let second = InboxId::from_hex(member_two_inbox_id)?;
681 if first == second {
682 return Err(MigrationError::DmMembersSelfReference(first.to_hex()));
683 }
684 (ConversationType::Dm, Some((first, second)), None)
685 }
686 };
687 let mut registry = build_registry(policy_set, dm_members.is_some(), oneshot_message.is_some())?;
688 register_configured_fields(&mut registry, conversation_type, catalogue)?;
689 let mut dictionary = AppDataDictionary::new();
690 dictionary.insert(
691 ComponentId::COMPONENT_REGISTRY.as_u16(),
692 registry.to_bytes()?,
693 );
694 let empty = TlsSet::<InboxId>::new().tls_serialize_detached()?;
695 dictionary.insert(ComponentId::ADMIN_LIST.as_u16(), empty.clone());
696 let super_admins = if dm_members.is_some() {
697 empty
698 } else {
699 [creator]
700 .into_iter()
701 .collect::<TlsSet<_>>()
702 .tls_serialize_detached()?
703 };
704 dictionary.insert(ComponentId::SUPER_ADMIN_LIST.as_u16(), super_admins);
705 let membership = BTreeMap::from([(
706 creator,
707 GroupMembershipEntry {
708 version: Some(Version::V1(V1 {
709 sequence_id: 0,
710 failed_installations: vec![],
711 })),
712 },
713 )]);
714 dictionary.insert(
715 ComponentId::GROUP_MEMBERSHIP.as_u16(),
716 super::migration::encode_group_membership_dict(&membership)?,
717 );
718 for (id, value) in [
719 (ComponentId::GROUP_NAME, opts.name.as_ref()),
720 (ComponentId::GROUP_DESCRIPTION, opts.description.as_ref()),
721 (ComponentId::GROUP_IMAGE_URL, opts.image_url_square.as_ref()),
722 (ComponentId::APP_DATA, opts.app_data.as_ref()),
723 ] {
724 if let Some(value) = value {
725 dictionary.insert(id.as_u16(), encode_metadata_attribute_value(id, value)?);
726 }
727 }
728 if let Some(settings) = &opts.message_disappearing_settings {
729 dictionary.insert(
730 ComponentId::MESSAGE_DISAPPEAR_FROM_NS.as_u16(),
731 settings.from_ns.to_be_bytes().to_vec(),
732 );
733 dictionary.insert(
734 ComponentId::MESSAGE_DISAPPEAR_IN_NS.as_u16(),
735 settings.in_ns.to_be_bytes().to_vec(),
736 );
737 }
738 if let Some(signer) = commit_log_signer {
739 let expected = xmtp_cryptography::configuration::ED25519_KEY_LENGTH;
740 if signer.len() != expected {
741 return Err(MigrationError::InvalidCommitLogSignerLength {
742 expected,
743 actual: signer.len(),
744 });
745 }
746 dictionary.insert(ComponentId::COMMIT_LOG_SIGNER.as_u16(), signer.to_vec());
747 }
748 dictionary.insert(
749 ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION.as_u16(),
750 xmtp_configuration::PROPOSALS_MIN_PROTOCOL_VERSION
751 .as_bytes()
752 .to_vec(),
753 );
754 let conversation_type: xmtp_proto::xmtp::mls::message_contents::ConversationType =
755 conversation_type.into();
756 dictionary.insert(
757 ComponentId::CONVERSATION_TYPE.as_u16(),
758 encode_conversation_type(conversation_type as i32),
759 );
760 dictionary.insert(
761 ComponentId::CREATOR_INBOX_ID.as_u16(),
762 creator.tls_serialize_detached()?,
763 );
764 if let Some((first, second)) = dm_members {
765 let members: TlsSet<_> = [first, second].into_iter().collect();
766 dictionary.insert(
767 ComponentId::DM_MEMBERS.as_u16(),
768 members.tls_serialize_detached()?,
769 );
770 }
771 if let Some(message) = oneshot_message {
772 dictionary.insert(
773 ComponentId::ONESHOT_MESSAGE.as_u16(),
774 message.encode_to_vec(),
775 );
776 }
777 Ok(dictionary)
778}
779
780#[cfg(test)]
781mod tests {
782 use super::*;
783 use prost::Message as _;
784 use tls_codec::Serialize;
785 use xmtp_proto::{
786 types::ConversationType,
787 xmtp::mls::message_contents::{
788 MembershipPolicy, PermissionsUpdatePolicy,
789 membership_policy::{BasePolicy as MembershipBasePolicy, Kind as MembershipPolicyKind},
790 permissions_update_policy::{Kind as PermissionsPolicyKind, PermissionsBasePolicy},
791 },
792 };
793
794 fn policy_set() -> PolicySetProto {
795 let membership = MembershipPolicy {
796 kind: Some(MembershipPolicyKind::Base(
797 MembershipBasePolicy::Allow as i32,
798 )),
799 };
800 let admin = PermissionsUpdatePolicy {
801 kind: Some(PermissionsPolicyKind::Base(
802 PermissionsBasePolicy::AllowIfAdmin as i32,
803 )),
804 };
805 let super_admin = PermissionsUpdatePolicy {
806 kind: Some(PermissionsPolicyKind::Base(
807 PermissionsBasePolicy::AllowIfSuperAdmin as i32,
808 )),
809 };
810 PolicySetProto {
811 add_member_policy: Some(membership.clone()),
812 remove_member_policy: Some(membership),
813 update_metadata_policy: Default::default(),
814 add_admin_policy: Some(admin.clone()),
815 remove_admin_policy: Some(admin),
816 update_permissions_policy: Some(super_admin),
817 }
818 }
819
820 fn inbox(byte: u8) -> String {
821 hex::encode([byte; 32])
822 }
823
824 #[xmtp_common::test(unwrap_try = true)]
825 fn initial_group_dictionary_seeds_required_and_optional_values() {
827 let creator = inbox(1);
828 let options = crate::group::GroupMetadataOptions {
829 name: Some("name".into()),
830 description: Some("description".into()),
831 image_url_square: Some("image".into()),
832 app_data: Some("app data".into()),
833 message_disappearing_settings: Some(
834 crate::group_mutable_metadata::MessageDisappearingSettings::new(7, 9),
835 ),
836 };
837 let oneshot = xmtp_proto::xmtp::mls::message_contents::OneshotMessage::default();
838 let signer = [3; xmtp_cryptography::configuration::ED25519_KEY_LENGTH];
839 let dictionary = initial_dictionary(
840 InitialGroupKind::Group {
841 conversation_type: ConversationType::Group,
842 oneshot_message: Some(&oneshot),
843 },
844 &policy_set(),
845 &options,
846 &creator,
847 Some(&signer),
848 &[],
849 )?;
850
851 let creator_id = InboxId::from_hex(&creator)?;
852 let empty_admins = crate::tls_set::TlsSet::<InboxId>::new().tls_serialize_detached()?;
853 assert_eq!(
854 dictionary.get(&ComponentId::ADMIN_LIST.as_u16()),
855 Some(empty_admins.as_slice())
856 );
857 let super_admins = [creator_id]
858 .into_iter()
859 .collect::<crate::tls_set::TlsSet<_>>()
860 .tls_serialize_detached()?;
861 assert_eq!(
862 dictionary.get(&ComponentId::SUPER_ADMIN_LIST.as_u16()),
863 Some(super_admins.as_slice())
864 );
865 let membership = super::super::migration::decode_group_membership_dict(
866 dictionary
867 .get(&ComponentId::GROUP_MEMBERSHIP.as_u16())
868 .unwrap(),
869 )?;
870 assert!(matches!(
871 membership.get(&creator_id).unwrap().version,
872 Some(xmtp_proto::xmtp::mls::message_contents::group_membership_entry::Version::V1(ref entry))
873 if entry.sequence_id == 0 && entry.failed_installations.is_empty()
874 ));
875 for (id, value) in [
876 (ComponentId::GROUP_NAME, b"name".as_slice()),
877 (ComponentId::GROUP_DESCRIPTION, b"description".as_slice()),
878 (ComponentId::GROUP_IMAGE_URL, b"image".as_slice()),
879 (ComponentId::APP_DATA, b"app data".as_slice()),
880 (ComponentId::MESSAGE_DISAPPEAR_FROM_NS, &7_i64.to_be_bytes()),
881 (ComponentId::MESSAGE_DISAPPEAR_IN_NS, &9_i64.to_be_bytes()),
882 ] {
883 assert_eq!(dictionary.get(&id.as_u16()), Some(value));
884 }
885 assert_eq!(
886 dictionary.get(&ComponentId::COMMIT_LOG_SIGNER.as_u16()),
887 Some(signer.as_slice())
888 );
889 assert_eq!(
890 dictionary.get(&ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION.as_u16()),
891 Some(xmtp_configuration::PROPOSALS_MIN_PROTOCOL_VERSION.as_bytes())
892 );
893 let conversation_type = encode_conversation_type(
894 xmtp_proto::xmtp::mls::message_contents::ConversationType::Group as i32,
895 );
896 assert_eq!(
897 dictionary.get(&ComponentId::CONVERSATION_TYPE.as_u16()),
898 Some(conversation_type.as_slice())
899 );
900 let serialized_creator_id = creator_id.tls_serialize_detached()?;
901 assert_eq!(
902 dictionary.get(&ComponentId::CREATOR_INBOX_ID.as_u16()),
903 Some(serialized_creator_id.as_slice())
904 );
905 let encoded_oneshot = oneshot.encode_to_vec();
906 assert_eq!(
907 dictionary.get(&ComponentId::ONESHOT_MESSAGE.as_u16()),
908 Some(encoded_oneshot.as_slice())
909 );
910 }
911
912 #[xmtp_common::test(unwrap_try = true)]
913 fn initial_dm_dictionary_has_empty_admin_sets_and_dm_members() {
915 let creator = inbox(1);
916 let target = inbox(2);
917 let dictionary = initial_dictionary(
918 InitialGroupKind::Dm {
919 target_inbox_id: &target,
920 },
921 &policy_set(),
922 &crate::group::GroupMetadataOptions::default(),
923 &creator,
924 None,
925 &[],
926 )?;
927
928 let creator_id = InboxId::from_hex(&creator)?;
929 let target_id = InboxId::from_hex(&target)?;
930 let empty = crate::tls_set::TlsSet::<InboxId>::new().tls_serialize_detached()?;
931 assert_eq!(
932 dictionary.get(&ComponentId::ADMIN_LIST.as_u16()),
933 Some(empty.as_slice())
934 );
935 assert_eq!(
936 dictionary.get(&ComponentId::SUPER_ADMIN_LIST.as_u16()),
937 Some(empty.as_slice())
938 );
939 let dm_members = [creator_id, target_id]
940 .into_iter()
941 .collect::<crate::tls_set::TlsSet<_>>()
942 .tls_serialize_detached()?;
943 assert_eq!(
944 dictionary.get(&ComponentId::DM_MEMBERS.as_u16()),
945 Some(dm_members.as_slice())
946 );
947 let conversation_type = encode_conversation_type(
948 xmtp_proto::xmtp::mls::message_contents::ConversationType::Dm as i32,
949 );
950 assert_eq!(
951 dictionary.get(&ComponentId::CONVERSATION_TYPE.as_u16()),
952 Some(conversation_type.as_slice())
953 );
954 let serialized_creator_id = creator_id.tls_serialize_detached()?;
955 assert_eq!(
956 dictionary.get(&ComponentId::CREATOR_INBOX_ID.as_u16()),
957 Some(serialized_creator_id.as_slice())
958 );
959 for id in [
960 ComponentId::GROUP_NAME,
961 ComponentId::GROUP_DESCRIPTION,
962 ComponentId::GROUP_IMAGE_URL,
963 ComponentId::APP_DATA,
964 ComponentId::MESSAGE_DISAPPEAR_FROM_NS,
965 ComponentId::MESSAGE_DISAPPEAR_IN_NS,
966 ComponentId::COMMIT_LOG_SIGNER,
967 ComponentId::ONESHOT_MESSAGE,
968 ] {
969 assert!(!dictionary.contains(&id.as_u16()));
970 }
971 }
972
973 fn definition(
974 component_id: u16,
975 in_groups: bool,
976 in_dms: bool,
977 ) -> ApplicationComponentDefinition {
978 let policy =
979 xmtp_configuration::MetadataPolicy::Base(MetadataBasePolicy::AllowIfAdmin as i32);
980 ApplicationComponentDefinition {
981 component_id,
982 name: format!("field_{component_id:x}"),
983 component_type: ComponentType::String as i32,
984 permissions: xmtp_configuration::ComponentPermissions {
985 insert: Some(policy.clone()),
986 update: Some(policy.clone()),
987 delete: Some(policy),
988 },
989 in_groups,
990 in_dms,
991 }
992 }
993
994 fn registry_of(dictionary: &openmls::extensions::AppDataDictionary) -> ComponentRegistry {
995 ComponentRegistry::from_bytes(
996 dictionary
997 .get(&ComponentId::COMPONENT_REGISTRY.as_u16())
998 .unwrap(),
999 )
1000 .unwrap()
1001 }
1002
1003 fn group_dictionary(
1004 conversation_type: ConversationType,
1005 policy_set: &PolicySetProto,
1006 catalogue: &[ApplicationComponentDefinition],
1007 ) -> openmls::extensions::AppDataDictionary {
1008 initial_dictionary(
1009 InitialGroupKind::Group {
1010 conversation_type,
1011 oneshot_message: None,
1012 },
1013 policy_set,
1014 &crate::group::GroupMetadataOptions::default(),
1015 &inbox(1),
1016 None,
1017 catalogue,
1018 )
1019 .unwrap()
1020 }
1021
1022 fn self_owned() -> ComponentPermissions {
1023 let policy = metadata_policy(MetadataBasePolicy::AllowIfSelfOrNonMember);
1024 ComponentPermissions {
1025 insert_policy: Some(policy.clone()),
1026 update_policy: Some(policy.clone()),
1027 delete_policy: Some(policy),
1028 }
1029 }
1030
1031 #[xmtp_common::test(unwrap_try = true)]
1036 fn new_group_registers_configured_fields_without_values() {
1038 let catalogue = [
1039 definition(0xC000, true, false),
1040 definition(0xC001, false, true),
1041 definition(0xFD00, true, true),
1042 ];
1043 let dictionary = group_dictionary(ConversationType::Group, &policy_set(), &catalogue);
1044 let registry = registry_of(&dictionary);
1045
1046 for id in [0xC000, 0xFD00] {
1047 let expected = &catalogue.iter().find(|d| d.component_id == id).unwrap();
1048 let entry = registry.get(&ComponentId::new(id))?.unwrap();
1049 assert_eq!(entry.component_type, ComponentType::String as i32);
1050 assert_eq!(entry.permissions, Some(expected.permissions.clone().into()));
1051 }
1052 assert!(!registry.contains_raw(&ComponentId::new(0xC001)));
1053 assert_eq!(
1054 registry.get(&ComponentId::USER_DISPLAY_NAME)?,
1055 Some(new_component_metadata(
1056 self_owned(),
1057 ComponentType::TlsMapInboxIdString
1058 ))
1059 );
1060 let image = registry.get(&ComponentId::GROUP_IMAGE)?.unwrap();
1061 assert_eq!(image.component_type, ComponentType::Bytes as i32);
1062 for id in [
1063 ComponentId::new(0xC000),
1064 ComponentId::new(0xFD00),
1065 ComponentId::USER_DISPLAY_NAME,
1066 ComponentId::GROUP_IMAGE,
1067 ] {
1068 assert!(!dictionary.contains(&id.as_u16()));
1069 }
1070 }
1071
1072 #[xmtp_common::test(unwrap_try = true)]
1076 fn group_image_copies_the_image_url_policies() {
1078 let mut policies = policy_set();
1079 policies.update_metadata_policy.insert(
1080 MetadataField::GroupImageUrlSquare.as_str().to_string(),
1081 metadata_policy(MetadataBasePolicy::Allow),
1082 );
1083 let registry = registry_of(&group_dictionary(ConversationType::Group, &policies, &[]));
1084 let image_url = registry.get(&ComponentId::GROUP_IMAGE_URL)?.unwrap();
1085 let image = registry.get(&ComponentId::GROUP_IMAGE)?.unwrap();
1086 assert_eq!(image.permissions, image_url.permissions);
1087 let permissions = image.permissions.unwrap();
1088 assert_eq!(
1089 permissions.update_policy,
1090 Some(metadata_policy(MetadataBasePolicy::Allow))
1091 );
1092 assert_eq!(
1093 permissions.delete_policy,
1094 Some(metadata_policy(MetadataBasePolicy::AllowIfSuperAdmin))
1095 );
1096 }
1097
1098 #[xmtp_common::test(unwrap_try = true)]
1101 fn new_dm_registers_dm_fields_only() {
1103 let catalogue = [
1104 definition(0xC000, true, false),
1105 definition(0xC001, false, true),
1106 ];
1107 let dictionary = initial_dictionary(
1108 InitialGroupKind::Dm {
1109 target_inbox_id: &inbox(2),
1110 },
1111 &policy_set(),
1112 &crate::group::GroupMetadataOptions::default(),
1113 &inbox(1),
1114 None,
1115 &catalogue,
1116 )?;
1117 let registry = registry_of(&dictionary);
1118 assert!(registry.contains(&ComponentId::new(0xC001)));
1119 assert!(!registry.contains_raw(&ComponentId::new(0xC000)));
1120 assert!(!registry.contains_raw(&ComponentId::GROUP_IMAGE));
1121 assert_eq!(
1122 registry.get(&ComponentId::USER_DISPLAY_NAME)?,
1123 Some(new_component_metadata(
1124 self_owned(),
1125 ComponentType::TlsMapInboxIdString
1126 ))
1127 );
1128 assert!(!dictionary.contains(&ComponentId::USER_DISPLAY_NAME.as_u16()));
1129 }
1130
1131 #[xmtp_common::test(unwrap_try = true)]
1134 fn sync_group_registers_no_configured_fields() {
1136 let catalogue = [definition(0xC000, true, true)];
1137 let registry = registry_of(&group_dictionary(
1138 ConversationType::Sync,
1139 &policy_set(),
1140 &catalogue,
1141 ));
1142 for id in [
1143 ComponentId::new(0xC000),
1144 ComponentId::USER_DISPLAY_NAME,
1145 ComponentId::GROUP_IMAGE,
1146 ] {
1147 assert!(!registry.contains_raw(&id));
1148 }
1149 }
1150
1151 #[xmtp_common::test(unwrap_try = true)]
1155 fn definitions_outside_the_application_range_are_skipped() {
1157 use xmtp_configuration::MetadataPolicy;
1158 let mut membership = definition(ComponentId::GROUP_MEMBERSHIP.as_u16(), true, true);
1159 let allow = Some(MetadataPolicy::Base(MetadataBasePolicy::Allow as i32));
1160 membership.permissions.insert = allow.clone();
1161 membership.permissions.update = allow.clone();
1162 membership.permissions.delete = allow;
1163 let expected = registry_of(&group_dictionary(
1164 ConversationType::Group,
1165 &policy_set(),
1166 &[],
1167 ));
1168 let registry = registry_of(&group_dictionary(
1169 ConversationType::Group,
1170 &policy_set(),
1171 &[membership.clone()],
1172 ));
1173 assert_eq!(
1174 registry.get(&ComponentId::GROUP_MEMBERSHIP)?,
1175 expected.get(&ComponentId::GROUP_MEMBERSHIP)?
1176 );
1177 assert_eq!(
1178 catalogue_registry_entries(&[membership], ConversationType::Group).count(),
1179 0
1180 );
1181 }
1182
1183 #[xmtp_common::test(unwrap_try = true)]
1187 fn only_the_first_definition_of_an_id_is_registered() {
1189 let catalogue: Vec<_> = [0xC000, 0xFD00]
1190 .into_iter()
1191 .flat_map(|id| {
1192 let mut duplicate = definition(id, true, true);
1193 duplicate.component_type = ComponentType::Bytes as i32;
1194 [definition(id, true, true), duplicate]
1195 })
1196 .collect();
1197 let registry = registry_of(&group_dictionary(
1198 ConversationType::Group,
1199 &policy_set(),
1200 &catalogue,
1201 ));
1202 for id in [0xC000, 0xFD00] {
1203 assert_eq!(
1204 registry
1205 .get(&ComponentId::new(id))?
1206 .map(|metadata| metadata.component_type),
1207 Some(ComponentType::String as i32)
1208 );
1209 }
1210 }
1211
1212 #[xmtp_common::test(unwrap_try = true)]
1216 fn definitions_with_unknown_tags_are_skipped() {
1218 use xmtp_configuration::MetadataPolicy;
1219 let mut unknown_type = definition(0xC000, true, true);
1220 unknown_type.component_type = 99;
1221 let mut unknown_policy = definition(0xC001, true, true);
1222 unknown_policy.permissions.update = Some(MetadataPolicy::Any(vec![
1223 MetadataPolicy::Base(MetadataBasePolicy::Allow as i32),
1224 MetadataPolicy::Base(99),
1225 ]));
1226 let mut empty_condition = definition(0xC002, true, true);
1227 empty_condition.permissions.delete = Some(MetadataPolicy::And(vec![]));
1228 let catalogue = [
1229 unknown_type,
1230 unknown_policy,
1231 empty_condition,
1232 definition(0xC003, true, true),
1233 ];
1234 let registry = registry_of(&group_dictionary(
1235 ConversationType::Group,
1236 &policy_set(),
1237 &catalogue,
1238 ));
1239 for id in [0xC000, 0xC001, 0xC002] {
1240 assert!(!registry.contains_raw(&ComponentId::new(id)));
1241 }
1242 assert!(registry.contains(&ComponentId::new(0xC003)));
1243 }
1244}