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.room.redaction" => super::room::redaction,
"m.sticker" => super::sticker, "m.sticker" => super::sticker,
#[cfg(feature = "unstable-msc3553")] #[cfg(feature = "unstable-msc3553")]
"m.video" => super::video, #[ruma_enum(alias = "m.video")]
"org.matrix.msc1767.video" => super::video,
#[cfg(feature = "unstable-msc3245")] #[cfg(feature = "unstable-msc3245")]
"m.voice" => super::voice, "m.voice" => super::voice,
} }

View File

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

View File

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