From 60f754322ed96fec04cb604984371edb972bfdf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Thu, 5 Jan 2023 10:41:24 +0100 Subject: [PATCH] events: Update types according to changes in MSC3553 --- crates/ruma-common/src/events/enums.rs | 3 +- crates/ruma-common/src/events/video.rs | 80 +++++++++------- crates/ruma-common/src/serde/duration.rs | 1 + .../src/serde/duration/opt_secs.rs | 96 +++++++++++++++++++ crates/ruma-common/tests/events/video.rs | 84 ++++++++-------- 5 files changed, 183 insertions(+), 81 deletions(-) create mode 100644 crates/ruma-common/src/serde/duration/opt_secs.rs diff --git a/crates/ruma-common/src/events/enums.rs b/crates/ruma-common/src/events/enums.rs index f8be7ef7..5abf838d 100644 --- a/crates/ruma-common/src/events/enums.rs +++ b/crates/ruma-common/src/events/enums.rs @@ -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, } diff --git a/crates/ruma-common/src/events/video.rs b/crates/ruma-common/src/events/video.rs index 04ab20dd..e0282e8e 100644 --- a/crates/ruma-common/src/events/video.rs +++ b/crates/ruma-common/src/events/video.rs @@ -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, + /// 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, - /// 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, + + /// 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, file: FileContentBlock) -> Self { + pub fn with_plain_text(plain_text: impl Into, 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, - +pub struct VideoDetailsContentBlock { /// The width of the video in pixels. - #[serde(skip_serializing_if = "Option::is_none")] - pub width: Option, + 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, } -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 } } } diff --git a/crates/ruma-common/src/serde/duration.rs b/crates/ruma-common/src/serde/duration.rs index d8c895fa..4f8de31b 100644 --- a/crates/ruma-common/src/serde/duration.rs +++ b/crates/ruma-common/src/serde/duration.rs @@ -1,4 +1,5 @@ //! De-/serialization functions for `std::time::Duration` objects pub mod opt_ms; +pub mod opt_secs; pub mod secs; diff --git a/crates/ruma-common/src/serde/duration/opt_secs.rs b/crates/ruma-common/src/serde/duration/opt_secs.rs new file mode 100644 index 00000000..21ec24f8 --- /dev/null +++ b/crates/ruma-common/src/serde/duration/opt_secs.rs @@ -0,0 +1,96 @@ +//! De-/serialization functions for `Option` 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`. +/// +/// Will fail if integer is greater than the maximum integer that can be +/// unambiguously represented by an f64. +pub fn serialize(opt_duration: &Option, serializer: S) -> Result +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`. +/// +/// 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, D::Error> +where + D: Deserializer<'de>, +{ + Ok(Option::::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, + } + + #[test] + fn deserialize_some() { + let json = json!({ "timeout": 300 }); + + assert_eq!( + serde_json::from_value::(json).unwrap(), + DurationTest { timeout: Some(Duration::from_secs(300)) }, + ); + } + + #[test] + fn deserialize_none_by_absence() { + let json = json!({}); + + assert_eq!( + serde_json::from_value::(json).unwrap(), + DurationTest { timeout: None }, + ); + } + + #[test] + fn deserialize_none_by_null() { + let json = json!({ "timeout": null }); + + assert_eq!( + serde_json::from_value::(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!({})); + } +} diff --git a/crates/ruma-common/tests/events/video.rs b/crates/ruma-common/tests/events/video.rs index 5c0c67a5..e42c0a33 100644 --- a/crates/ruma-common/tests/events/video.rs +++ b/crates/ruma-common/tests/events/video.rs @@ -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::(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); }