From c6d11c78a7dceb64eb1df21459d4de8ef6a2300d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sat, 26 Mar 2022 16:00:10 +0100 Subject: [PATCH] events: Add support for transitional extensible voice messages According to MSC3245 --- crates/ruma-common/src/events/audio.rs | 13 ++- crates/ruma-common/src/events/room/message.rs | 52 ++++++++- .../src/events/room/message/content_serde.rs | 29 ++++- crates/ruma-common/src/events/voice.rs | 43 +++++++- crates/ruma-common/tests/events/voice.rs | 103 +++++++++++++++++- 5 files changed, 232 insertions(+), 8 deletions(-) diff --git a/crates/ruma-common/src/events/audio.rs b/crates/ruma-common/src/events/audio.rs index eb46b626..76d3c1a3 100644 --- a/crates/ruma-common/src/events/audio.rs +++ b/crates/ruma-common/src/events/audio.rs @@ -75,7 +75,18 @@ impl AudioEventContent { content: AudioMessageEventContent, relates_to: Option, ) -> Self { - let AudioMessageEventContent { body, source, info, message, file, audio } = content; + // Otherwise the `voice` field has an extra indentation + #[rustfmt::skip] + let AudioMessageEventContent { + body, + source, + info, + message, + file, + audio, + #[cfg(feature = "unstable-msc3245")] + voice: _, + } = content; let message = message.unwrap_or_else(|| MessageContent::plain(body)); let file = file.unwrap_or_else(|| { diff --git a/crates/ruma-common/src/events/room/message.rs b/crates/ruma-common/src/events/room/message.rs index 92434d2a..13889068 100644 --- a/crates/ruma-common/src/events/room/message.rs +++ b/crates/ruma-common/src/events/room/message.rs @@ -18,6 +18,8 @@ use crate::events::file::{FileContent, FileContentInfo, FileEventContent}; use crate::events::image::{ImageContent, ImageEventContent, ThumbnailContent}; #[cfg(feature = "unstable-msc3553")] use crate::events::video::{VideoContent, VideoEventContent}; +#[cfg(feature = "unstable-msc3245")] +use crate::events::voice::{VoiceContent, VoiceEventContent}; #[cfg(feature = "unstable-msc1767")] use crate::events::{ emote::EmoteEventContent, @@ -279,6 +281,20 @@ impl From for RoomMessageEventContent { } } +#[cfg(feature = "unstable-msc3245")] +impl From for RoomMessageEventContent { + fn from(content: VoiceEventContent) -> Self { + let VoiceEventContent { message, file, audio, voice, relates_to } = content; + + Self { + msgtype: MessageType::Audio(AudioMessageEventContent::from_extensible_voice_content( + message, file, audio, voice, + )), + relates_to, + } + } +} + /// The content that is specific to each message type variant. #[derive(Clone, Debug, Serialize)] #[serde(untagged)] @@ -570,6 +586,13 @@ pub struct AudioMessageEventContent { #[cfg(feature = "unstable-msc3246")] #[serde(rename = "org.matrix.msc1767.audio", skip_serializing_if = "Option::is_none")] pub audio: Option, + + /// Extensible-event voice flag of the message. + /// + /// If present, this should be represented as a voice message. + #[cfg(feature = "unstable-msc3245")] + #[serde(rename = "org.matrix.msc3245.voice", skip_serializing_if = "Option::is_none")] + pub voice: Option, } impl AudioMessageEventContent { @@ -586,6 +609,8 @@ impl AudioMessageEventContent { )), #[cfg(feature = "unstable-msc3246")] audio: Some(info.as_deref().map_or_else(AudioContent::default, Into::into)), + #[cfg(feature = "unstable-msc3245")] + voice: None, body, source: MediaSource::Plain(url), info, @@ -602,6 +627,8 @@ impl AudioMessageEventContent { file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)), #[cfg(feature = "unstable-msc3246")] audio: Some(AudioContent::default()), + #[cfg(feature = "unstable-msc3245")] + voice: None, body, source: MediaSource::Encrypted(Box::new(file)), info: None, @@ -623,7 +650,30 @@ impl AudioMessageEventContent { 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 } + Self { + message: Some(message), + file: Some(file), + audio: Some(audio), + #[cfg(feature = "unstable-msc3245")] + voice: None, + body, + source, + info, + } + } + + /// Create a new `AudioMessageEventContent` with the given message, file info, audio info and + /// voice flag. + #[cfg(feature = "unstable-msc3245")] + pub fn from_extensible_voice_content( + message: MessageContent, + ext_file: FileContent, + audio: AudioContent, + voice: VoiceContent, + ) -> Self { + let mut content = Self::from_extensible_content(message, ext_file, audio); + content.voice = Some(voice); + content } } diff --git a/crates/ruma-common/src/events/room/message/content_serde.rs b/crates/ruma-common/src/events/room/message/content_serde.rs index db491dfe..e07949c2 100644 --- a/crates/ruma-common/src/events/room/message/content_serde.rs +++ b/crates/ruma-common/src/events/room/message/content_serde.rs @@ -3,6 +3,8 @@ use serde::{de, Deserialize}; use serde_json::value::RawValue as RawJsonValue; +#[cfg(feature = "unstable-msc3245")] +use super::VoiceContent; #[cfg(feature = "unstable-msc3246")] use super::{AudioContent, AudioInfo, AudioMessageEventContent}; #[cfg(feature = "unstable-msc3551")] @@ -95,6 +97,16 @@ pub struct AudioMessageEventContentDeHelper { /// Extensible-event audio info of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.audio")] pub audio_unstable: Option, + + /// Extensible-event voice flag of the message, with stable name. + #[cfg(feature = "unstable-msc3245")] + #[serde(rename = "m.voice")] + pub voice_stable: Option, + + /// Extensible-event voice flag of the message, with unstable name. + #[cfg(feature = "unstable-msc3245")] + #[serde(rename = "org.matrix.msc3245.voice")] + pub voice_unstable: Option, } #[cfg(feature = "unstable-msc3246")] @@ -109,12 +121,27 @@ impl From for AudioMessageEventContent { file_unstable, audio_stable, audio_unstable, + #[cfg(feature = "unstable-msc3245")] + voice_stable, + #[cfg(feature = "unstable-msc3245")] + voice_unstable, } = helper; let file = file_stable.or(file_unstable); let audio = audio_stable.or(audio_unstable); + #[cfg(feature = "unstable-msc3245")] + let voice = voice_stable.or(voice_unstable); - Self { body, source, info, message, file, audio } + Self { + body, + source, + info, + message, + file, + audio, + #[cfg(feature = "unstable-msc3245")] + voice, + } } } diff --git a/crates/ruma-common/src/events/voice.rs b/crates/ruma-common/src/events/voice.rs index 7196cda3..7410eefa 100644 --- a/crates/ruma-common/src/events/voice.rs +++ b/crates/ruma-common/src/events/voice.rs @@ -6,10 +6,25 @@ use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::{ - audio::AudioContent, file::FileContent, message::MessageContent, room::message::Relation, + audio::AudioContent, + file::FileContent, + message::{MessageContent, TryFromExtensibleError}, + room::message::{AudioMessageEventContent, Relation}, }; -/// The payload for an extensible audio message. +/// The payload for an extensible voice message. +/// +/// This is the new primary type introduced in [MSC3245] and should not be sent before the end of +/// the transition period. See the documentation of the [`message`] module for more information. +/// +/// `VoiceEventContent` can be converted to a [`RoomMessageEventContent`] with a +/// [`MessageType::Audio`] with the `m.voice` flag. You can convert it back with +/// [`VoiceEventContent::try_from_audio_room_message()`]. +/// +/// [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245 +/// [`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.voice", kind = MessageLike)] @@ -57,6 +72,30 @@ impl VoiceEventContent { relates_to: None, } } + + /// Create a new `VoiceEventContent` from the given `AudioMessageEventContent` and optional + /// relation. + /// + /// This can fail if the `AudioMessageEventContent` is not a voice message. + pub fn try_from_audio_room_message( + content: AudioMessageEventContent, + relates_to: Option, + ) -> Result { + let AudioMessageEventContent { body, source, info, message, file, audio, voice } = 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(); + let voice = if let Some(voice) = voice { + voice + } else { + return Err(TryFromExtensibleError::MissingField("m.voice".to_owned())); + }; + + Ok(Self { message, file, audio, voice, relates_to }) + } } /// Voice content. diff --git a/crates/ruma-common/tests/events/voice.rs b/crates/ruma-common/tests/events/voice.rs index 4f2c4de1..a92697f5 100644 --- a/crates/ruma-common/tests/events/voice.rs +++ b/crates/ruma-common/tests/events/voice.rs @@ -10,8 +10,13 @@ use ruma_common::{ events::{ audio::AudioContent, file::{FileContent, FileContentInfo}, - room::message::{InReplyTo, Relation}, - voice::VoiceEventContent, + room::{ + message::{ + AudioMessageEventContent, InReplyTo, MessageType, Relation, RoomMessageEventContent, + }, + MediaSource, + }, + voice::{VoiceContent, VoiceEventContent}, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, }, mxc_uri, room_id, user_id, MilliSecondsSinceUnixEpoch, @@ -89,7 +94,7 @@ fn event_serialization() { fn message_event_deserialization() { let json_data = json!({ "content": { - "org.matrix.msc1767.text": "Voice message", + "m.text": "Voice message", "m.file": { "url": "mxc://notareal.hs/abcdef", "name": "voice_message.ogg", @@ -141,3 +146,95 @@ fn message_event_deserialization() { && unsigned.is_empty() ); } + +#[test] +fn room_message_serialization() { + let message_event_content = RoomMessageEventContent::new(MessageType::Audio(assign!( + AudioMessageEventContent::plain( + "Upload: voice_message.ogg".to_owned(), + mxc_uri!("mxc://notareal.hs/file").to_owned(), + None, + ), { + voice: Some(VoiceContent::new()), + } + ))); + + assert_eq!( + to_json_value(&message_event_content).unwrap(), + json!({ + "body": "Upload: voice_message.ogg", + "url": "mxc://notareal.hs/file", + "msgtype": "m.audio", + "org.matrix.msc1767.text": "Upload: voice_message.ogg", + "org.matrix.msc1767.file": { + "url": "mxc://notareal.hs/file", + }, + "org.matrix.msc1767.audio": {}, + "org.matrix.msc3245.voice": {}, + }) + ); +} + +#[test] +fn room_message_stable_deserialization() { + let json_data = json!({ + "body": "Upload: voice_message.ogg", + "url": "mxc://notareal.hs/file", + "msgtype": "m.audio", + "m.text": "Upload: voice_message.ogg", + "m.file": { + "url": "mxc://notareal.hs/file", + }, + "m.audio": {}, + "m.voice": {}, + }); + + let event_content = from_json_value::(json_data).unwrap(); + assert_matches!(event_content.msgtype, MessageType::Audio(_)); + if let MessageType::Audio(content) = event_content.msgtype { + assert_eq!(content.body, "Upload: voice_message.ogg"); + 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: voice_message.ogg"); + let file = content.file.unwrap(); + assert_eq!(file.url, "mxc://notareal.hs/file"); + assert!(!file.is_encrypted()); + assert!(content.voice.is_some()); + } +} + +#[test] +fn room_message_unstable_deserialization() { + let json_data = json!({ + "body": "Upload: voice_message.ogg", + "url": "mxc://notareal.hs/file", + "msgtype": "m.audio", + "org.matrix.msc1767.text": "Upload: voice_message.ogg", + "org.matrix.msc1767.file": { + "url": "mxc://notareal.hs/file", + }, + "org.matrix.msc1767.audio": {}, + "org.matrix.msc3245.voice": {}, + }); + + let event_content = from_json_value::(json_data).unwrap(); + assert_matches!(event_content.msgtype, MessageType::Audio(_)); + if let MessageType::Audio(content) = event_content.msgtype { + assert_eq!(content.body, "Upload: voice_message.ogg"); + 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: voice_message.ogg"); + let file = content.file.unwrap(); + assert_eq!(file.url, "mxc://notareal.hs/file"); + assert!(!file.is_encrypted()); + assert!(content.voice.is_some()); + } +}