1use super::*;
4use xmtp_mls_group_state::app_data::fields::{ComponentMutation, FieldValue, MetadataFieldRef};
5
6impl<Context> MlsGroup<Context>
7where
8 Context: XmtpSharedContext,
9{
10 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
13 #[cfg_attr(
14 not(any(test, feature = "test-utils")),
15 tracing::instrument(level = "trace", skip(self))
16 )]
17 pub async fn update_group_name(&self, group_name: String) -> Result<(), GroupError> {
18 self.ensure_not_paused().await?;
19
20 if group_name.len() > MAX_GROUP_NAME_LENGTH {
21 return Err(GroupError::TooManyCharacters {
22 length: MAX_GROUP_NAME_LENGTH,
23 });
24 }
25 if self.metadata().await?.conversation_type == ConversationType::Dm {
26 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
27 }
28 let intent_data: Vec<u8> =
29 UpdateMetadataIntentData::new_update_group_name(group_name).into();
30 let intent = QueueIntent::metadata_update()
31 .data(intent_data)
32 .queue(self)?;
33
34 let _ = self.sync_until_intent_resolved(intent.id).await?;
35 Ok(())
36 }
37
38 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
51 #[cfg_attr(
52 not(any(test, feature = "test-utils")),
53 tracing::instrument(level = "trace", skip(self))
54 )]
55 pub async fn update_app_data(
56 &self,
57 app_data: String,
58 expected_app_data: Option<String>,
59 ) -> Result<(), GroupError> {
60 self.ensure_not_paused().await?;
61
62 if app_data.len() > MAX_APP_DATA_LENGTH {
63 return Err(GroupError::TooManyCharacters {
64 length: MAX_APP_DATA_LENGTH,
65 });
66 }
67 if self.metadata().await?.conversation_type == ConversationType::Dm {
68 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
69 }
70
71 if let Some(expected) = &expected_app_data {
75 let actual = self.read_single_component::<AppDataComponent>()?;
82 if actual.as_deref() != Some(expected.as_str()) {
83 return Err(GroupError::AppDataSuperseded {
84 expected: expected.clone(),
85 actual: actual.unwrap_or_default(),
91 });
92 }
93 }
94
95 let intent_data: Vec<u8> =
96 UpdateMetadataIntentData::new_update_app_data(app_data, expected_app_data.clone())
97 .into();
98 let intent = QueueIntent::metadata_update()
99 .data(intent_data)
100 .queue(self)?;
101
102 match self.sync_until_intent_resolved(intent.id).await {
103 Ok(_) => Ok(()),
104 Err(err) => {
105 if let Some(expected) = expected_app_data
109 && matches!(
110 self.context.db().fetch(&intent.id),
111 Ok(Some(StoredGroupIntent {
112 state: IntentState::Superseded,
113 ..
114 }))
115 )
116 {
117 return Err(GroupError::AppDataSuperseded {
121 expected,
122 actual: self.app_data()?,
123 });
124 }
125 Err(err)
126 }
127 }
128 }
129
130 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
136 #[cfg_attr(
137 not(any(test, feature = "test-utils")),
138 tracing::instrument(level = "trace", skip(self))
139 )]
140 #[allow(dead_code)]
141 pub(crate) async fn update_group_min_version_to_match_self(&self) -> Result<(), GroupError> {
142 let version = self.context.version_info().pkg_version();
143 self.update_group_min_version(version).await
144 }
145
146 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(in crate::groups) fn min_protocol_version_from_extensions(
240 mutable_metadata: &GroupMutableMetadata,
241 ) -> Option<String> {
242 mutable_metadata
243 .attributes()
244 .get(&MetadataField::MinimumSupportedProtocolVersion.to_string())
245 .map(|v| v.to_string())
246 }
247
248 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
250 #[cfg_attr(
251 not(any(test, feature = "test-utils")),
252 tracing::instrument(level = "trace", skip(self))
253 )]
254 pub async fn update_permission_policy(
255 &self,
256 permission_update_type: PermissionUpdateType,
257 permission_policy: PermissionPolicyOption,
258 metadata_field: Option<MetadataField>,
259 ) -> Result<(), GroupError> {
260 self.ensure_not_paused().await?;
261
262 if self.metadata().await?.conversation_type == ConversationType::Dm {
263 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
264 }
265 if permission_update_type == PermissionUpdateType::UpdateMetadata
266 && metadata_field.is_none()
267 {
268 return Err(MetadataPermissionsError::InvalidPermissionUpdate.into());
269 }
270
271 if matches!(
272 permission_update_type,
273 PermissionUpdateType::AddAdmin | PermissionUpdateType::RemoveAdmin
274 ) && permission_policy == PermissionPolicyOption::Allow
275 {
276 return Err(MetadataPermissionsError::InvalidPermissionUpdate.into());
277 }
278
279 let intent_data: Vec<u8> = UpdatePermissionIntentData::new(
280 permission_update_type,
281 permission_policy,
282 metadata_field.as_ref().map(|field| field.to_string()),
283 )
284 .into();
285
286 let intent = QueueIntent::update_permission()
287 .data(intent_data)
288 .queue(self)?;
289
290 let _ = self.sync_until_intent_resolved(intent.id).await?;
291 Ok(())
292 }
293
294 pub fn group_name(&self) -> Result<String, GroupError> {
296 Ok(self
297 .read_single_component::<GroupNameComponent>()?
298 .unwrap_or_default())
299 }
300
301 pub fn app_data(&self) -> Result<String, GroupError> {
303 Ok(self
304 .read_single_component::<AppDataComponent>()?
305 .unwrap_or_default())
306 }
307
308 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
310 #[cfg_attr(
311 not(any(test, feature = "test-utils")),
312 tracing::instrument(level = "trace", skip(self))
313 )]
314 pub async fn update_group_description(
315 &self,
316 group_description: String,
317 ) -> Result<(), GroupError> {
318 self.ensure_not_paused().await?;
319
320 if group_description.len() > MAX_GROUP_DESCRIPTION_LENGTH {
321 return Err(GroupError::TooManyCharacters {
322 length: MAX_GROUP_DESCRIPTION_LENGTH,
323 });
324 }
325
326 if self.metadata().await?.conversation_type == ConversationType::Dm {
327 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
328 }
329 let intent_data: Vec<u8> =
330 UpdateMetadataIntentData::new_update_group_description(group_description).into();
331 let intent = QueueIntent::metadata_update()
332 .data(intent_data)
333 .queue(self)?;
334
335 let _ = self.sync_until_intent_resolved(intent.id).await?;
336 Ok(())
337 }
338
339 pub fn group_description(&self) -> Result<String, GroupError> {
340 Ok(self
341 .read_single_component::<GroupDescriptionComponent>()?
342 .unwrap_or_default())
343 }
344
345 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
347 #[cfg_attr(
348 not(any(test, feature = "test-utils")),
349 tracing::instrument(level = "trace", skip(self))
350 )]
351 pub async fn update_group_image_url_square(
352 &self,
353 group_image_url_square: String,
354 ) -> Result<(), GroupError> {
355 self.ensure_not_paused().await?;
356
357 if group_image_url_square.len() > MAX_GROUP_IMAGE_URL_LENGTH {
358 return Err(GroupError::TooManyCharacters {
359 length: MAX_GROUP_IMAGE_URL_LENGTH,
360 });
361 }
362
363 if self.metadata().await?.conversation_type == ConversationType::Dm {
364 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
365 }
366 let intent_data: Vec<u8> =
367 UpdateMetadataIntentData::new_update_group_image_url_square(group_image_url_square)
368 .into();
369 let intent = QueueIntent::metadata_update()
370 .data(intent_data)
371 .queue(self)?;
372
373 let _ = self.sync_until_intent_resolved(intent.id).await?;
374 Ok(())
375 }
376
377 pub fn group_image_url_square(&self) -> Result<String, GroupError> {
379 Ok(self
380 .read_single_component::<GroupImageUrlComponent>()?
381 .unwrap_or_default())
382 }
383
384 pub async fn update_conversation_message_disappearing_settings(
386 &self,
387 settings: MessageDisappearingSettings,
388 ) -> Result<(), GroupError> {
389 let replace = |value: i64| {
390 ComponentMutation::Replace(FieldValue::Bytes(value.to_be_bytes().to_vec()))
391 };
392 self.write_fields(|fields| {
393 Ok(vec![
394 fields.field_write(
395 &MetadataFieldRef::MESSAGE_DISAPPEAR_FROM_NS,
396 &replace(settings.from_ns),
397 )?,
398 fields.field_write(
399 &MetadataFieldRef::MESSAGE_DISAPPEAR_IN_NS,
400 &replace(settings.in_ns),
401 )?,
402 ])
403 })
404 .await
405 }
406
407 pub async fn remove_conversation_message_disappearing_settings(
408 &self,
409 ) -> Result<(), GroupError> {
410 self.ensure_not_paused().await?;
411
412 self.update_conversation_message_disappearing_settings(
413 MessageDisappearingSettings::default(),
414 )
415 .await
416 }
417
418 pub fn paused_for_version(&self) -> Result<Option<String>, GroupError> {
420 let paused_for_version = self.context.db().get_group_paused_version(&self.group_id)?;
421 Ok(paused_for_version)
422 }
423
424 #[tracing::instrument(skip_all, level = "trace")]
425 pub(in crate::groups) async fn ensure_not_paused(&self) -> Result<(), GroupError> {
426 if let Some(min_version) = self.context.db().get_group_paused_version(&self.group_id)? {
427 Err(GroupError::GroupPausedUntilUpdate(min_version))
428 } else {
429 Ok(())
430 }
431 }
432
433 pub fn conversation_message_disappearing_settings(
434 &self,
435 ) -> Result<MessageDisappearingSettings, GroupError> {
436 let stored = self
437 .context
438 .db()
439 .find_group(&self.group_id)?
440 .ok_or(NotFound::GroupById(self.group_id))?;
441 if stored.membership_state == GroupMembershipState::Restored {
442 return match (
443 stored.message_disappear_from_ns,
444 stored.message_disappear_in_ns,
445 ) {
446 (Some(from_ns), Some(in_ns)) => {
447 Ok(MessageDisappearingSettings::new(from_ns, in_ns))
448 }
449 _ => Err(GroupError::MetadataPermissionsError(
450 GroupMetadataError::MissingExtension.into(),
451 )),
452 };
453 }
454 let metadata = self.live_mutable_metadata()?;
455 Self::conversation_message_disappearing_settings_from_extensions(&metadata)
456 }
457
458 pub fn conversation_message_disappearing_settings_from_extensions(
459 mutable_metadata: &GroupMutableMetadata,
460 ) -> Result<MessageDisappearingSettings, GroupError> {
461 let disappear_from_ns = mutable_metadata
462 .attributes()
463 .get(&MetadataField::MessageDisappearFromNS.to_string());
464 let disappear_in_ns = mutable_metadata
465 .attributes()
466 .get(&MetadataField::MessageDisappearInNS.to_string());
467
468 if let (Some(Ok(message_disappear_from_ns)), Some(Ok(message_disappear_in_ns))) = (
469 disappear_from_ns.map(|s| s.parse::<i64>()),
470 disappear_in_ns.map(|s| s.parse::<i64>()),
471 ) {
472 Ok(MessageDisappearingSettings::new(
473 message_disappear_from_ns,
474 message_disappear_in_ns,
475 ))
476 } else {
477 Err(GroupError::MetadataPermissionsError(
478 GroupMetadataError::MissingExtension.into(),
479 ))
480 }
481 }
482
483 pub fn pending_remove_list(&self) -> Result<Vec<String>, GroupError> {
484 self.context
485 .db()
486 .get_pending_remove_users(&self.group_id)
487 .map_err(Into::into)
488 }
489
490 pub fn is_in_pending_remove(&self, inbox_id: &str) -> Result<bool, GroupError> {
492 self.context
493 .db()
494 .get_user_pending_remove_status(&self.group_id, inbox_id)
495 .map_err(Into::into)
496 }
497
498 pub fn admin_list(&self) -> Result<Vec<String>, GroupError> {
503 self.read_admin_set(AdminListKind::Admin)
504 }
505
506 pub fn super_admin_list(&self) -> Result<Vec<String>, GroupError> {
510 self.read_admin_set(AdminListKind::SuperAdmin)
511 }
512
513 fn read_admin_set(&self, kind: AdminListKind) -> Result<Vec<String>, GroupError> {
514 if self.membership_state()? == GroupMembershipState::Restored {
515 return Ok(Vec::new());
516 }
517 let ctx = self.load_group_context()?;
518 let extensions = ctx.extensions();
519 let facade = xmtp_mls_group_state::app_data::typed_facade::MlsGroupAppData::new(extensions);
520 let set = match kind {
521 AdminListKind::Admin => facade.get::<AdminListComponent>(),
522 AdminListKind::SuperAdmin => facade.get::<SuperAdminListComponent>(),
523 }
524 .map_err(|e| {
525 GroupError::MetadataPermissionsError(MetadataPermissionsError::ComponentSource(e))
526 })?;
527 Ok(set
528 .map(|s| s.iter().map(|id| id.to_hex()).collect())
529 .unwrap_or_default())
530 }
531
532 pub fn is_admin(&self, inbox_id: String) -> Result<bool, GroupError> {
534 let mutable_metadata = self.mutable_metadata()?;
535 Ok(mutable_metadata.admin_list.contains(&inbox_id))
536 }
537
538 pub fn is_super_admin(&self, inbox_id: String) -> Result<bool, GroupError> {
540 let mutable_metadata = self.mutable_metadata()?;
541 Ok(mutable_metadata.super_admin_list.contains(&inbox_id))
542 }
543
544 pub(crate) fn live_is_super_admin(&self, inbox_id: &str) -> Result<bool, GroupError> {
551 if self.membership_state()? == GroupMembershipState::Restored {
552 return Ok(false);
553 }
554 Ok(self
555 .live_mutable_metadata()?
556 .super_admin_list
557 .iter()
558 .any(|id| id == inbox_id))
559 }
560
561 pub fn is_super_admin_without_lock(
563 &self,
564 mls_group: &OpenMlsGroup,
565 inbox_id: String,
566 ) -> Result<bool, GroupMutableMetadataError> {
567 let list =
568 xmtp_mls_group_state::app_data::component_source::read_super_admin_list_from_dict(
569 mls_group,
570 )
571 .map_err(GroupMutableMetadataError::from)?
572 .unwrap_or_default();
573 Ok(list.contains(&inbox_id))
574 }
575
576 pub async fn conversation_type(&self) -> Result<ConversationType, GroupError> {
578 let conversation_type = self.context.db().get_conversation_type(&self.group_id)?;
579 Ok(conversation_type)
580 }
581}