Skip to main content

xmtp_mls/groups/
mod.rs

1pub mod app_data;
2mod builders;
3mod lifecycle;
4mod membership;
5mod messages;
6mod metadata;
7mod metadata_fields;
8mod state;
9pub use builders::*;
10pub use state::{ConversationStateSnapshot, GroupMetadataSnapshot};
11pub mod change_callbacks;
12pub mod commit_log;
13pub mod commit_log_key;
14mod error;
15pub mod intents;
16pub mod members;
17pub mod message_list;
18pub(super) mod mls_ext;
19pub(super) mod mls_sync;
20pub mod oneshot;
21pub mod send_message_opts;
22pub(super) mod subscriptions;
23pub mod summary;
24#[cfg(test)]
25mod tests;
26pub mod validated_commit;
27pub mod welcome_pointer;
28pub mod welcome_sync;
29mod welcomes;
30pub use welcomes::*;
31
32pub use self::group_permissions::PreconfiguredPolicies;
33use self::{
34    group_membership::GroupMembership,
35    group_permissions::GroupMutablePermissions,
36    group_permissions::PolicySet,
37    intents::{
38        AdminListActionType, PermissionPolicyOption, PermissionUpdateType,
39        UpdateAdminListIntentData, UpdateMetadataIntentData, UpdatePermissionIntentData,
40    },
41};
42#[cfg(test)]
43use crate::GroupCommitLock;
44use crate::context::XmtpSharedContext;
45use crate::groups::{
46    intents::{QueueIntent, ReaddInstallationsIntentData},
47    mls_ext::CommitLogStorer,
48};
49use crate::state_tx::state_write;
50use crate::{client::ClientError, utils::id::calculate_message_id};
51use crate::{
52    groups::send_message_opts::SendMessageOpts,
53    worker::device_sync::preference_sync::PreferenceUpdate,
54};
55pub use error::*;
56use intents::SendMessageIntentData;
57pub use intents::UpdateGroupMembershipResult;
58// An error type that other errors wrap transparently, so a caller that walks
59// an error chain can open it.
60pub use mls_sync::GroupMessageProcessingError;
61#[cfg(test)]
62use openmls::extensions::Metadata;
63use openmls::{
64    credentials::CredentialType,
65    extensions::{
66        Extension, ExtensionType, Extensions, RequiredCapabilitiesExtension, UnknownExtension,
67    },
68    group::MlsGroupCreateConfig,
69    messages::proposals::ProposalType,
70    prelude::{Capabilities, MlsGroup as OpenMlsGroup, WireFormatPolicy},
71};
72use prost::Message;
73use std::collections::HashMap;
74use std::{collections::HashSet, sync::Arc};
75use tokio::sync::Mutex;
76use xmtp_common::{Event, log_event, time::now_ns};
77#[cfg(test)]
78use xmtp_configuration::GROUP_PERMISSIONS_EXTENSION_ID;
79use xmtp_configuration::{
80    CIPHERSUITE, GROUP_MEMBERSHIP_EXTENSION_ID, MAX_PAST_EPOCHS, MUTABLE_METADATA_EXTENSION_ID,
81    SEND_MESSAGE_UPDATE_INSTALLATIONS_INTERVAL_NS,
82    WELCOME_POINTEE_ENCRYPTION_AEAD_TYPES_EXTENSION_ID, WELCOME_WRAPPER_ENCRYPTION_EXTENSION_ID,
83};
84use xmtp_content_types::delete_message::DeleteMessageCodec;
85use xmtp_content_types::leave_request::LeaveRequestCodec;
86use xmtp_content_types::{ContentCodec, encoded_content_to_bytes};
87use xmtp_content_types::{
88    reaction::{LegacyReaction, ReactionCodec},
89    reply::ReplyCodec,
90};
91use xmtp_cryptography::configuration::ED25519_KEY_LENGTH;
92use xmtp_db::message_deletion::{QueryMessageDeletion, StoredMessageDeletion};
93use xmtp_db::pending_remove::QueryPendingRemove;
94use xmtp_db::prelude::*;
95use xmtp_db::user_preferences::HmacKey;
96use xmtp_db::{Fetch, consent_record::ConsentType};
97use xmtp_db::{
98    NotFound, StorageError,
99    group_intent::{IntentState, StoredGroupIntent},
100    group_message::ContentType,
101    refresh_state::EntityKind,
102};
103use xmtp_db::{Store, StoreOrIgnore};
104use xmtp_db::{TransactionOutcome, TransactionOutcome::Continue, XmtpOpenMlsProviderRef};
105use xmtp_db::{
106    XmtpMlsStorageProvider,
107    remote_commit_log::{RemoteCommitLog, RemoteCommitLogOrder},
108};
109use xmtp_db::{
110    consent_record::{ConsentState, StoredConsentRecord},
111    group::{ConversationType, GroupMembershipState, StoredGroup},
112    group_message::{DeliveryStatus, GroupMessageKind, MsgQueryArgs, StoredGroupMessage},
113};
114use xmtp_db::{group_message::LatestMessageTimeBySender, local_commit_log::LocalCommitLog};
115use xmtp_id::associations::Identifier;
116use xmtp_id::{AsIdRef, InboxId, InboxIdRef};
117use xmtp_mls_common::libxmtp_version::LibXMTPVersion;
118use xmtp_mls_common::{
119    app_data::components::{
120        inbox_id_set::{AdminListComponent, SuperAdminListComponent},
121        metadata_attributes::{
122            AppDataComponent, GroupDescriptionComponent, GroupImageUrlComponent, GroupNameComponent,
123        },
124    },
125    group::{DMMetadataOptions, GroupMetadataOptions},
126    group_metadata::{DmMembers, GroupMetadata, GroupMetadataError, extract_group_metadata},
127    group_mutable_metadata::{
128        GroupMutableMetadata, GroupMutableMetadataError, MessageDisappearingSettings, MetadataField,
129    },
130};
131pub use xmtp_mls_validation::{group_membership, group_permissions};
132use xmtp_proto::xmtp::mls::message_contents::content_types::{DeleteMessage, LeaveRequest};
133use xmtp_proto::{
134    types::{Cursor, GroupId},
135    xmtp::mls::message_contents::{
136        EncodedContent, OneshotMessage, PlaintextEnvelope,
137        content_types::ReactionV2,
138        plaintext_envelope::{Content, V1},
139    },
140};
141
142use xmtp_mls_common::app_data::components::metadata_attributes::{
143    MAX_APP_DATA_LENGTH, MAX_GROUP_DESCRIPTION_LENGTH, MAX_GROUP_IMAGE_URL_LENGTH,
144    MAX_GROUP_NAME_LENGTH,
145};
146const DEFAULT_IDEMPOTENCY_KEY_BYTES: usize = 16;
147
148/// An LibXMTP MlsGroup
149/// _NOTE:_ The Eq implementation compares [`GroupId`], so a dm group with the same identity will be
150/// different.
151/// the Hash implementation hashes the [`GroupId`]
152pub struct MlsGroup<Context> {
153    pub group_id: GroupId,
154    pub dm_id: Option<String>,
155    pub conversation_type: ConversationType,
156    pub created_at_ns: i64,
157    pub context: Context,
158    #[cfg(test)]
159    mls_commit_lock: Arc<GroupCommitLock>,
160    mutex: Arc<Mutex<()>>,
161}
162
163impl<C> std::hash::Hash for MlsGroup<C> {
164    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
165        self.group_id.hash(state);
166    }
167}
168
169impl<C> PartialEq for MlsGroup<C> {
170    fn eq(&self, other: &Self) -> bool {
171        self.group_id == other.group_id
172    }
173}
174
175impl<C> Eq for MlsGroup<C> {}
176
177impl<Context> std::fmt::Debug for MlsGroup<Context>
178where
179    Context: XmtpSharedContext,
180{
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
182        let id = xmtp_common::fmt::truncate_hex(hex::encode(self.group_id));
183        let inbox_id = self.context.inbox_id();
184        let installation = self.context.installation_id().to_string();
185        let time = chrono::DateTime::from_timestamp_nanos(self.created_at_ns);
186        write!(
187            f,
188            "Group {{ id: [{}], created: [{}], client: [{}], installation: [{}] }}",
189            id,
190            time.format("%H:%M:%S"),
191            inbox_id,
192            installation
193        )
194    }
195}
196
197pub struct ConversationListItem<Context> {
198    pub group: MlsGroup<Context>,
199    pub membership_state: GroupMembershipState,
200    pub added_by_inbox_id: String,
201    pub last_message: Option<StoredGroupMessage>,
202    pub is_commit_log_forked: Option<bool>,
203}
204
205impl<Context: XmtpSharedContext> Clone for MlsGroup<Context> {
206    fn clone(&self) -> Self {
207        Self {
208            group_id: self.group_id,
209            dm_id: self.dm_id.clone(),
210            conversation_type: self.conversation_type,
211            created_at_ns: self.created_at_ns,
212            context: self.context.clone(),
213            mutex: self.mutex.clone(),
214            #[cfg(test)]
215            mls_commit_lock: self.mls_commit_lock.clone(),
216        }
217    }
218}
219
220#[derive(Debug, Clone, PartialEq)]
221pub struct ConversationDebugInfo {
222    pub epoch: u64,
223    pub maybe_forked: bool,
224    pub fork_details: String,
225    pub is_commit_log_forked: Option<bool>,
226    pub local_commit_log: String,
227    pub remote_commit_log: String,
228    pub cursor: Vec<Cursor>,
229}
230
231#[derive(Debug, Clone, PartialEq)]
232pub enum UpdateAdminListType {
233    Add,
234    Remove,
235    AddSuper,
236    RemoveSuper,
237}
238
239#[derive(Debug, Clone, Copy)]
240enum AdminListKind {
241    Admin,
242    SuperAdmin,
243}
244
245/// Fields extracted from content of a message that should be stored in the DB
246pub struct QueryableContentFields {
247    pub content_type: ContentType,
248    pub version_major: i32,
249    pub version_minor: i32,
250    pub authority_id: String,
251    pub reference_id: Option<Vec<u8>>,
252}
253
254impl Default for QueryableContentFields {
255    fn default() -> Self {
256        Self {
257            content_type: ContentType::Unknown, // Or whatever the appropriate default is
258            version_major: 0,
259            version_minor: 0,
260            authority_id: String::new(),
261            reference_id: None,
262        }
263    }
264}
265
266impl TryFrom<EncodedContent> for QueryableContentFields {
267    type Error = prost::DecodeError;
268
269    fn try_from(content: EncodedContent) -> Result<Self, Self::Error> {
270        let content_type_id = content.r#type.clone().unwrap_or_default();
271
272        let type_id_str = content_type_id.type_id.clone();
273
274        // Invalid compressed content has no searchable reference. Message
275        // decoding still exposes the original envelope to the app.
276        let decoded = xmtp_content_types::compression::decompress(content).ok();
277        let reference_id = decoded.and_then(|content| {
278            if content_type_id.authority_id != "xmtp.org" {
279                return None;
280            }
281            match (type_id_str.as_str(), content_type_id.version_major) {
282                (ReplyCodec::TYPE_ID, 1) => ReplyCodec::decode(content)
283                    .ok()
284                    .and_then(|reply| hex::decode(reply.reference).ok()),
285                (ReactionCodec::TYPE_ID, ReactionCodec::MAJOR_VERSION) => {
286                    ReactionV2::decode(content.content.as_slice())
287                        .ok()
288                        .and_then(|reaction| hex::decode(reaction.reference).ok())
289                }
290                (ReactionCodec::TYPE_ID, 1) => LegacyReaction::decode(&content.content)
291                    .and_then(|legacy_reaction| hex::decode(legacy_reaction.reference).ok()),
292                (DeleteMessageCodec::TYPE_ID, DeleteMessageCodec::MAJOR_VERSION) => {
293                    DeleteMessage::decode(content.content.as_slice())
294                        .ok()
295                        .and_then(|delete_msg| hex::decode(delete_msg.message_id).ok())
296                }
297                _ => None,
298            }
299        });
300
301        Ok(QueryableContentFields {
302            content_type: ContentType::from_identifier(
303                &content_type_id.authority_id,
304                &content_type_id.type_id,
305                content_type_id.version_major,
306            ),
307            version_major: content_type_id.version_major as i32,
308            version_minor: content_type_id.version_minor as i32,
309            authority_id: content_type_id.authority_id.to_string(),
310            reference_id,
311        })
312    }
313}
314
315impl<Context: Clone> From<MlsGroup<&Context>> for MlsGroup<Context> {
316    fn from(group: MlsGroup<&Context>) -> MlsGroup<Context> {
317        MlsGroup::<Context> {
318            context: group.context.clone(),
319            group_id: group.group_id,
320            dm_id: group.dm_id,
321            created_at_ns: group.created_at_ns,
322            #[cfg(test)]
323            mls_commit_lock: group.mls_commit_lock,
324            mutex: group.mutex,
325            conversation_type: group.conversation_type,
326        }
327    }
328}
329
330/// An MLS extension type advertised by an installation's key package or
331/// present in a group's context.
332///
333/// Mirrors openmls [`ExtensionType`]; unknown/forward-compatibility variants
334/// are preserved verbatim so callers can match on what they understand and
335/// ignore the rest. This is a generic capability primitive — callers filter
336/// it to answer specific questions (e.g. "is this group migrated to the
337/// proposal flow?" = a list contains [`MlsExtensionType::AppDataDictionary`]).
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub enum MlsExtensionType {
340    ApplicationId,
341    RatchetTree,
342    RequiredCapabilities,
343    ExternalPub,
344    ExternalSenders,
345    LastResort,
346    ImmutableMetadata,
347    AppDataDictionary,
348    /// An extension type this build does not have a named variant for.
349    Unknown(u16),
350    /// A GREASE value used to exercise extensibility.
351    Grease(u16),
352}
353
354impl From<ExtensionType> for MlsExtensionType {
355    fn from(value: ExtensionType) -> Self {
356        match value {
357            ExtensionType::ApplicationId => MlsExtensionType::ApplicationId,
358            ExtensionType::RatchetTree => MlsExtensionType::RatchetTree,
359            ExtensionType::RequiredCapabilities => MlsExtensionType::RequiredCapabilities,
360            ExtensionType::ExternalPub => MlsExtensionType::ExternalPub,
361            ExtensionType::ExternalSenders => MlsExtensionType::ExternalSenders,
362            ExtensionType::LastResort => MlsExtensionType::LastResort,
363            ExtensionType::ImmutableMetadata => MlsExtensionType::ImmutableMetadata,
364            ExtensionType::AppDataDictionary => MlsExtensionType::AppDataDictionary,
365            ExtensionType::Unknown(id) => MlsExtensionType::Unknown(id),
366            ExtensionType::Grease(id) => MlsExtensionType::Grease(id),
367        }
368    }
369}
370
371/// Capability snapshot for a single installation (device) of a member.
372#[derive(Debug, Clone)]
373pub struct InstallationCapabilities {
374    pub installation_id: Vec<u8>,
375    /// True for the local (this device's) installation.
376    pub is_own: bool,
377    /// The MLS extension types this installation advertises, taken from its
378    /// *latest published* key package. Empty when `capabilities_known` is
379    /// false.
380    pub supported_extensions: Vec<MlsExtensionType>,
381    /// Whether capabilities were determined. `false` means the key package
382    /// could not be fetched or failed verification — distinct from an
383    /// installation that advertises no extensions.
384    pub capabilities_known: bool,
385}
386
387/// Per-inbox grouping of installation capabilities. Callers map `inbox_id`
388/// back to a profile to attribute capabilities to a person.
389#[derive(Debug, Clone)]
390pub struct InboxCapabilities {
391    pub inbox_id: InboxId,
392    pub installations: Vec<InstallationCapabilities>,
393}
394
395/// A generic membership/capability snapshot for a group.
396///
397/// This intentionally reports raw facts rather than answers, so callers can
398/// filter it to whatever question they care about. For the proposal
399/// (app-data-dictionary) migration specifically: the group is already
400/// migrated when `context_extensions` contains
401/// [`MlsExtensionType::AppDataDictionary`], it is eligible to migrate when
402/// every installation's `supported_extensions` contains it, and the inboxes
403/// blocking migration are those with an installation that does not.
404#[derive(Debug, Clone)]
405pub struct GroupMembershipCapabilities {
406    /// Extension types present in the group's context.
407    pub context_extensions: Vec<MlsExtensionType>,
408    /// Per-inbox, per-installation capability breakdown — one entry per member
409    /// inbox, in no particular order.
410    pub members: Vec<InboxCapabilities>,
411}