1use super::*;
4
5impl<Context> MlsGroup<Context>
6where
7 Context: XmtpSharedContext,
8{
9 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
12 #[cfg_attr(
13 not(any(test, feature = "test-utils")),
14 tracing::instrument(level = "trace", skip(self))
15 )]
16 pub async fn update_group_name(&self, group_name: String) -> Result<(), GroupError> {
17 self.ensure_not_paused().await?;
18
19 if group_name.len() > MAX_GROUP_NAME_LENGTH {
20 return Err(GroupError::TooManyCharacters {
21 length: MAX_GROUP_NAME_LENGTH,
22 });
23 }
24 if self.metadata().await?.conversation_type == ConversationType::Dm {
25 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
26 }
27 let intent_data: Vec<u8> =
28 UpdateMetadataIntentData::new_update_group_name(group_name).into();
29 let intent = QueueIntent::metadata_update()
30 .data(intent_data)
31 .queue(self)?;
32
33 let _ = self.sync_until_intent_resolved(intent.id).await?;
34 Ok(())
35 }
36
37 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
50 #[cfg_attr(
51 not(any(test, feature = "test-utils")),
52 tracing::instrument(level = "trace", skip(self))
53 )]
54 pub async fn update_app_data(
55 &self,
56 app_data: String,
57 expected_app_data: Option<String>,
58 ) -> Result<(), GroupError> {
59 self.ensure_not_paused().await?;
60
61 if app_data.len() > MAX_APP_DATA_LENGTH {
62 return Err(GroupError::TooManyCharacters {
63 length: MAX_APP_DATA_LENGTH,
64 });
65 }
66 if self.metadata().await?.conversation_type == ConversationType::Dm {
67 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
68 }
69
70 if let Some(expected) = &expected_app_data {
74 let actual = self.read_single_component::<AppDataComponent>()?;
81 if actual.as_deref() != Some(expected.as_str()) {
82 return Err(GroupError::AppDataSuperseded {
83 expected: expected.clone(),
84 actual: actual.unwrap_or_default(),
90 });
91 }
92 }
93
94 let intent_data: Vec<u8> =
95 UpdateMetadataIntentData::new_update_app_data(app_data, expected_app_data.clone())
96 .into();
97 let intent = QueueIntent::metadata_update()
98 .data(intent_data)
99 .queue(self)?;
100
101 match self.sync_until_intent_resolved(intent.id).await {
102 Ok(_) => Ok(()),
103 Err(err) => {
104 if let Some(expected) = expected_app_data
108 && matches!(
109 self.context.db().fetch(&intent.id),
110 Ok(Some(StoredGroupIntent {
111 state: IntentState::Superseded,
112 ..
113 }))
114 )
115 {
116 return Err(GroupError::AppDataSuperseded {
120 expected,
121 actual: self.app_data()?,
122 });
123 }
124 Err(err)
125 }
126 }
127 }
128
129 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
135 #[cfg_attr(
136 not(any(test, feature = "test-utils")),
137 tracing::instrument(level = "trace", skip(self))
138 )]
139 #[allow(dead_code)]
140 pub(crate) async fn update_group_min_version_to_match_self(&self) -> Result<(), GroupError> {
141 let version = self.context.version_info().pkg_version();
142 self.update_group_min_version(version).await
143 }
144
145 pub async fn update_group_min_version(&self, version: &str) -> Result<(), GroupError> {
161 self.ensure_not_paused().await?;
162
163 let target_v =
177 LibXMTPVersion::parse(version).map_err(|e| GroupError::InvalidMinVersion {
178 value: version.to_string(),
179 reason: e.to_string(),
180 })?;
181 let own_version_str = self.context.version_info().pkg_version().to_string();
182 let own_v =
183 LibXMTPVersion::parse(&own_version_str).map_err(|e| GroupError::InvalidMinVersion {
184 value: own_version_str.clone(),
185 reason: format!("own pkg_version: {e}"),
186 })?;
187 if target_v > own_v {
188 return Err(GroupError::MinVersionExceedsOwnVersion {
189 requested: version.to_string(),
190 own: own_version_str,
191 });
192 }
193 let current_str = self
194 .live_mutable_metadata()?
195 .attributes
196 .get(MetadataField::MinimumSupportedProtocolVersion.as_str())
197 .cloned();
198 if let Some(current_str) = current_str.as_deref()
199 && !current_str.is_empty()
200 {
201 match LibXMTPVersion::parse(current_str) {
202 Ok(current_v) => {
203 if target_v < current_v {
204 return Err(GroupError::MinVersionDowngrade {
205 requested: version.to_string(),
206 current: current_str.to_string(),
207 });
208 }
209 }
210 Err(e) => {
211 tracing::warn!(
216 current = %current_str,
217 error = %e,
218 "update_group_min_version: existing min_version is unparseable; \
219 proceeding without downgrade check"
220 );
221 }
222 }
223 }
224
225 tracing::info!("update_group_min_version: queuing bump to {}", version);
226 let intent_data: Vec<u8> =
227 UpdateMetadataIntentData::new_update_group_min_version_to_match_self(
228 version.to_string(),
229 )
230 .into();
231 let intent = QueueIntent::metadata_update()
232 .data(intent_data)
233 .queue(self)?;
234
235 let _ = self.sync_until_intent_resolved(intent.id).await?;
236 Ok(())
237 }
238
239 pub async fn update_commit_log_signer(
242 &self,
243 commit_log_signer: xmtp_cryptography::Secret,
244 ) -> Result<(), GroupError> {
245 self.ensure_not_paused().await?;
246
247 if self.metadata().await?.conversation_type == ConversationType::Dm {
248 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
249 }
250 let intent_data: Vec<u8> =
251 UpdateMetadataIntentData::new_update_commit_log_signer(commit_log_signer).into();
252 let intent = QueueIntent::metadata_update()
253 .data(intent_data)
254 .queue(self)?;
255
256 let _ = self.sync_until_intent_resolved(intent.id).await?;
257 Ok(())
258 }
259
260 pub(in crate::groups) fn min_protocol_version_from_extensions(
261 mutable_metadata: &GroupMutableMetadata,
262 ) -> Option<String> {
263 mutable_metadata
264 .attributes
265 .get(&MetadataField::MinimumSupportedProtocolVersion.to_string())
266 .map(|v| v.to_string())
267 }
268
269 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
271 #[cfg_attr(
272 not(any(test, feature = "test-utils")),
273 tracing::instrument(level = "trace", skip(self))
274 )]
275 pub async fn update_permission_policy(
276 &self,
277 permission_update_type: PermissionUpdateType,
278 permission_policy: PermissionPolicyOption,
279 metadata_field: Option<MetadataField>,
280 ) -> Result<(), GroupError> {
281 self.ensure_not_paused().await?;
282
283 if self.metadata().await?.conversation_type == ConversationType::Dm {
284 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
285 }
286 if permission_update_type == PermissionUpdateType::UpdateMetadata
287 && metadata_field.is_none()
288 {
289 return Err(MetadataPermissionsError::InvalidPermissionUpdate.into());
290 }
291
292 if matches!(
293 permission_update_type,
294 PermissionUpdateType::AddAdmin | PermissionUpdateType::RemoveAdmin
295 ) && permission_policy == PermissionPolicyOption::Allow
296 {
297 return Err(MetadataPermissionsError::InvalidPermissionUpdate.into());
298 }
299
300 let intent_data: Vec<u8> = UpdatePermissionIntentData::new(
301 permission_update_type,
302 permission_policy,
303 metadata_field.as_ref().map(|field| field.to_string()),
304 )
305 .into();
306
307 let intent = QueueIntent::update_permission()
308 .data(intent_data)
309 .queue(self)?;
310
311 let _ = self.sync_until_intent_resolved(intent.id).await?;
312 Ok(())
313 }
314
315 pub fn group_name(&self) -> Result<String, GroupError> {
317 Ok(self
318 .read_single_component::<GroupNameComponent>()?
319 .unwrap_or_default())
320 }
321
322 pub fn app_data(&self) -> Result<String, GroupError> {
324 Ok(self
325 .read_single_component::<AppDataComponent>()?
326 .unwrap_or_default())
327 }
328
329 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
331 #[cfg_attr(
332 not(any(test, feature = "test-utils")),
333 tracing::instrument(level = "trace", skip(self))
334 )]
335 pub async fn update_group_description(
336 &self,
337 group_description: String,
338 ) -> Result<(), GroupError> {
339 self.ensure_not_paused().await?;
340
341 if group_description.len() > MAX_GROUP_DESCRIPTION_LENGTH {
342 return Err(GroupError::TooManyCharacters {
343 length: MAX_GROUP_DESCRIPTION_LENGTH,
344 });
345 }
346
347 if self.metadata().await?.conversation_type == ConversationType::Dm {
348 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
349 }
350 let intent_data: Vec<u8> =
351 UpdateMetadataIntentData::new_update_group_description(group_description).into();
352 let intent = QueueIntent::metadata_update()
353 .data(intent_data)
354 .queue(self)?;
355
356 let _ = self.sync_until_intent_resolved(intent.id).await?;
357 Ok(())
358 }
359
360 pub fn group_description(&self) -> Result<String, GroupError> {
361 Ok(self
362 .read_single_component::<GroupDescriptionComponent>()?
363 .unwrap_or_default())
364 }
365
366 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
368 #[cfg_attr(
369 not(any(test, feature = "test-utils")),
370 tracing::instrument(level = "trace", skip(self))
371 )]
372 pub async fn update_group_image_url_square(
373 &self,
374 group_image_url_square: String,
375 ) -> Result<(), GroupError> {
376 self.ensure_not_paused().await?;
377
378 if group_image_url_square.len() > MAX_GROUP_IMAGE_URL_LENGTH {
379 return Err(GroupError::TooManyCharacters {
380 length: MAX_GROUP_IMAGE_URL_LENGTH,
381 });
382 }
383
384 if self.metadata().await?.conversation_type == ConversationType::Dm {
385 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
386 }
387 let intent_data: Vec<u8> =
388 UpdateMetadataIntentData::new_update_group_image_url_square(group_image_url_square)
389 .into();
390 let intent = QueueIntent::metadata_update()
391 .data(intent_data)
392 .queue(self)?;
393
394 let _ = self.sync_until_intent_resolved(intent.id).await?;
395 Ok(())
396 }
397
398 pub fn group_image_url_square(&self) -> Result<String, GroupError> {
400 Ok(self
401 .read_single_component::<GroupImageUrlComponent>()?
402 .unwrap_or_default())
403 }
404
405 pub async fn update_conversation_message_disappearing_settings(
406 &self,
407 settings: MessageDisappearingSettings,
408 ) -> Result<(), GroupError> {
409 self.ensure_not_paused().await?;
410
411 self.update_conversation_message_disappear_from_ns(settings.from_ns)
412 .await?;
413 self.update_conversation_message_disappear_in_ns(settings.in_ns)
414 .await
415 }
416
417 pub async fn remove_conversation_message_disappearing_settings(
418 &self,
419 ) -> Result<(), GroupError> {
420 self.ensure_not_paused().await?;
421
422 self.update_conversation_message_disappearing_settings(
423 MessageDisappearingSettings::default(),
424 )
425 .await
426 }
427
428 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
429 #[cfg_attr(
430 not(any(test, feature = "test-utils")),
431 tracing::instrument(level = "trace", skip(self))
432 )]
433 pub(in crate::groups) async fn update_conversation_message_disappear_from_ns(
434 &self,
435 expire_from_ms: i64,
436 ) -> Result<(), GroupError> {
437 self.ensure_not_paused().await?;
438
439 let intent_data: Vec<u8> =
440 UpdateMetadataIntentData::new_update_conversation_message_disappear_from_ns(
441 expire_from_ms,
442 )
443 .into();
444 let intent = QueueIntent::metadata_update()
445 .data(intent_data)
446 .queue(self)?;
447 let _ = self.sync_until_intent_resolved(intent.id).await?;
448 Ok(())
449 }
450
451 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
452 #[cfg_attr(
453 not(any(test, feature = "test-utils")),
454 tracing::instrument(level = "trace", skip(self))
455 )]
456 pub(in crate::groups) async fn update_conversation_message_disappear_in_ns(
457 &self,
458 expire_in_ms: i64,
459 ) -> Result<(), GroupError> {
460 self.ensure_not_paused().await?;
461
462 let intent_data: Vec<u8> =
463 UpdateMetadataIntentData::new_update_conversation_message_disappear_in_ns(expire_in_ms)
464 .into();
465 let intent = QueueIntent::metadata_update()
466 .data(intent_data)
467 .queue(self)?;
468 let _ = self.sync_until_intent_resolved(intent.id).await?;
469 Ok(())
470 }
471
472 pub fn paused_for_version(&self) -> Result<Option<String>, GroupError> {
474 let paused_for_version = self.context.db().get_group_paused_version(&self.group_id)?;
475 Ok(paused_for_version)
476 }
477
478 #[tracing::instrument(skip_all, level = "trace")]
479 pub(in crate::groups) async fn ensure_not_paused(&self) -> Result<(), GroupError> {
480 if let Some(min_version) = self.context.db().get_group_paused_version(&self.group_id)? {
481 Err(GroupError::GroupPausedUntilUpdate(min_version))
482 } else {
483 Ok(())
484 }
485 }
486
487 pub fn conversation_message_disappearing_settings(
488 &self,
489 ) -> Result<MessageDisappearingSettings, GroupError> {
490 let stored = self
491 .context
492 .db()
493 .find_group(&self.group_id)?
494 .ok_or(NotFound::GroupById(self.group_id))?;
495 if stored.membership_state == GroupMembershipState::Restored {
496 return match (
497 stored.message_disappear_from_ns,
498 stored.message_disappear_in_ns,
499 ) {
500 (Some(from_ns), Some(in_ns)) => {
501 Ok(MessageDisappearingSettings::new(from_ns, in_ns))
502 }
503 _ => Err(GroupError::MetadataPermissionsError(
504 GroupMetadataError::MissingExtension.into(),
505 )),
506 };
507 }
508 let metadata = self.live_mutable_metadata()?;
509 Self::conversation_message_disappearing_settings_from_extensions(&metadata)
510 }
511
512 pub fn conversation_message_disappearing_settings_from_extensions(
513 mutable_metadata: &GroupMutableMetadata,
514 ) -> Result<MessageDisappearingSettings, GroupError> {
515 let disappear_from_ns = mutable_metadata
516 .attributes
517 .get(&MetadataField::MessageDisappearFromNS.to_string());
518 let disappear_in_ns = mutable_metadata
519 .attributes
520 .get(&MetadataField::MessageDisappearInNS.to_string());
521
522 if let (Some(Ok(message_disappear_from_ns)), Some(Ok(message_disappear_in_ns))) = (
523 disappear_from_ns.map(|s| s.parse::<i64>()),
524 disappear_in_ns.map(|s| s.parse::<i64>()),
525 ) {
526 Ok(MessageDisappearingSettings::new(
527 message_disappear_from_ns,
528 message_disappear_in_ns,
529 ))
530 } else {
531 Err(GroupError::MetadataPermissionsError(
532 GroupMetadataError::MissingExtension.into(),
533 ))
534 }
535 }
536
537 pub fn pending_remove_list(&self) -> Result<Vec<String>, GroupError> {
538 self.context
539 .db()
540 .get_pending_remove_users(&self.group_id)
541 .map_err(Into::into)
542 }
543
544 pub fn is_in_pending_remove(&self, inbox_id: &str) -> Result<bool, GroupError> {
546 self.context
547 .db()
548 .get_user_pending_remove_status(&self.group_id, inbox_id)
549 .map_err(Into::into)
550 }
551
552 pub fn admin_list(&self) -> Result<Vec<String>, GroupError> {
557 self.read_admin_set(AdminListKind::Admin)
558 }
559
560 pub fn super_admin_list(&self) -> Result<Vec<String>, GroupError> {
564 self.read_admin_set(AdminListKind::SuperAdmin)
565 }
566
567 fn read_admin_set(&self, kind: AdminListKind) -> Result<Vec<String>, GroupError> {
568 if self.membership_state()? == GroupMembershipState::Restored {
569 return Ok(Vec::new());
570 }
571 let ctx = self.load_group_context()?;
572 let extensions = ctx.extensions();
573 let facade = xmtp_mls_common::app_data::typed_facade::MlsGroupAppData::new(extensions);
574 let set = match kind {
575 AdminListKind::Admin => facade.get::<AdminListComponent>(),
576 AdminListKind::SuperAdmin => facade.get::<SuperAdminListComponent>(),
577 }
578 .map_err(|e| {
579 GroupError::MetadataPermissionsError(MetadataPermissionsError::ComponentSource(e))
580 })?;
581 Ok(set
582 .map(|s| s.iter().map(|id| id.to_hex()).collect())
583 .unwrap_or_default())
584 }
585
586 pub fn is_admin(&self, inbox_id: String) -> Result<bool, GroupError> {
588 let mutable_metadata = self.mutable_metadata()?;
589 Ok(mutable_metadata.admin_list.contains(&inbox_id))
590 }
591
592 pub fn is_super_admin(&self, inbox_id: String) -> Result<bool, GroupError> {
594 let mutable_metadata = self.mutable_metadata()?;
595 Ok(mutable_metadata.super_admin_list.contains(&inbox_id))
596 }
597
598 pub(crate) fn live_is_super_admin(&self, inbox_id: &str) -> Result<bool, GroupError> {
605 if self.membership_state()? == GroupMembershipState::Restored {
606 return Ok(false);
607 }
608 Ok(self
609 .live_mutable_metadata()?
610 .super_admin_list
611 .iter()
612 .any(|id| id == inbox_id))
613 }
614
615 pub fn is_super_admin_without_lock(
617 &self,
618 mls_group: &OpenMlsGroup,
619 inbox_id: String,
620 ) -> Result<bool, GroupMutableMetadataError> {
621 let list =
622 xmtp_mls_common::app_data::component_source::read_super_admin_list_from_dict(mls_group)
623 .map_err(GroupMutableMetadataError::from)?
624 .unwrap_or_default();
625 Ok(list.contains(&inbox_id))
626 }
627
628 pub async fn conversation_type(&self) -> Result<ConversationType, GroupError> {
630 let conversation_type = self.context.db().get_conversation_type(&self.group_id)?;
631 Ok(conversation_type)
632 }
633}