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)))]
11 #[cfg_attr(
12 not(any(test, feature = "test-utils")),
13 tracing::instrument(level = "trace", skip(self))
14 )]
15 pub async fn update_admin_list(
16 &self,
17 action_type: UpdateAdminListType,
18 inbox_id: String,
19 ) -> Result<(), GroupError> {
20 if self.metadata().await?.conversation_type == ConversationType::Dm {
21 return Err(MetadataPermissionsError::DmGroupMetadataForbidden.into());
22 }
23 let intent_action_type = match action_type {
24 UpdateAdminListType::Add => AdminListActionType::Add,
25 UpdateAdminListType::Remove => AdminListActionType::Remove,
26 UpdateAdminListType::AddSuper => AdminListActionType::AddSuper,
27 UpdateAdminListType::RemoveSuper => AdminListActionType::RemoveSuper,
28 };
29 let intent_data: Vec<u8> =
30 UpdateAdminListIntentData::new(intent_action_type, inbox_id).into();
31 let intent = QueueIntent::update_admin_list()
32 .data(intent_data)
33 .queue(self)?;
34
35 let _ = self.sync_until_intent_resolved(intent.id).await?;
36 Ok(())
37 }
38
39 pub fn added_by_inbox_id(&self) -> Result<String, GroupError> {
41 let conn = self.context.db();
42 let group = conn
43 .find_group(&self.group_id)?
44 .ok_or(NotFound::GroupById(self.group_id))?;
45 Ok(group.added_by_inbox_id)
46 }
47
48 pub fn consent_state(&self) -> Result<ConsentState, GroupError> {
50 let conn = self.context.db();
51 let record =
52 conn.get_consent_record(hex::encode(self.group_id), ConsentType::ConversationId)?;
53
54 match record {
55 Some(rec) => Ok(rec.state),
56 None => Ok(ConsentState::Unknown),
57 }
58 }
59
60 pub fn quietly_update_consent_state(
62 &self,
63 state: ConsentState,
64 db: &impl DbQuery,
65 ) -> Result<Vec<StoredConsentRecord>, GroupError> {
66 let consent_record = StoredConsentRecord::new(
67 ConsentType::ConversationId,
68 state,
69 hex::encode(self.group_id),
70 );
71
72 Ok(db.insert_or_replace_consent_records(std::slice::from_ref(&consent_record))?)
73 }
74
75 #[tracing::instrument(skip_all, level = "trace")]
76 pub fn update_consent_state(&self, state: ConsentState) -> Result<(), GroupError> {
77 let db = self.context.db();
78 let new_records: Vec<PreferenceUpdate> = self
79 .quietly_update_consent_state(state, &db)?
80 .into_iter()
81 .map(PreferenceUpdate::Consent)
82 .collect();
83
84 if !new_records.is_empty() {
85 self.context.task_channels().wake_notifications();
86 let _ = self
88 .context
89 .worker_events()
90 .send(SyncWorkerEvent::SyncPreferences(new_records.clone()));
91 let _ = self
93 .context
94 .local_events()
95 .send(LocalEvents::PreferencesChanged(new_records));
96 }
97
98 Ok(())
99 }
100
101 pub async fn epoch(&self) -> Result<u64, GroupError> {
103 self.with_group_snapshot(|mls_group| Ok(mls_group.epoch().as_u64()))
104 }
105
106 #[cfg(test)]
109 pub(crate) async fn epoch_authenticator(&self) -> Result<Vec<u8>, GroupError> {
110 self.with_group_snapshot(|mls_group| {
111 Ok(mls_group.epoch_authenticator().as_slice().to_vec())
112 })
113 }
114
115 pub async fn cursor(&self) -> Result<Cursor, GroupError> {
116 let db = self.context.db();
117 let msgs = db.get_last_cursor(self.group_id, EntityKind::ApplicationMessage)?;
118 Ok(msgs)
119 }
120
121 pub async fn local_commit_log(&self) -> Result<Vec<LocalCommitLog>, GroupError> {
122 Ok(self.context.db().get_group_logs(&self.group_id)?)
123 }
124
125 pub async fn remote_commit_log(&self) -> Result<Vec<RemoteCommitLog>, GroupError> {
126 Ok(self.context.db().get_remote_commit_log_after_cursor(
127 &self.group_id,
128 0,
129 RemoteCommitLogOrder::AscendingByRowid,
130 )?)
131 }
132
133 pub async fn debug_info(&self) -> Result<ConversationDebugInfo, GroupError> {
134 let epoch = self.epoch().await?;
135 let cursor = self.cursor().await?;
136 let commit_log = self.local_commit_log().await?;
137 let remote_commit_log = self.remote_commit_log().await?;
138 let db = self.context.db();
139
140 let stored_group = match db.find_group(&self.group_id)? {
141 Some(group) => group,
142 None => {
143 return Err(GroupError::NotFound(NotFound::GroupById(self.group_id)));
144 }
145 };
146
147 Ok(ConversationDebugInfo {
148 epoch,
149 maybe_forked: stored_group.maybe_forked,
150 fork_details: stored_group.fork_details,
151 is_commit_log_forked: stored_group.is_commit_log_forked,
152 local_commit_log: format!("{:?}", commit_log),
153 remote_commit_log: format!("{:?}", remote_commit_log),
154 cursor: vec![cursor],
155 })
156 }
157
158 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
160 #[cfg_attr(
161 not(any(test, feature = "test-utils")),
162 tracing::instrument(level = "trace", skip(self))
163 )]
164 pub async fn key_update(&self) -> Result<(), GroupError> {
165 let intent = QueueIntent::key_update().queue(self)?;
166 let _ = self.sync_until_intent_resolved(intent.id).await?;
167 Ok(())
168 }
169
170 #[tracing::instrument(skip_all, level = "trace")]
174 pub fn is_active(&self) -> Result<bool, GroupError> {
175 let Some(stored_group) = self.context.db().find_group(&self.group_id)? else {
177 return Err(GroupError::NotFound(NotFound::GroupById(self.group_id)));
178 };
179 if matches!(
180 stored_group.membership_state,
181 GroupMembershipState::Restored
182 ) {
183 return Ok(false);
184 }
185
186 self.with_group_snapshot(|mls_group| Ok(mls_group.is_active()))
187 }
188
189 #[tracing::instrument(skip_all, level = "trace")]
191 pub fn membership_state(&self) -> Result<GroupMembershipState, GroupError> {
192 let stored_group = self
193 .context
194 .db()
195 .find_group(&self.group_id)?
196 .ok_or_else(|| GroupError::NotFound(NotFound::GroupById(self.group_id)))?;
197 Ok(stored_group.membership_state)
198 }
199
200 pub async fn metadata(&self) -> Result<GroupMetadata, GroupError> {
216 self.with_group_snapshot(|mls_group| {
217 if self::app_data::is_migrated_group(mls_group) {
218 let seed =
219 self::app_data::component_source::read_group_metadata_from_dict(mls_group)
220 .map_err(MetadataPermissionsError::from)?
221 .ok_or_else(|| {
222 MetadataPermissionsError::from(GroupMetadataError::MissingExtension)
223 })?;
224 use xmtp_proto::xmtp::mls::message_contents::GroupMetadataV1 as GroupMetadataProto;
225 let proto = GroupMetadataProto {
233 conversation_type: seed.conversation_type,
234 creator_inbox_id: seed.creator_inbox_id,
235 creator_account_address: String::new(),
236 dm_members: seed.dm_members,
237 oneshot_message: seed.oneshot,
238 };
239 return Ok(GroupMetadata::try_from(proto).map_err(MetadataPermissionsError::from)?);
240 }
241 extract_group_metadata(mls_group.extensions())
242 .map_err(MetadataPermissionsError::from)
243 .map_err(Into::into)
244 })
245 }
246
247 pub(crate) fn load_group_context(&self) -> Result<openmls::group::GroupContext, GroupError> {
257 use openmls_traits::storage::StorageProvider as _;
258 self.context
259 .mls_storage()
260 .group_context::<_, openmls::group::GroupContext>(&self.group_id.to_openmls())
261 .map_err(GroupError::from)?
262 .ok_or_else(|| GroupError::from(StorageError::from(NotFound::GroupById(self.group_id))))
263 }
264
265 pub fn mutable_metadata(&self) -> Result<GroupMutableMetadata, GroupError> {
282 use self::app_data::component_source::ComponentSourceError;
283 let ctx = self.load_group_context()?;
284 self::app_data::component_source::extract_group_mutable_metadata_capability_aware_from_extensions(
285 ctx.extensions(),
286 )
287 .map_err(|e| match e {
288 ComponentSourceError::GroupMutableMetadata(inner) => {
294 GroupError::MetadataPermissionsError(MetadataPermissionsError::Mutable(inner))
295 }
296 other => GroupError::MetadataPermissionsError(
297 MetadataPermissionsError::ComponentSource(other),
298 ),
299 })
300 }
301
302 #[cfg(test)]
306 pub(crate) fn mutable_metadata_via_full_load(
307 &self,
308 ) -> Result<GroupMutableMetadata, GroupError> {
309 use self::app_data::component_source::ComponentSourceError;
310 self.load_mls_group_with_lock(self.context.mls_storage(), |mls_group| {
311 self::app_data::component_source::extract_group_mutable_metadata_capability_aware(
312 &mls_group,
313 )
314 .map_err(|e| match e {
315 ComponentSourceError::GroupMutableMetadata(inner) => {
316 GroupError::MetadataPermissionsError(MetadataPermissionsError::Mutable(inner))
317 }
318 other => GroupError::MetadataPermissionsError(
319 MetadataPermissionsError::ComponentSource(other),
320 ),
321 })
322 })
323 }
324
325 pub fn permissions(&self) -> Result<GroupMutablePermissions, GroupError> {
326 let ctx = self.load_group_context()?;
327 let permissions: GroupMutablePermissions = ctx
328 .extensions()
329 .try_into()
330 .map_err(MetadataPermissionsError::from)?;
331 Ok(permissions)
332 }
333
334 pub(in crate::groups) fn read_single_component<C>(&self) -> Result<Option<C::Value>, GroupError>
356 where
357 C: xmtp_mls_common::app_data::typed::Component,
358 {
359 use self::app_data::component_source::ComponentSourceError;
360 let ctx = self.load_group_context()?;
361 let facade = self::app_data::typed_facade::MlsGroupAppData::new(ctx.extensions());
362 facade.get::<C>().map_err(|e| match e {
363 ComponentSourceError::GroupMutableMetadata(inner) => {
364 GroupError::MetadataPermissionsError(MetadataPermissionsError::Mutable(inner))
365 }
366 other => GroupError::MetadataPermissionsError(
367 MetadataPermissionsError::ComponentSource(other),
368 ),
369 })
370 }
371
372 pub fn disappearing_settings(&self) -> Result<Option<MessageDisappearingSettings>, GroupError> {
377 let conn = self.context.db();
378 let stored_group: Option<StoredGroup> = conn.fetch(&self.group_id)?;
379
380 let settings = stored_group.and_then(|group| {
381 let from_ns = group.message_disappear_from_ns?;
382 let in_ns = group.message_disappear_in_ns?;
383
384 Some(MessageDisappearingSettings { from_ns, in_ns })
385 });
386
387 Ok(settings)
388 }
389
390 pub fn find_duplicate_dms(&self) -> Result<Vec<MlsGroup<Context>>, ClientError> {
392 let duplicates = self.context.db().other_dms(&self.group_id)?;
393
394 let mls_groups = duplicates
395 .into_iter()
396 .map(|g| {
397 MlsGroup::new(
398 self.context.clone(),
399 g.id,
400 g.dm_id,
401 g.conversation_type,
402 g.created_at_ns,
403 )
404 })
405 .collect();
406
407 Ok(mls_groups)
408 }
409
410 #[cfg(test)]
414 pub fn create_test_dm_group(
415 context: Context,
416 dm_target_inbox_id: InboxId,
417 custom_protected_metadata: Option<Extension>,
418 custom_mutable_metadata: Option<Extension>,
419 custom_group_membership: Option<Extension>,
420 custom_mutable_permissions: Option<PolicySet>,
421 opts: Option<DMMetadataOptions>,
422 ) -> Result<Self, GroupError> {
423 let provider = context.mls_provider();
424 let commit_log_enabled = context.server_configuration().commit_log_enabled();
425
426 let protected_metadata = custom_protected_metadata.unwrap_or_else(|| {
427 build_dm_protected_metadata_extension(context.inbox_id(), dm_target_inbox_id.clone())
428 .unwrap()
429 });
430 let mutable_metadata = custom_mutable_metadata.unwrap_or_else(|| {
431 build_dm_mutable_metadata_extension_default(
432 context.inbox_id(),
433 &dm_target_inbox_id,
434 opts.unwrap_or_default(),
435 commit_log_enabled,
436 )
437 .unwrap()
438 });
439 let group_membership = custom_group_membership
440 .unwrap_or_else(|| build_starting_group_membership_extension(context.inbox_id(), 0));
441 let mutable_permissions = custom_mutable_permissions.unwrap_or_else(PolicySet::new_dm);
442 let mutable_permission_extension =
443 build_mutable_permissions_extension(mutable_permissions)?;
444
445 let group_config = build_group_config(
446 protected_metadata,
447 mutable_metadata,
448 group_membership,
449 mutable_permission_extension,
450 )?;
451
452 let mls_group = OpenMlsGroup::from_creation_logged(
453 &provider,
454 context.identity(),
455 &group_config,
456 commit_log_enabled,
457 )?;
458 let group_id: GroupId = mls_group.group_id().try_into()?;
459 let stored_group = StoredGroup::builder()
460 .id(group_id)
461 .created_at_ns(now_ns())
462 .membership_state(GroupMembershipState::Allowed)
463 .added_by_inbox_id(context.inbox_id().to_string())
464 .dm_id(Some(
465 DmMembers {
466 member_one_inbox_id: context.inbox_id().to_string(),
467 member_two_inbox_id: dm_target_inbox_id,
468 }
469 .to_string(),
470 ))
471 .build()?;
472
473 stored_group.store(&context.db())?;
474 Ok(Self::new_from_arc(
475 context,
476 group_id,
477 stored_group.dm_id.clone(),
478 ConversationType::Dm,
479 stored_group.created_at_ns,
480 ))
481 }
482}