Skip to main content

de_mls/app/session/
query.rs

1//! Read-only queries over a conversation's state (UI and diagnostics).
2//! Callers reach these via `User::lookup_entry`; the registry-wide
3//! `list_conversations` getter lives on `User`.
4
5use crate::{
6    app::{ConversationState, MemberRole, SessionRunner, UserError},
7    core::{ConsensusPlugin, ConversationPluginsFactory, PeerScoringPlugin, StewardListPlugin},
8    mls_crypto::MlsService,
9    protos::de_mls::messages::v1::ConversationUpdateRequest,
10};
11
12impl<P: ConsensusPlugin, CP: ConversationPluginsFactory> SessionRunner<P, CP> {
13    pub fn get_conversation_state(&self) -> ConversationState {
14        self.handle.current_state()
15    }
16
17    /// Current MLS epoch + reelection retry round. `(0, 0)` when the
18    /// conversation has no MLS state yet (pending join). Intended for UI
19    /// status display.
20    pub fn get_epoch_and_retry(&self) -> Result<(u64, u32), UserError> {
21        let epoch = match self.handle.mls() {
22            Some(mls) => mls.current_epoch()?,
23            None => 0,
24        };
25        Ok((epoch, self.handle.steward_list.retry_round()))
26    }
27
28    /// Count of buffered pending membership updates. Used by tests and the UI
29    /// to verify buffer hygiene (e.g., that a joiner's buffer is empty right
30    /// after they receive the welcome).
31    pub fn get_pending_update_count(&self) -> usize {
32        self.handle.conversation.pending_update_count()
33    }
34
35    /// Freeze round progress: `(received, expected)`. Returns `(0, 0)` if not
36    /// in freeze or no steward list is known.
37    pub fn get_freeze_candidate_count(&self) -> (usize, usize) {
38        let received = self.handle.conversation.freeze_candidate_count();
39        let expected = self
40            .handle
41            .steward_list
42            .current_list()
43            .map(|l| l.len())
44            .unwrap_or(0);
45        (received, expected)
46    }
47
48    pub fn is_steward_for_self(&self) -> bool {
49        self.handle.steward_list.is_steward(&self.self_identity)
50    }
51
52    /// Identity bytes of every current member of this conversation, as
53    /// reported by MLS. Returns an empty vec when the local user has no
54    /// MLS state yet (pending join).
55    pub fn get_conversation_members(&self) -> Result<Vec<Vec<u8>>, UserError> {
56        match self.handle.mls() {
57            Some(mls) => Ok(mls.members()?),
58            None => Ok(Vec::new()),
59        }
60    }
61
62    pub fn get_member_scores(&self) -> Vec<(Vec<u8>, i64)> {
63        self.handle.scoring.all_members_with_scores()
64    }
65
66    pub fn get_member_score(&self, member_id: &[u8]) -> Option<i64> {
67        self.handle.scoring.score_for(member_id)
68    }
69
70    /// Identities that have an in-flight self-leave request. Used by the UI
71    /// to render a "pending leave" indicator.
72    pub fn get_pending_leave_identities(&self) -> Result<Vec<Vec<u8>>, UserError> {
73        let members = self.handle.expect_mls()?.members()?;
74        Ok(members
75            .into_iter()
76            .filter(|id| self.handle.conversation.is_pending_self_leave(id))
77            .collect())
78    }
79
80    /// Steward role for each member. Uses live rotation so removed or
81    /// pending-leave stewards are skipped in role display.
82    pub fn get_member_roles(&self) -> Result<Vec<(Vec<u8>, MemberRole)>, UserError> {
83        let mls = self.handle.expect_mls()?;
84        let epoch = mls.current_epoch()?;
85        let members = mls.members()?;
86
87        let eligible = self.handle.conversation.steward_eligibility(&members);
88        let (live_epoch, live_backup) = self.handle.steward_list.epoch_and_backup(epoch, &eligible);
89        let live_epoch = live_epoch.map(|s| s.to_vec());
90        let live_backup = live_backup.map(|s| s.to_vec());
91        let exhausted = self.handle.steward_list.is_exhausted(epoch);
92        let has_list = self.handle.steward_list.current_list().is_some();
93        let roles = members
94            .iter()
95            .cloned()
96            .map(|id| {
97                let role = if has_list && !exhausted {
98                    if live_epoch.as_deref().is_some_and(|es| es == id) {
99                        MemberRole::EpochSteward
100                    } else if live_backup.as_deref().is_some_and(|bs| bs == id) {
101                        MemberRole::BackupSteward
102                    } else if self.handle.steward_list.is_steward(&id) {
103                        MemberRole::Steward
104                    } else {
105                        MemberRole::Member
106                    }
107                } else if has_list && self.handle.steward_list.is_steward(&id) {
108                    MemberRole::Steward
109                } else {
110                    MemberRole::Member
111                };
112                (id, role)
113            })
114            .collect();
115        Ok(roles)
116    }
117
118    pub fn get_approved_proposal_for_current_epoch(&self) -> Vec<ConversationUpdateRequest> {
119        self.handle
120            .conversation
121            .approved_proposals()
122            .values()
123            .cloned()
124            .collect()
125    }
126}