ruma_events/rtc/
notification.rs1use 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
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, 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::{
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 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 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 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 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 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}