1use std::collections::HashSet;
5
6use crate::groups::MetadataPermissionsError;
7use crate::groups::mls_ext::CommitLogStorer;
8use crate::groups::mls_ext::ResolvedWelcome;
9use crate::groups::oneshot::Oneshot;
10use crate::groups::welcomes::WelcomeMembership;
11use crate::identity_updates::{
12 IdentityDependencyError, IdentityRequirement, InstallationDiffError,
13};
14use crate::state_tx::{state_write, state_write_with_events};
15use crate::subscriptions::internal::{InternalEvent, PreferenceOrigin, emit_preference_updates};
16use crate::worker::device_sync::is_own_sync_group;
17use crate::{
18 context::XmtpSharedContext,
19 groups::{
20 GroupError, MlsGroup, ValidateGroupMembership, mls_ext::DecryptedWelcome, validate_dm_group,
21 },
22 intents::ProcessIntentError,
23};
24use derive_builder::Builder;
25use openmls::group::MlsGroup as OpenMlsGroup;
26use prost::Message;
27use xmtp_common::{snippet::Snippet, time::now_ns};
28use xmtp_content_types::ContentCodec;
29use xmtp_content_types::group_updated::GroupUpdatedCodec;
30use xmtp_db::TransactionOutcome::{Continue, Rollback};
31use xmtp_db::{
32 TransactionOutcome, XmtpOpenMlsProviderRef,
33 consent_record::{ConsentState, StoredConsentRecord},
34 group::{ConversationType, GroupMembershipState, StoredGroup},
35 group_message::{ContentType, DeliveryStatus, GroupMessageKind, StoredGroupMessage},
36 incoming_envelope::{JoinAnchorMode, NetworkEntityKind, StoredIncomingEnvelope, StreamTopic},
37 prelude::*,
38 refresh_state::EntityKind,
39};
40use xmtp_events::{
41 ClientEvent, ConversationJoined, ConversationType as EventConversationType, EventBuffer,
42 EventWriter, JoinOrigin,
43};
44use xmtp_mls_common::group_metadata::extract_group_metadata;
45use xmtp_mls_common::libxmtp_version::LibXMTPVersion;
46
47use xmtp_mls_common::app_data::component_source::extract_group_mutable_metadata_capability_aware;
48use xmtp_proto::types::Cursor;
49use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, GroupUpdated, group_updated::Inbox};
50
51use xmtp_proto::types::GroupId;
52#[derive(Builder)]
61#[builder(
62 pattern = "owned",
63 setter(strip_option),
64 build_fn(error = "GroupError", private)
65)]
66pub struct XmtpWelcome<'a, C, V> {
67 context: C,
68 welcome: &'a xmtp_proto::types::WelcomeMessage,
70 pending: StoredIncomingEnvelope,
72 validator: V,
73}
74
75impl<'a, C, V> XmtpWelcome<'a, C, V> {
76 pub fn builder() -> XmtpWelcomeBuilder<'a, C, V> {
77 Default::default()
78 }
79}
80
81enum CommitResult<C> {
83 FailedForever(GroupError),
85 Ok(Option<MlsGroup<C>>),
87}
88
89pub(crate) fn terminal_welcome_error(error: &GroupError) -> bool {
91 use openmls::prelude::WelcomeError;
92 matches!(
93 error,
94 GroupError::InvalidWelcomeMetadata
95 | GroupError::InvalidGroupMembership
96 | GroupError::MetadataPermissionsError(_)
97 | GroupError::NoPSKSupport
98 | GroupError::TlsError(_)
99 | GroupError::CredentialError(_)
100 | GroupError::Identity(
101 crate::identity::IdentityError::Decode(_)
102 | crate::identity::IdentityError::BasicCredential(_)
103 )
104 | GroupError::ConversionError(_)
105 | GroupError::UnwrapWelcome(_)
106 | GroupError::ProcessIntent(ProcessIntentError::WelcomeAlreadyProcessed(_))
107 | GroupError::InstallationDiff(InstallationDiffError::IdentityDependency(
108 IdentityDependencyError::InvalidSequence(_)
109 | IdentityDependencyError::MissingReference(_)
110 ))
111 | GroupError::WelcomeError(
112 WelcomeError::GroupSecrets(_)
113 | WelcomeError::CiphersuiteMismatch
114 | WelcomeError::GroupInfo(_)
115 | WelcomeError::JoinerSecretNotFound
116 | WelcomeError::MissingRatchetTree
117 | WelcomeError::ConfirmationTagMismatch
118 | WelcomeError::InvalidGroupInfoSignature
119 | WelcomeError::UnknownSender
120 | WelcomeError::NotAWelcomeMessage
121 | WelcomeError::MalformedWelcomeMessage
122 | WelcomeError::UnableToDecrypt
123 | WelcomeError::PublicTreeError(_)
124 | WelcomeError::LeafNodeValidation(_)
125 )
126 )
127}
128
129impl<C> CommitResult<C> {
130 fn into_result(self) -> Result<Option<MlsGroup<C>>, GroupError> {
131 match self {
132 Self::FailedForever(err) => Err(err),
133 Self::Ok(group) => Ok(group),
134 }
135 }
136}
137
138impl<'a, C, V> XmtpWelcomeBuilder<'a, C, V>
139where
140 C: XmtpSharedContext,
141 V: ValidateGroupMembership,
142{
143 #[tracing::instrument(skip_all, fields(operation = "mls.process_welcome"))]
148 pub async fn process(self) -> Result<Option<MlsGroup<C>>, GroupError> {
149 let this = self.build()?;
150 this.check_pending(&this.context.db())?;
151
152 let (resolved, membership) = match this.validate_membership().await {
153 Err(error) => return this.reject_or_retry(error),
154 Ok(validated) => validated,
155 };
156 let commit_result = this.commit_or_fail_forever(&resolved, Some(&membership), None)?;
157 commit_result.into_result()
158 }
159
160 pub(crate) fn process_resolved(
163 self,
164 resolved: &ResolvedWelcome,
165 missing_reference: Option<&IdentityRequirement>,
166 ) -> Result<Option<MlsGroup<C>>, GroupError> {
167 let this = self.build()?;
168 this.commit_or_fail_forever(resolved, None, missing_reference)?
169 .into_result()
170 }
171}
172
173impl<'a, C, V> XmtpWelcome<'a, C, V>
174where
175 C: XmtpSharedContext,
176 V: ValidateGroupMembership,
177 <C::MlsStorage as XmtpMlsStorageProvider>::Connection: xmtp_db::ConnectionExt,
178{
179 fn topic(&self) -> StreamTopic {
180 StreamTopic {
181 entity_id: self.context.installation_id().to_vec(),
182 kind: NetworkEntityKind::Welcome,
183 }
184 }
185
186 fn check_pending(&self, db: &impl DbQuery) -> Result<(), GroupError> {
188 let current = db.pending_envelope(&self.topic(), self.welcome.cursor)?;
189 if current.is_none_or(|row| row.envelope != self.pending.envelope)
190 || self.pending.sequence_id != self.welcome.cursor.0 as i64
191 || self.pending.entity_id != self.context.installation_id()
192 {
193 return Err(ProcessIntentError::WelcomeAlreadyProcessed(self.welcome.cursor).into());
194 }
195 Ok(())
196 }
197
198 fn reject_or_retry(&self, error: GroupError) -> Result<Option<MlsGroup<C>>, GroupError> {
199 if terminal_welcome_error(&error) {
200 state_write(self.context.mls_storage(), |tx| {
201 let storage = tx.storage();
202 let db = storage.db();
203 self.check_pending(&db)?;
204 db.record_terminal_rejection(
205 &self.topic(),
206 self.welcome.cursor,
207 "invalid_welcome",
208 )?;
209 db.complete_pending_envelope(&self.topic(), self.welcome.cursor)?;
210 Ok::<_, GroupError>(Continue(()))
211 })?;
212 }
213 Err(error)
214 }
215
216 async fn validate_membership(
219 &self,
220 ) -> Result<(ResolvedWelcome, WelcomeMembership), GroupError> {
221 let resolved = ResolvedWelcome::resolve(self.welcome, &self.context).await?;
222 let mut membership = None;
223 state_write(self.context.mls_storage(), |tx| {
224 let storage = tx.storage();
225 let decrypted = resolved.stage(self.welcome, &storage)?;
226 self.join_anchor(&decrypted)?;
227 membership = Some(WelcomeMembership::from_staged(&decrypted.staged_welcome)?);
228 Ok::<_, GroupError>(Rollback::<()>)
229 })?;
230 let membership = membership.ok_or(GroupError::UninitializedResult)?;
231 membership.validate_sequences(self.welcome.sequence_id())?;
232 self.validator.check_initial_membership(&membership).await?;
233 Ok((resolved, membership))
234 }
235
236 fn join_anchor(&self, decrypted: &DecryptedWelcome) -> Result<Cursor, GroupError> {
240 let metadata = extract_group_metadata(
241 decrypted
242 .staged_welcome
243 .public_group()
244 .group_context()
245 .extensions(),
246 )
247 .map_err(MetadataPermissionsError::from)?;
248 let anchor = match &decrypted.welcome_metadata {
249 Some(metadata) => metadata.message_cursor,
250 None if metadata.conversation_type == ConversationType::Oneshot => 0,
251 None => return Err(GroupError::InvalidWelcomeMetadata),
252 };
253 let initial = metadata.conversation_type == ConversationType::Oneshot
254 || decrypted
255 .staged_welcome
256 .public_group()
257 .group_context()
258 .epoch()
259 .as_u64()
260 == 0;
261 if anchor >= self.welcome.sequence_id() || (anchor == 0 && !initial) {
262 return Err(GroupError::InvalidWelcomeMetadata);
263 }
264 Ok(Cursor(anchor))
265 }
266
267 fn commit_or_fail_forever(
270 &self,
271 resolved: &ResolvedWelcome,
272 membership: Option<&WelcomeMembership>,
273 missing_reference: Option<&IdentityRequirement>,
274 ) -> Result<CommitResult<C>, GroupError> {
275 tracing::debug!("attempting to commit welcome={}", &self.welcome.cursor);
276 let commit_result = state_write_with_events(
277 self.context.mls_storage(),
278 self.context.events(),
279 |tx, event_buffer| {
280 let storage = tx.storage();
281 self.check_pending(&storage.db())?;
282 let result = event_buffer.savepoint(|event_buffer| {
284 storage.savepoint(|conn| {
285 self.commit(conn, resolved, membership, event_buffer)
286 .map(Continue)
287 })
288 });
289 let db = storage.db();
290 let result = result.map_err(|error| match (&error, missing_reference) {
292 (
293 GroupError::InstallationDiff(InstallationDiffError::IdentityDependency(
294 IdentityDependencyError::Need(required),
295 )),
296 Some(missing),
297 ) if required == missing => InstallationDiffError::IdentityDependency(
298 IdentityDependencyError::MissingReference(missing.clone()),
299 )
300 .into(),
301 _ => error,
302 });
303 match result {
304 Err(err) if terminal_welcome_error(&err) => {
305 db.record_terminal_rejection(
306 &self.topic(),
307 self.welcome.cursor,
308 "invalid_welcome",
309 )?;
310 db.complete_pending_envelope(&self.topic(), self.welcome.cursor)?;
311 Ok(Continue(CommitResult::FailedForever(err)))
313 }
314 Err(e) => Err(e),
316 Ok(Continue(group)) => {
317 db.complete_pending_envelope(&self.topic(), self.welcome.cursor)?;
318 self.context.task_channels().mark_notification_changed();
319 Ok(Continue(CommitResult::Ok(group)))
320 }
321 Ok(Rollback) => {
322 unreachable!("savepoint never intentionally rolls back here")
323 }
324 }
325 },
326 )
327 .map(TransactionOutcome::into_continued)?;
328 Ok(commit_result)
329 }
330
331 fn commit(
336 &self,
337 tx: &mut impl TransactionalKeyStore,
338 resolved: &ResolvedWelcome,
339 expected_membership: Option<&WelcomeMembership>,
340 event_buffer: &EventBuffer<'_, InternalEvent>,
341 ) -> Result<Option<MlsGroup<C>>, GroupError> {
342 let Self {
343 welcome, context, ..
344 } = self;
345
346 let storage = tx.key_store();
347 let db = storage.db();
348 let provider = XmtpOpenMlsProviderRef::new(&storage);
349
350 self.check_pending(&db)?;
351 let decrypted = resolved.stage(welcome, &storage)?;
352 let anchor = self.join_anchor(&decrypted)?;
353 let membership = WelcomeMembership::from_staged(&decrypted.staged_welcome)?;
354 membership.validate_sequences(welcome.sequence_id())?;
355 if expected_membership.is_some_and(|expected| *expected != membership) {
356 return Err(GroupError::LockUnavailable);
357 }
358 let DecryptedWelcome {
359 staged_welcome,
360 added_by_inbox_id,
361 added_by_installation_id,
362 welcome_metadata: _,
363 } = decrypted;
364 let metadata =
365 extract_group_metadata(staged_welcome.public_group().group_context().extensions())
366 .map_err(MetadataPermissionsError::from)?;
367 if metadata.conversation_type == ConversationType::Sync
370 && !is_own_sync_group(
371 context.inbox_id(),
372 &added_by_inbox_id,
373 staged_welcome.public_group().members(),
374 )
375 {
376 tracing::warn!(
377 added_by_inbox_id,
378 "rejecting a sync group Welcome that names another inbox"
379 );
380 return Err(GroupError::InvalidGroupMembership);
381 }
382 self.validator.check_verified_membership(&membership, &db)?;
383 let group_id = GroupId::try_from(staged_welcome.public_group().group_id())?;
387 let existing_group = db.find_group(&group_id)?;
388 if let Some(existing) = &existing_group
389 && existing.membership_state == GroupMembershipState::Restored
390 {
391 if existing.conversation_type != metadata.conversation_type {
392 return Err(GroupError::InvalidWelcomeMetadata);
393 }
394 if existing.conversation_type == ConversationType::Dm {
395 let archived_pair =
396 crate::groups::parse_canonical_dm_id(existing.dm_id.as_deref())?;
397 if metadata.dm_members.as_ref().map(String::from).as_deref()
398 != Some(archived_pair.to_string().as_str())
399 {
400 return Err(MetadataPermissionsError::from(
401 crate::groups::DmValidationError::StoredDmIdMismatch,
402 )
403 .into());
404 }
405 }
406 }
407 if metadata.conversation_type == ConversationType::Oneshot {
408 Oneshot::process_welcome(
409 &provider,
410 welcome.cursor,
411 added_by_inbox_id,
412 added_by_installation_id,
413 metadata,
414 )?;
415 return Ok(None);
416 }
417
418 let mut anchor_mode = JoinAnchorMode::Advance;
419
420 if let Some(existing) = &existing_group
421 && existing.membership_state != GroupMembershipState::Restored
422 {
423 let mut current = OpenMlsGroup::load(&storage, &group_id.to_openmls())?
424 .ok_or(xmtp_db::NotFound::MlsGroup(group_id))?;
425 let processed = db.latest_cursor_for_id(group_id, &[EntityKind::ApplicationMessage])?;
426 let incoming_epoch = staged_welcome.public_group().group_context().epoch();
427 let active = current.is_active();
428 if processed == anchor && !current.is_active() && current.epoch() <= incoming_epoch {
432 anchor_mode = JoinAnchorMode::InactiveReadd;
433 }
434 if processed > anchor
435 || (processed == anchor && anchor_mode != JoinAnchorMode::InactiveReadd)
436 || (active && current.epoch() >= incoming_epoch)
437 {
438 return Err(ProcessIntentError::WelcomeAlreadyProcessed(welcome.cursor).into());
439 }
440 if active {
441 return Err(GroupError::WelcomeGroupPrefixPending {
442 group_id,
443 anchor: anchor.0,
444 });
445 }
446 current.clear_pending_proposals(&storage)?;
450 db.forget_received_proposals(group_id)?;
451 }
452
453 let is_readd_after_leaving = existing_group
456 .as_ref()
457 .is_some_and(|g| g.membership_state == GroupMembershipState::PendingRemove);
458
459 let mls_group = OpenMlsGroup::from_welcome_logged(
460 &provider,
461 staged_welcome,
462 &added_by_inbox_id,
463 &added_by_installation_id,
464 context.server_configuration().commit_log_enabled(),
465 )?;
466 let dm_members = metadata.dm_members;
467 let conversation_type = metadata.conversation_type;
468 let mutable_metadata = Some(
470 extract_group_mutable_metadata_capability_aware(&mls_group)
471 .map_err(|_| GroupError::InvalidWelcomeMetadata)?,
472 );
473 let disappearing_settings = mutable_metadata.as_ref().and_then(|metadata| {
474 MlsGroup::<C>::conversation_message_disappearing_settings_from_extensions(metadata).ok()
475 });
476
477 if let Some(min_version) = mutable_metadata
478 .as_ref()
479 .and_then(MlsGroup::<C>::min_protocol_version_from_extensions)
480 {
481 let required = LibXMTPVersion::parse(&min_version)
482 .map_err(|_| GroupError::InvalidWelcomeMetadata)?;
483 if required > *context.version_info().pkg_semver() {
484 return Err(GroupError::UnsupportedWelcomeVersion(min_version));
485 }
486 }
487
488 {
491 let registry = crate::groups::app_data::load_component_registry(&mls_group)
492 .map_err(|_| GroupError::InvalidWelcomeMetadata)?;
493 xmtp_mls_common::app_data::policy_set::validate_registry_action_policies(®istry)
494 .map_err(|error| {
495 tracing::warn!(%error, "invalid registry action policy in Welcome");
496 GroupError::InvalidWelcomeMetadata
497 })?;
498 }
499
500 let membership_state = if is_readd_after_leaving {
504 tracing::info!(
505 group_id = %group_id,
506 "User is being re-added after leaving/removal, setting membership state to ALLOWED"
507 );
508 GroupMembershipState::Allowed
509 } else {
510 tracing::debug!(
511 group_id = %group_id,
512 "User is being added to new group, setting membership state to PENDING"
513 );
514 GroupMembershipState::Pending
515 };
516
517 let mut group = StoredGroup::builder();
518 group
519 .id(group_id)
520 .created_at_ns(now_ns())
521 .added_by_inbox_id(&added_by_inbox_id)
522 .cursor(welcome.cursor)
523 .conversation_type(conversation_type)
524 .dm_id(dm_members.map(String::from))
525 .message_disappear_from_ns(disappearing_settings.as_ref().map(|m| m.from_ns))
526 .message_disappear_in_ns(disappearing_settings.as_ref().map(|m| m.in_ns))
527 .should_publish_commit_log(MlsGroup::<C>::check_should_publish_commit_log(
528 context.inbox_id().to_string(),
529 mutable_metadata,
530 ));
531
532 let to_store = match conversation_type {
533 ConversationType::Group => group.membership_state(membership_state).build()?,
534 ConversationType::Dm => {
535 validate_dm_group(context, &mls_group, &added_by_inbox_id)?;
536 group
537 .membership_state(membership_state)
538 .last_message_ns(welcome.timestamp())
539 .build()?
540 }
541 ConversationType::Sync => {
542 group
544 .membership_state(GroupMembershipState::Allowed)
545 .build()?
546 }
547 ConversationType::Oneshot => {
548 unreachable!("StagedWelcome of type Oneshot should already be handled")
549 }
550 };
551
552 tracing::debug!("storing group with welcome id {}", welcome.cursor);
553
554 if is_readd_after_leaving && let Some(ref existing) = existing_group {
557 tracing::info!(
558 group_id = %existing.id,
559 "Updating existing group membership state from PENDING_REMOVE to ALLOWED"
560 );
561 db.update_group_membership(existing.id, GroupMembershipState::Allowed)?;
562 }
563
564 let stored_group = db.insert_or_replace_group(to_store)?;
566 let consent_entity = hex::encode(stored_group.id);
567 let prior_consent = db.get_consent_record(
568 consent_entity.clone(),
569 xmtp_db::consent_record::ConsentType::ConversationId,
570 )?;
571
572 StoredConsentRecord::stitch_dm_consent(&db, &stored_group)?;
573
574 let current_inbox_id = context.inbox_id().to_string();
576 let added_payload = GroupUpdated {
577 initiated_by_inbox_id: added_by_inbox_id.clone(),
578 added_inboxes: vec![Inbox {
579 inbox_id: current_inbox_id.clone(),
580 }],
581 removed_inboxes: vec![],
582 metadata_field_changes: vec![],
583 left_inboxes: vec![],
584 added_admin_inboxes: vec![],
585 removed_admin_inboxes: vec![],
586 added_super_admin_inboxes: vec![],
587 removed_super_admin_inboxes: vec![],
588 };
589
590 let encoded_added_payload = GroupUpdatedCodec::encode(added_payload)?;
591 let mut encoded_added_payload_bytes = Vec::new();
592 encoded_added_payload.encode(&mut encoded_added_payload_bytes)?;
593
594 let added_idempotency_key = format!("{}_welcome_added", welcome.created_ns);
595 let added_message_id = crate::utils::id::calculate_message_id(
596 stored_group.id,
597 encoded_added_payload_bytes.as_slice(),
598 &added_idempotency_key,
599 );
600
601 let added_content_type = encoded_added_payload.r#type.unwrap_or_else(|| {
602 tracing::warn!("Missing content type in encoded added payload, using default values");
603 ContentTypeId {
604 authority_id: "unknown".to_string(),
605 type_id: "unknown".to_string(),
606 version_major: 0,
607 version_minor: 0,
608 }
609 });
610
611 let cursor = anchor.0 as i64;
612
613 let added_msg = StoredGroupMessage {
615 id: added_message_id,
616 group_id: stored_group.id,
617 decrypted_message_bytes: encoded_added_payload_bytes,
618 sent_at_ns: welcome.timestamp(),
619 kind: GroupMessageKind::MembershipChange,
620 sender_installation_id: added_by_installation_id,
621 sender_inbox_id: added_by_inbox_id.clone(),
622 delivery_status: DeliveryStatus::Published,
623 content_type: ContentType::from_identifier(
624 &added_content_type.authority_id,
625 &added_content_type.type_id,
626 added_content_type.version_major,
627 ),
628 version_major: added_content_type.version_major as i32,
629 version_minor: added_content_type.version_minor as i32,
630 authority_id: added_content_type.authority_id,
631 reference_id: None,
632 sequence_id: cursor,
633 envelope_hash: None,
634 expiry_ns: None,
635 expire_at_ns: None,
636 inserted_at_ns: 0, should_push: true,
638 idempotency_key: added_idempotency_key,
640 };
641
642 added_msg.store_or_ignore(&db)?;
643
644 tracing::debug!("created GroupUpdated message for welcome, inbox_id={current_inbox_id}");
645
646 let group = MlsGroup::new(
647 context.clone(),
648 stored_group.id,
649 stored_group.dm_id,
650 stored_group.conversation_type,
651 stored_group.created_at_ns,
652 );
653
654 if context.inbox_id() == metadata.creator_inbox_id {
656 group.quietly_update_consent_state(ConsentState::Allowed, &db)?;
657 } else if is_readd_after_leaving {
658 tracing::info!(
661 group_id = %group.group_id,
662 "Resetting consent state to Unknown for re-added user"
663 );
664 group.quietly_update_consent_state(ConsentState::Unknown, &db)?;
665 }
666
667 if !stored_group.conversation_type.is_virtual() {
668 let final_consent = db.get_consent_record(
669 consent_entity,
670 xmtp_db::consent_record::ConsentType::ConversationId,
671 )?;
672 if prior_consent.as_ref().map(|record| record.state)
673 != final_consent.as_ref().map(|record| record.state)
674 && let Some(record) = final_consent
675 {
676 emit_preference_updates(
677 event_buffer,
678 vec![
679 crate::worker::device_sync::preference_sync::PreferenceUpdate::Consent(
680 record,
681 ),
682 ],
683 PreferenceOrigin::Local,
684 &db,
685 )?;
686 }
687 }
688
689 db.install_group_anchor(group.group_id, anchor, anchor_mode)?;
691 db.record_welcome_discovery(group.group_id, welcome.cursor)?;
692 MlsGroup::<C>::mark_readd_requests_as_responded(
693 &storage,
694 &group.group_id,
695 &HashSet::from([context.installation_id().to_vec()]),
696 cursor,
697 )?;
698 event_buffer.emit(
699 (!stored_group.conversation_type.is_virtual()).then(|| {
700 ClientEvent::ConversationJoined(ConversationJoined {
701 group_id: group.group_id.to_vec(),
702 conversation_type: if stored_group.conversation_type == ConversationType::Dm {
703 EventConversationType::Dm
704 } else {
705 EventConversationType::Group
706 },
707 origin: JoinOrigin::Welcomed,
708 adder_inbox_id: Some(added_by_inbox_id.clone()),
709 })
710 }),
711 Some(InternalEvent::GroupJoined {
712 group_id: group.group_id,
713 is_sync: stored_group.conversation_type == ConversationType::Sync,
714 origin: crate::subscriptions::internal::GroupOrigin::Welcomed,
715 }),
716 );
717
718 tracing::debug!(
719 inbox_id = %current_inbox_id,
720 installation_id = %self.context.installation_id().as_slice().snippet(),
721 group_id = %group.group_id,
722 welcome_id = welcome.cursor.0,
723
724 cursor = cursor,
725 "updated message cursor from welcome metadata"
726 );
727
728 Ok(Some(group))
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use crate::groups::InitialMembershipValidator;
736 use crate::tester;
737 use crate::utils::test::MlsGroupExt;
738
739 struct UnavailableValidator;
740
741 impl ValidateGroupMembership for UnavailableValidator {
742 async fn check_initial_membership(
743 &self,
744 _welcome: &WelcomeMembership,
745 ) -> Result<(), GroupError> {
746 Err(GroupError::LockUnavailable)
747 }
748 }
749
750 #[xmtp_common::test(unwrap_try = true)]
751 async fn trial_validation_failure_preserves_welcome_keys() {
752 tester!(alix, disable_workers);
753 tester!(bo, disable_workers);
754 let alix_group = alix.create_group(None, None)?;
755 alix_group.invite(&bo).await?;
756 let welcome = bo
757 .context
758 .api()
759 .query_welcome_messages(bo.context.installation_id())
760 .await?
761 .pop()?;
762
763 let events = bo.context.events().subscribe(
764 xmtp_events::EventFilter::new([xmtp_events::EventKind::ConversationJoined]),
765 Some(10),
766 );
767 let result = XmtpWelcome::builder()
768 .context(bo.context.clone())
769 .welcome(&welcome)
770 .pending(
771 crate::groups::welcome_sync::pending_welcome_for_test(&bo.context, &welcome)
772 .await?,
773 )
774 .validator(UnavailableValidator)
775 .process()
776 .await;
777 assert!(matches!(result, Err(GroupError::LockUnavailable)));
778 assert!(events.drain().is_empty());
779 assert!(bo.context.db().find_group(&alix_group.group_id)?.is_none());
780 assert_eq!(
781 bo.context
782 .db()
783 .get_last_cursor(bo.context.installation_id(), EntityKind::Welcome)?,
784 Cursor(0)
785 );
786
787 let bo_group = bo.sync_welcomes().await?.pop()?;
788 assert!(matches!(
789 events.drain().pop().and_then(|event| event.client),
790 Some(xmtp_events::ClientEvent::ConversationJoined(joined)) if joined.group_id == bo_group.group_id.to_vec()
791 ));
792 alix_group.test_can_talk_with(&bo_group).await?;
793 }
794
795 struct JoinDuringValidation<C> {
796 context: C,
797 welcome: xmtp_proto::types::WelcomeMessage,
798 }
799
800 impl<C: XmtpSharedContext> ValidateGroupMembership for JoinDuringValidation<C> {
801 async fn check_initial_membership(
802 &self,
803 _welcome: &WelcomeMembership,
804 ) -> Result<(), GroupError> {
805 let group = XmtpWelcome::builder()
806 .context(self.context.clone())
807 .welcome(&self.welcome)
808 .pending(
809 crate::groups::welcome_sync::pending_welcome_for_test(
810 &self.context,
811 &self.welcome,
812 )
813 .await?,
814 )
815 .validator(InitialMembershipValidator::new(self.context.clone()))
816 .process()
817 .await?
818 .ok_or(GroupError::UninitializedResult)?;
819 group.sync_with_conn().await?;
820 Ok(())
821 }
822 }
823
824 #[xmtp_common::test(unwrap_try = true)]
825 async fn fresh_install_rejects_welcome_after_another_writer_advances_group() {
826 tester!(alix, disable_workers);
827 tester!(bo, disable_workers);
828 let alix_group = alix.create_group(None, None)?;
829 alix_group.invite(&bo).await?;
830 alix_group
831 .update_group_name("advanced during validation".into())
832 .await?;
833 let welcome = bo
834 .context
835 .api()
836 .query_welcome_messages(bo.context.installation_id())
837 .await?
838 .pop()?;
839
840 let result = XmtpWelcome::builder()
841 .context(bo.context.clone())
842 .welcome(&welcome)
843 .pending(
844 crate::groups::welcome_sync::pending_welcome_for_test(&bo.context, &welcome)
845 .await?,
846 )
847 .validator(JoinDuringValidation {
848 context: bo.context.clone(),
849 welcome: welcome.clone(),
850 })
851 .process()
852 .await;
853 assert!(matches!(
854 result,
855 Err(GroupError::ProcessIntent(
856 ProcessIntentError::WelcomeAlreadyProcessed(_)
857 ))
858 ));
859 let bo_group = bo.group(&alix_group.group_id)?;
860 assert_eq!(
861 alix_group.epoch_authenticator().await?,
862 bo_group.epoch_authenticator().await?
863 );
864 alix_group.test_can_talk_with(&bo_group).await?;
865 }
866
867 #[rstest::rstest]
869 #[case::late_active_prefix(false)]
870 #[case::live_retired_controller(true)]
871 #[xmtp_common::test(unwrap_try = true)]
872 async fn rejoin_keeps_messages_before_removal(#[case] live_receiver: bool) {
873 use crate::subscriptions::incoming::{
874 IncomingCoordinator, IncomingRegistration, IncomingScope,
875 };
876 use xmtp_common::time::{Duration, timeout};
877
878 tester!(alix, disable_workers);
879 tester!(bo, disable_workers);
880 let alix_group = alix.create_group(None, None).unwrap();
881 alix_group.invite(&bo).await.unwrap();
882 let bo_group = bo.sync_welcomes().await.unwrap().pop().unwrap();
883 let lease = live_receiver.then(|| {
884 IncomingCoordinator::for_context(&bo.context).acquire(IncomingScope::AllGroups)
885 });
886 let topic = xmtp_proto::types::Topic::new_group_message(bo_group.group_id);
887 alix_group.send_msg(b"before removal").await;
888 alix_group.remove_members(&[bo.inbox_id()]).await.unwrap();
889 if let Some(lease) = &lease {
890 timeout(Duration::from_secs(10), async {
891 loop {
892 if lease.snapshot().topics.iter().any(|entry| {
893 entry.topic == topic && entry.registration == IncomingRegistration::Removed
894 }) {
895 break;
896 }
897 lease.changed().await;
898 }
899 })
900 .await
901 .unwrap();
902 }
903 alix_group.invite(&bo).await.unwrap();
904
905 bo.sync_welcomes().await.unwrap();
906 let messages = bo
907 .context
908 .db()
909 .get_group_messages(&bo_group.group_id, &Default::default())
910 .unwrap();
911 assert!(
912 messages
913 .iter()
914 .any(|message| message.decrypted_message_bytes == b"before removal")
915 );
916 assert_eq!(
917 alix_group.epoch_authenticator().await.unwrap(),
918 bo_group.epoch_authenticator().await.unwrap()
919 );
920 if let Some(lease) = &lease {
921 alix_group.send_msg(b"after rejoin").await;
922 timeout(Duration::from_secs(10), async {
923 loop {
924 let messages = bo
925 .context
926 .db()
927 .get_group_messages(&bo_group.group_id, &Default::default())
928 .unwrap();
929 if messages
930 .iter()
931 .any(|message| message.decrypted_message_bytes == b"after rejoin")
932 {
933 return Ok::<_, xmtp_db::StorageError>(());
934 }
935 lease.changed().await;
936 }
937 })
938 .await
939 .unwrap()
940 .unwrap();
941 }
942 alix_group.test_can_talk_with(&bo_group).await.unwrap();
943 }
944}