1use super::*;
4use prost::Message;
5use xmtp_mls_group_state::app_data::{
6 components::tls_map_components::GroupMembershipComponent, typed_facade::MlsGroupAppData,
7};
8use xmtp_proto::ShortHex;
9use xmtp_proto::xmtp::mls::message_contents::{GroupMembershipEntry, group_membership_entry};
10
11impl<Context> MlsGroup<Context>
12where
13 Context: XmtpSharedContext,
14{
15 #[tracing::instrument(level = "trace", skip_all)]
32 pub async fn add_members_by_identity(
33 &self,
34 account_identifiers: &[Identifier],
35 ) -> Result<UpdateGroupMembershipResult, GroupError> {
36 self.add_members_by_targets(&crate::inbox_handles::account_targets(account_identifiers))
39 .await
40 }
41
42 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", skip_all, fields(inbox_id = %self.context.inbox_id(), inbox_ids = ?inbox_ids.as_ref().iter().map(|i| i.as_ref()).collect::<Vec<_>>())))]
43 #[cfg_attr(
44 not(any(test, feature = "test-utils")),
45 tracing::instrument(level = "trace", skip_all)
46 )]
47 pub async fn add_members<S: AsIdRef>(
48 &self,
49 inbox_ids: impl AsRef<[S]>,
50 ) -> Result<UpdateGroupMembershipResult, GroupError> {
51 self.ensure_not_paused().await?;
52
53 let ids = inbox_ids
54 .as_ref()
55 .iter()
56 .map(AsIdRef::as_ref)
57 .collect::<Vec<&str>>();
58 let intent_data = self
59 .get_membership_update_intent(ids.as_slice(), &[])
60 .await?;
61
62 let mut result = UpdateGroupMembershipResult::from(intent_data.clone());
66
67 if intent_data.is_empty() {
68 tracing::warn!("Member already added");
69 return Ok(result);
70 }
71
72 let max_members = self
79 .context
80 .server_configuration()
81 .configuration()
82 .mls
83 .max_group_members;
84 let existing = self.with_group_snapshot(|group| {
85 Ok(xmtp_mls_validation::commit::extract_group_membership(
86 group.extensions(),
87 )?)
88 })?;
89 let resulting: HashSet<&str> = existing
90 .inbox_ids()
91 .into_iter()
92 .chain(intent_data.membership_updates.keys().map(String::as_str))
93 .collect();
94 if resulting.len() > max_members {
95 return Err(GroupError::UserLimitExceeded);
96 }
97
98 let intent = QueueIntent::update_group_membership()
99 .data(intent_data)
100 .queue(self)?;
101
102 self.sync_until_intent_resolved(intent.id).await?;
103 let epoch = self.epoch().await?;
104
105 result.failed_installations = self.with_group_snapshot(|group| {
109 let entries = MlsGroupAppData::new(group.extensions())
110 .get::<GroupMembershipComponent>()?
111 .ok_or(GroupError::InvalidGroupMembership)?;
112 let mut failed = Vec::new();
113 for (inbox, bytes) in entries.iter() {
114 if !result.added_members.contains_key(&inbox.to_hex()) {
115 continue;
116 }
117 let entry = GroupMembershipEntry::decode(bytes.as_slice())?;
118 let Some(group_membership_entry::Version::V1(entry)) = entry.version else {
119 return Err(GroupError::InvalidGroupMembership);
120 };
121 failed.extend(entry.failed_installations);
122 }
123 failed.sort_unstable();
124 failed.dedup();
125 Ok(failed)
126 })?;
127
128 log_event!(
129 Event::AddedMembers,
130 self.context.installation_id(),
131 group_id = self.group_id,
132 members = ?ids,
133 epoch
134 );
135
136 Ok(result)
137 }
138
139 pub async fn ensure_dm_peer(&self) -> Result<(), GroupError> {
160 match self.add_missing_dm_peer().await {
161 Err(error @ GroupError::PublishedButUnconfirmed { .. }) => {
162 tracing::warn!(
163 group_id = self.group_id.short_hex(),
164 ?error,
165 "the DM peer add is published but unconfirmed; later intents wait behind it"
166 );
167 Ok(())
168 }
169 result => result,
170 }
171 }
172
173 pub(crate) async fn add_missing_dm_peer(&self) -> Result<(), GroupError> {
177 let Some(peer) = self.missing_dm_peer()? else {
178 return Ok(());
179 };
180 tracing::info!(
181 group_id = self.group_id.short_hex(),
182 "adding the missing peer of an interrupted DM creation"
183 );
184 self.add_members(&[peer]).await?;
185 if self.missing_dm_peer()?.is_some() {
188 return Err(GroupError::InvalidGroupMembership);
189 }
190 Ok(())
191 }
192
193 fn missing_dm_peer(&self) -> Result<Option<String>, GroupError> {
196 if self.conversation_type != ConversationType::Dm || !self.is_active()? {
197 return Ok(None);
198 }
199 self.with_group_snapshot(|group| {
202 let Some(pair) = extract_group_metadata(group.extensions())
203 .map_err(MetadataPermissionsError::from)?
204 .dm_members
205 else {
206 return Ok(None);
207 };
208 let own = self.context.inbox_id();
209 let peer = if pair.member_one_inbox_id == own {
210 pair.member_two_inbox_id
211 } else if pair.member_two_inbox_id == own {
212 pair.member_one_inbox_id
213 } else {
214 return Ok(None);
215 };
216 let membership =
217 xmtp_mls_validation::commit::extract_group_membership(group.extensions())?;
218 Ok::<_, GroupError>(membership.get(&peer).is_none().then_some(peer))
219 })
220 }
221
222 pub async fn remove_members_by_identity(
233 &self,
234 account_addresses_to_remove: &[Identifier],
235 ) -> Result<(), GroupError> {
236 self.remove_members_by_targets(&crate::inbox_handles::account_targets(
237 account_addresses_to_remove,
238 ))
239 .await
240 }
241
242 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", skip_all, fields(inbox_id = %self.context.inbox_id(), inbox_ids = ?inbox_ids)))]
251 #[cfg_attr(
252 not(any(test, feature = "test-utils")),
253 tracing::instrument(level = "trace", skip_all)
254 )]
255 pub async fn remove_members(&self, inbox_ids: &[InboxIdRef<'_>]) -> Result<(), GroupError> {
256 self.remove_inboxes(inbox_ids, false).await
257 }
258
259 async fn remove_inboxes(&self, ids: &[InboxIdRef<'_>], leave: bool) -> Result<(), GroupError> {
260 self.ensure_not_paused().await?;
261 let mut intent_data = self.get_membership_update_intent(&[], ids).await?;
262 if leave {
263 intent_data =
264 intent_data.with_leave_removals(ids.iter().map(|s| s.to_string()).collect());
265 }
266 let intent = QueueIntent::update_group_membership()
267 .data(intent_data)
268 .queue(self)?;
269
270 let _ = self.sync_until_intent_resolved(intent.id).await?;
271
272 Ok(())
273 }
274
275 #[allow(dead_code)]
286 pub(crate) async fn readd_installations(
287 &self,
288 installations: Vec<Vec<u8>>,
289 ) -> Result<(), GroupError> {
290 self.ensure_not_paused().await?;
291
292 let readd_min_version =
293 LibXMTPVersion::parse(xmtp_configuration::MIN_RECOVERY_REQUEST_VERSION)
294 .map_err(super::validated_commit::CommitValidationError::from)?;
295 let metadata = self.live_mutable_metadata()?;
296 let group_version = metadata
297 .attributes()
298 .get(MetadataField::MinimumSupportedProtocolVersion.as_str());
299 let group_min_version =
300 LibXMTPVersion::parse(group_version.unwrap_or(&"0.0.0".to_string()))
301 .map_err(super::validated_commit::CommitValidationError::from)?;
302
303 if readd_min_version > group_min_version {
304 self.update_group_min_version(xmtp_configuration::MIN_RECOVERY_REQUEST_VERSION)
305 .await?;
306 }
307
308 let intent_data: Vec<u8> = ReaddInstallationsIntentData::new(installations.clone()).into();
309 let intent = QueueIntent::readd_installations()
310 .data(intent_data)
311 .queue(self)?;
312
313 let _ = self.sync_until_intent_resolved(intent.id).await?;
314
315 Ok(())
316 }
317
318 pub(crate) async fn process_pending_self_removals(&self) -> Result<(), GroupError> {
324 self.remove_members_pending_removal().await?;
329 self.cleanup_pending_removal_list().await?;
330 Ok(())
331 }
332
333 pub async fn remove_members_pending_removal(&self) -> Result<(), GroupError> {
342 let pending_removal_list = self.pending_remove_list()?;
343
344 if pending_removal_list.is_empty() {
345 tracing::debug!(
346 group_id = %self.group_id,
347 inbox_id = %self.context.inbox_id(),
348 "Group has no pending removal members"
349 );
350 return Ok(());
351 }
352
353 let is_super_admin = self.live_is_super_admin(self.context.inbox_id())?;
354 if !is_super_admin {
355 tracing::debug!(
356 group_id = %self.group_id,
357 inbox_id = %self.context.inbox_id(),
358 "Current inbox ID is not in admin or super admin list, skipping pending removal processing"
359 );
360 return Ok(());
361 }
362
363 let members = self.members().await?;
365 let member_inbox_ids: HashSet<String> =
366 members.iter().map(|m| m.inbox_id.clone()).collect();
367
368 let valid_removals: Vec<&str> = pending_removal_list
370 .iter()
371 .filter(|inbox_id| member_inbox_ids.contains(*inbox_id))
372 .map(|s| s.as_str())
373 .collect();
374
375 if valid_removals.is_empty() {
376 tracing::warn!(
377 group_id = %self.group_id,
378 pending_count = pending_removal_list.len(),
379 "No valid members found in pending removal list"
380 );
381 return Ok(());
382 }
383 let invalid_removals: Vec<&String> = pending_removal_list
385 .iter()
386 .filter(|inbox_id| !member_inbox_ids.contains(*inbox_id))
387 .collect();
388
389 if !invalid_removals.is_empty() {
390 tracing::warn!(
391 group_id = %self.group_id,
392 invalid_members = ?invalid_removals,
393 "Some members in pending removal list are not in the group"
394 );
395 }
396
397 tracing::info!(
399 group_id = %self.group_id,
400 removing_count = valid_removals.len(),
401 members_to_remove = ?valid_removals,
402 "Removing pending members from group"
403 );
404
405 match self.remove_inboxes(&valid_removals, true).await {
406 Ok(_) => {
407 tracing::info!(
408 group_id = %self.group_id,
409 removed_count = valid_removals.len(),
410 removed_members = ?valid_removals,
411 "Successfully removed all pending members from group"
412 );
413 }
414 Err(e) => {
415 tracing::error!(
416 group_id = %self.group_id,
417 removed_members = ?valid_removals,
418 error = %e,
419 "Failed to remove pending members from group"
420 );
421 return Err(e);
422 }
423 }
424
425 Ok(())
426 }
427
428 pub async fn cleanup_pending_removal_list(&self) -> Result<(), GroupError> {
439 tracing::debug!(
440 group_id = %self.group_id,
441 "Starting pending removal list cleanup"
442 );
443
444 let pending_removal_list = self.pending_remove_list()?;
446
447 if pending_removal_list.is_empty() {
448 tracing::debug!(
449 group_id = %self.group_id,
450 "No pending removals to clean up"
451 );
452 self.context
454 .db()
455 .set_group_has_pending_leave_request_status(&self.group_id, Some(false))?;
456 return Ok(());
457 }
458
459 let current_members = self.members().await?;
461 let current_member_ids: Vec<String> = current_members
462 .iter()
463 .map(|member| member.inbox_id.clone())
464 .collect();
465
466 let removed_members: Vec<String> = pending_removal_list
468 .iter()
469 .filter(|pending_user| !current_member_ids.contains(pending_user))
470 .cloned()
471 .collect();
472
473 if !removed_members.is_empty() {
474 tracing::info!(
475 group_id = %self.group_id,
476 removed_count = removed_members.len(),
477 removed_members = ?removed_members,
478 "Removing members from pending removal list - they are no longer in the group"
479 );
480
481 self.context
483 .db()
484 .delete_pending_remove_users(&self.group_id, removed_members)?;
485 }
486
487 let remaining_pending_list = self.pending_remove_list()?;
489 if remaining_pending_list.is_empty() {
490 self.context
492 .db()
493 .set_group_has_pending_leave_request_status(&self.group_id, Some(false))?;
494 }
495
496 tracing::info!(
497 group_id = %self.group_id,
498 remaining_pending = remaining_pending_list.len(),
499 "Finished cleaning up pending removal list"
500 );
501
502 Ok(())
503 }
504
505 pub async fn leave_group(&self) -> Result<(), GroupError> {
507 self.ensure_not_paused().await?;
508
509 let is_member = self.is_member().await?;
511 if !is_member {
512 return Err(GroupLeaveValidationError::NotAGroupMember.into());
513 }
514
515 let members = self.members().await?;
517
518 if members.len() == 1 {
520 return Err(GroupLeaveValidationError::SingleMemberLeaveRejected.into());
521 }
522
523 if self.metadata().await?.conversation_type == ConversationType::Dm {
525 return Err(GroupLeaveValidationError::DmLeaveForbidden.into());
526 }
527
528 let is_super_admin = self.live_is_super_admin(self.context.inbox_id())?;
529
530 if is_super_admin {
533 return Err(GroupLeaveValidationError::SuperAdminLeaveForbidden.into());
534 }
535
536 if !self.is_in_pending_remove(self.context.inbox_id())? {
537 let content = LeaveRequestCodec::encode(LeaveRequest {
538 authenticated_note: None,
539 })?;
540 self.send_message(
541 &encoded_content_to_bytes(content),
542 SendMessageOpts::default(),
543 )
544 .await?;
545 };
546 Ok(())
547 }
548
549 #[tracing::instrument(level = "debug", skip(self))]
552 async fn is_member(&self) -> Result<bool, GroupError> {
553 let members = self.members().await?;
554 Ok(members
555 .iter()
556 .any(|m| m.inbox_id == self.context.inbox_id()))
557 }
558}