ruma_events/rtc/
notification.rs

1//! Type for the MatrixRTC notification event ([MSC4075]).
2//!
3//! Stable: `m.rtc.notification`
4//! Unstable: `org.matrix.msc4075.rtc.notification`
5//!
6//! [MSC4075]: https://github.com/matrix-org/matrix-spec-proposals/pull/4075
7
8use std::time::Duration;
9
10use js_int::UInt;
11use ruma_common::MilliSecondsSinceUnixEpoch;
12use ruma_events::{Mentions, relation::Reference};
13use ruma_macros::{EventContent, StringEnum};
14use serde::{Deserialize, Serialize};
15
16use crate::PrivOwnedStr;
17
18/// The content of an `m.rtc.notification` event.
19#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
20#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
21#[ruma_event(
22    type = "m.rtc.notification",
23    kind = MessageLike
24)]
25pub struct RtcNotificationEventContent {
26    /// Local timestamp observed by the sender device.
27    ///
28    /// Used with `lifetime` to determine validity; receivers SHOULD compare with
29    /// `origin_server_ts` and prefer it if the difference is large.
30    pub sender_ts: MilliSecondsSinceUnixEpoch,
31
32    /// Relative time from `sender_ts` during which the notification is considered valid.
33    #[serde(with = "ruma_common::serde::duration::ms")]
34    pub lifetime: Duration,
35
36    /// Intentional mentions determining who should be notified.
37    #[serde(rename = "m.mentions", default, skip_serializing_if = "Option::is_none")]
38    pub mentions: Option<Mentions>,
39
40    /// Optional reference to the related `m.rtc.member` event.
41    #[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")]
42    pub relates_to: Option<Reference>,
43
44    /// How this notification should notify the receiver.
45    pub notification_type: NotificationType,
46}
47
48impl RtcNotificationEventContent {
49    /// Creates a new `RtcNotificationEventContent` with the given configuration.
50    pub fn new(
51        sender_ts: MilliSecondsSinceUnixEpoch,
52        lifetime: Duration,
53        notification_type: NotificationType,
54    ) -> Self {
55        Self { sender_ts, lifetime, mentions: None, relates_to: None, notification_type }
56    }
57
58    /// Calculates the timestamp at which this notification is considered invalid.
59    /// This calculation is based on MSC4075 and tries to use the `sender_ts` as the starting point
60    /// and the `lifetime` as the duration for which the notification is valid.
61    ///
62    /// The `sender_ts` cannot be trusted since it is a generated value by the sending client.
63    /// To mitigate issue because of misconfigured client clocks, the MSC requires
64    /// that the `origin_server_ts` is used as the starting point if the difference is large.
65    ///
66    /// # Arguments:
67    ///
68    /// - `max_sender_ts_offset` is the maximum allowed offset between the two timestamps. (default
69    ///   20s)
70    /// - `origin_server_ts` has to be set to the origin_server_ts from the event containing this
71    ///   event content.
72    ///
73    /// # Examples
74    /// To start a timer until this client should stop ringing for this notification:
75    /// `let duration_ring =
76    /// MilliSecondsSinceUnixEpoch::now().saturated_sub(content.expiration_ts(event.
77    /// origin_server_ts(), None));`
78    pub fn expiration_ts(
79        &self,
80        origin_server_ts: MilliSecondsSinceUnixEpoch,
81        max_sender_ts_offset: Option<u32>,
82    ) -> MilliSecondsSinceUnixEpoch {
83        let (larger, smaller) = if self.sender_ts.get() > origin_server_ts.get() {
84            (self.sender_ts.get(), origin_server_ts.get())
85        } else {
86            (origin_server_ts.get(), self.sender_ts.get())
87        };
88        let use_origin_server_ts =
89            larger.saturating_sub(smaller) > max_sender_ts_offset.unwrap_or(20_000).into();
90        let start_ts =
91            if use_origin_server_ts { origin_server_ts.get() } else { self.sender_ts.get() };
92        MilliSecondsSinceUnixEpoch(
93            start_ts.saturating_add(UInt::from(self.lifetime.as_millis() as u32)),
94        )
95    }
96}
97
98/// How this notification should notify the receiver.
99#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
100#[derive(Clone, StringEnum)]
101#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
102#[ruma_enum(rename_all = "snake_case")]
103pub enum NotificationType {
104    /// The receiving client should ring with an audible sound.
105    Ring,
106
107    /// The receiving client should display a visual notification.
108    Notification,
109
110    #[doc(hidden)]
111    _Custom(PrivOwnedStr),
112}
113
114#[cfg(test)]
115mod tests {
116    use std::time::Duration;
117
118    use assert_matches2::assert_matches;
119    use js_int::UInt;
120    use ruma_common::{
121        MilliSecondsSinceUnixEpoch, canonical_json::assert_to_canonical_json_eq, owned_event_id,
122    };
123    use serde_json::{from_value as from_json_value, json};
124
125    use super::{NotificationType, RtcNotificationEventContent};
126    use crate::{AnyMessageLikeEvent, Mentions, MessageLikeEvent};
127
128    #[test]
129    fn notification_event_serialization() {
130        let mut content = RtcNotificationEventContent::new(
131            MilliSecondsSinceUnixEpoch(UInt::new(1_752_583_130_365).unwrap()),
132            Duration::from_millis(30_000),
133            NotificationType::Ring,
134        );
135        content.mentions = Some(Mentions::with_room_mention());
136        content.relates_to = Some(ruma_events::relation::Reference::new(owned_event_id!("$m:ex")));
137
138        assert_to_canonical_json_eq!(
139            content,
140            json!({
141                "sender_ts": 1_752_583_130_365_u64,
142                "lifetime": 30_000_u32,
143                "m.mentions": {"room": true},
144                "m.relates_to": {"rel_type": "m.reference", "event_id": "$m:ex"},
145                "notification_type": "ring"
146            })
147        );
148    }
149
150    #[test]
151    fn notification_event_deserialization() {
152        let json_data = json!({
153            "content": {
154                "sender_ts": 1_752_583_130_365_u64,
155                "lifetime": 30_000_u32,
156                "m.mentions": {"room": true},
157                "m.relates_to": {"rel_type": "m.reference", "event_id": "$m:ex"},
158                "notification_type": "notification"
159            },
160            "event_id": "$event:notareal.hs",
161            "origin_server_ts": 134_829_848,
162            "room_id": "!roomid:notareal.hs",
163            "sender": "@user:notareal.hs",
164            "type": "m.rtc.notification"
165        });
166
167        let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
168        assert_matches!(
169            event,
170            AnyMessageLikeEvent::RtcNotification(MessageLikeEvent::Original(ev))
171        );
172        assert_eq!(ev.content.lifetime, Duration::from_millis(30_000));
173    }
174
175    #[test]
176    fn expiration_ts_computation() {
177        let content = RtcNotificationEventContent::new(
178            MilliSecondsSinceUnixEpoch(UInt::new(100_365).unwrap()),
179            Duration::from_millis(30_000),
180            NotificationType::Ring,
181        );
182
183        // sender_ts is trustworthy
184        let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(120_000).unwrap());
185        assert_eq!(
186            content.expiration_ts(origin_server_ts, None),
187            MilliSecondsSinceUnixEpoch(UInt::new(130_365).unwrap())
188        );
189
190        // sender_ts is not trustworthy (sender_ts too small), origin_server_ts is used instead
191        let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(200_000).unwrap());
192        assert_eq!(
193            content.expiration_ts(origin_server_ts, None),
194            MilliSecondsSinceUnixEpoch(UInt::new(230_000).unwrap())
195        );
196
197        // sender_ts is not trustworthy (sender_ts too large), origin_server_ts is used instead
198        let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(50_000).unwrap());
199        assert_eq!(
200            content.expiration_ts(origin_server_ts, None),
201            MilliSecondsSinceUnixEpoch(UInt::new(80_000).unwrap())
202        );
203
204        // using a custom max offset (result in origin_server_ts)
205        let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(130_200).unwrap());
206        assert_eq!(
207            content.expiration_ts(origin_server_ts, Some(100)),
208            MilliSecondsSinceUnixEpoch(UInt::new(160_200).unwrap())
209        );
210
211        // using a custom max offset (result in sender_ts)
212        let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(100_300).unwrap());
213        assert_eq!(
214            content.expiration_ts(origin_server_ts, Some(100)),
215            MilliSecondsSinceUnixEpoch(UInt::new(130_365).unwrap())
216        );
217    }
218}