xmtp_mls/groups/
membership.rs1use super::*;
4
5impl<Context> MlsGroup<Context>
6where
7 Context: XmtpSharedContext,
8{
9 #[tracing::instrument(level = "trace", skip_all)]
21 pub async fn add_members_by_identity(
22 &self,
23 account_identifiers: &[Identifier],
24 ) -> Result<UpdateGroupMembershipResult, GroupError> {
25 let requests = account_identifiers.iter().map(Into::into).collect();
27 let inbox_id_map: HashMap<Identifier, String> = self
28 .context
29 .api()
30 .get_inbox_ids(requests)
31 .await?
32 .into_iter()
33 .zip(account_identifiers.iter().cloned())
34 .filter_map(|(inbox, identifier)| inbox.map(|inbox| (identifier, inbox)))
35 .collect();
36
37 if inbox_id_map.len() != account_identifiers.len() {
40 let missing_addresses = account_identifiers
42 .iter()
43 .filter(|identifier| !inbox_id_map.contains_key(*identifier))
44 .map(|identifier| identifier.to_string())
45 .collect();
46 return Err(GroupError::AddressNotFound(missing_addresses));
47 }
48
49 self.add_members(&inbox_id_map.into_values().collect::<Vec<_>>())
50 .await
51 }
52
53 #[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<_>>())))]
54 #[cfg_attr(
55 not(any(test, feature = "test-utils")),
56 tracing::instrument(level = "trace", skip_all)
57 )]
58 pub async fn add_members<S: AsIdRef>(
59 &self,
60 inbox_ids: impl AsRef<[S]>,
61 ) -> Result<UpdateGroupMembershipResult, GroupError> {
62 self.ensure_not_paused().await?;
63
64 let ids = inbox_ids
65 .as_ref()
66 .iter()
67 .map(AsIdRef::as_ref)
68 .collect::<Vec<&str>>();
69 let intent_data = self
70 .get_membership_update_intent(ids.as_slice(), &[])
71 .await?;
72
73 let ok_result = Ok(UpdateGroupMembershipResult::from(intent_data.clone()));
77
78 if intent_data.is_empty() {
79 tracing::warn!("Member already added");
80 return ok_result;
81 }
82
83 let max_members = self
90 .context
91 .server_configuration()
92 .configuration()
93 .mls
94 .max_group_members;
95 let existing = self.with_group_snapshot(|group| {
96 Ok(xmtp_mls_validation::commit::extract_group_membership(
97 group.extensions(),
98 )?)
99 })?;
100 let resulting: HashSet<&str> = existing
101 .inbox_ids()
102 .into_iter()
103 .chain(intent_data.membership_updates.keys().map(String::as_str))
104 .collect();
105 if resulting.len() > max_members {
106 return Err(GroupError::UserLimitExceeded);
107 }
108
109 let intent = QueueIntent::update_group_membership()
110 .data(intent_data)
111 .queue(self)?;
112
113 self.sync_until_intent_resolved(intent.id).await?;
114 let epoch = self.epoch().await?;
115
116 log_event!(
117 Event::AddedMembers,
118 self.context.installation_id(),
119 group_id = self.group_id,
120 members = ?ids,
121 epoch
122 );
123
124 ok_result
125 }
126
127 pub async fn remove_members_by_identity(
136 &self,
137 account_addresses_to_remove: &[Identifier],
138 ) -> Result<(), GroupError> {
139 let account_addresses_to_remove =
140 account_addresses_to_remove.iter().map(Into::into).collect();
141
142 let inbox_id_map = self
143 .context
144 .api()
145 .get_inbox_ids(account_addresses_to_remove)
146 .await?;
147
148 let ids = inbox_id_map
149 .iter()
150 .flatten()
151 .map(AsRef::as_ref)
152 .collect::<Vec<&str>>();
153 self.remove_members(ids.as_slice()).await
154 }
155
156 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", skip_all, fields(inbox_id = %self.context.inbox_id(), inbox_ids = ?inbox_ids)))]
165 #[cfg_attr(
166 not(any(test, feature = "test-utils")),
167 tracing::instrument(level = "trace", skip_all)
168 )]
169 pub async fn remove_members(&self, inbox_ids: &[InboxIdRef<'_>]) -> Result<(), GroupError> {
170 self.ensure_not_paused().await?;
171 let intent_data = self.get_membership_update_intent(&[], inbox_ids).await?;
172 let intent = QueueIntent::update_group_membership()
173 .data(intent_data)
174 .queue(self)?;
175
176 let _ = self.sync_until_intent_resolved(intent.id).await?;
177
178 Ok(())
179 }
180
181 #[allow(dead_code)]
192 pub(crate) async fn readd_installations(
193 &self,
194 installations: Vec<Vec<u8>>,
195 ) -> Result<(), GroupError> {
196 self.ensure_not_paused().await?;
197
198 let readd_min_version =
199 LibXMTPVersion::parse(xmtp_configuration::MIN_RECOVERY_REQUEST_VERSION)
200 .map_err(super::validated_commit::CommitValidationError::from)?;
201 let metadata = self.live_mutable_metadata()?;
202 let group_version = metadata
203 .attributes
204 .get(MetadataField::MinimumSupportedProtocolVersion.as_str());
205 let group_min_version =
206 LibXMTPVersion::parse(group_version.unwrap_or(&"0.0.0".to_string()))
207 .map_err(super::validated_commit::CommitValidationError::from)?;
208
209 if readd_min_version > group_min_version {
210 self.update_group_min_version(xmtp_configuration::MIN_RECOVERY_REQUEST_VERSION)
211 .await?;
212 }
213
214 let intent_data: Vec<u8> = ReaddInstallationsIntentData::new(installations.clone()).into();
215 let intent = QueueIntent::readd_installations()
216 .data(intent_data)
217 .queue(self)?;
218
219 let _ = self.sync_until_intent_resolved(intent.id).await?;
220
221 Ok(())
222 }
223
224 pub(crate) async fn process_pending_self_removals(&self) -> Result<(), GroupError> {
230 self.remove_members_pending_removal().await?;
235 self.cleanup_pending_removal_list().await?;
236 Ok(())
237 }
238
239 pub async fn remove_members_pending_removal(&self) -> Result<(), GroupError> {
248 let pending_removal_list = self.pending_remove_list()?;
249
250 if pending_removal_list.is_empty() {
251 tracing::debug!(
252 group_id = %self.group_id,
253 inbox_id = %self.context.inbox_id(),
254 "Group has no pending removal members"
255 );
256 return Ok(());
257 }
258
259 let is_super_admin = self.live_is_super_admin(self.context.inbox_id())?;
260 if !is_super_admin {
261 tracing::debug!(
262 group_id = %self.group_id,
263 inbox_id = %self.context.inbox_id(),
264 "Current inbox ID is not in admin or super admin list, skipping pending removal processing"
265 );
266 return Ok(());
267 }
268
269 let members = self.members().await?;
271 let member_inbox_ids: HashSet<String> =
272 members.iter().map(|m| m.inbox_id.clone()).collect();
273
274 let valid_removals: Vec<&str> = pending_removal_list
276 .iter()
277 .filter(|inbox_id| member_inbox_ids.contains(*inbox_id))
278 .map(|s| s.as_str())
279 .collect();
280
281 if valid_removals.is_empty() {
282 tracing::warn!(
283 group_id = %self.group_id,
284 pending_count = pending_removal_list.len(),
285 "No valid members found in pending removal list"
286 );
287 return Ok(());
288 }
289 let invalid_removals: Vec<&String> = pending_removal_list
291 .iter()
292 .filter(|inbox_id| !member_inbox_ids.contains(*inbox_id))
293 .collect();
294
295 if !invalid_removals.is_empty() {
296 tracing::warn!(
297 group_id = %self.group_id,
298 invalid_members = ?invalid_removals,
299 "Some members in pending removal list are not in the group"
300 );
301 }
302
303 tracing::info!(
305 group_id = %self.group_id,
306 removing_count = valid_removals.len(),
307 members_to_remove = ?valid_removals,
308 "Removing pending members from group"
309 );
310
311 match self.remove_members(&valid_removals).await {
312 Ok(_) => {
313 tracing::info!(
314 group_id = %self.group_id,
315 removed_count = valid_removals.len(),
316 removed_members = ?valid_removals,
317 "Successfully removed all pending members from group"
318 );
319 }
320 Err(e) => {
321 tracing::error!(
322 group_id = %self.group_id,
323 removed_members = ?valid_removals,
324 error = %e,
325 "Failed to remove pending members from group"
326 );
327 return Err(e);
328 }
329 }
330
331 Ok(())
332 }
333
334 pub async fn cleanup_pending_removal_list(&self) -> Result<(), GroupError> {
345 tracing::debug!(
346 group_id = %self.group_id,
347 "Starting pending removal list cleanup"
348 );
349
350 let pending_removal_list = self.pending_remove_list()?;
352
353 if pending_removal_list.is_empty() {
354 tracing::debug!(
355 group_id = %self.group_id,
356 "No pending removals to clean up"
357 );
358 self.context
360 .db()
361 .set_group_has_pending_leave_request_status(&self.group_id, Some(false))?;
362 return Ok(());
363 }
364
365 let current_members = self.members().await?;
367 let current_member_ids: Vec<String> = current_members
368 .iter()
369 .map(|member| member.inbox_id.clone())
370 .collect();
371
372 let removed_members: Vec<String> = pending_removal_list
374 .iter()
375 .filter(|pending_user| !current_member_ids.contains(pending_user))
376 .cloned()
377 .collect();
378
379 if !removed_members.is_empty() {
380 tracing::info!(
381 group_id = %self.group_id,
382 removed_count = removed_members.len(),
383 removed_members = ?removed_members,
384 "Removing members from pending removal list - they are no longer in the group"
385 );
386
387 self.context
389 .db()
390 .delete_pending_remove_users(&self.group_id, removed_members)?;
391 }
392
393 let remaining_pending_list = self.pending_remove_list()?;
395 if remaining_pending_list.is_empty() {
396 self.context
398 .db()
399 .set_group_has_pending_leave_request_status(&self.group_id, Some(false))?;
400 }
401
402 tracing::info!(
403 group_id = %self.group_id,
404 remaining_pending = remaining_pending_list.len(),
405 "Finished cleaning up pending removal list"
406 );
407
408 Ok(())
409 }
410
411 pub async fn leave_group(&self) -> Result<(), GroupError> {
413 self.ensure_not_paused().await?;
414
415 let is_member = self.is_member().await?;
417 if !is_member {
418 return Err(GroupLeaveValidationError::NotAGroupMember.into());
419 }
420
421 let members = self.members().await?;
423
424 if members.len() == 1 {
426 return Err(GroupLeaveValidationError::SingleMemberLeaveRejected.into());
427 }
428
429 if self.metadata().await?.conversation_type == ConversationType::Dm {
431 return Err(GroupLeaveValidationError::DmLeaveForbidden.into());
432 }
433
434 let is_super_admin = self.live_is_super_admin(self.context.inbox_id())?;
435
436 if is_super_admin {
439 return Err(GroupLeaveValidationError::SuperAdminLeaveForbidden.into());
440 }
441
442 if !self.is_in_pending_remove(self.context.inbox_id())? {
443 let content = LeaveRequestCodec::encode(LeaveRequest {
444 authenticated_note: None,
445 })?;
446 self.send_message(
447 &encoded_content_to_bytes(content),
448 SendMessageOpts::default(),
449 )
450 .await?;
451 };
452 Ok(())
453 }
454
455 #[tracing::instrument(level = "debug", skip(self))]
458 async fn is_member(&self) -> Result<bool, GroupError> {
459 let members = self.members().await?;
460 Ok(members
461 .iter()
462 .any(|m| m.inbox_id == self.context.inbox_id()))
463 }
464}