events: Use an enum for a media's source

Have stricter media types that accept either an encrypted or plain file.

Co-authored-by: Jonas Platte <jplatte@element.io>
This commit is contained in:
Kévin Commaille 2022-03-22 16:42:42 +01:00 committed by GitHub
parent 9da6bd4861
commit 12ee658e96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 273 additions and 104 deletions

View File

@ -1,5 +1,10 @@
# [unreleased]
Breaking changes:
* Change `events::room` media types to accept either a plain file or an
encrypted file, not both simultaneously
# 0.8.0
Breaking changes:

View File

@ -29,9 +29,23 @@ pub mod power_levels;
pub mod redaction;
pub mod server_acl;
pub mod third_party_invite;
mod thumbnail_src_serde;
pub mod tombstone;
pub mod topic;
/// The source of a media file.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub enum MediaSource {
/// The MXC URI to the unencrypted media file.
#[serde(rename = "url")]
Plain(Box<MxcUri>),
/// The encryption info of the encrypted media file.
#[serde(rename = "file")]
Encrypted(Box<EncryptedFile>),
}
/// Metadata about an image.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
@ -52,21 +66,13 @@ pub struct ImageInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
/// Metadata about the image referred to in `thumbnail_url`.
/// Metadata about the image referred to in `thumbnail_src`.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
/// The URL to the thumbnail of the image.
///
/// Only present if the thumbnail is unencrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<Box<MxcUri>>,
/// Information on the encrypted thumbnail image.
///
/// Only present if the thumbnail is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<Box<EncryptedFile>>,
/// The source of the thumbnail of the image.
#[serde(flatten, with = "thumbnail_src_serde", skip_serializing_if = "Option::is_none")]
pub thumbnail_src: Option<MediaSource>,
/// The [BlurHash](https://blurha.sh) for this image.
///

View File

@ -9,7 +9,7 @@ use ruma_macros::EventContent;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue;
use super::{EncryptedFile, ImageInfo, ThumbnailInfo};
use super::{EncryptedFile, ImageInfo, MediaSource, ThumbnailInfo};
#[cfg(feature = "unstable-msc1767")]
use crate::events::{
emote::EmoteEventContent,
@ -474,17 +474,9 @@ pub struct AudioMessageEventContent {
/// The textual representation of this message.
pub body: String,
/// The URL to the audio clip.
///
/// Required if the file is unencrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Box<MxcUri>>,
/// Information on the encrypted audio clip.
///
/// Required if the audio clip is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
/// The source of the audio clip.
#[serde(flatten)]
pub src: MediaSource,
/// Metadata for the audio clip referred to in `url`.
#[serde(skip_serializing_if = "Option::is_none")]
@ -495,13 +487,13 @@ impl AudioMessageEventContent {
/// Creates a new non-encrypted `RoomAudioMessageEventContent` with the given body, url and
/// optional extra info.
pub fn plain(body: String, url: Box<MxcUri>, info: Option<Box<AudioInfo>>) -> Self {
Self { body, url: Some(url), info, file: None }
Self { body, src: MediaSource::Plain(url), info }
}
/// Creates a new encrypted `RoomAudioMessageEventContent` with the given body and encrypted
/// file.
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self { body, url: None, info: None, file: Some(Box::new(file)) }
Self { body, src: MediaSource::Encrypted(Box::new(file)), info: None }
}
}
@ -616,15 +608,9 @@ pub struct FileMessageEventContent {
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
/// The URL to the file.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Box<MxcUri>>,
/// Information on the encrypted file.
///
/// Required if file is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
/// The source of the file.
#[serde(flatten)]
pub src: MediaSource,
/// Metadata about the file referred to in `url`.
#[serde(skip_serializing_if = "Option::is_none")]
@ -635,13 +621,13 @@ impl FileMessageEventContent {
/// Creates a new non-encrypted `RoomFileMessageEventContent` with the given body, url and
/// optional extra info.
pub fn plain(body: String, url: Box<MxcUri>, info: Option<Box<FileInfo>>) -> Self {
Self { body, filename: None, url: Some(url), info, file: None }
Self { body, filename: None, src: MediaSource::Plain(url), info }
}
/// Creates a new encrypted `RoomFileMessageEventContent` with the given body and encrypted
/// file.
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self { body, filename: None, url: None, info: None, file: Some(Box::new(file)) }
Self { body, filename: None, src: MediaSource::Encrypted(Box::new(file)), info: None }
}
}
@ -657,21 +643,13 @@ pub struct FileInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
/// Metadata about the image referred to in `thumbnail_url`.
/// Metadata about the image referred to in `thumbnail_src`.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
/// The URL to the thumbnail of the file.
///
/// Only present if the thumbnail is unencrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<Box<MxcUri>>,
/// Information on the encrypted thumbnail file.
///
/// Only present if the thumbnail is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<Box<EncryptedFile>>,
/// The source of the thumbnail of the file.
#[serde(flatten, with = "super::thumbnail_src_serde", skip_serializing_if = "Option::is_none")]
pub thumbnail_src: Option<MediaSource>,
}
impl FileInfo {
@ -692,15 +670,9 @@ pub struct ImageMessageEventContent {
/// description for accessibility e.g. "image attachment".
pub body: String,
/// The URL to the image.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Box<MxcUri>>,
/// Information on the encrypted image.
///
/// Required if image is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
/// The source of the image.
#[serde(flatten)]
pub src: MediaSource,
/// Metadata about the image referred to in `url`.
#[serde(skip_serializing_if = "Option::is_none")]
@ -711,13 +683,13 @@ impl ImageMessageEventContent {
/// Creates a new non-encrypted `RoomImageMessageEventContent` with the given body, url and
/// optional extra info.
pub fn plain(body: String, url: Box<MxcUri>, info: Option<Box<ImageInfo>>) -> Self {
Self { body, url: Some(url), info, file: None }
Self { body, src: MediaSource::Plain(url), info }
}
/// Creates a new encrypted `RoomImageMessageEventContent` with the given body and encrypted
/// file.
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self { body, url: None, info: None, file: Some(Box::new(file)) }
Self { body, src: MediaSource::Encrypted(Box::new(file)), info: None }
}
}
@ -749,19 +721,11 @@ impl LocationMessageEventContent {
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct LocationInfo {
/// The URL to a thumbnail of the location being represented.
///
/// Only present if the thumbnail is unencrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<Box<MxcUri>>,
/// The URL to a thumbnail of the location.
#[serde(flatten, with = "super::thumbnail_src_serde", skip_serializing_if = "Option::is_none")]
pub thumbnail_src: Option<MediaSource>,
/// Information on an encrypted thumbnail of the location being represented.
///
/// Only present if the thumbnail is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<Box<EncryptedFile>>,
/// Metadata about the image referred to in `thumbnail_url` or `thumbnail_file`.
/// Metadata about the image referred to in `thumbnail_src.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
}
@ -1053,15 +1017,9 @@ pub struct VideoMessageEventContent {
/// accessibility, e.g. "video attachment".
pub body: String,
/// The URL to the video clip.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Box<MxcUri>>,
/// Information on the encrypted video clip.
///
/// Required if video clip is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
/// The source of the video clip.
#[serde(flatten)]
pub src: MediaSource,
/// Metadata about the video clip referred to in `url`.
#[serde(skip_serializing_if = "Option::is_none")]
@ -1072,13 +1030,13 @@ impl VideoMessageEventContent {
/// Creates a new non-encrypted `RoomVideoMessageEventContent` with the given body, url and
/// optional extra info.
pub fn plain(body: String, url: Box<MxcUri>, info: Option<Box<VideoInfo>>) -> Self {
Self { body, url: Some(url), info, file: None }
Self { body, src: MediaSource::Plain(url), info }
}
/// Creates a new encrypted `RoomVideoMessageEventContent` with the given body and encrypted
/// file.
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self { body, url: None, info: None, file: Some(Box::new(file)) }
Self { body, src: MediaSource::Encrypted(Box::new(file)), info: None }
}
}
@ -1110,21 +1068,13 @@ pub struct VideoInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
/// Metadata about an image.
/// Metadata about the image referred to in `thumbnail_src`.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
/// The URL to an image thumbnail of the video clip.
///
/// Only present if the thumbnail is unencrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<Box<MxcUri>>,
/// Information on the encrypted thumbnail file.
///
/// Only present if the thumbnail is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<Box<EncryptedFile>>,
/// The source of the thumbnail of the video clip.
#[serde(flatten, with = "super::thumbnail_src_serde", skip_serializing_if = "Option::is_none")]
pub thumbnail_src: Option<MediaSource>,
/// The [BlurHash](https://blurha.sh) for this video.
///

View File

@ -0,0 +1,207 @@
//! De-/serialization functions for `Option<MediaSource>` objects representing a thumbnail source.
use serde::{
de::Deserializer,
ser::{SerializeStruct, Serializer},
Deserialize,
};
use crate::MxcUri;
use super::{EncryptedFile, MediaSource};
/// Serializes a MediaSource to a thumbnail source.
pub fn serialize<S>(src: &Option<MediaSource>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(src) = src {
let mut st = serializer.serialize_struct("ThumbnailSource", 1)?;
match src {
MediaSource::Plain(url) => st.serialize_field("thumbnail_url", url)?,
MediaSource::Encrypted(file) => st.serialize_field("thumbnail_file", file)?,
}
st.end()
} else {
serializer.serialize_none()
}
}
/// Deserializes a thumbnail source to a MediaSource.
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<MediaSource>, D::Error>
where
D: Deserializer<'de>,
{
Option::<ThumbnailSource>::deserialize(deserializer).map(|src| src.map(Into::into))
}
#[derive(Clone, Debug, Deserialize)]
enum ThumbnailSource {
/// The MXC URI to the unencrypted media file.
#[serde(rename = "thumbnail_url")]
Plain(Box<MxcUri>),
/// The encryption info of the encrypted media file.
#[serde(rename = "thumbnail_file")]
Encrypted(Box<EncryptedFile>),
}
impl From<ThumbnailSource> for MediaSource {
fn from(src: ThumbnailSource) -> Self {
match src {
ThumbnailSource::Plain(url) => Self::Plain(url),
ThumbnailSource::Encrypted(file) => Self::Encrypted(file),
}
}
}
#[cfg(test)]
mod tests {
use matches::assert_matches;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::{
events::room::{EncryptedFileInit, JsonWebKeyInit, MediaSource},
mxc_uri,
serde::Base64,
};
#[derive(Clone, Debug, Deserialize, Serialize)]
struct ThumbnailSourceTest {
#[serde(flatten, with = "super", skip_serializing_if = "Option::is_none")]
src: Option<MediaSource>,
}
#[test]
fn deserialize_plain() {
let json = json!({ "thumbnail_url": "mxc://notareal.hs/abcdef" });
assert_matches!(
serde_json::from_value::<ThumbnailSourceTest>(json).unwrap(),
ThumbnailSourceTest { src: Some(MediaSource::Plain(url)) }
if url == "mxc://notareal.hs/abcdef"
);
}
#[test]
fn deserialize_encrypted() {
let json = json!({
"thumbnail_file": {
"url": "mxc://notareal.hs/abcdef",
"key": {
"kty": "oct",
"key_ops": ["encrypt", "decrypt"],
"alg": "A256CTR",
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
"ext": true
},
"iv": "S22dq3NAX8wAAAAAAAAAAA",
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "v2",
},
});
assert_matches!(
serde_json::from_value::<ThumbnailSourceTest>(json).unwrap(),
ThumbnailSourceTest { src: Some(MediaSource::Encrypted(file)) }
if file.url == "mxc://notareal.hs/abcdef"
);
}
#[test]
fn deserialize_none_by_absence() {
let json = json!({});
assert_matches!(
serde_json::from_value::<ThumbnailSourceTest>(json).unwrap(),
ThumbnailSourceTest { src: None }
);
}
#[test]
fn deserialize_none_by_null_plain() {
let json = json!({ "thumbnail_url": null });
assert_matches!(
serde_json::from_value::<ThumbnailSourceTest>(json).unwrap(),
ThumbnailSourceTest { src: None }
);
}
#[test]
fn deserialize_none_by_null_encrypted() {
let json = json!({ "thumbnail_file": null });
assert_matches!(
serde_json::from_value::<ThumbnailSourceTest>(json).unwrap(),
ThumbnailSourceTest { src: None }
);
}
#[test]
fn serialize_plain() {
let request = ThumbnailSourceTest {
src: Some(MediaSource::Plain(mxc_uri!("mxc://notareal.hs/abcdef").into())),
};
assert_eq!(
serde_json::to_value(&request).unwrap(),
json!({ "thumbnail_url": "mxc://notareal.hs/abcdef" })
);
}
#[test]
fn serialize_encrypted() {
let request = ThumbnailSourceTest {
src: Some(MediaSource::Encrypted(Box::new(
EncryptedFileInit {
url: mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
key: JsonWebKeyInit {
kty: "oct".to_owned(),
key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
alg: "A256CTR".to_owned(),
k: Base64::parse("TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A").unwrap(),
ext: true,
}
.into(),
iv: Base64::parse("S22dq3NAX8wAAAAAAAAAAA").unwrap(),
hashes: [(
"sha256".to_owned(),
Base64::parse("aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q").unwrap(),
)]
.into(),
v: "v2".to_owned(),
}
.into(),
))),
};
assert_eq!(
serde_json::to_value(&request).unwrap(),
json!({
"thumbnail_file": {
"url": "mxc://notareal.hs/abcdef",
"key": {
"kty": "oct",
"key_ops": ["encrypt", "decrypt"],
"alg": "A256CTR",
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
"ext": true
},
"iv": "S22dq3NAX8wAAAAAAAAAAA",
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "v2",
},
})
);
}
#[test]
fn serialize_none() {
let request = ThumbnailSourceTest { src: None };
assert_eq!(serde_json::to_value(&request).unwrap(), json!({}));
}
}

View File

@ -5,7 +5,7 @@ use ruma_common::{
event_id,
events::{
call::{answer::CallAnswerEventContent, SessionDescription, SessionDescriptionType},
room::{ImageInfo, ThumbnailInfo},
room::{ImageInfo, MediaSource, ThumbnailInfo},
sticker::StickerEventContent,
AnyMessageLikeEvent, AnyMessageLikeEventContent, AnySyncMessageLikeEvent, MessageLikeEvent,
MessageLikeEventType, MessageLikeUnsigned,
@ -32,7 +32,7 @@ fn message_serialize_sticker() {
mimetype: Some("image/png".into()),
size: UInt::new(82595),
}))),
thumbnail_url: Some(mxc_uri!("mxc://matrix.org/irsns989Rrsn").to_owned()),
thumbnail_src: Some(MediaSource::Plain(mxc_uri!("mxc://matrix.org/irsns989Rrsn").to_owned())),
}),
mxc_uri!("mxc://matrix.org/rnsldl8srs98IRrs").to_owned(),
),
@ -184,8 +184,7 @@ fn deserialize_message_sticker() {
mimetype: Some(mimetype),
size,
thumbnail_info: Some(thumbnail_info),
thumbnail_url: Some(thumbnail_url),
thumbnail_file: None,
thumbnail_src: Some(MediaSource::Plain(thumbnail_url)),
#[cfg(feature = "unstable-msc2448")]
blurhash: None,
..

View File

@ -12,9 +12,12 @@ use ruma_common::{
event_id,
events::{
key::verification::VerificationMethod,
room::message::{
AudioMessageEventContent, KeyVerificationRequestEventContent, MessageType,
RoomMessageEvent, RoomMessageEventContent, TextMessageEventContent,
room::{
message::{
AudioMessageEventContent, KeyVerificationRequestEventContent, MessageType,
RoomMessageEvent, RoomMessageEventContent, TextMessageEventContent,
},
MediaSource,
},
MessageLikeUnsigned,
},
@ -443,8 +446,7 @@ fn content_deserialization() {
msgtype: MessageType::Audio(AudioMessageEventContent {
body,
info: None,
url: Some(url),
file: None,
src: MediaSource::Plain(url),
..
}),
..