1use 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#[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 #[serde(rename = "m.call.intent", skip_serializing_if = "Option::is_none")]
52 pub call_intent: Option<CallIntent>,
53}
54
55impl RtcNotificationEventContent {
56 pub fn new(
58 sender_ts: MilliSecondsSinceUnixEpoch,
59 lifetime: Duration,
60 notification_type: NotificationType,
61 ) -> Self {
62 Self {
63 sender_ts,
64 lifetime,
65 mentions: None,
66 relates_to: None,
67 notification_type,
68 call_intent: None,
69 }
70 }
71
72 pub fn expiration_ts(
93 &self,
94 origin_server_ts: MilliSecondsSinceUnixEpoch,
95 max_sender_ts_offset: Option<u32>,
96 ) -> MilliSecondsSinceUnixEpoch {
97 let (larger, smaller) = if self.sender_ts.get() > origin_server_ts.get() {
98 (self.sender_ts.get(), origin_server_ts.get())
99 } else {
100 (origin_server_ts.get(), self.sender_ts.get())
101 };
102 let use_origin_server_ts =
103 larger.saturating_sub(smaller) > max_sender_ts_offset.unwrap_or(20_000).into();
104 let start_ts =
105 if use_origin_server_ts { origin_server_ts.get() } else { self.sender_ts.get() };
106 MilliSecondsSinceUnixEpoch(
107 start_ts.saturating_add(UInt::from(self.lifetime.as_millis() as u32)),
108 )
109 }
110}
111
112#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
114#[derive(Clone, StringEnum)]
115#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
116#[ruma_enum(rename_all = "snake_case")]
117pub enum NotificationType {
118 Ring,
120
121 Notification,
123
124 #[doc(hidden)]
125 _Custom(PrivOwnedStr),
126}
127
128#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
130#[derive(Clone, StringEnum)]
131#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
132#[ruma_enum(rename_all = "snake_case")]
133pub enum CallIntent {
134 Audio,
136
137 Video,
140
141 #[doc(hidden)]
142 _Custom(PrivOwnedStr),
143}
144
145#[cfg(test)]
146mod tests {
147 use std::time::Duration;
148
149 use assert_matches2::assert_matches;
150 use js_int::UInt;
151 use ruma_common::{
152 MilliSecondsSinceUnixEpoch, canonical_json::assert_to_canonical_json_eq, owned_event_id,
153 };
154 use serde_json::{from_value as from_json_value, json};
155
156 use super::{CallIntent, NotificationType, RtcNotificationEventContent};
157 use crate::{AnyMessageLikeEvent, Mentions, MessageLikeEvent};
158
159 #[test]
160 fn notification_event_serialization() {
161 let mut content = RtcNotificationEventContent::new(
162 MilliSecondsSinceUnixEpoch(UInt::new(1_752_583_130_365).unwrap()),
163 Duration::from_millis(30_000),
164 NotificationType::Ring,
165 );
166 content.mentions = Some(Mentions::with_room_mention());
167 content.relates_to = Some(ruma_events::relation::Reference::new(owned_event_id!("$m:ex")));
168
169 assert_to_canonical_json_eq!(
170 content,
171 json!({
172 "sender_ts": 1_752_583_130_365_u64,
173 "lifetime": 30_000_u32,
174 "m.mentions": {"room": true},
175 "m.relates_to": {"rel_type": "m.reference", "event_id": "$m:ex"},
176 "notification_type": "ring",
177 })
178 );
179 }
180
181 #[test]
182 fn notification_event_call_intent_serialization() {
183 let mut content = RtcNotificationEventContent::new(
184 MilliSecondsSinceUnixEpoch(UInt::new(0).unwrap()),
185 Duration::from_millis(30_000),
186 NotificationType::Notification,
187 );
188 content.call_intent = Some(CallIntent::Audio);
189
190 assert_to_canonical_json_eq!(
191 content,
192 json!({
193 "sender_ts": 0,
194 "lifetime": 30_000_u32,
195 "notification_type": "notification",
196 "m.call.intent": "audio",
197 })
198 );
199 }
200
201 #[test]
202 fn call_intent_deserialization_default() {
203 let raw_content = json!({
204 "m.mentions": {
205 "user_ids": [],
206 "room": true
207 },
208 "notification_type": "ring",
209 "m.relates_to": {
210 "event_id": "$IACrEkEKgDa-n4cMk-lEJ3vqLLUL9zX1nVyAnpmFaec",
211 "rel_type": "m.reference"
212 },
213 "sender_ts": 17_709_890_710_u64,
214 "lifetime": 30000,
215 });
216 let content: RtcNotificationEventContent = from_json_value(raw_content).unwrap();
217 assert_eq!(content.call_intent, None);
218 }
219
220 #[test]
221 fn test_call_intent_serialization() {
222 assert_eq!(serde_json::to_string(&CallIntent::Audio).unwrap(), r#""audio""#);
223 assert_eq!(serde_json::to_string(&CallIntent::Video).unwrap(), r#""video""#);
224 }
225
226 #[test]
227 fn notification_event_deserialization() {
228 let json_data = json!({
229 "content": {
230 "sender_ts": 1_752_583_130_365_u64,
231 "lifetime": 30_000_u32,
232 "m.mentions": {"room": true},
233 "m.relates_to": {"rel_type": "m.reference", "event_id": "$m:ex"},
234 "notification_type": "notification"
235 },
236 "event_id": "$event:notareal.hs",
237 "origin_server_ts": 134_829_848,
238 "room_id": "!roomid:notareal.hs",
239 "sender": "@user:notareal.hs",
240 "type": "m.rtc.notification"
241 });
242
243 let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
244 assert_matches!(
245 event,
246 AnyMessageLikeEvent::RtcNotification(MessageLikeEvent::Original(ev))
247 );
248 assert_eq!(ev.content.lifetime, Duration::from_millis(30_000));
249 }
250
251 #[test]
252 fn expiration_ts_computation() {
253 let content = RtcNotificationEventContent::new(
254 MilliSecondsSinceUnixEpoch(UInt::new(100_365).unwrap()),
255 Duration::from_millis(30_000),
256 NotificationType::Ring,
257 );
258
259 let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(120_000).unwrap());
261 assert_eq!(
262 content.expiration_ts(origin_server_ts, None),
263 MilliSecondsSinceUnixEpoch(UInt::new(130_365).unwrap())
264 );
265
266 let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(200_000).unwrap());
268 assert_eq!(
269 content.expiration_ts(origin_server_ts, None),
270 MilliSecondsSinceUnixEpoch(UInt::new(230_000).unwrap())
271 );
272
273 let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(50_000).unwrap());
275 assert_eq!(
276 content.expiration_ts(origin_server_ts, None),
277 MilliSecondsSinceUnixEpoch(UInt::new(80_000).unwrap())
278 );
279
280 let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(130_200).unwrap());
282 assert_eq!(
283 content.expiration_ts(origin_server_ts, Some(100)),
284 MilliSecondsSinceUnixEpoch(UInt::new(160_200).unwrap())
285 );
286
287 let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(100_300).unwrap());
289 assert_eq!(
290 content.expiration_ts(origin_server_ts, Some(100)),
291 MilliSecondsSinceUnixEpoch(UInt::new(130_365).unwrap())
292 );
293 }
294}