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}