events: Update types according to changes in MSC3553

This commit is contained in:
Kévin Commaille 2023-01-05 10:41:24 +01:00 committed by Kévin Commaille
parent 5985bbe803
commit 60f754322e
5 changed files with 183 additions and 81 deletions

View File

@ -85,7 +85,8 @@ event_enum! {
"m.room.redaction" => super::room::redaction,
"m.sticker" => super::sticker,
#[cfg(feature = "unstable-msc3553")]
"m.video" => super::video,
#[ruma_enum(alias = "m.video")]
"org.matrix.msc1767.video" => super::video,
#[cfg(feature = "unstable-msc3245")]
"m.voice" => super::voice,
}

View File

@ -9,7 +9,9 @@ use ruma_macros::EventContent;
use serde::{Deserialize, Serialize};
use super::{
file::FileContentBlock, image::ThumbnailContentBlock, message::TextContentBlock,
file::{CaptionContentBlock, FileContentBlock},
image::ThumbnailContentBlock,
message::TextContentBlock,
room::message::Relation,
};
@ -22,7 +24,7 @@ use super::{
/// [`message`]: super::message
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.video", kind = MessageLike, without_relation)]
#[ruma_event(type = "org.matrix.msc1767.video", kind = MessageLike, without_relation)]
pub struct VideoEventContent {
/// The text representation of the message.
#[serde(rename = "org.matrix.msc1767.text")]
@ -32,11 +34,13 @@ pub struct VideoEventContent {
#[serde(rename = "org.matrix.msc1767.file")]
pub file: FileContentBlock,
/// The video content of the message.
#[serde(rename = "m.video")]
pub video: Box<VideoContent>,
/// The video details of the message, if any.
#[serde(rename = "org.matrix.msc1767.video_details", skip_serializing_if = "Option::is_none")]
pub video_details: Option<VideoDetailsContentBlock>,
/// The thumbnails of the message.
/// The thumbnails of the message, if any.
///
/// This is optional and defaults to an empty array.
#[serde(
rename = "org.matrix.msc1767.thumbnail",
default,
@ -44,9 +48,18 @@ pub struct VideoEventContent {
)]
pub thumbnail: ThumbnailContentBlock,
/// The captions of the message.
#[serde(rename = "m.caption", default, skip_serializing_if = "TextContentBlock::is_empty")]
pub caption: TextContentBlock,
/// The caption of the message, if any.
#[serde(rename = "org.matrix.msc1767.caption", skip_serializing_if = "Option::is_none")]
pub caption: Option<CaptionContentBlock>,
/// Whether this message is automated.
#[cfg(feature = "unstable-msc3955")]
#[serde(
default,
skip_serializing_if = "crate::serde::is_default",
rename = "org.matrix.msc1767.automated"
)]
pub automated: bool,
/// Information about related messages.
#[serde(
@ -63,56 +76,53 @@ impl VideoEventContent {
Self {
text,
file,
video: Default::default(),
video_details: None,
thumbnail: Default::default(),
caption: Default::default(),
caption: None,
#[cfg(feature = "unstable-msc3955")]
automated: false,
relates_to: None,
}
}
/// Creates a new `VideoEventContent` with the given plain text fallback representation and
/// file.
pub fn plain(text: impl Into<String>, file: FileContentBlock) -> Self {
pub fn with_plain_text(plain_text: impl Into<String>, file: FileContentBlock) -> Self {
Self {
text: TextContentBlock::plain(text),
text: TextContentBlock::plain(plain_text),
file,
video: Default::default(),
video_details: None,
thumbnail: Default::default(),
caption: Default::default(),
caption: None,
#[cfg(feature = "unstable-msc3955")]
automated: false,
relates_to: None,
}
}
}
/// Video content.
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
/// A block for details of video content.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct VideoContent {
/// The height of the video in pixels.
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<UInt>,
pub struct VideoDetailsContentBlock {
/// The width of the video in pixels.
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<UInt>,
pub width: UInt,
/// The duration of the video in milliseconds.
/// The height of the video in pixels.
pub height: UInt,
/// The duration of the video in seconds.
#[serde(
with = "crate::serde::duration::opt_ms",
with = "crate::serde::duration::opt_secs",
default,
skip_serializing_if = "Option::is_none"
)]
pub duration: Option<Duration>,
}
impl VideoContent {
/// Creates a new empty `VideoContent`.
pub fn new() -> Self {
Self::default()
}
/// Whether this `VideoContent` is empty.
pub fn is_empty(&self) -> bool {
self.height.is_none() && self.width.is_none() && self.duration.is_none()
impl VideoDetailsContentBlock {
/// Creates a new `VideoDetailsContentBlock` with the given height and width.
pub fn new(width: UInt, height: UInt) -> Self {
Self { width, height, duration: None }
}
}

View File

@ -1,4 +1,5 @@
//! De-/serialization functions for `std::time::Duration` objects
pub mod opt_ms;
pub mod opt_secs;
pub mod secs;

View File

@ -0,0 +1,96 @@
//! De-/serialization functions for `Option<std::time::Duration>` objects represented as
//! milliseconds.
//!
//! Delegates to `js_int::UInt` to ensure integer size is within bounds.
use std::time::Duration;
use js_int::UInt;
use serde::{
de::{Deserialize, Deserializer},
ser::{Error, Serialize, Serializer},
};
/// Serialize an `Option<Duration>`.
///
/// Will fail if integer is greater than the maximum integer that can be
/// unambiguously represented by an f64.
pub fn serialize<S>(opt_duration: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match opt_duration {
Some(duration) => match UInt::try_from(duration.as_secs()) {
Ok(uint) => uint.serialize(serializer),
Err(err) => Err(S::Error::custom(err)),
},
None => serializer.serialize_none(),
}
}
/// Deserializes an `Option<Duration>`.
///
/// Will fail if integer is greater than the maximum integer that can be
/// unambiguously represented by an f64.
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
where
D: Deserializer<'de>,
{
Ok(Option::<UInt>::deserialize(deserializer)?.map(|secs| Duration::from_secs(secs.into())))
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
struct DurationTest {
#[serde(with = "super", default, skip_serializing_if = "Option::is_none")]
timeout: Option<Duration>,
}
#[test]
fn deserialize_some() {
let json = json!({ "timeout": 300 });
assert_eq!(
serde_json::from_value::<DurationTest>(json).unwrap(),
DurationTest { timeout: Some(Duration::from_secs(300)) },
);
}
#[test]
fn deserialize_none_by_absence() {
let json = json!({});
assert_eq!(
serde_json::from_value::<DurationTest>(json).unwrap(),
DurationTest { timeout: None },
);
}
#[test]
fn deserialize_none_by_null() {
let json = json!({ "timeout": null });
assert_eq!(
serde_json::from_value::<DurationTest>(json).unwrap(),
DurationTest { timeout: None },
);
}
#[test]
fn serialize_some() {
let request = DurationTest { timeout: Some(Duration::new(2, 0)) };
assert_eq!(serde_json::to_value(request).unwrap(), json!({ "timeout": 2 }));
}
#[test]
fn serialize_none() {
let request = DurationTest { timeout: None };
assert_eq!(serde_json::to_value(request).unwrap(), json!({}));
}
}

View File

@ -3,17 +3,16 @@
use std::time::Duration;
use assert_matches::assert_matches;
use assign::assign;
use js_int::uint;
use ruma_common::{
event_id,
events::{
file::{EncryptedContentInit, FileContentBlock},
file::{CaptionContentBlock, EncryptedContentInit, FileContentBlock},
image::{Thumbnail, ThumbnailFileContentBlock, ThumbnailImageDetailsContentBlock},
message::TextContentBlock,
relation::InReplyTo,
room::{message::Relation, JsonWebKeyInit},
video::{VideoContent, VideoEventContent},
video::{VideoDetailsContentBlock, VideoEventContent},
AnyMessageLikeEvent, MessageLikeEvent,
},
mxc_uri,
@ -24,7 +23,7 @@ use serde_json::{from_value as from_json_value, json, to_value as to_json_value}
#[test]
fn plain_content_serialization() {
let event_content = VideoEventContent::plain(
let event_content = VideoEventContent::with_plain_text(
"Upload: my_video.webm",
FileContentBlock::plain(
mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
@ -42,14 +41,13 @@ fn plain_content_serialization() {
"url": "mxc://notareal.hs/abcdef",
"name": "my_video.webm",
},
"m.video": {}
})
);
}
#[test]
fn encrypted_content_serialization() {
let event_content = VideoEventContent::plain(
let event_content = VideoEventContent::with_plain_text(
"Upload: my_video.webm",
FileContentBlock::encrypted(
mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
@ -97,7 +95,6 @@ fn encrypted_content_serialization() {
},
"v": "v2"
},
"m.video": {}
})
);
}
@ -117,14 +114,9 @@ fn event_serialization() {
content.file.mimetype = Some("video/webm".to_owned());
content.file.size = Some(uint!(1_897_774));
content.video = Box::new(assign!(
VideoContent::new(),
{
width: Some(uint!(1920)),
height: Some(uint!(1080)),
duration: Some(Duration::from_secs(15)),
}
));
let mut video_details = VideoDetailsContentBlock::new(uint!(1920), uint!(1080));
video_details.duration = Some(Duration::from_secs(15));
content.video_details = Some(video_details);
let mut thumbnail = Thumbnail::new(
ThumbnailFileContentBlock::plain(
mxc_uri!("mxc://notareal.hs/thumbnail").to_owned(),
@ -134,7 +126,7 @@ fn event_serialization() {
);
thumbnail.file.size = Some(uint!(334_593));
content.thumbnail = vec![thumbnail].into();
content.caption = TextContentBlock::plain("This is my awesome vintage lava lamp");
content.caption = Some(CaptionContentBlock::plain("This is my awesome vintage lava lamp"));
content.relates_to = Some(Relation::Reply {
in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()),
});
@ -152,10 +144,10 @@ fn event_serialization() {
"mimetype": "video/webm",
"size": 1_897_774,
},
"m.video": {
"org.matrix.msc1767.video_details": {
"width": 1920,
"height": 1080,
"duration": 15_000,
"duration": 15,
},
"org.matrix.msc1767.thumbnail": [
{
@ -170,16 +162,16 @@ fn event_serialization() {
},
}
],
"m.caption": [
{
"body": "This is my awesome vintage lava lamp",
}
],
"org.matrix.msc1767.caption": {
"org.matrix.msc1767.text": [
{ "body": "This is my awesome vintage lava lamp" },
],
},
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$replyevent:example.com"
}
}
},
})
);
}
@ -194,14 +186,16 @@ fn plain_content_deserialization() {
"url": "mxc://notareal.hs/abcdef",
"name": "my_cat.mp4",
},
"m.video": {
"org.matrix.msc1767.video_details": {
"width": 720,
"height": 480,
"duration": 5_668,
},
"m.caption": [
{
"body": "Look at my cat!",
}
]
"org.matrix.msc1767.caption": {
"org.matrix.msc1767.text": [
{ "body": "Look at my cat!" },
],
},
});
let content = from_json_value::<VideoEventContent>(json_data).unwrap();
@ -210,12 +204,14 @@ fn plain_content_deserialization() {
assert_eq!(content.file.url, "mxc://notareal.hs/abcdef");
assert_eq!(content.file.name, "my_cat.mp4");
assert_matches!(content.file.encryption_info, None);
assert_eq!(content.video.width, None);
assert_eq!(content.video.height, None);
assert_eq!(content.video.duration, Some(Duration::from_millis(5_668)));
let video_details = content.video_details.unwrap();
assert_eq!(video_details.width, uint!(720));
assert_eq!(video_details.height, uint!(480));
assert_eq!(video_details.duration, Some(Duration::from_secs(5_668)));
assert_eq!(content.thumbnail.len(), 0);
assert_eq!(content.caption.find_plain(), Some("Look at my cat!"));
assert_eq!(content.caption.find_html(), None);
let caption = content.caption.unwrap();
assert_eq!(caption.text.find_plain(), Some("Look at my cat!"));
assert_eq!(caption.text.find_html(), None);
}
#[test]
@ -240,7 +236,6 @@ fn encrypted_content_deserialization() {
},
"v": "v2"
},
"m.video": {},
"org.matrix.msc1767.thumbnail": [
{
"org.matrix.msc1767.file": {
@ -261,16 +256,14 @@ fn encrypted_content_deserialization() {
assert_eq!(content.file.url, "mxc://notareal.hs/abcdef");
assert_eq!(content.file.name, "my_cat.mp4");
assert!(content.file.encryption_info.is_some());
assert_eq!(content.video.width, None);
assert_eq!(content.video.height, None);
assert_eq!(content.video.duration, None);
assert!(content.video_details.is_none());
assert_eq!(content.thumbnail.len(), 1);
let thumbnail = &content.thumbnail[0];
assert_eq!(thumbnail.file.url, "mxc://notareal.hs/thumbnail");
assert_eq!(thumbnail.file.mimetype, "image/png");
assert_eq!(thumbnail.image_details.width, uint!(560));
assert_eq!(thumbnail.image_details.height, uint!(480));
assert!(content.caption.is_empty());
assert!(content.caption.is_none());
}
#[test]
@ -286,7 +279,7 @@ fn message_event_deserialization() {
"mimetype": "video/webm",
"size": 123_774,
},
"m.video": {
"org.matrix.msc1767.video_details": {
"width": 1300,
"height": 837,
}
@ -295,7 +288,7 @@ fn message_event_deserialization() {
"origin_server_ts": 134_829_848,
"room_id": "!roomid:notareal.hs",
"sender": "@user:notareal.hs",
"type": "m.video",
"type": "org.matrix.msc1767.video",
});
let ev = assert_matches!(
@ -315,8 +308,9 @@ fn message_event_deserialization() {
assert_eq!(content.file.name, "my_gnome.webm");
assert_eq!(content.file.mimetype.as_deref(), Some("video/webm"));
assert_eq!(content.file.size, Some(uint!(123_774)));
assert_eq!(content.video.width, Some(uint!(1300)));
assert_eq!(content.video.height, Some(uint!(837)));
assert_eq!(content.video.duration, None);
let video_details = content.video_details.unwrap();
assert_eq!(video_details.width, uint!(1300));
assert_eq!(video_details.height, uint!(837));
assert_eq!(video_details.duration, None);
assert_eq!(content.thumbnail.len(), 0);
}