Skip to main content

mdk_storage_traits/groups/
mod.rs

1//! Groups module
2//!
3//! This module is responsible for storing and retrieving groups
4//! It also handles the parsing of group content
5//!
6//! The groups are stored in the database and can be retrieved by MLS group ID or Nostr group ID
7//!
8//! Here we also define the storage traits that are used to store and retrieve groups
9
10use std::collections::BTreeSet;
11
12use crate::GroupId;
13use nostr::{PublicKey, RelayUrl, Timestamp};
14
15pub mod error;
16pub mod types;
17
18use self::error::GroupError;
19use self::types::*;
20use crate::messages::types::Message;
21
22/// Default limit for messages queries to prevent unbounded memory usage
23pub const DEFAULT_MESSAGE_LIMIT: usize = 1000;
24
25/// Maximum allowed limit for messages queries to prevent resource exhaustion
26pub const MAX_MESSAGE_LIMIT: usize = 10000;
27
28/// Sort order for message queries
29///
30/// Controls the column priority used when ordering messages.
31/// Both orderings are descending (newest first) and use three columns
32/// as a compound sort key to guarantee stable, deterministic results.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum MessageSortOrder {
35    /// Sort by `created_at DESC, processed_at DESC, id DESC` (default).
36    ///
37    /// Best for showing messages in sender-timestamp order.
38    /// This is the natural ordering when the sender's clock is trusted.
39    #[default]
40    CreatedAtFirst,
41
42    /// Sort by `processed_at DESC, created_at DESC, id DESC`.
43    ///
44    /// Best for showing messages in local reception order.
45    /// This avoids visual reordering caused by clock skew between senders
46    /// and ensures that the most recently received messages always appear first.
47    ProcessedAtFirst,
48}
49
50/// Pagination parameters for querying messages
51#[derive(Debug, Clone, Copy)]
52pub struct Pagination {
53    /// Maximum number of messages to return
54    pub limit: Option<usize>,
55    /// Number of messages to skip
56    pub offset: Option<usize>,
57    /// Sort order for the query results. Defaults to [`MessageSortOrder::CreatedAtFirst`].
58    pub sort_order: Option<MessageSortOrder>,
59}
60
61impl Pagination {
62    /// Create a new Pagination with specified limit and offset
63    pub fn new(limit: Option<usize>, offset: Option<usize>) -> Self {
64        Self {
65            limit,
66            offset,
67            sort_order: None,
68        }
69    }
70
71    /// Create a new Pagination with specified limit, offset, and sort order
72    pub fn with_sort_order(
73        limit: Option<usize>,
74        offset: Option<usize>,
75        sort_order: MessageSortOrder,
76    ) -> Self {
77        Self {
78            limit,
79            offset,
80            sort_order: Some(sort_order),
81        }
82    }
83
84    /// Get the limit value, using default if not specified
85    pub fn limit(&self) -> usize {
86        self.limit.unwrap_or(DEFAULT_MESSAGE_LIMIT)
87    }
88
89    /// Get the offset value, using 0 if not specified
90    pub fn offset(&self) -> usize {
91        self.offset.unwrap_or(0)
92    }
93
94    /// Get the sort order, using default if not specified
95    pub fn sort_order(&self) -> MessageSortOrder {
96        self.sort_order.unwrap_or_default()
97    }
98}
99
100impl Default for Pagination {
101    fn default() -> Self {
102        Self {
103            limit: Some(DEFAULT_MESSAGE_LIMIT),
104            offset: Some(0),
105            sort_order: None,
106        }
107    }
108}
109
110/// Storage traits for the groups module
111pub trait GroupStorage {
112    /// Get all groups
113    fn all_groups(&self) -> Result<Vec<Group>, GroupError>;
114
115    /// Find a group by MLS group ID
116    fn find_group_by_mls_group_id(&self, group_id: &GroupId) -> Result<Option<Group>, GroupError>;
117
118    /// Find a group by Nostr group ID
119    fn find_group_by_nostr_group_id(
120        &self,
121        nostr_group_id: &[u8; 32],
122    ) -> Result<Option<Group>, GroupError>;
123
124    /// Save a group
125    fn save_group(&self, group: Group) -> Result<(), GroupError>;
126
127    /// Get messages for a group with optional pagination and sort order
128    ///
129    /// Returns messages ordered according to the sort order specified in
130    /// [`Pagination::sort_order`] (defaults to [`MessageSortOrder::CreatedAtFirst`]).
131    ///
132    /// ## Sort orders
133    ///
134    /// **[`MessageSortOrder::CreatedAtFirst`]** (default):
135    /// `created_at DESC, processed_at DESC, id DESC`
136    /// - Primary sort by the sender's timestamp
137    /// - `processed_at` tiebreaker keeps reception order when `created_at` matches
138    /// - `id` ensures deterministic ordering when both timestamps are equal
139    ///
140    /// **[`MessageSortOrder::ProcessedAtFirst`]**:
141    /// `processed_at DESC, created_at DESC, id DESC`
142    /// - Primary sort by when this client received the message
143    /// - Best for local reception ordering; avoids visual reordering from clock skew
144    /// - `created_at` and `id` provide secondary/tertiary tiebreakers
145    ///
146    /// # Arguments
147    /// * `group_id` - The group ID to fetch messages for
148    /// * `pagination` - Optional pagination parameters. If `None`, uses default limit, offset,
149    ///   and sort order.
150    ///
151    /// # Returns
152    ///
153    /// Returns a vector of messages in the requested sort order
154    ///
155    /// # Errors
156    ///
157    /// Returns [`GroupError::InvalidParameters`] if:
158    /// - `limit` is 0
159    /// - `limit` exceeds [`MAX_MESSAGE_LIMIT`]
160    /// - Group with the specified ID does not exist
161    ///
162    /// # Examples
163    /// ```ignore
164    /// // Get messages with default pagination (created_at first)
165    /// let messages = storage.messages(&group_id, None)?;
166    ///
167    /// // Get first 100 messages sorted by created_at
168    /// let messages = storage.messages(&group_id, Some(Pagination::new(Some(100), Some(0))))?;
169    ///
170    /// // Get first 100 messages sorted by processed_at
171    /// let messages = storage.messages(
172    ///     &group_id,
173    ///     Some(Pagination::with_sort_order(Some(100), Some(0), MessageSortOrder::ProcessedAtFirst)),
174    /// )?;
175    /// ```
176    fn messages(
177        &self,
178        group_id: &GroupId,
179        pagination: Option<Pagination>,
180    ) -> Result<Vec<Message>, GroupError>;
181
182    /// Get the most recent message in a group according to the given sort order.
183    ///
184    /// This is equivalent to calling [`messages()`](GroupStorage::messages) with `limit=1, offset=0`
185    /// and the specified sort order, but may be implemented more efficiently.
186    ///
187    /// Clients can use this to obtain the "last message" that is consistent with the
188    /// sort order they pass to [`messages()`](GroupStorage::messages), which may differ
189    /// from the cached [`Group::last_message_id`] (which always reflects
190    /// [`MessageSortOrder::CreatedAtFirst`]).
191    ///
192    /// # Arguments
193    /// * `group_id` - The group ID to fetch the last message for
194    /// * `sort_order` - The sort order to use when determining the "last" message
195    ///
196    /// # Returns
197    ///
198    /// Returns the first message in the given sort order, or `None` if the group has no messages.
199    ///
200    /// # Errors
201    ///
202    /// Returns [`GroupError::InvalidParameters`] if the group does not exist.
203    fn last_message(
204        &self,
205        group_id: &GroupId,
206        sort_order: MessageSortOrder,
207    ) -> Result<Option<Message>, GroupError>;
208
209    /// Get all admins for a group
210    fn admins(&self, group_id: &GroupId) -> Result<BTreeSet<PublicKey>, GroupError>;
211
212    /// Get all relays for a group
213    fn group_relays(&self, group_id: &GroupId) -> Result<BTreeSet<GroupRelay>, GroupError>;
214
215    /// Replace all relays for a group with the provided set
216    /// This operation is atomic - either all relays are replaced or none are changed
217    fn replace_group_relays(
218        &self,
219        group_id: &GroupId,
220        relays: BTreeSet<RelayUrl>,
221    ) -> Result<(), GroupError>;
222
223    /// Get a MIP-03 group-event exporter secret for a group and epoch.
224    ///
225    /// Returns the secret derived via `MLS-Exporter("marmot", "group-event", 32)`,
226    /// used as the ChaCha20-Poly1305 encryption key for kind:445 messages.
227    fn get_group_exporter_secret(
228        &self,
229        group_id: &GroupId,
230        epoch: u64,
231    ) -> Result<Option<GroupExporterSecret>, GroupError>;
232
233    /// Save a MIP-03 group-event exporter secret for a group and epoch.
234    fn save_group_exporter_secret(
235        &self,
236        group_exporter_secret: GroupExporterSecret,
237    ) -> Result<(), GroupError>;
238
239    /// Get a MIP-04 encrypted-media exporter secret for a group and epoch.
240    ///
241    /// Returns the secret derived via `MLS-Exporter("marmot", "encrypted-media", 32)`,
242    /// used as HKDF input keying material for per-file encryption key derivation.
243    fn get_group_mip04_exporter_secret(
244        &self,
245        group_id: &GroupId,
246        epoch: u64,
247    ) -> Result<Option<GroupExporterSecret>, GroupError>;
248
249    /// Save a MIP-04 encrypted-media exporter secret for a group and epoch.
250    fn save_group_mip04_exporter_secret(
251        &self,
252        group_exporter_secret: GroupExporterSecret,
253    ) -> Result<(), GroupError>;
254
255    /// Prune exporter secrets older than `min_epoch_to_keep` for the group.
256    ///
257    /// Implementations must remove both MIP-03 (`group-event`) and MIP-04
258    /// (`encrypted-media`) exporter secrets with `epoch < min_epoch_to_keep`.
259    fn prune_group_exporter_secrets_before_epoch(
260        &self,
261        group_id: &GroupId,
262        min_epoch_to_keep: u64,
263    ) -> Result<(), GroupError>;
264
265    /// Returns active groups that need a self-update: either because
266    /// `self_update_state` is [`SelfUpdateState::Required`] (post-join
267    /// requirement per MIP-02) or because the last self-update is older than
268    /// `threshold_secs` seconds ago (periodic rotation per MIP-00).
269    fn groups_needing_self_update(&self, threshold_secs: u64) -> Result<Vec<GroupId>, GroupError> {
270        let now = Timestamp::now().as_secs();
271        let groups = self.all_groups()?;
272        Ok(groups
273            .into_iter()
274            .filter(|g| {
275                if g.state != types::GroupState::Active {
276                    return false;
277                }
278                match g.self_update_state {
279                    types::SelfUpdateState::Required => true,
280                    types::SelfUpdateState::CompletedAt(ts) => {
281                        now.saturating_sub(ts.as_secs()) >= threshold_secs
282                    }
283                }
284            })
285            .map(|g| g.mls_group_id)
286            .collect())
287    }
288}