Skip to main content

xmtp_mls/groups/
members.rs

1use crate::{context::XmtpSharedContext, identity_updates::IdentityUpdates};
2
3use super::{GroupError, MlsGroup};
4use xmtp_db::prelude::*;
5use xmtp_db::{
6    StorageError,
7    consent_record::{ConsentState, ConsentType},
8};
9use xmtp_id::{
10    InboxId,
11    associations::{AssociationState, Identifier, handle::Handle},
12};
13use xmtp_mls_validation::commit::extract_group_membership;
14
15#[derive(Debug, Clone)]
16pub struct GroupMember {
17    pub inbox_id: InboxId,
18    pub account_identifiers: Vec<Identifier>,
19    pub installation_ids: Vec<Vec<u8>>,
20    pub permission_level: PermissionLevel,
21    pub consent_state: ConsentState,
22    /// The display handle of the newest association state the client holds
23    /// for the member, or `None` when it has claimed no handle.
24    pub handle: Option<Handle>,
25    /// The other handles of that state, the most recently claimed first.
26    pub other_handles: Vec<Handle>,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum PermissionLevel {
31    Member,
32    Admin,
33    SuperAdmin,
34}
35
36impl<Context> MlsGroup<Context>
37where
38    Context: XmtpSharedContext,
39{
40    /// Load the member list for the group from the DB, merging together multiple installations into a single entry
41    // implements: IDENT-086
42    pub async fn members(&self) -> Result<Vec<GroupMember>, GroupError> {
43        let db = self.context.db();
44        let group_membership = self.with_group_snapshot(|mls_group| {
45            Ok(extract_group_membership(mls_group.extensions())?)
46        })?;
47        let requests = group_membership
48            .members
49            .into_iter()
50            .map(|(inbox_id, sequence_id)| (inbox_id, sequence_id as i64))
51            .filter(|(_, sequence_id)| *sequence_id != 0) // Skip the initial state
52            .collect::<Vec<_>>();
53
54        let association_states = db.batch_read_from_cache(requests.clone())?;
55        let mut association_states: Vec<AssociationState> = association_states
56            .into_iter()
57            .map(|a| a.try_into())
58            .collect::<Result<_, _>>()
59            .map_err(StorageError::from)?;
60        if association_states.len() != requests.len() {
61            // Attempt to rebuild the cache.
62            let missing_requests: Vec<_> = requests
63                .iter()
64                .filter_map(|(id, sequence)| {
65                    // Filter out association states we already have to avoid unnecessary requests.
66                    if association_states
67                        .iter()
68                        .any(|state| state.inbox_id() == id)
69                    {
70                        return None;
71                    }
72                    Some((id.as_str(), Some(*sequence)))
73                })
74                .collect();
75            let identity_updates = IdentityUpdates::new(&self.context);
76            let mut new_states = identity_updates
77                .batch_get_association_state(&db, &missing_requests)
78                .await?;
79            association_states.append(&mut new_states);
80
81            if association_states.len() != requests.len() {
82                // Cache miss - not expected to happen because:
83                // 1. We don't allow updates to the group metadata unless we have already validated the association state
84                // 2. When validating the association state, we must have written it to the cache
85                tracing::error!(
86                    "Failed to load all members for group - metadata: {:?}, computed members: {:?}",
87                    requests,
88                    association_states
89                );
90                return Err(GroupError::InvalidGroupMembership);
91            }
92        }
93        // The membership state gives identifiers and installations. Handles
94        // come from the newest state held, which can be newer.
95        let inbox_ids: Vec<&str> = requests.iter().map(|(id, _)| id.as_str()).collect();
96        let mut newest_states = db.newest_cached_states(&inbox_ids)?;
97        let mutable_metadata = self.live_mutable_metadata()?;
98        let members = association_states
99            .into_iter()
100            .map(|association_state| {
101                let inbox_id_str = association_state.inbox_id().to_string();
102                // The membership state is cached, so the inbox has an entry at
103                // least as new as it.
104                let newest_state: Option<AssociationState> = newest_states
105                    .remove(&inbox_id_str)
106                    .map(|(_, state)| state.try_into())
107                    .transpose()
108                    .map_err(StorageError::from)?;
109                let handle_state = newest_state.as_ref().unwrap_or(&association_state);
110                let is_admin = mutable_metadata.is_admin(&inbox_id_str);
111                let is_super_admin = mutable_metadata.is_super_admin(&inbox_id_str);
112                let permission_level = if is_super_admin {
113                    PermissionLevel::SuperAdmin
114                } else if is_admin {
115                    PermissionLevel::Admin
116                } else {
117                    PermissionLevel::Member
118                };
119
120                let consent = db.get_consent_record(inbox_id_str.clone(), ConsentType::InboxId)?;
121
122                Ok(GroupMember {
123                    inbox_id: inbox_id_str.clone(),
124                    account_identifiers: association_state.identifiers(),
125                    installation_ids: association_state.installation_ids(),
126                    permission_level,
127                    consent_state: consent.map_or(ConsentState::Unknown, |c| c.state),
128                    handle: handle_state.display_handle().cloned(),
129                    other_handles: handle_state.other_handles().cloned().collect(),
130                })
131            })
132            .collect::<Result<Vec<GroupMember>, GroupError>>()?;
133
134        Ok(members)
135    }
136}