use std::time::Duration;
use js_int::UInt;
use ruma_common::MilliSecondsSinceUnixEpoch;
use ruma_events::{Mentions, relation::Reference};
use ruma_macros::{EventContent, StringEnum};
use serde::{Deserialize, Serialize};
use crate::PrivOwnedStr;
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
#[ruma_event(
type = "m.rtc.notification",
kind = MessageLike
)]
pub struct RtcNotificationEventContent {
pub sender_ts: MilliSecondsSinceUnixEpoch,
#[serde(with = "ruma_common::serde::duration::ms")]
pub lifetime: Duration,
#[serde(rename = "m.mentions", default, skip_serializing_if = "Option::is_none")]
pub mentions: Option<Mentions>,
#[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")]
pub relates_to: Option<Reference>,
pub notification_type: NotificationType,
#[serde(rename = "m.call.intent", skip_serializing_if = "Option::is_none")]
pub call_intent: Option<CallIntent>,
}
impl RtcNotificationEventContent {
pub fn new(
sender_ts: MilliSecondsSinceUnixEpoch,
lifetime: Duration,
notification_type: NotificationType,
) -> Self {
Self {
sender_ts,
lifetime,
mentions: None,
relates_to: None,
notification_type,
call_intent: None,
}
}
pub fn expiration_ts(
&self,
origin_server_ts: MilliSecondsSinceUnixEpoch,
max_sender_ts_offset: Option<u32>,
) -> MilliSecondsSinceUnixEpoch {
let (larger, smaller) = if self.sender_ts.get() > origin_server_ts.get() {
(self.sender_ts.get(), origin_server_ts.get())
} else {
(origin_server_ts.get(), self.sender_ts.get())
};
let use_origin_server_ts =
larger.saturating_sub(smaller) > max_sender_ts_offset.unwrap_or(20_000).into();
let start_ts =
if use_origin_server_ts { origin_server_ts.get() } else { self.sender_ts.get() };
MilliSecondsSinceUnixEpoch(
start_ts.saturating_add(UInt::from(self.lifetime.as_millis() as u32)),
)
}
}
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, StringEnum)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
#[ruma_enum(rename_all = "snake_case")]
pub enum NotificationType {
Ring,
Notification,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, StringEnum)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
#[ruma_enum(rename_all = "snake_case")]
pub enum CallIntent {
Audio,
Video,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use assert_matches2::assert_matches;
use js_int::UInt;
use ruma_common::{
MilliSecondsSinceUnixEpoch, canonical_json::assert_to_canonical_json_eq, owned_event_id,
};
use serde_json::{from_value as from_json_value, json};
use super::{CallIntent, NotificationType, RtcNotificationEventContent};
use crate::{AnyMessageLikeEvent, Mentions, MessageLikeEvent};
#[test]
fn notification_event_serialization() {
let mut content = RtcNotificationEventContent::new(
MilliSecondsSinceUnixEpoch(UInt::new(1_752_583_130_365).unwrap()),
Duration::from_millis(30_000),
NotificationType::Ring,
);
content.mentions = Some(Mentions::with_room_mention());
content.relates_to = Some(ruma_events::relation::Reference::new(owned_event_id!("$m:ex")));
assert_to_canonical_json_eq!(
content,
json!({
"sender_ts": 1_752_583_130_365_u64,
"lifetime": 30_000_u32,
"m.mentions": {"room": true},
"m.relates_to": {"rel_type": "m.reference", "event_id": "$m:ex"},
"notification_type": "ring",
})
);
}
#[test]
fn notification_event_call_intent_serialization() {
let mut content = RtcNotificationEventContent::new(
MilliSecondsSinceUnixEpoch(UInt::new(0).unwrap()),
Duration::from_millis(30_000),
NotificationType::Notification,
);
content.call_intent = Some(CallIntent::Audio);
assert_to_canonical_json_eq!(
content,
json!({
"sender_ts": 0,
"lifetime": 30_000_u32,
"notification_type": "notification",
"m.call.intent": "audio",
})
);
}
#[test]
fn call_intent_deserialization_default() {
let raw_content = json!({
"m.mentions": {
"user_ids": [],
"room": true
},
"notification_type": "ring",
"m.relates_to": {
"event_id": "$IACrEkEKgDa-n4cMk-lEJ3vqLLUL9zX1nVyAnpmFaec",
"rel_type": "m.reference"
},
"sender_ts": 17_709_890_710_u64,
"lifetime": 30000,
});
let content: RtcNotificationEventContent = from_json_value(raw_content).unwrap();
assert_eq!(content.call_intent, None);
}
#[test]
fn test_call_intent_serialization() {
assert_eq!(serde_json::to_string(&CallIntent::Audio).unwrap(), r#""audio""#);
assert_eq!(serde_json::to_string(&CallIntent::Video).unwrap(), r#""video""#);
}
#[test]
fn notification_event_deserialization() {
let json_data = json!({
"content": {
"sender_ts": 1_752_583_130_365_u64,
"lifetime": 30_000_u32,
"m.mentions": {"room": true},
"m.relates_to": {"rel_type": "m.reference", "event_id": "$m:ex"},
"notification_type": "notification"
},
"event_id": "$event:notareal.hs",
"origin_server_ts": 134_829_848,
"room_id": "!roomid:notareal.hs",
"sender": "@user:notareal.hs",
"type": "m.rtc.notification"
});
let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
assert_matches!(
event,
AnyMessageLikeEvent::RtcNotification(MessageLikeEvent::Original(ev))
);
assert_eq!(ev.content.lifetime, Duration::from_millis(30_000));
}
#[test]
fn expiration_ts_computation() {
let content = RtcNotificationEventContent::new(
MilliSecondsSinceUnixEpoch(UInt::new(100_365).unwrap()),
Duration::from_millis(30_000),
NotificationType::Ring,
);
let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(120_000).unwrap());
assert_eq!(
content.expiration_ts(origin_server_ts, None),
MilliSecondsSinceUnixEpoch(UInt::new(130_365).unwrap())
);
let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(200_000).unwrap());
assert_eq!(
content.expiration_ts(origin_server_ts, None),
MilliSecondsSinceUnixEpoch(UInt::new(230_000).unwrap())
);
let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(50_000).unwrap());
assert_eq!(
content.expiration_ts(origin_server_ts, None),
MilliSecondsSinceUnixEpoch(UInt::new(80_000).unwrap())
);
let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(130_200).unwrap());
assert_eq!(
content.expiration_ts(origin_server_ts, Some(100)),
MilliSecondsSinceUnixEpoch(UInt::new(160_200).unwrap())
);
let origin_server_ts = MilliSecondsSinceUnixEpoch(UInt::new(100_300).unwrap());
assert_eq!(
content.expiration_ts(origin_server_ts, Some(100)),
MilliSecondsSinceUnixEpoch(UInt::new(130_365).unwrap())
);
}
}