ruma_events/room/
retention.rs1use 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#[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 #[serde(skip_serializing_if = "Option::is_none")]
29 min_lifetime: Option<UInt>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
33 max_lifetime: Option<UInt>,
34}
35
36impl RoomRetentionEventContent {
37 pub fn new() -> Self {
55 Self::default()
56 }
57
58 fn new_impl(min_lifetime: Option<Duration>, max_lifetime: Option<Duration>) -> Option<Self> {
63 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 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 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 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 pub fn max_lifetime(&self) -> Option<Duration> {
132 self.max_lifetime.map(|l| Duration::from_millis(l.into()))
133 }
134
135 pub fn min_lifetime(&self) -> Option<Duration> {
137 self.min_lifetime.map(|l| Duration::from_millis(l.into()))
138 }
139}
140
141pub 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
179pub 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}