1use super::{
2 group_membership::GroupMembership,
3 group_permissions::{MembershipPolicies, MetadataPolicies, PermissionsPolicies},
4};
5use crate::groups::mls_ext::WelcomePointersExtension;
6use openmls::prelude::{
7 MlsMessageOut,
8 tls_codec::{Error as TlsCodecError, Serialize},
9};
10use prost::{Message, bytes::Bytes};
11use std::collections::{HashMap, HashSet};
12use thiserror::Error;
13use xmtp_common::types::Address;
14use xmtp_configuration::{
15 GROUP_KEY_ROTATION_INTERVAL_NS, WELCOME_POINTEE_ENCRYPTION_AEAD_TYPES_EXTENSION_ID,
16};
17use xmtp_id::key_package::{
18 KeyPackageVerificationError, VerifiedKeyPackageV2, WrapperAlgorithm, WrapperEncryptionExtension,
19};
20use xmtp_mls_common::group_mutable_metadata::MetadataField;
21use xmtp_proto::{
22 ConversionError,
23 xmtp::mls::database::{
24 AccountAddresses, AddressesOrInstallationIds as AddressesOrInstallationIdsProtoWrapper,
25 CommitPendingProposalsData, InstallationIds, PostCommitAction as PostCommitActionProto,
26 ProposeGroupContextExtensionData, ProposeMemberUpdateData, ReaddInstallationsData,
27 SendMessageData, UpdateAdminListsData, UpdateGroupMembershipData, UpdateMetadataData,
28 UpdatePermissionData,
29 addresses_or_installation_ids::AddressesOrInstallationIds as AddressesOrInstallationIdsProto,
30 commit_pending_proposals_data,
31 post_commit_action::{
32 Installation as InstallationProto, Kind as PostCommitActionKind,
33 SendWelcomes as SendWelcomesProto,
34 },
35 propose_group_context_extension_data, propose_member_update_data,
36 readd_installations_data::{
37 V1 as ReaddInstallationsV1, Version as ReaddInstallationsVersion,
38 },
39 send_message_data::{V1 as SendMessageV1, Version as SendMessageVersion},
40 update_admin_lists_data::{V1 as UpdateAdminListsV1, Version as UpdateAdminListsVersion},
41 update_group_membership_data::{
42 V1 as UpdateGroupMembershipV1, Version as UpdateGroupMembershipVersion,
43 },
44 update_metadata_data::{V1 as UpdateMetadataV1, Version as UpdateMetadataVersion},
45 update_permission_data::{
46 self, V1 as UpdatePermissionV1, Version as UpdatePermissionVersion,
47 },
48 },
49};
50
51mod queue;
52pub use queue::*;
53
54#[derive(Debug, Error)]
55pub enum IntentError {
56 #[error("conversion error: {0}")]
57 Conversion(#[from] xmtp_proto::ConversionError),
58 #[error("key package verification: {0}")]
59 KeyPackageVerification(#[from] KeyPackageVerificationError),
60 #[error("TLS Codec error: {0}")]
61 TlsError(#[from] TlsCodecError),
62 #[error(transparent)]
63 Storage(#[from] xmtp_db::StorageError),
64 #[error("missing update permission")]
65 MissingUpdatePermissionVersion,
66 #[error("missing payload")]
67 MissingPayload,
68 #[error("missing update admin version")]
69 MissingUpdateAdminVersion,
70 #[error("missing post commit action")]
71 MissingPostCommit,
72 #[error("unsupported permission version")]
73 UnsupportedPermissionVersion,
74 #[error("unknown permission update type")]
75 UnknownPermissionUpdateType,
76 #[error("unknown value for PermissionPolicyOption")]
77 UnknownPermissionPolicyOption,
78 #[error("unknown value for AdminListActionType")]
79 UnknownAdminListAction,
80 #[error("intent payload error: {0}")]
81 Generic(String),
82}
83
84impl From<prost::DecodeError> for IntentError {
85 fn from(error: prost::DecodeError) -> Self {
86 IntentError::Conversion(xmtp_proto::ConversionError::Decode(error))
87 }
88}
89
90#[derive(Debug, Clone)]
91pub struct SendMessageIntentData {
92 pub message: Vec<u8>,
93}
94
95impl SendMessageIntentData {
96 pub fn new(message: Vec<u8>) -> Self {
97 Self { message }
98 }
99
100 pub(crate) fn to_bytes(&self) -> Vec<u8> {
101 SendMessageData {
102 version: Some(SendMessageVersion::V1(SendMessageV1 {
103 payload_bytes: self.message.clone(),
104 })),
105 }
106 .encode_to_vec()
107 }
108
109 pub(crate) fn from_bytes(data: &[u8]) -> Result<Self, IntentError> {
110 let msg = SendMessageData::decode(data)?;
111 let payload_bytes = match msg.version {
112 Some(SendMessageVersion::V1(v1)) => v1.payload_bytes,
113 None => return Err(IntentError::MissingPayload),
114 };
115
116 Ok(Self::new(payload_bytes))
117 }
118}
119
120impl From<SendMessageIntentData> for Vec<u8> {
121 fn from(intent: SendMessageIntentData) -> Self {
122 intent.to_bytes()
123 }
124}
125
126#[derive(Debug, PartialEq, Eq, Clone)]
127pub enum AddressesOrInstallationIds {
128 AccountAddresses(Vec<String>),
129 InstallationIds(Vec<Vec<u8>>),
130}
131
132impl From<AddressesOrInstallationIds> for AddressesOrInstallationIdsProtoWrapper {
133 fn from(address_or_id: AddressesOrInstallationIds) -> Self {
134 match address_or_id {
135 AddressesOrInstallationIds::AccountAddresses(account_addresses) => {
136 AddressesOrInstallationIdsProtoWrapper {
137 addresses_or_installation_ids: Some(
138 AddressesOrInstallationIdsProto::AccountAddresses(AccountAddresses {
139 account_addresses,
140 }),
141 ),
142 }
143 }
144 AddressesOrInstallationIds::InstallationIds(installation_ids) => {
145 AddressesOrInstallationIdsProtoWrapper {
146 addresses_or_installation_ids: Some(
147 AddressesOrInstallationIdsProto::InstallationIds(InstallationIds {
148 installation_ids,
149 }),
150 ),
151 }
152 }
153 }
154 }
155}
156
157impl TryFrom<AddressesOrInstallationIdsProtoWrapper> for AddressesOrInstallationIds {
158 type Error = IntentError;
159
160 fn try_from(wrapper: AddressesOrInstallationIdsProtoWrapper) -> Result<Self, Self::Error> {
161 match wrapper.addresses_or_installation_ids {
162 Some(AddressesOrInstallationIdsProto::AccountAddresses(addrs)) => Ok(
163 AddressesOrInstallationIds::AccountAddresses(addrs.account_addresses),
164 ),
165 Some(AddressesOrInstallationIdsProto::InstallationIds(ids)) => Ok(
166 AddressesOrInstallationIds::InstallationIds(ids.installation_ids),
167 ),
168 _ => Err(IntentError::MissingPayload),
169 }
170 }
171}
172
173impl From<Vec<Address>> for AddressesOrInstallationIds {
174 fn from(addrs: Vec<Address>) -> Self {
175 AddressesOrInstallationIds::AccountAddresses(addrs)
176 }
177}
178
179impl From<Vec<Vec<u8>>> for AddressesOrInstallationIds {
180 fn from(installation_ids: Vec<Vec<u8>>) -> Self {
181 AddressesOrInstallationIds::InstallationIds(installation_ids)
182 }
183}
184
185#[derive(Debug, Clone)]
186pub struct UpdateMetadataIntentData {
187 pub field_name: String,
188 pub field_value: String,
189 pub expected_field_value: Option<String>,
193}
194
195impl UpdateMetadataIntentData {
196 pub fn new(field_name: String, field_value: String) -> Self {
197 Self {
198 field_name,
199 field_value,
200 expected_field_value: None,
201 }
202 }
203
204 pub fn new_guarded(
208 field_name: String,
209 field_value: String,
210 expected_field_value: String,
211 ) -> Self {
212 Self {
213 field_name,
214 field_value,
215 expected_field_value: Some(expected_field_value),
216 }
217 }
218
219 pub fn new_update_group_name(group_name: String) -> Self {
220 Self::new(MetadataField::GroupName.to_string(), group_name)
221 }
222
223 pub fn new_update_group_image_url_square(group_image_url_square: String) -> Self {
224 Self::new(
225 MetadataField::GroupImageUrlSquare.to_string(),
226 group_image_url_square,
227 )
228 }
229
230 pub fn new_update_group_description(group_description: String) -> Self {
231 Self::new(MetadataField::Description.to_string(), group_description)
232 }
233
234 pub fn new_update_app_data(app_data: String, expected_app_data: Option<String>) -> Self {
235 match expected_app_data {
236 Some(expected) => {
237 Self::new_guarded(MetadataField::AppData.to_string(), app_data, expected)
238 }
239 None => Self::new(MetadataField::AppData.to_string(), app_data),
240 }
241 }
242
243 pub fn new_update_conversation_message_disappear_from_ns(from_ns: i64) -> Self {
244 Self::new(
245 MetadataField::MessageDisappearFromNS.to_string(),
246 from_ns.to_string(),
247 )
248 }
249 pub fn new_update_conversation_message_disappear_in_ns(in_ns: i64) -> Self {
250 Self::new(
251 MetadataField::MessageDisappearInNS.to_string(),
252 in_ns.to_string(),
253 )
254 }
255
256 pub fn new_update_group_min_version_to_match_self(min_version: String) -> Self {
257 Self::new(
258 MetadataField::MinimumSupportedProtocolVersion.to_string(),
259 min_version,
260 )
261 }
262
263 pub fn new_update_commit_log_signer(commit_log_signer: xmtp_cryptography::Secret) -> Self {
264 Self::new(
265 MetadataField::CommitLogSigner.to_string(),
266 hex::encode(commit_log_signer.as_slice()),
267 )
268 }
269}
270
271impl From<UpdateMetadataIntentData> for Vec<u8> {
272 fn from(intent: UpdateMetadataIntentData) -> Self {
273 let mut buf = Vec::new();
274
275 UpdateMetadataData {
276 version: Some(UpdateMetadataVersion::V1(UpdateMetadataV1 {
277 field_name: intent.field_name.to_string(),
278 field_value: intent.field_value.clone(),
279 expected_field_value: intent.expected_field_value.clone(),
280 })),
281 }
282 .encode(&mut buf)
283 .expect("encode error");
284
285 buf
286 }
287}
288
289impl TryFrom<Vec<u8>> for UpdateMetadataIntentData {
290 type Error = IntentError;
291
292 fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
293 let msg = UpdateMetadataData::decode(Bytes::from(data))?;
294
295 let field_name = match msg.version {
296 Some(UpdateMetadataVersion::V1(ref v1)) => v1.field_name.clone(),
297 None => return Err(IntentError::MissingPayload),
298 };
299 let field_value = match msg.version {
300 Some(UpdateMetadataVersion::V1(ref v1)) => v1.field_value.clone(),
301 None => return Err(IntentError::MissingPayload),
302 };
303 let expected_field_value = match msg.version {
306 Some(UpdateMetadataVersion::V1(ref v1)) => v1.expected_field_value.clone(),
307 None => return Err(IntentError::MissingPayload),
308 };
309
310 Ok(match expected_field_value {
311 Some(expected) => Self::new_guarded(field_name, field_value, expected),
312 None => Self::new(field_name, field_value),
313 })
314 }
315}
316
317#[derive(Debug, Default, Clone)]
318pub struct UpdateGroupMembershipResult {
319 pub added_members: HashMap<String, u64>,
320 pub removed_members: Vec<String>,
321 pub failed_installations: Vec<Vec<u8>>,
322}
323
324impl UpdateGroupMembershipResult {
325 pub fn new(
326 added_members: HashMap<String, u64>,
327 removed_members: Vec<String>,
328 failed_installations: Vec<Vec<u8>>,
329 ) -> Self {
330 Self {
331 added_members,
332 removed_members,
333 failed_installations,
334 }
335 }
336}
337
338impl From<UpdateGroupMembershipIntentData> for UpdateGroupMembershipResult {
339 fn from(value: UpdateGroupMembershipIntentData) -> Self {
340 UpdateGroupMembershipResult::new(
341 value.membership_updates,
342 value.removed_members,
343 value.failed_installations,
344 )
345 }
346}
347
348#[derive(Debug, Clone)]
349pub(crate) struct UpdateGroupMembershipIntentData {
350 pub membership_updates: HashMap<String, u64>,
351 pub removed_members: Vec<String>,
352 pub failed_installations: Vec<Vec<u8>>,
353}
354
355impl UpdateGroupMembershipIntentData {
356 pub fn new(
357 membership_updates: HashMap<String, u64>,
358 removed_members: Vec<String>,
359 failed_installations: Vec<Vec<u8>>,
360 ) -> Self {
361 Self {
362 membership_updates,
363 removed_members,
364 failed_installations,
365 }
366 }
367
368 pub fn is_empty(&self) -> bool {
369 self.membership_updates.is_empty()
370 && self.removed_members.is_empty()
371 && self.failed_installations.is_empty()
372 }
373
374 pub fn apply_to_group_membership(&self, group_membership: &GroupMembership) -> GroupMembership {
375 tracing::info!("old group membership: {:?}", group_membership.members);
376 let mut new_membership = group_membership.clone();
377 for (inbox_id, sequence_id) in self.membership_updates.iter() {
378 new_membership.add(inbox_id.clone(), *sequence_id);
379 }
380
381 for inbox_id in self.removed_members.iter() {
382 new_membership.remove(inbox_id)
383 }
384
385 new_membership.failed_installations = new_membership
386 .failed_installations
387 .into_iter()
388 .chain(self.failed_installations.iter().cloned())
389 .collect::<HashSet<_>>()
390 .into_iter()
391 .collect();
392
393 tracing::info!("updated group membership: {:?}", new_membership.members);
394 new_membership
395 }
396}
397
398impl From<UpdateGroupMembershipIntentData> for Vec<u8> {
399 fn from(intent: UpdateGroupMembershipIntentData) -> Self {
400 let mut buf = Vec::new();
401
402 UpdateGroupMembershipData {
403 version: Some(UpdateGroupMembershipVersion::V1(UpdateGroupMembershipV1 {
404 membership_updates: intent.membership_updates,
405 removed_members: intent.removed_members,
406 failed_installations: intent.failed_installations,
407 })),
408 }
409 .encode(&mut buf)
410 .expect("encode error");
411
412 buf
413 }
414}
415
416impl TryFrom<Vec<u8>> for UpdateGroupMembershipIntentData {
417 type Error = IntentError;
418
419 fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
420 if let UpdateGroupMembershipData {
421 version: Some(UpdateGroupMembershipVersion::V1(v1)),
422 } = UpdateGroupMembershipData::decode(data.as_slice())?
423 {
424 Ok(Self::new(
425 v1.membership_updates,
426 v1.removed_members,
427 v1.failed_installations,
428 ))
429 } else {
430 Err(IntentError::MissingPayload)
431 }
432 }
433}
434
435impl<'a> TryFrom<&'a [u8]> for UpdateGroupMembershipIntentData {
436 type Error = IntentError;
437
438 fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
439 if let UpdateGroupMembershipData {
440 version: Some(UpdateGroupMembershipVersion::V1(v1)),
441 } = UpdateGroupMembershipData::decode(data)?
442 {
443 Ok(Self::new(
444 v1.membership_updates,
445 v1.removed_members,
446 v1.failed_installations,
447 ))
448 } else {
449 Err(IntentError::MissingPayload)
450 }
451 }
452}
453#[repr(i32)]
454#[derive(Debug, Clone, PartialEq)]
455pub enum AdminListActionType {
456 Add = 1, Remove = 2, AddSuper = 3, RemoveSuper = 4, }
461
462impl TryFrom<i32> for AdminListActionType {
463 type Error = IntentError;
464
465 fn try_from(value: i32) -> Result<Self, Self::Error> {
466 match value {
467 1 => Ok(AdminListActionType::Add),
468 2 => Ok(AdminListActionType::Remove),
469 3 => Ok(AdminListActionType::AddSuper),
470 4 => Ok(AdminListActionType::RemoveSuper),
471 _ => Err(IntentError::UnknownAdminListAction),
472 }
473 }
474}
475
476#[derive(Debug, Clone)]
477pub struct UpdateAdminListIntentData {
478 pub action_type: AdminListActionType,
479 pub inbox_id: String,
480}
481
482impl UpdateAdminListIntentData {
483 pub fn new(action_type: AdminListActionType, inbox_id: String) -> Self {
484 Self {
485 action_type,
486 inbox_id,
487 }
488 }
489}
490
491impl From<UpdateAdminListIntentData> for Vec<u8> {
492 fn from(intent: UpdateAdminListIntentData) -> Self {
493 let mut buf = Vec::new();
494 let action_type = intent.action_type as i32;
495
496 UpdateAdminListsData {
497 version: Some(UpdateAdminListsVersion::V1(UpdateAdminListsV1 {
498 admin_list_update_type: action_type,
499 inbox_id: intent.inbox_id,
500 })),
501 }
502 .encode(&mut buf)
503 .expect("encode error");
504
505 buf
506 }
507}
508
509impl TryFrom<Vec<u8>> for UpdateAdminListIntentData {
510 type Error = IntentError;
511
512 fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
513 let msg = UpdateAdminListsData::decode(Bytes::from(data))?;
514
515 let action_type: AdminListActionType = match msg.version {
516 Some(UpdateAdminListsVersion::V1(ref v1)) => {
517 AdminListActionType::try_from(v1.admin_list_update_type)?
518 }
519 None => return Err(IntentError::MissingUpdateAdminVersion),
520 };
521 let inbox_id = match msg.version {
522 Some(UpdateAdminListsVersion::V1(ref v1)) => v1.inbox_id.clone(),
523 None => return Err(IntentError::MissingUpdateAdminVersion),
524 };
525
526 Ok(Self::new(action_type, inbox_id))
527 }
528}
529
530#[repr(i32)]
531#[derive(Debug, Clone, PartialEq)]
532pub enum PermissionUpdateType {
533 AddMember = 1, RemoveMember = 2, AddAdmin = 3, RemoveAdmin = 4, UpdateMetadata = 5, }
539
540impl TryFrom<i32> for PermissionUpdateType {
541 type Error = IntentError;
542
543 fn try_from(value: i32) -> Result<Self, Self::Error> {
544 match value {
545 1 => Ok(PermissionUpdateType::AddMember),
546 2 => Ok(PermissionUpdateType::RemoveMember),
547 3 => Ok(PermissionUpdateType::AddAdmin),
548 4 => Ok(PermissionUpdateType::RemoveAdmin),
549 5 => Ok(PermissionUpdateType::UpdateMetadata),
550 _ => Err(IntentError::UnknownPermissionUpdateType),
551 }
552 }
553}
554
555#[repr(i32)]
556#[derive(Debug, Clone, PartialEq)]
557pub enum PermissionPolicyOption {
558 Allow = 1, Deny = 2, AdminOnly = 3, SuperAdminOnly = 4, }
563
564impl TryFrom<i32> for PermissionPolicyOption {
565 type Error = IntentError;
566
567 fn try_from(value: i32) -> Result<Self, Self::Error> {
568 match value {
569 1 => Ok(PermissionPolicyOption::Allow),
570 2 => Ok(PermissionPolicyOption::Deny),
571 3 => Ok(PermissionPolicyOption::AdminOnly),
572 4 => Ok(PermissionPolicyOption::SuperAdminOnly),
573 _ => Err(IntentError::UnknownPermissionPolicyOption),
574 }
575 }
576}
577
578impl From<PermissionPolicyOption> for MembershipPolicies {
579 fn from(value: PermissionPolicyOption) -> Self {
580 match value {
581 PermissionPolicyOption::Allow => MembershipPolicies::allow(),
582 PermissionPolicyOption::Deny => MembershipPolicies::deny(),
583 PermissionPolicyOption::AdminOnly => MembershipPolicies::allow_if_actor_admin(),
584 PermissionPolicyOption::SuperAdminOnly => {
585 MembershipPolicies::allow_if_actor_super_admin()
586 }
587 }
588 }
589}
590
591impl From<PermissionPolicyOption> for MetadataPolicies {
592 fn from(value: PermissionPolicyOption) -> Self {
593 match value {
594 PermissionPolicyOption::Allow => MetadataPolicies::allow(),
595 PermissionPolicyOption::Deny => MetadataPolicies::deny(),
596 PermissionPolicyOption::AdminOnly => MetadataPolicies::allow_if_actor_admin(),
597 PermissionPolicyOption::SuperAdminOnly => {
598 MetadataPolicies::allow_if_actor_super_admin()
599 }
600 }
601 }
602}
603
604impl From<PermissionPolicyOption> for PermissionsPolicies {
605 fn from(value: PermissionPolicyOption) -> Self {
606 match value {
607 PermissionPolicyOption::Allow => {
608 tracing::error!(
609 "PermissionPolicyOption::Allow is not allowed for PermissionsPolicies, set to super_admin only instead"
610 );
611 PermissionsPolicies::allow_if_actor_super_admin()
612 }
613 PermissionPolicyOption::Deny => PermissionsPolicies::deny(),
614 PermissionPolicyOption::AdminOnly => PermissionsPolicies::allow_if_actor_admin(),
615 PermissionPolicyOption::SuperAdminOnly => {
616 PermissionsPolicies::allow_if_actor_super_admin()
617 }
618 }
619 }
620}
621
622#[derive(Debug, Clone)]
623pub struct UpdatePermissionIntentData {
624 pub update_type: PermissionUpdateType,
625 pub policy_option: PermissionPolicyOption,
626 pub metadata_field_name: Option<String>,
627}
628
629impl UpdatePermissionIntentData {
630 pub fn new(
631 update_type: PermissionUpdateType,
632 policy_option: PermissionPolicyOption,
633 metadata_field_name: Option<String>,
634 ) -> Self {
635 Self {
636 update_type,
637 policy_option,
638 metadata_field_name,
639 }
640 }
641}
642
643impl From<UpdatePermissionIntentData> for Vec<u8> {
644 fn from(intent: UpdatePermissionIntentData) -> Self {
645 let mut buf = Vec::new();
646 let update_type = intent.update_type as i32;
647 let policy_option = intent.policy_option as i32;
648
649 UpdatePermissionData {
650 version: Some(UpdatePermissionVersion::V1(UpdatePermissionV1 {
651 permission_update_type: update_type,
652 permission_policy_option: policy_option,
653 metadata_field_name: intent.metadata_field_name,
654 })),
655 }
656 .encode(&mut buf)
657 .expect("encode error");
658
659 buf
660 }
661}
662
663impl TryFrom<Vec<u8>> for UpdatePermissionIntentData {
664 type Error = IntentError;
665
666 fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
667 let msg = UpdatePermissionData::decode(Bytes::from(data))?;
668 let Some(UpdatePermissionVersion::V1(update_permission_data::V1 {
669 permission_update_type,
670 permission_policy_option,
671 metadata_field_name,
672 })) = msg.version
673 else {
674 return Err(IntentError::UnsupportedPermissionVersion);
675 };
676 let update_type: PermissionUpdateType = permission_update_type.try_into()?;
677 let policy_option: PermissionPolicyOption = permission_policy_option.try_into()?;
678 Ok(Self::new(update_type, policy_option, metadata_field_name))
679 }
680}
681
682pub(crate) struct ReaddInstallationsIntentData {
683 pub readded_installations: Vec<Vec<u8>>,
684}
685
686impl ReaddInstallationsIntentData {
687 pub fn new(readded_installations: Vec<Vec<u8>>) -> Self {
688 Self {
689 readded_installations,
690 }
691 }
692}
693
694impl From<ReaddInstallationsIntentData> for Vec<u8> {
695 fn from(intent: ReaddInstallationsIntentData) -> Self {
696 let mut buf = Vec::new();
697 ReaddInstallationsData {
698 version: Some(ReaddInstallationsVersion::V1(ReaddInstallationsV1 {
699 readded_installations: intent.readded_installations,
700 })),
701 }
702 .encode(&mut buf)
703 .expect("encode error");
704
705 buf
706 }
707}
708
709impl TryFrom<Vec<u8>> for ReaddInstallationsIntentData {
710 type Error = IntentError;
711
712 fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
713 if let ReaddInstallationsData {
714 version: Some(ReaddInstallationsVersion::V1(v1)),
715 } = ReaddInstallationsData::decode(data.as_slice())?
716 {
717 Ok(Self::new(v1.readded_installations))
718 } else {
719 Err(IntentError::MissingPayload)
720 }
721 }
722}
723
724impl TryFrom<&[u8]> for ReaddInstallationsIntentData {
725 type Error = IntentError;
726
727 fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
728 if let ReaddInstallationsData {
729 version: Some(ReaddInstallationsVersion::V1(v1)),
730 } = ReaddInstallationsData::decode(data)?
731 {
732 Ok(Self::new(v1.readded_installations))
733 } else {
734 Err(IntentError::MissingPayload)
735 }
736 }
737}
738
739#[derive(Debug, Clone)]
741pub(crate) struct ProposeMemberUpdateIntentData {
742 pub add_inbox_ids: Vec<String>,
743 pub remove_inbox_ids: Vec<String>,
744}
745
746impl ProposeMemberUpdateIntentData {
747 pub fn new(add_inbox_ids: Vec<String>, remove_inbox_ids: Vec<String>) -> Self {
748 Self {
749 add_inbox_ids,
750 remove_inbox_ids,
751 }
752 }
753}
754
755impl TryFrom<ProposeMemberUpdateIntentData> for Vec<u8> {
756 type Error = IntentError;
757 fn try_from(intent: ProposeMemberUpdateIntentData) -> Result<Self, Self::Error> {
758 let decode_inbox_ids =
759 |inbox_ids: Vec<String>, item: &'static str| -> Result<Vec<Vec<u8>>, IntentError> {
760 inbox_ids
761 .into_iter()
762 .map(|s| {
763 hex::decode(&s)
764 .map_err(|_| xmtp_proto::ConversionError::InvalidValue {
765 item,
766 expected: "hex encoded string",
767 got: s,
768 })
769 .map_err(Into::into)
770 })
771 .collect::<Result<Vec<_>, _>>()
772 };
773 let proposal = ProposeMemberUpdateData {
774 version: Some(propose_member_update_data::Version::V1(
775 propose_member_update_data::V1 {
776 add_inbox_ids: decode_inbox_ids(intent.add_inbox_ids, "add_inbox_ids")?,
777 remove_inbox_ids: decode_inbox_ids(
778 intent.remove_inbox_ids,
779 "remove_inbox_ids",
780 )?,
781 },
782 )),
783 }
784 .encode_to_vec();
785 Ok(proposal)
786 }
787}
788
789impl TryFrom<&[u8]> for ProposeMemberUpdateIntentData {
790 type Error = IntentError;
791
792 fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
793 let proto = ProposeMemberUpdateData::decode(data)?;
794 let decode_inbox_ids = |inbox_ids: Vec<Vec<u8>>| -> Vec<String> {
795 inbox_ids
796 .into_iter()
797 .map(|b| hex::encode(&b))
798 .collect::<Vec<_>>()
799 };
800 match proto.version {
801 Some(propose_member_update_data::Version::V1(v1)) => {
802 let add_inbox_ids = decode_inbox_ids(v1.add_inbox_ids);
803 let remove_inbox_ids = decode_inbox_ids(v1.remove_inbox_ids);
804 Ok(Self::new(add_inbox_ids, remove_inbox_ids))
805 }
806 None => Err(IntentError::MissingPayload),
807 }
808 }
809}
810
811#[derive(Debug, Clone)]
813pub(crate) struct ProposeGroupContextExtensionsIntentData {
814 pub extensions_bytes: Vec<u8>,
816}
817
818impl ProposeGroupContextExtensionsIntentData {
819 pub fn new(extensions_bytes: Vec<u8>) -> Self {
820 Self { extensions_bytes }
821 }
822}
823
824impl From<ProposeGroupContextExtensionsIntentData> for Vec<u8> {
825 fn from(intent: ProposeGroupContextExtensionsIntentData) -> Self {
826 ProposeGroupContextExtensionData {
827 version: Some(propose_group_context_extension_data::Version::V1(
828 propose_group_context_extension_data::V1 {
829 group_context_extension: intent.extensions_bytes,
830 },
831 )),
832 }
833 .encode_to_vec()
834 }
835}
836
837impl TryFrom<&[u8]> for ProposeGroupContextExtensionsIntentData {
838 type Error = IntentError;
839
840 fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
841 let proto = ProposeGroupContextExtensionData::decode(data)?;
842 match proto.version {
843 Some(propose_group_context_extension_data::Version::V1(v1)) => {
844 Ok(Self::new(v1.group_context_extension))
845 }
846 None => Err(IntentError::MissingPayload),
847 }
848 }
849}
850
851#[derive(Debug, Clone, Default)]
853pub(crate) struct CommitPendingProposalsIntentData {
854 }
856
857impl CommitPendingProposalsIntentData {
858 pub fn new() -> Self {
859 Self {}
860 }
861}
862
863impl From<CommitPendingProposalsIntentData> for Vec<u8> {
864 fn from(_intent: CommitPendingProposalsIntentData) -> Self {
865 CommitPendingProposalsData {
866 version: Some(commit_pending_proposals_data::Version::V1(
867 commit_pending_proposals_data::V1 {
868 proposal_hashes: vec![],
869 },
870 )),
871 }
872 .encode_to_vec()
873 }
874}
875
876impl TryFrom<&[u8]> for CommitPendingProposalsIntentData {
877 type Error = IntentError;
878
879 fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
880 if data.is_empty() {
882 return Ok(Self::new());
883 }
884 let proto = CommitPendingProposalsData::decode(data)?;
885 match proto.version {
886 Some(commit_pending_proposals_data::Version::V1(_)) => Ok(Self::new()),
887 None => Err(IntentError::MissingPayload),
888 }
889 }
890}
891
892#[derive(Debug, Clone)]
893pub enum PostCommitAction {
894 SendWelcomes(SendWelcomesAction),
895}
896
897#[derive(Debug, Clone)]
898pub struct Installation {
899 pub(crate) installation_key: Vec<u8>,
900 pub(crate) hpke_public_key: Vec<u8>,
901 pub(crate) welcome_wrapper_algorithm: WrapperAlgorithm,
902 pub(crate) welcome_pointee_encryption_aead_types: WelcomePointersExtension,
903}
904
905impl Installation {
906 pub fn from_verified_key_package(
907 key_package: &VerifiedKeyPackageV2,
908 ) -> Result<Self, IntentError> {
909 let wrapper_encryption = key_package.wrapper_encryption()?.unwrap_or_else(|| {
910 WrapperEncryptionExtension::new(
913 WrapperAlgorithm::Curve25519,
914 key_package.hpke_init_key(),
915 )
916 });
917
918 let welcome_pointee_encryption_aead_types = key_package
919 .inner
920 .extensions()
921 .unknown(WELCOME_POINTEE_ENCRYPTION_AEAD_TYPES_EXTENSION_ID)
922 .map(|ext| ext.0.as_slice().try_into())
923 .transpose()?;
924
925 Ok(Self {
926 installation_key: key_package.installation_id(),
927 hpke_public_key: wrapper_encryption.pub_key_bytes,
928 welcome_wrapper_algorithm: wrapper_encryption.algorithm,
929 welcome_pointee_encryption_aead_types: welcome_pointee_encryption_aead_types
930 .unwrap_or_else(WelcomePointersExtension::empty),
931 })
932 }
933}
934
935impl From<Installation> for InstallationProto {
936 fn from(installation: Installation) -> Self {
937 Self {
938 installation_key: installation.installation_key,
939 hpke_public_key: installation.hpke_public_key,
940 welcome_wrapper_algorithm: installation.welcome_wrapper_algorithm.into(),
941 welcome_pointee_encryption_aead_types: Some(
942 installation.welcome_pointee_encryption_aead_types.into(),
943 ),
944 }
945 }
946}
947
948impl TryFrom<InstallationProto> for Installation {
949 type Error = ConversionError;
950 fn try_from(installation: InstallationProto) -> Result<Self, Self::Error> {
951 Ok(Self {
952 installation_key: installation.installation_key,
953 hpke_public_key: installation.hpke_public_key,
954 welcome_wrapper_algorithm: installation.welcome_wrapper_algorithm.try_into()?,
955 welcome_pointee_encryption_aead_types: installation
956 .welcome_pointee_encryption_aead_types
957 .map(Into::into)
958 .unwrap_or_else(WelcomePointersExtension::empty),
959 })
960 }
961}
962
963#[derive(Debug, Clone)]
964pub struct SendWelcomesAction {
965 pub installations: Vec<Installation>,
966 pub welcome_message: Vec<u8>,
967}
968
969impl SendWelcomesAction {
970 pub fn new(installations: Vec<Installation>, welcome_message: Vec<u8>) -> Self {
971 Self {
972 installations,
973 welcome_message,
974 }
975 }
976
977 pub(crate) fn to_bytes(&self) -> Vec<u8> {
978 PostCommitActionProto {
979 kind: Some(PostCommitActionKind::SendWelcomes(SendWelcomesProto {
980 installations: self
981 .installations
982 .clone()
983 .into_iter()
984 .map(|i| i.into())
985 .collect(),
986 welcome_message: self.welcome_message.clone(),
987 })),
988 }
989 .encode_to_vec()
990 }
991}
992
993impl PostCommitAction {
994 pub(crate) fn to_bytes(&self) -> Vec<u8> {
995 match self {
996 PostCommitAction::SendWelcomes(action) => action.to_bytes(),
997 }
998 }
999
1000 pub(crate) fn from_bytes(data: &[u8]) -> Result<Self, IntentError> {
1001 let decoded = PostCommitActionProto::decode(data)?
1002 .kind
1003 .ok_or(IntentError::MissingPostCommit)?;
1004 match decoded {
1005 PostCommitActionKind::SendWelcomes(proto) => {
1006 Ok(Self::SendWelcomes(SendWelcomesAction::new(
1007 proto
1008 .installations
1009 .into_iter()
1010 .map(|i| i.try_into())
1011 .collect::<Result<_, _>>()?,
1012 proto.welcome_message,
1013 )))
1014 }
1015 }
1016 }
1017
1018 pub(crate) fn from_welcome(
1019 welcome: MlsMessageOut,
1020 installations: Vec<Installation>,
1021 ) -> Result<Self, IntentError> {
1022 let welcome_bytes = welcome.tls_serialize_detached()?;
1023
1024 Ok(Self::SendWelcomes(SendWelcomesAction::new(
1025 installations,
1026 welcome_bytes,
1027 )))
1028 }
1029}
1030
1031impl TryFrom<Vec<u8>> for PostCommitAction {
1032 type Error = IntentError;
1033
1034 fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
1035 PostCommitAction::from_bytes(data.as_slice())
1036 }
1037}
1038
1039#[derive(Debug, Clone, PartialEq, Eq)]
1056pub struct AppDataUpdateIntentData {
1057 pub component_id: u16,
1059 pub payload: Vec<u8>,
1062}
1063
1064impl AppDataUpdateIntentData {
1065 pub fn new(component_id: u16, payload: Vec<u8>) -> Self {
1068 Self {
1069 component_id,
1070 payload,
1071 }
1072 }
1073}
1074
1075impl From<AppDataUpdateIntentData> for Vec<u8> {
1089 fn from(intent: AppDataUpdateIntentData) -> Self {
1090 use prost::Message;
1091 use xmtp_proto::xmtp::mls::database::{
1092 AppDataUpdateData, app_data_update_data::V1 as AppDataUpdateDataV1,
1093 app_data_update_data::Version as AppDataUpdateVersion,
1094 };
1095 AppDataUpdateData {
1096 version: Some(AppDataUpdateVersion::V1(AppDataUpdateDataV1 {
1097 component_id: intent.component_id as u32,
1098 payload: intent.payload,
1099 })),
1100 }
1101 .encode_to_vec()
1102 }
1103}
1104
1105impl TryFrom<Vec<u8>> for AppDataUpdateIntentData {
1106 type Error = IntentError;
1107
1108 fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
1109 Self::try_from(data.as_slice())
1110 }
1111}
1112
1113impl TryFrom<&[u8]> for AppDataUpdateIntentData {
1114 type Error = IntentError;
1115
1116 fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
1117 use prost::Message;
1118 use xmtp_proto::xmtp::mls::database::{
1119 AppDataUpdateData, app_data_update_data::Version as AppDataUpdateVersion,
1120 };
1121 let proto = AppDataUpdateData::decode(data)?;
1122 let v1 = match proto.version {
1123 Some(AppDataUpdateVersion::V1(v1)) => v1,
1124 None => {
1125 return Err(IntentError::Generic(
1126 "AppDataUpdateIntentData missing version oneof variant".into(),
1127 ));
1128 }
1129 };
1130 let component_id = u16::try_from(v1.component_id).map_err(|_| {
1131 IntentError::Generic(format!(
1132 "AppDataUpdateIntentData component_id {} exceeds u16 range",
1133 v1.component_id
1134 ))
1135 })?;
1136 Ok(Self {
1137 component_id,
1138 payload: v1.payload,
1139 })
1140 }
1141}
1142
1143#[cfg(test)]
1144mod app_data_update_intent_tests {
1145 use super::*;
1146
1147 #[test]
1148 fn round_trip_basic() {
1149 let intent = AppDataUpdateIntentData::new(0x800C, vec![0xAA, 0xBB, 0xCC]);
1150 let bytes: Vec<u8> = intent.clone().into();
1151 let restored = AppDataUpdateIntentData::try_from(bytes).unwrap();
1152 assert_eq!(restored, intent);
1153 }
1154
1155 #[test]
1156 fn empty_payload_round_trips() {
1157 let intent = AppDataUpdateIntentData::new(0x800C, vec![]);
1162 let bytes: Vec<u8> = intent.clone().into();
1163 let restored = AppDataUpdateIntentData::try_from(bytes).unwrap();
1164 assert_eq!(restored, intent);
1165 }
1166
1167 #[test]
1168 fn missing_version_variant_surfaces_error() {
1169 use prost::Message;
1173 use xmtp_proto::xmtp::mls::database::AppDataUpdateData;
1174 let bytes = AppDataUpdateData { version: None }.encode_to_vec();
1175 let err = AppDataUpdateIntentData::try_from(bytes).unwrap_err();
1176 match err {
1177 IntentError::Generic(msg) => assert!(msg.contains("missing version")),
1178 _ => panic!("expected Generic error, got {err:?}"),
1179 }
1180 }
1181
1182 #[test]
1183 fn component_id_overflow_surfaces_error() {
1184 use prost::Message;
1191 use xmtp_proto::xmtp::mls::database::{
1192 AppDataUpdateData, app_data_update_data::V1 as AppDataUpdateDataV1,
1193 app_data_update_data::Version as AppDataUpdateVersion,
1194 };
1195 let bytes = AppDataUpdateData {
1196 version: Some(AppDataUpdateVersion::V1(AppDataUpdateDataV1 {
1197 component_id: u16::MAX as u32 + 1,
1198 payload: vec![],
1199 })),
1200 }
1201 .encode_to_vec();
1202 let err = AppDataUpdateIntentData::try_from(bytes).unwrap_err();
1203 match err {
1204 IntentError::Generic(msg) => assert!(msg.contains("exceeds u16")),
1205 _ => panic!("expected Generic error, got {err:?}"),
1206 }
1207 }
1208
1209 #[test]
1210 fn malformed_proto_bytes_surface_decode_error() {
1211 let bytes = vec![0xFF; 16];
1214 assert!(AppDataUpdateIntentData::try_from(bytes).is_err());
1215 }
1216}
1217
1218#[cfg(test)]
1219pub(crate) mod tests {
1220 use crate::context::XmtpSharedContext;
1221 use crate::groups::send_message_opts::SendMessageOpts;
1222 use openmls::prelude::{ProcessedMessageContent, ProtocolMessage};
1223 use xmtp_cryptography::utils::generate_local_wallet;
1224 use xmtp_db::XmtpOpenMlsProviderRef;
1225
1226 use crate::{builder::ClientBuilder, utils::TestMlsGroup};
1227
1228 use super::*;
1229
1230 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1231 #[cfg_attr(not(target_arch = "wasm32"), test)]
1232 fn test_serialize_send_message() {
1233 let message = vec![1, 2, 3];
1234 let intent = SendMessageIntentData::new(message.clone());
1235 let as_bytes: Vec<u8> = intent.into();
1236 let restored_intent = SendMessageIntentData::from_bytes(as_bytes.as_slice()).unwrap();
1237
1238 assert_eq!(restored_intent.message, message);
1239 }
1240
1241 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1242 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
1243 async fn test_serialize_update_membership() {
1244 let mut membership_updates = HashMap::new();
1245 membership_updates.insert("foo".to_string(), 123);
1246
1247 let intent = UpdateGroupMembershipIntentData::new(
1248 membership_updates,
1249 vec!["bar".to_string()],
1250 vec![vec![1, 2, 3]],
1251 );
1252
1253 let as_bytes: Vec<u8> = intent.clone().into();
1254 let restored_intent: UpdateGroupMembershipIntentData = as_bytes.try_into().unwrap();
1255
1256 assert_eq!(
1257 intent.membership_updates,
1258 restored_intent.membership_updates
1259 );
1260
1261 assert_eq!(intent.removed_members, restored_intent.removed_members);
1262
1263 assert_eq!(
1264 intent.failed_installations,
1265 restored_intent.failed_installations
1266 );
1267 }
1268
1269 #[xmtp_common::test]
1270 async fn test_serialize_update_metadata() {
1271 let intent = UpdateMetadataIntentData::new_update_group_name("group name".to_string());
1272 let as_bytes: Vec<u8> = intent.clone().into();
1273 let restored_intent: UpdateMetadataIntentData =
1274 UpdateMetadataIntentData::try_from(as_bytes).unwrap();
1275
1276 assert_eq!(intent.field_value, restored_intent.field_value);
1277 }
1278
1279 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1280 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
1281 async fn test_serialize_readd_installations() {
1282 let readded_installations = vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];
1283
1284 let intent = ReaddInstallationsIntentData::new(readded_installations.clone());
1285
1286 let as_bytes: Vec<u8> = intent.into();
1287 let restored_intent: ReaddInstallationsIntentData = as_bytes.try_into().unwrap();
1288
1289 assert_eq!(readded_installations, restored_intent.readded_installations);
1290 }
1291
1292 #[xmtp_common::test]
1293 async fn test_key_rotation_before_first_message() {
1294 let client_a = ClientBuilder::new_test_client_vanilla(&generate_local_wallet()).await;
1295 let client_b = ClientBuilder::new_test_client_vanilla(&generate_local_wallet()).await;
1296
1297 let group_a = client_a.create_group(None, None).expect("create group");
1299 group_a.add_members(&[client_b.inbox_id()]).await.unwrap();
1300 group_a
1301 .send_message(b"First message from A", SendMessageOpts::default())
1302 .await
1303 .unwrap();
1304
1305 verify_num_payloads_in_group(&group_a, 2).await;
1308
1309 let groups_b = client_b.sync_welcomes().await.unwrap();
1311 assert_eq!(groups_b.len(), 1);
1312 let group_b = groups_b[0].clone();
1313 group_b
1314 .send_message(b"First message from B", SendMessageOpts::default())
1315 .await
1316 .expect("send message");
1317
1318 let payloads_a = verify_num_payloads_in_group(&group_a, 4).await;
1321 let payloads_b = verify_num_payloads_in_group(&group_b, 4).await;
1322
1323 for i in 0..payloads_a.len() {
1325 assert_eq!(payloads_a[i].payload_hash, payloads_b[i].payload_hash);
1326 }
1327 verify_commit_updates_leaf_node(&group_a, &payloads_a[2]);
1328
1329 group_b
1331 .send_message(b"Second message from B", SendMessageOpts::default())
1332 .await
1333 .expect("send message");
1334 group_a
1335 .send_message(b"Second message from A", SendMessageOpts::default())
1336 .await
1337 .expect("send message");
1338
1339 verify_num_payloads_in_group(&group_a, 6).await;
1341 verify_num_payloads_in_group(&group_b, 6).await;
1342 }
1343
1344 async fn verify_num_payloads_in_group(
1345 group: &TestMlsGroup,
1346 num_messages: usize,
1347 ) -> Vec<xmtp_proto::types::GroupMessage> {
1348 let messages = group
1349 .context
1350 .api()
1351 .query_group_messages(group.group_id)
1352 .await
1353 .unwrap();
1354 assert_eq!(messages.len(), num_messages);
1355 messages
1356 }
1357
1358 fn verify_commit_updates_leaf_node(
1359 group: &TestMlsGroup,
1360 message: &xmtp_proto::types::GroupMessage,
1361 ) {
1362 let mls_message = message.message.clone();
1363 let mls_message = match mls_message {
1364 ProtocolMessage::PrivateMessage(mls_message) => mls_message,
1365 _ => panic!("error mls_message"),
1366 };
1367
1368 let storage = group.context.mls_storage();
1369 let decrypted_message = group
1370 .load_mls_group_with_lock(storage, |mut mls_group| {
1371 Ok(mls_group
1372 .process_message(&XmtpOpenMlsProviderRef::new(storage), mls_message.clone())
1373 .unwrap())
1374 })
1375 .unwrap();
1376
1377 let staged_commit = match decrypted_message.into_content() {
1378 ProcessedMessageContent::StagedCommitMessage(staged_commit) => *staged_commit,
1379 _ => panic!("error staged_commit"),
1380 };
1381
1382 let path_update_leaf_node = staged_commit.update_path_leaf_node();
1384 assert!(path_update_leaf_node.is_some());
1385 }
1386}