events: Add support for transitional extensible audio messages

According to MSC3246
This commit is contained in:
Kévin Commaille 2022-03-28 14:30:31 +02:00 committed by Kévin Commaille
parent 33108d22bc
commit f3abeed5c1
6 changed files with 347 additions and 9 deletions

View File

@ -13,9 +13,25 @@ mod waveform_serde;
use waveform_serde::WaveformSerDeHelper;
use super::{file::FileContent, message::MessageContent, room::message::Relation};
use super::{
file::FileContent,
message::MessageContent,
room::message::{AudioInfo, AudioMessageEventContent, Relation},
};
/// The payload for an extensible audio message.
///
/// This is the new primary type introduced in [MSC3246] and should not be sent before the end of
/// the transition period. See the documentation of the [`message`] module for more information.
///
/// `AudioEventContent` can be converted to a [`RoomMessageEventContent`] with a
/// [`MessageType::Audio`]. You can convert it back with
/// [`AudioEventContent::from_audio_room_message()`].
///
/// [MSC3246]: https://github.com/matrix-org/matrix-spec-proposals/pull/3246
/// [`message`]: super::message
/// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent
/// [`MessageType::Audio`]: super::room::message::MessageType::Audio
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.audio", kind = MessageLike)]
@ -52,6 +68,23 @@ impl AudioEventContent {
pub fn with_message(message: MessageContent, file: FileContent) -> Self {
Self { message, file, audio: Default::default(), relates_to: None }
}
/// Create a new `AudioEventContent` from the given `AudioMessageEventContent` and optional
/// relation.
pub fn from_audio_room_message(
content: AudioMessageEventContent,
relates_to: Option<Relation>,
) -> Self {
let AudioMessageEventContent { body, source, info, message, file, audio } = 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(), None)
});
let audio = audio.or_else(|| info.as_deref().map(Into::into)).unwrap_or_default();
Self { message, file, audio, relates_to }
}
}
/// Audio content.
@ -76,6 +109,18 @@ impl AudioContent {
pub fn new() -> Self {
Self::default()
}
/// Whether this `AudioContent` is empty.
pub fn is_empty(&self) -> bool {
self.duration.is_none() && self.waveform.is_none()
}
}
impl From<&AudioInfo> for AudioContent {
fn from(info: &AudioInfo) -> Self {
let AudioInfo { duration, .. } = info;
Self { duration: duration.to_owned(), ..Default::default() }
}
}
/// The waveform representation of audio content.

View File

@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
use super::{
message::MessageContent,
room::{
message::{FileInfo, FileMessageEventContent, Relation, VideoInfo},
message::{AudioInfo, FileInfo, FileMessageEventContent, Relation, VideoInfo},
EncryptedFile, ImageInfo, JsonWebKey, MediaSource,
},
};
@ -211,6 +211,13 @@ impl FileContentInfo {
}
}
impl From<&AudioInfo> for FileContentInfo {
fn from(info: &AudioInfo) -> Self {
let AudioInfo { mimetype, size, .. } = info;
Self { mimetype: mimetype.to_owned(), size: size.to_owned(), ..Default::default() }
}
}
impl From<&FileInfo> for FileContentInfo {
fn from(info: &FileInfo) -> Self {
let FileInfo { mimetype, size, .. } = info;

View File

@ -10,6 +10,8 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue;
use super::{EncryptedFile, ImageInfo, MediaSource, ThumbnailInfo};
#[cfg(feature = "unstable-msc3246")]
use crate::events::audio::{AudioContent, AudioEventContent};
#[cfg(feature = "unstable-msc3551")]
use crate::events::file::{FileContent, FileContentInfo, FileEventContent};
#[cfg(feature = "unstable-msc3552")]
@ -194,6 +196,20 @@ impl RoomMessageEventContent {
}
}
#[cfg(feature = "unstable-msc3246")]
impl From<AudioEventContent> for RoomMessageEventContent {
fn from(content: AudioEventContent) -> Self {
let AudioEventContent { message, file, audio, relates_to } = content;
Self {
msgtype: MessageType::Audio(AudioMessageEventContent::from_extensible_content(
message, file, audio,
)),
relates_to,
}
}
}
#[cfg(feature = "unstable-msc1767")]
impl From<EmoteEventContent> for RoomMessageEventContent {
fn from(content: EmoteEventContent) -> Self {
@ -518,6 +534,10 @@ impl Thread {
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.audio")]
#[cfg_attr(
feature = "unstable-msc3246",
serde(from = "content_serde::AudioMessageEventContentDeHelper")
)]
pub struct AudioMessageEventContent {
/// The textual representation of this message.
pub body: String,
@ -529,19 +549,81 @@ pub struct AudioMessageEventContent {
/// Metadata for the audio clip referred to in `url`.
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<AudioInfo>>,
/// Extensible-event text representation of the message.
///
/// If present, this should be preferred over the `body` field.
#[cfg(feature = "unstable-msc3246")]
#[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-msc3246")]
#[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")]
pub file: Option<FileContent>,
/// Extensible-event audio info of the message.
///
/// If present, this should be preferred over the `info` field.
#[cfg(feature = "unstable-msc3246")]
#[serde(rename = "org.matrix.msc1767.audio", skip_serializing_if = "Option::is_none")]
pub audio: Option<AudioContent>,
}
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, source: MediaSource::Plain(url), info }
Self {
#[cfg(feature = "unstable-msc3246")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3246")]
file: Some(FileContent::plain(
url.clone(),
info.as_deref().map(|info| Box::new(info.into())),
)),
#[cfg(feature = "unstable-msc3246")]
audio: Some(info.as_deref().map_or_else(AudioContent::default, Into::into)),
body,
source: 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, source: MediaSource::Encrypted(Box::new(file)), info: None }
Self {
#[cfg(feature = "unstable-msc3246")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3246")]
file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)),
#[cfg(feature = "unstable-msc3246")]
audio: Some(AudioContent::default()),
body,
source: MediaSource::Encrypted(Box::new(file)),
info: None,
}
}
/// Create a new `AudioMessageEventContent` with the given message, file info and audio info.
#[cfg(feature = "unstable-msc3246")]
pub fn from_extensible_content(
message: MessageContent,
file: FileContent,
audio: AudioContent,
) -> Self {
let body = if let Some(body) = message.find_plain() {
body.to_owned()
} else {
message[0].body.clone()
};
let source = (&file).into();
let info = AudioInfo::from_extensible_content(file.info.as_deref(), &audio).map(Box::new);
Self { message: Some(message), file: Some(file), audio: Some(audio), body, source, info }
}
}
@ -571,6 +653,24 @@ impl AudioInfo {
pub fn new() -> Self {
Self::default()
}
/// Create an `AudioInfo` from the given file info and audio info.
#[cfg(feature = "unstable-msc3246")]
pub fn from_extensible_content(
file_info: Option<&FileContentInfo>,
audio: &AudioContent,
) -> Option<Self> {
if file_info.is_none() && audio.is_empty() {
None
} else {
let (mimetype, size) = file_info
.map(|info| (info.mimetype.to_owned(), info.size.to_owned()))
.unwrap_or_default();
let AudioContent { duration, .. } = audio;
Some(Self { duration: duration.to_owned(), mimetype, size })
}
}
}
/// The payload for an emote message.

View File

@ -3,6 +3,8 @@
use serde::{de, Deserialize};
use serde_json::value::RawValue as RawJsonValue;
#[cfg(feature = "unstable-msc3246")]
use super::{AudioContent, AudioInfo, AudioMessageEventContent};
#[cfg(feature = "unstable-msc3551")]
use super::{FileContent, FileInfo, FileMessageEventContent, MediaSource, MessageContent};
#[cfg(feature = "unstable-msc3552")]
@ -57,6 +59,65 @@ impl<'de> Deserialize<'de> for MessageType {
}
}
/// Helper struct for deserializing `AudioMessageEventContent` 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-msc3246")]
pub struct AudioMessageEventContentDeHelper {
/// The textual representation of this message.
pub body: String,
/// The source of the audio clip.
#[serde(flatten)]
pub source: MediaSource,
/// Metadata for the audio clip referred to in `source`.
pub info: Option<Box<AudioInfo>>,
/// 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>,
/// Extensible-event audio info of the message, with stable name.
#[serde(rename = "m.audio")]
pub audio_stable: Option<AudioContent>,
/// Extensible-event audio info of the message, with unstable name.
#[serde(rename = "org.matrix.msc1767.audio")]
pub audio_unstable: Option<AudioContent>,
}
#[cfg(feature = "unstable-msc3246")]
impl From<AudioMessageEventContentDeHelper> for AudioMessageEventContent {
fn from(helper: AudioMessageEventContentDeHelper) -> Self {
let AudioMessageEventContentDeHelper {
body,
source,
info,
message,
file_stable,
file_unstable,
audio_stable,
audio_unstable,
} = helper;
let file = file_stable.or(file_unstable);
let audio = audio_stable.or(audio_unstable);
Self { body, source, info, message, file, audio }
}
}
/// Helper struct for deserializing `FileMessageEventContent` with stable and unstable field names.
///
/// It's not possible to use the `alias` attribute of serde because of

View File

@ -12,8 +12,10 @@ use ruma_common::{
file::{EncryptedContentInit, FileContent, FileContentInfo},
message::MessageContent,
room::{
message::{InReplyTo, Relation},
JsonWebKeyInit,
message::{
AudioMessageEventContent, InReplyTo, MessageType, Relation, RoomMessageEventContent,
},
JsonWebKeyInit, MediaSource,
},
AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned,
},
@ -208,7 +210,7 @@ fn event_serialization() {
#[test]
fn plain_content_deserialization() {
let json_data = json!({
"org.matrix.msc1767.text": "Upload: my_new_song.webm",
"m.text": "Upload: my_new_song.webm",
"m.file": {
"url": "mxc://notareal.hs/abcdef",
},
@ -289,7 +291,7 @@ fn plain_content_deserialization() {
#[test]
fn encrypted_content_deserialization() {
let json_data = json!({
"org.matrix.msc1767.text": "Upload: my_file.txt",
"m.text": "Upload: my_file.txt",
"m.file": {
"url": "mxc://notareal.hs/abcdef",
"key": {
@ -328,7 +330,7 @@ fn encrypted_content_deserialization() {
fn message_event_deserialization() {
let json_data = json!({
"content": {
"org.matrix.msc1767.text": "Upload: airplane_sound.opus",
"m.text": "Upload: airplane_sound.opus",
"m.file": {
"url": "mxc://notareal.hs/abcdef",
"name": "airplane_sound.opus",
@ -379,3 +381,87 @@ fn message_event_deserialization() {
&& unsigned.is_empty()
);
}
#[test]
fn room_message_serialization() {
let message_event_content =
RoomMessageEventContent::new(MessageType::Audio(AudioMessageEventContent::plain(
"Upload: my_song.mp3".to_owned(),
mxc_uri!("mxc://notareal.hs/file").to_owned(),
None,
)));
assert_eq!(
to_json_value(&message_event_content).unwrap(),
json!({
"body": "Upload: my_song.mp3",
"url": "mxc://notareal.hs/file",
"msgtype": "m.audio",
"org.matrix.msc1767.text": "Upload: my_song.mp3",
"org.matrix.msc1767.file": {
"url": "mxc://notareal.hs/file",
},
"org.matrix.msc1767.audio": {},
})
);
}
#[test]
fn room_message_stable_deserialization() {
let json_data = json!({
"body": "Upload: my_song.mp3",
"url": "mxc://notareal.hs/file",
"msgtype": "m.audio",
"m.text": "Upload: my_song.mp3",
"m.file": {
"url": "mxc://notareal.hs/file",
},
"m.audio": {},
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::Audio(_));
if let MessageType::Audio(content) = event_content.msgtype {
assert_eq!(content.body, "Upload: my_song.mp3");
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_song.mp3");
let file = content.file.unwrap();
assert_eq!(file.url, "mxc://notareal.hs/file");
assert!(!file.is_encrypted());
}
}
#[test]
fn room_message_unstable_deserialization() {
let json_data = json!({
"body": "Upload: my_song.mp3",
"url": "mxc://notareal.hs/file",
"msgtype": "m.audio",
"org.matrix.msc1767.text": "Upload: my_song.mp3",
"org.matrix.msc1767.file": {
"url": "mxc://notareal.hs/file",
},
"org.matrix.msc1767.audio": {},
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::Audio(_));
if let MessageType::Audio(content) = event_content.msgtype {
assert_eq!(content.body, "Upload: my_song.mp3");
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_song.mp3");
let file = content.file.unwrap();
assert_eq!(file.url, "mxc://notareal.hs/file");
assert!(!file.is_encrypted());
}
}

View File

@ -52,6 +52,7 @@ fn serialization() {
unsigned: MessageLikeUnsigned::default(),
};
#[cfg(not(feature = "unstable-msc3246"))]
assert_eq!(
to_json_value(ev).unwrap(),
json!({
@ -67,6 +68,28 @@ fn serialization() {
}
})
);
#[cfg(feature = "unstable-msc3246")]
assert_eq!(
to_json_value(ev).unwrap(),
json!({
"type": "m.room.message",
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 10_000,
"room_id": "!testroomid:example.org",
"sender": "@user:example.org",
"content": {
"body": "test",
"msgtype": "m.audio",
"url": "mxc://example.org/ffed755USFFxlgbQYZGtryd",
"org.matrix.msc1767.text": "test",
"org.matrix.msc1767.file": {
"url": "mxc://example.org/ffed755USFFxlgbQYZGtryd",
},
"org.matrix.msc1767.audio": {},
}
})
);
}
#[test]
@ -78,6 +101,7 @@ fn content_serialization() {
None,
)));
#[cfg(not(feature = "unstable-msc3246"))]
assert_eq!(
to_json_value(&message_event_content).unwrap(),
json!({
@ -86,6 +110,21 @@ fn content_serialization() {
"url": "mxc://example.org/ffed755USFFxlgbQYZGtryd"
})
);
#[cfg(feature = "unstable-msc3246")]
assert_eq!(
to_json_value(&message_event_content).unwrap(),
json!({
"body": "test",
"msgtype": "m.audio",
"url": "mxc://example.org/ffed755USFFxlgbQYZGtryd",
"org.matrix.msc1767.text": "test",
"org.matrix.msc1767.file": {
"url": "mxc://example.org/ffed755USFFxlgbQYZGtryd",
},
"org.matrix.msc1767.audio": {},
})
);
}
#[test]