events: Add support for transitional extensible voice messages

According to MSC3245
This commit is contained in:
Kévin Commaille 2022-03-26 16:00:10 +01:00 committed by Kévin Commaille
parent f3abeed5c1
commit c6d11c78a7
5 changed files with 232 additions and 8 deletions

View File

@ -75,7 +75,18 @@ impl AudioEventContent {
content: AudioMessageEventContent, content: AudioMessageEventContent,
relates_to: Option<Relation>, relates_to: Option<Relation>,
) -> Self { ) -> 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 message = message.unwrap_or_else(|| MessageContent::plain(body));
let file = file.unwrap_or_else(|| { let file = file.unwrap_or_else(|| {

View File

@ -18,6 +18,8 @@ use crate::events::file::{FileContent, FileContentInfo, FileEventContent};
use crate::events::image::{ImageContent, ImageEventContent, ThumbnailContent}; use crate::events::image::{ImageContent, ImageEventContent, ThumbnailContent};
#[cfg(feature = "unstable-msc3553")] #[cfg(feature = "unstable-msc3553")]
use crate::events::video::{VideoContent, VideoEventContent}; use crate::events::video::{VideoContent, VideoEventContent};
#[cfg(feature = "unstable-msc3245")]
use crate::events::voice::{VoiceContent, VoiceEventContent};
#[cfg(feature = "unstable-msc1767")] #[cfg(feature = "unstable-msc1767")]
use crate::events::{ use crate::events::{
emote::EmoteEventContent, emote::EmoteEventContent,
@ -279,6 +281,20 @@ impl From<VideoEventContent> for RoomMessageEventContent {
} }
} }
#[cfg(feature = "unstable-msc3245")]
impl From<VoiceEventContent> 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. /// The content that is specific to each message type variant.
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(untagged)] #[serde(untagged)]
@ -570,6 +586,13 @@ pub struct AudioMessageEventContent {
#[cfg(feature = "unstable-msc3246")] #[cfg(feature = "unstable-msc3246")]
#[serde(rename = "org.matrix.msc1767.audio", skip_serializing_if = "Option::is_none")] #[serde(rename = "org.matrix.msc1767.audio", skip_serializing_if = "Option::is_none")]
pub audio: Option<AudioContent>, pub audio: Option<AudioContent>,
/// 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<VoiceContent>,
} }
impl AudioMessageEventContent { impl AudioMessageEventContent {
@ -586,6 +609,8 @@ impl AudioMessageEventContent {
)), )),
#[cfg(feature = "unstable-msc3246")] #[cfg(feature = "unstable-msc3246")]
audio: Some(info.as_deref().map_or_else(AudioContent::default, Into::into)), audio: Some(info.as_deref().map_or_else(AudioContent::default, Into::into)),
#[cfg(feature = "unstable-msc3245")]
voice: None,
body, body,
source: MediaSource::Plain(url), source: MediaSource::Plain(url),
info, info,
@ -602,6 +627,8 @@ impl AudioMessageEventContent {
file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)), file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)),
#[cfg(feature = "unstable-msc3246")] #[cfg(feature = "unstable-msc3246")]
audio: Some(AudioContent::default()), audio: Some(AudioContent::default()),
#[cfg(feature = "unstable-msc3245")]
voice: None,
body, body,
source: MediaSource::Encrypted(Box::new(file)), source: MediaSource::Encrypted(Box::new(file)),
info: None, info: None,
@ -623,7 +650,30 @@ impl AudioMessageEventContent {
let source = (&file).into(); let source = (&file).into();
let info = AudioInfo::from_extensible_content(file.info.as_deref(), &audio).map(Box::new); 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
} }
} }

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-msc3245")]
use super::VoiceContent;
#[cfg(feature = "unstable-msc3246")] #[cfg(feature = "unstable-msc3246")]
use super::{AudioContent, AudioInfo, AudioMessageEventContent}; use super::{AudioContent, AudioInfo, AudioMessageEventContent};
#[cfg(feature = "unstable-msc3551")] #[cfg(feature = "unstable-msc3551")]
@ -95,6 +97,16 @@ pub struct AudioMessageEventContentDeHelper {
/// Extensible-event audio info of the message, with unstable name. /// Extensible-event audio info of the message, with unstable name.
#[serde(rename = "org.matrix.msc1767.audio")] #[serde(rename = "org.matrix.msc1767.audio")]
pub audio_unstable: Option<AudioContent>, pub audio_unstable: Option<AudioContent>,
/// Extensible-event voice flag of the message, with stable name.
#[cfg(feature = "unstable-msc3245")]
#[serde(rename = "m.voice")]
pub voice_stable: Option<VoiceContent>,
/// Extensible-event voice flag of the message, with unstable name.
#[cfg(feature = "unstable-msc3245")]
#[serde(rename = "org.matrix.msc3245.voice")]
pub voice_unstable: Option<VoiceContent>,
} }
#[cfg(feature = "unstable-msc3246")] #[cfg(feature = "unstable-msc3246")]
@ -109,12 +121,27 @@ impl From<AudioMessageEventContentDeHelper> for AudioMessageEventContent {
file_unstable, file_unstable,
audio_stable, audio_stable,
audio_unstable, audio_unstable,
#[cfg(feature = "unstable-msc3245")]
voice_stable,
#[cfg(feature = "unstable-msc3245")]
voice_unstable,
} = helper; } = helper;
let file = file_stable.or(file_unstable); let file = file_stable.or(file_unstable);
let audio = audio_stable.or(audio_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,
}
} }
} }

View File

@ -6,10 +6,25 @@ use ruma_macros::EventContent;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{ 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)] #[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.voice", kind = MessageLike)] #[ruma_event(type = "m.voice", kind = MessageLike)]
@ -57,6 +72,30 @@ impl VoiceEventContent {
relates_to: None, 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<Relation>,
) -> Result<Self, TryFromExtensibleError> {
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. /// Voice content.

View File

@ -10,8 +10,13 @@ use ruma_common::{
events::{ events::{
audio::AudioContent, audio::AudioContent,
file::{FileContent, FileContentInfo}, file::{FileContent, FileContentInfo},
room::message::{InReplyTo, Relation}, room::{
voice::VoiceEventContent, message::{
AudioMessageEventContent, InReplyTo, MessageType, Relation, RoomMessageEventContent,
},
MediaSource,
},
voice::{VoiceContent, VoiceEventContent},
AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned,
}, },
mxc_uri, room_id, user_id, MilliSecondsSinceUnixEpoch, mxc_uri, room_id, user_id, MilliSecondsSinceUnixEpoch,
@ -89,7 +94,7 @@ fn event_serialization() {
fn message_event_deserialization() { fn message_event_deserialization() {
let json_data = json!({ let json_data = json!({
"content": { "content": {
"org.matrix.msc1767.text": "Voice message", "m.text": "Voice message",
"m.file": { "m.file": {
"url": "mxc://notareal.hs/abcdef", "url": "mxc://notareal.hs/abcdef",
"name": "voice_message.ogg", "name": "voice_message.ogg",
@ -141,3 +146,95 @@ fn message_event_deserialization() {
&& unsigned.is_empty() && 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::<RoomMessageEventContent>(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::<RoomMessageEventContent>(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());
}
}