ruma_events/rtc/
notification.rs1use 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#[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 pub sender_ts: MilliSecondsSinceUnixEpoch,
31
32 #[serde(with = "ruma_common::serde::duration::ms")]
34 pub lifetime: Duration,
35
36 #[serde(rename = "m.mentions", default, skip_serializing_if = "Option::is_none")]
38 pub mentions: Option<Mentions>,
39
40 #[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")]
42 pub relates_to: Option<Reference>,
43
44 pub notification_type: NotificationType,
46}
47
48impl RtcNotificationEventContent {
49 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 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#[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 Ring,
106
107 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 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 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 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 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 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}