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::{relation::Reference, Mentions};
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, PartialEq, Eq, 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::{owned_event_id, MilliSecondsSinceUnixEpoch};
121    use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
122
123    use super::{NotificationType, RtcNotificationEventContent};
124    use crate::{AnyMessageLikeEvent, Mentions, MessageLikeEvent};
125
126    #[test]
127    fn notification_event_serialization() {
128        let mut content = RtcNotificationEventContent::new(
129            MilliSecondsSinceUnixEpoch(UInt::new(1_752_583_130_365).unwrap()),
130            Duration::from_millis(30_000),
131            NotificationType::Ring,
132        );
133        content.mentions = Some(Mentions::with_room_mention());
134        content.relates_to = Some(ruma_events::relation::Reference::new(owned_event_id!("$m:ex")));
135
136        assert_eq!(
137            to_json_value(&content).unwrap(),
138            json!({
139                "sender_ts": 1_752_583_130_365_u64,
140                "lifetime": 30_000_u32,
141                "m.mentions": {"room": true},
142                "m.relates_to": {"rel_type": "m.reference", "event_id": "$m:ex"},
143                "notification_type": "ring"
144            })
145        );
146    }
147
148    #[test]
149    fn notification_event_deserialization() {
150        let json_data = json!({
151            "content": {
152                "sender_ts": 1_752_583_130_365_u64,
153                "lifetime": 30_000_u32,
154                "m.mentions": {"room": true},
155                "m.relates_to": {"rel_type": "m.reference", "event_id": "$m:ex"},
156                "notification_type": "notification"
157            },
158            "event_id": "$event:notareal.hs",
159            "origin_server_ts": 134_829_848,
160            "room_id": "!roomid:notareal.hs",
161            "sender": "@user:notareal.hs",
162            "type": "m.rtc.notification"
163        });
164
165        let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
166        assert_matches!(
167            event,
168            AnyMessageLikeEvent::RtcNotification(MessageLikeEvent::Original(ev))
169        );
170        assert_eq!(ev.content.lifetime, Duration::from_millis(30_000));
171    }
172
173    #[test]
174    fn expiration_ts_computation() {
175        let content = RtcNotificationEventContent::new(
176            MilliSecondsSinceUnixEpoch(UInt::new(100_365).unwrap()),
177            Duration::from_millis(30_000),
178            NotificationType::Ring,
179        );
180
181        // sender_ts is trustworthy
182        let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(120_000).unwrap());
183        assert_eq!(
184            content.expiration_ts(origin_server_ts, None),
185            MilliSecondsSinceUnixEpoch(UInt::new(130_365).unwrap())
186        );
187
188        // sender_ts is not trustworthy (sender_ts too small), origin_server_ts is used instead
189        let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(200_000).unwrap());
190        assert_eq!(
191            content.expiration_ts(origin_server_ts, None),
192            MilliSecondsSinceUnixEpoch(UInt::new(230_000).unwrap())
193        );
194
195        // sender_ts is not trustworthy (sender_ts too large), origin_server_ts is used instead
196        let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(50_000).unwrap());
197        assert_eq!(
198            content.expiration_ts(origin_server_ts, None),
199            MilliSecondsSinceUnixEpoch(UInt::new(80_000).unwrap())
200        );
201
202        // using a custom max offset (result in origin_server_ts)
203        let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(130_200).unwrap());
204        assert_eq!(
205            content.expiration_ts(origin_server_ts, Some(100)),
206            MilliSecondsSinceUnixEpoch(UInt::new(160_200).unwrap())
207        );
208
209        // using a custom max offset (result in sender_ts)
210        let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(100_300).unwrap());
211        assert_eq!(
212            content.expiration_ts(origin_server_ts, Some(100)),
213            MilliSecondsSinceUnixEpoch(UInt::new(130_365).unwrap())
214        );
215    }
216}