Skip to main content

ruma_events/room/
retention.rs

1//! Types for the `m.room.retention` state event.
2//!
3//! This event uses the unstable prefix defined in [MSC1763].
4//!
5//! [MSC1763]: https://github.com/matrix-org/matrix-spec-proposals/pull/1763
6
7use std::{ops::RangeBounds, time::Duration};
8
9use js_int::UInt;
10use ruma_macros::EventContent;
11use serde::{Deserialize, Serialize};
12
13use crate::{EmptyStateKey, PossiblyRedactedStateEventContent, StateEventType};
14
15/// The content of an `m.room.retention` state event.
16///
17/// The `m.room.retention` state event lets room admins or moderators set or modify the history
18/// retention behaviour for a given room.
19///
20/// This event uses the unstable prefix defined in [MSC1763].
21///
22/// [MSC1763]: https://github.com/matrix-org/matrix-spec-proposals/pull/1763
23#[derive(Clone, Debug, Default, Serialize, EventContent)]
24#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
25#[ruma_event(type = "org.matrix.msc1763.retention", kind = State, state_key_type = EmptyStateKey, custom_possibly_redacted)]
26pub struct RoomRetentionEventContent {
27    /// The minimum amount of time messages should be kept on the homeserver.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    min_lifetime: Option<UInt>,
30
31    /// The maximum amount of time messages should be kept on the homeserver.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    max_lifetime: Option<UInt>,
34}
35
36impl RoomRetentionEventContent {
37    /// Create a new [`RoomRetentionEventContent`] with no retention limits set.
38    ///
39    /// This method can be combined with the [`RoomRetentionEventContent::at_least`] and
40    /// [`RoomRetentionEventContent::at_most`] methods to configure the individual limits.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// # use std::time::Duration;
46    /// # use ruma_events::room::retention::RoomRetentionEventContent;
47    /// # fn doctest() -> Option<()> {
48    /// let content = RoomRetentionEventContent::new()
49    ///     .at_least(Duration::from_hours(24))?
50    ///     .at_most(Duration::from_hours(24 * 10))?;
51    /// # None
52    /// # }
53    /// ```
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// Create a new [`RoomRetentionEventContent`] with the given maximum and minimum limits.
59    ///
60    /// This will return `None` if the duration of one of the limits, expressed as milliseconds,
61    /// doesn't fall into the [0, (2^53)-1] range, or if `max_lifetime` < `min_lifetime`.
62    fn new_impl(min_lifetime: Option<Duration>, max_lifetime: Option<Duration>) -> Option<Self> {
63        // The lifetimes are defined as a duration in milliseconds represented as an integer in the
64        // range [0, (2^53)-1], this range is the same as what our UInt type enforces.
65
66        // First convert the duration into milliseconds, then attempt to convert the number of
67        // milliseconds into an UInt.
68        let max_lifetime = max_lifetime.map(|l| UInt::try_from(l.as_millis())).transpose().ok()?;
69        let min_lifetime = min_lifetime.map(|l| UInt::try_from(l.as_millis())).transpose().ok()?;
70
71        if is_valid_lifetime_combination(min_lifetime, max_lifetime) {
72            Some(Self { max_lifetime, min_lifetime })
73        } else {
74            None
75        }
76    }
77
78    /// Create a new [`RoomRetentionEventContent`] from a range.
79    ///
80    /// Returns `None` if the duration of one of the limits, expressed as milliseconds, doesn't
81    /// fall into the [0, (2^53)-1] range, or if the lower bound of the range is bigger than the
82    /// upper bound, i.e. `10..0`.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// # use std::time::Duration;
88    /// # use ruma_events::room::retention::RoomRetentionEventContent;
89    /// # fn doctest() -> Option<()> {
90    /// let content = RoomRetentionEventContent::from_range(
91    ///     Duration::from_hours(24)..Duration::from_hours(24 * 10),
92    /// )?;
93    /// # None
94    /// # }
95    /// ```
96    pub fn from_range(lifetime_range: impl RangeBounds<Duration>) -> Option<Self> {
97        let min_lifetime = match lifetime_range.start_bound() {
98            std::ops::Bound::Included(v) => Some(*v),
99            std::ops::Bound::Excluded(v) => Some(v.saturating_add(Duration::from_millis(1))),
100            std::ops::Bound::Unbounded => None,
101        };
102
103        let max_lifetime = match lifetime_range.end_bound() {
104            std::ops::Bound::Included(v) => Some(*v),
105            std::ops::Bound::Excluded(v) => Some(v.saturating_sub(Duration::from_millis(1))),
106            std::ops::Bound::Unbounded => None,
107        };
108
109        Self::new_impl(min_lifetime, max_lifetime)
110    }
111
112    /// Set the maximum amount of time a message should be kept on the homeserver.
113    ///
114    /// Returns `None` if the given limit, expressed as milliseconds, doesn't fall into the [0,
115    /// (2^53)-1] range, or if the limits don't adhere to the `max` < `min` constraint.
116    pub fn at_most(self, max: Duration) -> Option<Self> {
117        let min = self.min_lifetime();
118        Self::new_impl(min, Some(max))
119    }
120
121    /// Set the minimum amount of time a message should be kept on the homeserver.
122    ///
123    /// Returns `None` if the given limit, expressed as milliseconds, doesn't fall into the [0,
124    /// (2^53)-1] range, or if the limits don't adhere to the `max` < `min` constraint.
125    pub fn at_least(self, min: Duration) -> Option<Self> {
126        let max = self.max_lifetime();
127        Self::new_impl(Some(min), max)
128    }
129
130    /// Get the maximum event lifetime defined by this state event, if any.
131    pub fn max_lifetime(&self) -> Option<Duration> {
132        self.max_lifetime.map(|l| Duration::from_millis(l.into()))
133    }
134
135    /// Get the minimum event lifetime defined by this state event, if any.
136    pub fn min_lifetime(&self) -> Option<Duration> {
137        self.min_lifetime.map(|l| Duration::from_millis(l.into()))
138    }
139}
140
141/// Validate a retention lifetime pair.
142///
143/// Returns false if both lifetimes are defined and the max lifetime is smaller than the min
144/// lifetime.
145pub fn is_valid_lifetime_combination(
146    min_lifetime: Option<UInt>,
147    max_lifetime: Option<UInt>,
148) -> bool {
149    match (min_lifetime, max_lifetime) {
150        (Some(min), Some(max)) if max < min => false,
151        (Some(_), Some(_)) | (None, None) | (None, Some(_)) | (Some(_), None) => true,
152    }
153}
154
155impl<'de> Deserialize<'de> for RoomRetentionEventContent {
156    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
157    where
158        D: serde::Deserializer<'de>,
159    {
160        #[derive(Deserialize)]
161        struct Helper {
162            max_lifetime: Option<UInt>,
163            min_lifetime: Option<UInt>,
164        }
165
166        let Helper { max_lifetime, min_lifetime } = Helper::deserialize(deserializer)?;
167
168        if is_valid_lifetime_combination(min_lifetime, max_lifetime) {
169            Ok(Self { max_lifetime, min_lifetime })
170        } else {
171            Err(serde::de::Error::custom(
172                "Invalid lifetimes, max_lifetime must always be higher or equal to min_lifetime."
173                    .to_owned(),
174            ))
175        }
176    }
177}
178
179/// The PossiblyRedacted version of [`RoomRetentionEventContent`].
180///
181/// Since the event has only optional fields it's already compatible with the redacted version of
182/// the state event content.
183pub type PossiblyRedactedRoomRetentionEventContent = RoomRetentionEventContent;
184
185impl PossiblyRedactedStateEventContent for PossiblyRedactedRoomRetentionEventContent {
186    type StateKey = EmptyStateKey;
187
188    fn event_type(&self) -> StateEventType {
189        StateEventType::RoomRetention
190    }
191}
192
193impl From<RedactedRoomRetentionEventContent> for PossiblyRedactedRoomRetentionEventContent {
194    fn from(_value: RedactedRoomRetentionEventContent) -> Self {
195        Self { min_lifetime: None, max_lifetime: None }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use js_int::uint;
202    use ruma_common::canonical_json::assert_to_canonical_json_eq;
203    use serde_json::{Value as JsonValue, from_value as from_json_value, json};
204
205    use super::*;
206    use crate::OriginalStateEvent;
207
208    fn raw_json(
209        min_lifetime: impl Into<Option<UInt>>,
210        max_lifetime: impl Into<Option<UInt>>,
211    ) -> JsonValue {
212        json!({
213            "content": {
214                "max_lifetime": max_lifetime.into(),
215                "min_lifetime": min_lifetime.into(),
216            },
217            "event_id": "$h29iv0s8:example.com",
218            "origin_server_ts": 1,
219            "room_id": "!n8f893n9:example.com",
220            "sender": "@carl:example.com",
221            "state_key": "",
222            "type": "org.matrix.msc1763.retention"
223        })
224    }
225
226    #[test]
227    fn deserialization() {
228        let json_data = raw_json(None, None);
229        let RoomRetentionEventContent { max_lifetime, min_lifetime, .. } =
230            from_json_value::<OriginalStateEvent<RoomRetentionEventContent>>(json_data)
231                .expect("No lifetimes should deserliaze")
232                .content;
233
234        assert_eq!(max_lifetime, None);
235        assert_eq!(min_lifetime, None);
236
237        let json_data = raw_json(uint!(10), None);
238        let RoomRetentionEventContent { max_lifetime, min_lifetime, .. } =
239            from_json_value::<OriginalStateEvent<RoomRetentionEventContent>>(json_data)
240                .expect("A min lifetime and no max lifetime should deserialize")
241                .content;
242
243        assert_eq!(min_lifetime, Some(uint!(10)));
244        assert_eq!(max_lifetime, None);
245
246        let json_data = raw_json(uint!(10), uint!(10));
247        let RoomRetentionEventContent { max_lifetime, min_lifetime, .. } =
248            from_json_value::<OriginalStateEvent<RoomRetentionEventContent>>(json_data)
249                .expect("Setting both lifetimes, should still deserialize")
250                .content;
251
252        assert_eq!(min_lifetime, Some(uint!(10)));
253        assert_eq!(max_lifetime, Some(uint!(10)));
254
255        let json_data = raw_json(uint!(20), uint!(10));
256        from_json_value::<OriginalStateEvent<RoomRetentionEventContent>>(json_data).expect_err(
257            "If the max lifetime is smaller than the min lifetime, we should fail to deserialize",
258        );
259    }
260
261    #[test]
262    fn serialization() {
263        assert!(
264            RoomRetentionEventContent::from_range(
265                Duration::from_millis(10)..Duration::from_millis(0)
266            )
267            .is_none(),
268            "Giving a max lifetime that's smaller than the min lifetime should give you a None"
269        );
270
271        let content = RoomRetentionEventContent::new().at_least(Duration::from_millis(10)).unwrap();
272
273        assert_to_canonical_json_eq!(
274            content,
275            json!({
276                "min_lifetime": uint!(10),
277            }),
278        );
279    }
280}