events: Add support for transitional extensible file messages

According to MSC3551
This commit is contained in:
Kévin Commaille 2022-03-26 15:21:57 +01:00 committed by Kévin Commaille
parent 0e11996545
commit 8673d0d3f6
5 changed files with 499 additions and 9 deletions

View File

@ -10,11 +10,26 @@ use serde::{Deserialize, Serialize};
use super::{ use super::{
message::MessageContent, message::MessageContent,
room::{message::Relation, JsonWebKey}, room::{
message::{FileInfo, FileMessageEventContent, Relation},
EncryptedFile, JsonWebKey, MediaSource,
},
}; };
use crate::{serde::Base64, MxcUri}; use crate::{serde::Base64, MxcUri};
/// The payload for an extensible text message. /// The payload for an extensible file message.
///
/// This is the new primary type introduced in [MSC3551] and should not be sent before the end of
/// the transition period. See the documentation of the [`message`] module for more information.
///
/// `FileEventContent` can be converted to a [`RoomMessageEventContent`] with a
/// [`MessageType::File`]. You can convert it back with
/// [`FileEventContent::from_file_room_message()`].
///
/// [MSC3551]: https://github.com/matrix-org/matrix-spec-proposals/pull/3551
/// [`message`]: super::message
/// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent
/// [`MessageType::File`]: super::room::message::MessageType::File
#[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.file", kind = MessageLike)] #[ruma_event(type = "m.file", kind = MessageLike)]
@ -84,6 +99,22 @@ impl FileEventContent {
) -> Self { ) -> Self {
Self { message, file: FileContent::encrypted(url, encryption_info, info), relates_to: None } Self { message, file: FileContent::encrypted(url, encryption_info, info), relates_to: None }
} }
/// Create a new `FileEventContent` from the given `FileMessageEventContent` and optional
/// relation.
pub fn from_file_room_message(
content: FileMessageEventContent,
relates_to: Option<Relation>,
) -> Self {
let FileMessageEventContent { body, filename, source, info, message, file } = content;
let message = message.unwrap_or_else(|| MessageContent::plain(body));
let file = file.unwrap_or_else(|| {
FileContent::from_room_message_content(source, info.as_deref(), filename)
});
Self { message, file, relates_to }
}
} }
/// File content. /// File content.
@ -119,6 +150,21 @@ impl FileContent {
Self { url, info, encryption_info: Some(Box::new(encryption_info)) } Self { url, info, encryption_info: Some(Box::new(encryption_info)) }
} }
/// Create a new `FileContent` with the given media source, file info and filename.
pub fn from_room_message_content(
source: MediaSource,
info: Option<impl Into<FileContentInfo>>,
filename: Option<String>,
) -> Self {
let (url, encryption_info) = match source {
MediaSource::Plain(url) => (url, None),
MediaSource::Encrypted(file) => (file.url.to_owned(), Some(Box::new((&*file).into()))),
};
let info = FileContentInfo::from_room_message_content(info, filename).map(Box::new);
Self { url, encryption_info, info }
}
/// Whether the file is encrypted. /// Whether the file is encrypted.
pub fn is_encrypted(&self) -> bool { pub fn is_encrypted(&self) -> bool {
self.encryption_info.is_some() self.encryption_info.is_some()
@ -147,6 +193,29 @@ impl FileContentInfo {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
/// Create a new `FileContentInfo` with the given file info and filename.
///
/// Returns `None` if both parameters are `None`
pub fn from_room_message_content(
info: Option<impl Into<FileContentInfo>>,
filename: Option<String>,
) -> Option<Self> {
if filename.is_none() && info.is_none() {
None
} else {
let mut info: Self = info.map(Into::into).unwrap_or_default();
info.name = filename;
Some(info)
}
}
}
impl From<&FileInfo> for FileContentInfo {
fn from(info: &FileInfo) -> Self {
let FileInfo { mimetype, size, .. } = info;
Self { mimetype: mimetype.to_owned(), size: size.to_owned(), ..Default::default() }
}
} }
/// The encryption info of a file sent to a room with end-to-end encryption enabled. /// The encryption info of a file sent to a room with end-to-end encryption enabled.
@ -203,3 +272,10 @@ impl From<EncryptedContentInit> for EncryptedContent {
Self { key, iv, hashes, v } Self { key, iv, hashes, v }
} }
} }
impl From<&EncryptedFile> for EncryptedContent {
fn from(encrypted: &EncryptedFile) -> Self {
let EncryptedFile { key, iv, hashes, v, .. } = encrypted.to_owned();
Self { key, iv, hashes, v }
}
}

View File

@ -7,6 +7,8 @@ use std::collections::BTreeMap;
use js_int::UInt; use js_int::UInt;
use serde::{de, Deserialize, Serialize}; use serde::{de, Deserialize, Serialize};
#[cfg(feature = "unstable-msc3551")]
use super::file::{EncryptedContent, FileContent};
use crate::{ use crate::{
serde::{base64::UrlSafe, Base64}, serde::{base64::UrlSafe, Base64},
MxcUri, MxcUri,
@ -70,6 +72,18 @@ impl<'de> Deserialize<'de> for MediaSource {
} }
} }
#[cfg(feature = "unstable-msc3551")]
impl From<&FileContent> for MediaSource {
fn from(content: &FileContent) -> Self {
let FileContent { url, encryption_info, .. } = content;
if let Some(encryption_info) = encryption_info.as_deref() {
Self::Encrypted(Box::new(EncryptedFile::from_extensible_content(url, encryption_info)))
} else {
Self::Plain(url.to_owned())
}
}
}
/// Metadata about an image. /// Metadata about an image.
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
@ -173,6 +187,15 @@ pub struct EncryptedFile {
pub v: String, pub v: String,
} }
#[cfg(feature = "unstable-msc3551")]
impl EncryptedFile {
/// Create an `EncryptedFile` from the given url and encryption info.
pub fn from_extensible_content(url: &MxcUri, encryption_info: &EncryptedContent) -> Self {
let EncryptedContent { key, iv, hashes, v } = encryption_info.to_owned();
Self { url: url.to_owned(), key, iv, hashes, v }
}
}
/// Initial set of fields of `EncryptedFile`. /// Initial set of fields of `EncryptedFile`.
/// ///
/// This struct will not be updated even if additional fields are added to `EncryptedFile` in a new /// This struct will not be updated even if additional fields are added to `EncryptedFile` in a new

View File

@ -10,6 +10,8 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use super::{EncryptedFile, ImageInfo, MediaSource, ThumbnailInfo}; use super::{EncryptedFile, ImageInfo, MediaSource, ThumbnailInfo};
#[cfg(feature = "unstable-msc3551")]
use crate::events::file::{FileContent, FileContentInfo, FileEventContent};
#[cfg(feature = "unstable-msc1767")] #[cfg(feature = "unstable-msc1767")]
use crate::events::{ use crate::events::{
emote::EmoteEventContent, emote::EmoteEventContent,
@ -197,6 +199,20 @@ impl From<EmoteEventContent> for RoomMessageEventContent {
} }
} }
#[cfg(feature = "unstable-msc3551")]
impl From<FileEventContent> for RoomMessageEventContent {
fn from(content: FileEventContent) -> Self {
let FileEventContent { message, file, relates_to } = content;
Self {
msgtype: MessageType::File(FileMessageEventContent::from_extensible_content(
message, file,
)),
relates_to,
}
}
}
#[cfg(feature = "unstable-msc1767")] #[cfg(feature = "unstable-msc1767")]
impl From<MessageEventContent> for RoomMessageEventContent { impl From<MessageEventContent> for RoomMessageEventContent {
fn from(content: MessageEventContent) -> Self { fn from(content: MessageEventContent) -> Self {
@ -597,6 +613,10 @@ impl From<MessageContent> for EmoteMessageEventContent {
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.file")] #[serde(tag = "msgtype", rename = "m.file")]
#[cfg_attr(
feature = "unstable-msc3551",
serde(from = "content_serde::FileMessageEventContentDeHelper")
)]
pub struct FileMessageEventContent { pub struct FileMessageEventContent {
/// A human-readable description of the file. /// A human-readable description of the file.
/// ///
@ -614,19 +634,69 @@ pub struct FileMessageEventContent {
/// Metadata about the file referred to in `url`. /// Metadata about the file referred to in `url`.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<FileInfo>>, pub info: Option<Box<FileInfo>>,
/// Extensible-event text representation of the message.
///
/// If present, this should be preferred over the `body` field.
#[cfg(feature = "unstable-msc3551")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub message: Option<MessageContent>,
/// Extensible-event file content of the message.
///
/// If present, this should be preferred over the `source` and `info` fields.
#[cfg(feature = "unstable-msc3551")]
#[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")]
pub file: Option<FileContent>,
} }
impl FileMessageEventContent { impl FileMessageEventContent {
/// Creates a new non-encrypted `RoomFileMessageEventContent` with the given body, url and /// Creates a new non-encrypted `RoomFileMessageEventContent` with the given body, url and
/// optional extra info. /// optional extra info.
pub fn plain(body: String, url: Box<MxcUri>, info: Option<Box<FileInfo>>) -> Self { pub fn plain(body: String, url: Box<MxcUri>, info: Option<Box<FileInfo>>) -> Self {
Self { body, filename: None, source: MediaSource::Plain(url), info } Self {
#[cfg(feature = "unstable-msc3551")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3551")]
file: Some(FileContent::plain(
url.clone(),
info.as_deref().map(|info| Box::new(info.into())),
)),
body,
filename: None,
source: MediaSource::Plain(url),
info,
}
} }
/// Creates a new encrypted `RoomFileMessageEventContent` with the given body and encrypted /// Creates a new encrypted `RoomFileMessageEventContent` with the given body and encrypted
/// file. /// file.
pub fn encrypted(body: String, file: EncryptedFile) -> Self { pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self { body, filename: None, source: MediaSource::Encrypted(Box::new(file)), info: None } Self {
#[cfg(feature = "unstable-msc3551")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3551")]
file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)),
body,
filename: None,
source: MediaSource::Encrypted(Box::new(file)),
info: None,
}
}
/// Create a new `RoomFileMessageEventContent` with the given message and file info.
#[cfg(feature = "unstable-msc3551")]
pub fn from_extensible_content(message: MessageContent, file: FileContent) -> Self {
let body = if let Some(body) = message.find_plain() {
body.to_owned()
} else {
message[0].body.clone()
};
let filename = file.info.as_deref().and_then(|info| info.name.clone());
let info = file.info.as_deref().map(|info| Box::new(info.into()));
let source = (&file).into();
Self { message: Some(message), file: Some(file), body, filename, source, info }
} }
} }
@ -662,6 +732,14 @@ impl FileInfo {
} }
} }
#[cfg(feature = "unstable-msc3551")]
impl From<&FileContentInfo> for FileInfo {
fn from(info: &FileContentInfo) -> Self {
let FileContentInfo { mimetype, size, .. } = info;
Self { mimetype: mimetype.to_owned(), size: size.to_owned(), ..Default::default() }
}
}
/// The payload for an image message. /// The payload for an image message.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]

View File

@ -3,6 +3,8 @@
use serde::{de, Deserialize}; use serde::{de, Deserialize};
use serde_json::value::RawValue as RawJsonValue; use serde_json::value::RawValue as RawJsonValue;
#[cfg(feature = "unstable-msc3551")]
use super::{FileContent, FileInfo, FileMessageEventContent, MediaSource, MessageContent};
use super::{MessageType, Relation, RoomMessageEventContent}; use super::{MessageType, Relation, RoomMessageEventContent};
use crate::serde::from_raw_json_value; use crate::serde::from_raw_json_value;
@ -50,3 +52,55 @@ impl<'de> Deserialize<'de> for MessageType {
}) })
} }
} }
/// Helper struct for deserializing `FileMessageEventContent` with stable and unstable field names.
///
/// It's not possible to use the `alias` attribute of serde because of
/// https://github.com/serde-rs/serde/issues/1504.
#[derive(Clone, Debug, Deserialize)]
#[cfg(feature = "unstable-msc3551")]
pub struct FileMessageEventContentDeHelper {
/// A human-readable description of the file.
pub body: String,
/// The original filename of the uploaded file.
pub filename: Option<String>,
/// The source of the file.
#[serde(flatten)]
pub source: MediaSource,
/// Metadata about the file referred to in `url`.
pub info: Option<Box<FileInfo>>,
/// Extensible-event text representation of the message.
#[serde(flatten)]
pub message: Option<MessageContent>,
/// Extensible-event file content of the message, with stable name.
#[serde(rename = "m.file")]
pub file_stable: Option<FileContent>,
/// Extensible-event file content of the message, with unstable name.
#[serde(rename = "org.matrix.msc1767.file")]
pub file_unstable: Option<FileContent>,
}
#[cfg(feature = "unstable-msc3551")]
impl From<FileMessageEventContentDeHelper> for FileMessageEventContent {
fn from(helper: FileMessageEventContentDeHelper) -> Self {
let FileMessageEventContentDeHelper {
body,
filename,
source,
info,
message,
file_stable,
file_unstable,
} = helper;
let file = file_stable.or(file_unstable);
Self { body, filename, source, info, message, file }
}
}

View File

@ -9,8 +9,10 @@ use ruma_common::{
file::{EncryptedContentInit, FileContent, FileContentInfo, FileEventContent}, file::{EncryptedContentInit, FileContent, FileContentInfo, FileEventContent},
message::MessageContent, message::MessageContent,
room::{ room::{
message::{InReplyTo, Relation}, message::{
JsonWebKeyInit, FileMessageEventContent, InReplyTo, MessageType, Relation, RoomMessageEventContent,
},
EncryptedFileInit, JsonWebKeyInit, MediaSource,
}, },
AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned,
}, },
@ -152,7 +154,7 @@ fn file_event_serialization() {
#[test] #[test]
fn plain_content_deserialization() { fn plain_content_deserialization() {
let json_data = json!({ let json_data = json!({
"org.matrix.msc1767.text": "Upload: my_file.txt", "m.text": "Upload: my_file.txt",
"m.file": { "m.file": {
"url": "mxc://notareal.hs/abcdef", "url": "mxc://notareal.hs/abcdef",
} }
@ -171,7 +173,7 @@ fn plain_content_deserialization() {
#[test] #[test]
fn encrypted_content_deserialization() { fn encrypted_content_deserialization() {
let json_data = json!({ let json_data = json!({
"org.matrix.msc1767.text": "Upload: my_file.txt", "m.text": "Upload: my_file.txt",
"m.file": { "m.file": {
"url": "mxc://notareal.hs/abcdef", "url": "mxc://notareal.hs/abcdef",
"key": { "key": {
@ -204,7 +206,7 @@ fn encrypted_content_deserialization() {
fn message_event_deserialization() { fn message_event_deserialization() {
let json_data = json!({ let json_data = json!({
"content": { "content": {
"org.matrix.msc1767.message": [ "m.message": [
{ "body": "Upload: <strong>my_file.txt</strong>", "mimetype": "text/html"}, { "body": "Upload: <strong>my_file.txt</strong>", "mimetype": "text/html"},
{ "body": "Upload: my_file.txt", "mimetype": "text/plain"}, { "body": "Upload: my_file.txt", "mimetype": "text/plain"},
], ],
@ -252,3 +254,260 @@ fn message_event_deserialization() {
&& unsigned.is_empty() && unsigned.is_empty()
); );
} }
#[test]
fn room_message_plain_content_serialization() {
let message_event_content =
RoomMessageEventContent::new(MessageType::File(FileMessageEventContent::plain(
"Upload: my_file.txt".to_owned(),
mxc_uri!("mxc://notareal.hs/file").to_owned(),
None,
)));
assert_eq!(
to_json_value(&message_event_content).unwrap(),
json!({
"body": "Upload: my_file.txt",
"url": "mxc://notareal.hs/file",
"msgtype": "m.file",
"org.matrix.msc1767.text": "Upload: my_file.txt",
"org.matrix.msc1767.file": {
"url": "mxc://notareal.hs/file",
},
})
);
}
#[test]
fn room_message_encrypted_content_serialization() {
let message_event_content =
RoomMessageEventContent::new(MessageType::File(FileMessageEventContent::encrypted(
"Upload: my_file.txt".to_owned(),
EncryptedFileInit {
url: mxc_uri!("mxc://notareal.hs/file").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!(
to_json_value(&message_event_content).unwrap(),
json!({
"body": "Upload: my_file.txt",
"file": {
"url": "mxc://notareal.hs/file",
"key": {
"kty": "oct",
"key_ops": ["encrypt", "decrypt"],
"alg": "A256CTR",
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
"ext": true
},
"iv": "S22dq3NAX8wAAAAAAAAAAA",
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "v2",
},
"msgtype": "m.file",
"org.matrix.msc1767.text": "Upload: my_file.txt",
"org.matrix.msc1767.file": {
"url": "mxc://notareal.hs/file",
"key": {
"kty": "oct",
"key_ops": ["encrypt", "decrypt"],
"alg": "A256CTR",
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
"ext": true
},
"iv": "S22dq3NAX8wAAAAAAAAAAA",
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "v2",
},
})
);
}
#[test]
fn room_message_plain_content_stable_deserialization() {
let json_data = json!({
"body": "Upload: my_file.txt",
"url": "mxc://notareal.hs/file",
"msgtype": "m.file",
"m.text": "Upload: my_file.txt",
"m.file": {
"url": "mxc://notareal.hs/file",
},
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::File(_));
if let MessageType::File(content) = event_content.msgtype {
assert_eq!(content.body, "Upload: my_file.txt");
assert_matches!(content.source, MediaSource::Plain(_));
if let MediaSource::Plain(url) = content.source {
assert_eq!(url, "mxc://notareal.hs/file");
}
let message = content.message.unwrap();
assert_eq!(message.len(), 1);
assert_eq!(message[0].body, "Upload: my_file.txt");
let file = content.file.unwrap();
assert_eq!(file.url, "mxc://notareal.hs/file");
assert!(!file.is_encrypted());
}
}
#[test]
fn room_message_plain_content_unstable_deserialization() {
let json_data = json!({
"body": "Upload: my_file.txt",
"url": "mxc://notareal.hs/file",
"msgtype": "m.file",
"org.matrix.msc1767.text": "Upload: my_file.txt",
"org.matrix.msc1767.file": {
"url": "mxc://notareal.hs/file",
},
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::File(_));
if let MessageType::File(content) = event_content.msgtype {
assert_eq!(content.body, "Upload: my_file.txt");
assert_matches!(content.source, MediaSource::Plain(_));
if let MediaSource::Plain(url) = content.source {
assert_eq!(url, "mxc://notareal.hs/file");
}
let message = content.message.unwrap();
assert_eq!(message.len(), 1);
assert_eq!(message[0].body, "Upload: my_file.txt");
let file = content.file.unwrap();
assert_eq!(file.url, "mxc://notareal.hs/file");
assert!(!file.is_encrypted());
}
}
#[test]
fn room_message_encrypted_content_stable_deserialization() {
let json_data = json!({
"body": "Upload: my_file.txt",
"file": {
"url": "mxc://notareal.hs/file",
"key": {
"kty": "oct",
"key_ops": ["encrypt", "decrypt"],
"alg": "A256CTR",
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
"ext": true
},
"iv": "S22dq3NAX8wAAAAAAAAAAA",
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "v2",
},
"msgtype": "m.file",
"m.text": "Upload: my_file.txt",
"m.file": {
"url": "mxc://notareal.hs/file",
"key": {
"kty": "oct",
"key_ops": ["encrypt", "decrypt"],
"alg": "A256CTR",
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
"ext": true
},
"iv": "S22dq3NAX8wAAAAAAAAAAA",
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "v2",
},
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::File(_));
if let MessageType::File(content) = event_content.msgtype {
assert_eq!(content.body, "Upload: my_file.txt");
assert_matches!(content.source, MediaSource::Encrypted(_));
if let MediaSource::Encrypted(encrypted_file) = content.source {
assert_eq!(encrypted_file.url, "mxc://notareal.hs/file");
}
let message = content.message.unwrap();
assert_eq!(message.len(), 1);
assert_eq!(message[0].body, "Upload: my_file.txt");
let file = content.file.unwrap();
assert_eq!(file.url, "mxc://notareal.hs/file");
assert!(file.is_encrypted());
}
}
#[test]
fn room_message_encrypted_content_unstable_deserialization() {
let json_data = json!({
"body": "Upload: my_file.txt",
"file": {
"url": "mxc://notareal.hs/file",
"key": {
"kty": "oct",
"key_ops": ["encrypt", "decrypt"],
"alg": "A256CTR",
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
"ext": true
},
"iv": "S22dq3NAX8wAAAAAAAAAAA",
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "v2",
},
"msgtype": "m.file",
"org.matrix.msc1767.text": "Upload: my_file.txt",
"org.matrix.msc1767.file": {
"url": "mxc://notareal.hs/file",
"key": {
"kty": "oct",
"key_ops": ["encrypt", "decrypt"],
"alg": "A256CTR",
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
"ext": true
},
"iv": "S22dq3NAX8wAAAAAAAAAAA",
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "v2",
},
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::File(_));
if let MessageType::File(content) = event_content.msgtype {
assert_eq!(content.body, "Upload: my_file.txt");
assert_matches!(content.source, MediaSource::Encrypted(_));
if let MediaSource::Encrypted(encrypted_file) = content.source {
assert_eq!(encrypted_file.url, "mxc://notareal.hs/file");
}
let message = content.message.unwrap();
assert_eq!(message.len(), 1);
assert_eq!(message[0].body, "Upload: my_file.txt");
let file = content.file.unwrap();
assert_eq!(file.url, "mxc://notareal.hs/file");
assert!(file.is_encrypted());
}
}