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