events: Update types according to MSC3927 and changes in MSC3246

This commit is contained in:
Kévin Commaille 2023-01-05 12:24:59 +01:00 committed by Kévin Commaille
parent 60f754322e
commit 41be6ac18b
9 changed files with 169 additions and 245 deletions

View File

@ -35,13 +35,14 @@ unstable-msc2677 = []
unstable-msc2746 = []
unstable-msc2870 = []
unstable-msc3245 = ["unstable-msc3246"]
unstable-msc3246 = ["unstable-msc3551"]
unstable-msc3246 = ["unstable-msc3927"]
unstable-msc3381 = ["unstable-msc1767"]
unstable-msc3488 = ["unstable-msc1767"]
unstable-msc3551 = ["unstable-msc3956"]
unstable-msc3552 = ["unstable-msc3551"]
unstable-msc3553 = ["unstable-msc3552"]
unstable-msc3554 = ["unstable-msc1767"]
unstable-msc3927 = ["unstable-msc3551"]
unstable-msc3931 = []
unstable-msc3932 = ["unstable-msc3931"]
unstable-msc3954 = ["unstable-msc1767"]

View File

@ -120,7 +120,7 @@ pub mod macros {
pub use ruma_macros::{Event, EventContent};
}
#[cfg(feature = "unstable-msc3246")]
#[cfg(feature = "unstable-msc3927")]
pub mod audio;
pub mod call;
pub mod direct;

View File

@ -1,6 +1,6 @@
//! Types for extensible audio message events ([MSC3246]).
//! Types for extensible audio message events ([MSC3927]).
//!
//! [MSC3246]: https://github.com/matrix-org/matrix-spec-proposals/pull/3246
//! [MSC3927]: https://github.com/matrix-org/matrix-spec-proposals/pull/3927
use std::time::Duration;
@ -8,23 +8,25 @@ use js_int::UInt;
use ruma_macros::EventContent;
use serde::{Deserialize, Serialize};
#[cfg(feature = "unstable-msc3246")]
mod amplitude_serde;
mod waveform_serde;
use waveform_serde::WaveformSerDeHelper;
use super::{file::FileContentBlock, message::TextContentBlock, room::message::Relation};
use super::{
file::{CaptionContentBlock, FileContentBlock},
message::TextContentBlock,
room::message::Relation,
};
/// The payload for an extensible audio message.
///
/// This is the new primary type introduced in [MSC3246] and should only be sent in rooms with a
/// This is the new primary type introduced in [MSC3927] and should only be sent in rooms with a
/// version that supports it. See the documentation of the [`message`] module for more information.
///
/// [MSC3246]: https://github.com/matrix-org/matrix-spec-proposals/pull/3246
/// [MSC3927]: https://github.com/matrix-org/matrix-spec-proposals/pull/3927
/// [`message`]: super::message
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.audio", kind = MessageLike, without_relation)]
#[ruma_event(type = "org.matrix.msc1767.audio", kind = MessageLike, without_relation)]
pub struct AudioEventContent {
/// The text representations of the message.
#[serde(rename = "org.matrix.msc1767.text")]
@ -34,9 +36,22 @@ pub struct AudioEventContent {
#[serde(rename = "org.matrix.msc1767.file")]
pub file: FileContentBlock,
/// The audio content of the message.
#[serde(rename = "m.audio")]
pub audio: AudioContent,
/// The audio details of the message, if any.
#[serde(rename = "org.matrix.msc1767.audio_details", skip_serializing_if = "Option::is_none")]
pub audio_details: Option<AudioDetailsContentBlock>,
/// The caption of the message, if any.
#[serde(rename = "org.matrix.msc1767.caption", skip_serializing_if = "Option::is_none")]
pub caption: Option<CaptionContentBlock>,
/// Whether this message is automated.
#[cfg(feature = "unstable-msc3955")]
#[serde(
default,
skip_serializing_if = "crate::serde::is_default",
rename = "org.matrix.msc1767.automated"
)]
pub automated: bool,
/// Information about related messages.
#[serde(
@ -50,118 +65,77 @@ pub struct AudioEventContent {
impl AudioEventContent {
/// Creates a new `AudioEventContent` with the given text fallback and file.
pub fn new(text: TextContentBlock, file: FileContentBlock) -> Self {
Self { text, file, audio: Default::default(), relates_to: None }
Self {
text,
file,
audio_details: None,
caption: None,
#[cfg(feature = "unstable-msc3955")]
automated: false,
relates_to: None,
}
}
/// Creates a new `AudioEventContent` with the given plain text fallback representation and
/// file.
pub fn plain(text_fallback: impl Into<String>, file: FileContentBlock) -> Self {
pub fn with_plain_text(plain_text: impl Into<String>, file: FileContentBlock) -> Self {
Self {
text: TextContentBlock::plain(text_fallback),
text: TextContentBlock::plain(plain_text),
file,
audio: Default::default(),
audio_details: None,
caption: None,
#[cfg(feature = "unstable-msc3955")]
automated: false,
relates_to: None,
}
}
}
/// Audio content.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
/// A block for details of audio content.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct AudioContent {
/// The duration of the video in milliseconds.
pub struct AudioDetailsContentBlock {
/// The duration of the audio in seconds.
#[serde(with = "ruma_common::serde::duration::secs")]
pub duration: Duration,
/// The waveform representation of the audio content, if any.
///
/// This is optional and defaults to an empty array.
#[cfg(feature = "unstable-msc3246")]
#[serde(
with = "ruma_common::serde::duration::opt_ms",
rename = "org.matrix.msc3246.waveform",
default,
skip_serializing_if = "Option::is_none"
skip_serializing_if = "Vec::is_empty"
)]
pub duration: Option<Duration>,
/// The waveform representation of the audio content.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub waveform: Option<Waveform>,
pub waveform: Vec<Amplitude>,
}
impl AudioContent {
/// Creates a new empty `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()
}
}
/// The waveform representation of audio content.
///
/// Must include between 30 and 120 `Amplitude`s.
///
/// To build this, use the `TryFrom` implementations.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(try_from = "WaveformSerDeHelper")]
pub struct Waveform(Vec<Amplitude>);
impl Waveform {
/// The smallest number of values contained in a `Waveform`.
pub const MIN_LENGTH: usize = 30;
/// The largest number of values contained in a `Waveform`.
pub const MAX_LENGTH: usize = 120;
/// The amplitudes of this `Waveform`.
pub fn amplitudes(&self) -> &[Amplitude] {
&self.0
}
}
/// An error encountered when trying to convert to a `Waveform`.
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum WaveformError {
/// There are more than [`Waveform::MAX_LENGTH`] values.
#[error("too many values")]
TooManyValues,
/// There are less that [`Waveform::MIN_LENGTH`] values.
#[error("not enough values")]
NotEnoughValues,
}
impl TryFrom<Vec<Amplitude>> for Waveform {
type Error = WaveformError;
fn try_from(value: Vec<Amplitude>) -> Result<Self, Self::Error> {
if value.len() < Self::MIN_LENGTH {
Err(WaveformError::NotEnoughValues)
} else if value.len() > Self::MAX_LENGTH {
Err(WaveformError::TooManyValues)
} else {
Ok(Self(value))
impl AudioDetailsContentBlock {
/// Creates a new `AudioDetailsContentBlock` with the given duration.
pub fn new(duration: Duration) -> Self {
Self {
duration,
#[cfg(feature = "unstable-msc3246")]
waveform: Default::default(),
}
}
}
impl TryFrom<&[Amplitude]> for Waveform {
type Error = WaveformError;
fn try_from(value: &[Amplitude]) -> Result<Self, Self::Error> {
Self::try_from(value.to_owned())
}
}
/// The amplitude of a waveform sample.
///
/// Must be an integer between 0 and 1024.
/// Must be an integer between 0 and 256.
#[cfg(feature = "unstable-msc3246")]
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub struct Amplitude(UInt);
#[cfg(feature = "unstable-msc3246")]
impl Amplitude {
/// The smallest value that can be represented by this type, 0.
pub const MIN: u16 = 0;
/// The largest value that can be represented by this type, 1024.
pub const MAX: u16 = 1024;
/// The largest value that can be represented by this type, 256.
pub const MAX: u16 = 256;
/// Creates a new `Amplitude` with the given value.
///
@ -176,6 +150,7 @@ impl Amplitude {
}
}
#[cfg(feature = "unstable-msc3246")]
impl From<u16> for Amplitude {
fn from(value: u16) -> Self {
Self::new(value)

View File

@ -1,16 +0,0 @@
//! `Serialize` and `Deserialize` implementations for extensible events (MSC1767).
use serde::Deserialize;
use super::{Amplitude, Waveform, WaveformError};
#[derive(Debug, Default, Deserialize)]
pub(crate) struct WaveformSerDeHelper(Vec<Amplitude>);
impl TryFrom<WaveformSerDeHelper> for Waveform {
type Error = WaveformError;
fn try_from(helper: WaveformSerDeHelper) -> Result<Self, Self::Error> {
Waveform::try_from(helper.0)
}
}

View File

@ -33,8 +33,9 @@ event_enum! {
/// Any message-like event.
enum MessageLike {
#[cfg(feature = "unstable-msc3246")]
"m.audio" => super::audio,
#[cfg(feature = "unstable-msc3927")]
#[ruma_enum(alias = "m.audio")]
"org.matrix.msc1767.audio" => super::audio,
"m.call.answer" => super::call::answer,
"m.call.invite" => super::call::invite,
"m.call.hangup" => super::call::hangup,

View File

@ -6,7 +6,8 @@ use ruma_macros::EventContent;
use serde::{Deserialize, Serialize};
use super::{
audio::AudioContent, file::FileContentBlock, message::TextContentBlock, room::message::Relation,
audio::AudioDetailsContentBlock, file::FileContentBlock, message::TextContentBlock,
room::message::Relation,
};
/// The payload for an extensible voice message.
@ -30,8 +31,8 @@ pub struct VoiceEventContent {
pub file: FileContentBlock,
/// The audio content of the message.
#[serde(rename = "m.audio")]
pub audio: AudioContent,
#[serde(rename = "org.matrix.msc1767.audio_details", skip_serializing_if = "Option::is_none")]
pub audio_details: Option<AudioDetailsContentBlock>,
/// The voice content of the message.
#[serde(rename = "m.voice")]
@ -49,7 +50,7 @@ pub struct VoiceEventContent {
impl VoiceEventContent {
/// Creates a new `VoiceEventContent` with the given fallback representation and file.
pub fn new(text: TextContentBlock, file: FileContentBlock) -> Self {
Self { text, file, audio: Default::default(), voice: Default::default(), relates_to: None }
Self { text, file, audio_details: None, voice: Default::default(), relates_to: None }
}
/// Creates a new `VoiceEventContent` with the given plain text fallback representation and
@ -58,7 +59,7 @@ impl VoiceEventContent {
Self {
text: TextContentBlock::plain(text),
file,
audio: Default::default(),
audio_details: None,
voice: Default::default(),
relates_to: None,
}

View File

@ -1,14 +1,15 @@
#![cfg(feature = "unstable-msc3246")]
#![cfg(feature = "unstable-msc3927")]
use std::time::Duration;
use assert_matches::assert_matches;
use assign::assign;
use js_int::uint;
#[cfg(feature = "unstable-msc3246")]
use ruma_common::events::audio::Amplitude;
use ruma_common::{
event_id,
events::{
audio::{Amplitude, AudioContent, AudioEventContent, Waveform, WaveformError},
audio::{AudioDetailsContentBlock, AudioEventContent},
file::{EncryptedContentInit, FileContentBlock},
message::TextContentBlock,
relation::InReplyTo,
@ -21,41 +22,18 @@ use ruma_common::{
};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
#[cfg(feature = "unstable-msc3246")]
#[test]
fn waveform_deserialization_pass() {
let json_data = json!([
13, 34, 987, 937, 345, 648, 1, 366, 235, 125, 904, 783, 734, 13, 34, 987, 937, 345, 648, 1,
366, 235, 125, 904, 783, 734, 13, 34, 987, 937, 345, 648, 1, 366, 235, 125, 904, 783, 734,
13, 34, 987, 937, 345, 648, 1, 366, 235, 125, 904, 783, 734,
]);
fn amplitude_deserialization_clamp() {
let json_data = json!(2000);
let waveform = from_json_value::<Waveform>(json_data).unwrap();
assert_eq!(waveform.amplitudes().len(), 52);
}
#[test]
fn waveform_deserialization_not_enough() {
let json_data = json!([]);
let err = from_json_value::<Waveform>(json_data).unwrap_err();
assert!(err.is_data());
assert_eq!(err.to_string(), WaveformError::NotEnoughValues.to_string());
}
#[test]
fn waveform_deserialization_clamp_amplitude() {
let json_data = json!([
2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000,
2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000,
]);
let waveform = from_json_value::<Waveform>(json_data).unwrap();
assert!(waveform.amplitudes().iter().all(|amp| amp.get() == Amplitude::MAX.into()));
let amplitude = from_json_value::<Amplitude>(json_data).unwrap();
assert_eq!(amplitude.get(), Amplitude::MAX.into());
}
#[test]
fn plain_content_serialization() {
let event_content = AudioEventContent::plain(
let event_content = AudioEventContent::with_plain_text(
"Upload: my_sound.ogg",
FileContentBlock::plain(
mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
@ -73,14 +51,13 @@ fn plain_content_serialization() {
"url": "mxc://notareal.hs/abcdef",
"name": "my_sound.ogg",
},
"m.audio": {}
})
);
}
#[test]
fn encrypted_content_serialization() {
let event_content = AudioEventContent::plain(
let event_content = AudioEventContent::with_plain_text(
"Upload: my_sound.ogg",
FileContentBlock::encrypted(
mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
@ -128,42 +105,25 @@ fn encrypted_content_serialization() {
},
"v": "v2",
},
"m.audio": {}
})
);
}
#[test]
fn event_serialization() {
let content = assign!(
AudioEventContent::new(
TextContentBlock::html(
"Upload: my_mix.mp3",
"Upload: <strong>my_mix.mp3</strong>",
),
assign!(
FileContentBlock::plain(
mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
"my_mix.mp3".to_owned()
),
{
mimetype: Some("audio/mp3".to_owned()),
size: Some(uint!(897_774)),
}
)
let mut content = AudioEventContent::new(
TextContentBlock::html("Upload: my_mix.mp3", "Upload: <strong>my_mix.mp3</strong>"),
FileContentBlock::plain(
mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
"my_mix.mp3".to_owned(),
),
{
audio: assign!(
AudioContent::new(),
{
duration: Some(Duration::from_secs(123))
}
),
relates_to: Some(Relation::Reply {
in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()),
}),
}
);
content.file.mimetype = Some("audio/mp3".to_owned());
content.file.size = Some(uint!(897_774));
content.audio_details = Some(AudioDetailsContentBlock::new(Duration::from_secs(123)));
content.relates_to = Some(Relation::Reply {
in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()),
});
assert_eq!(
to_json_value(&content).unwrap(),
@ -178,8 +138,8 @@ fn event_serialization() {
"mimetype": "audio/mp3",
"size": 897_774,
},
"m.audio": {
"duration": 123_000,
"org.matrix.msc1767.audio_details": {
"duration": 123,
},
"m.relates_to": {
"m.in_reply_to": {
@ -190,6 +150,7 @@ fn event_serialization() {
);
}
#[cfg(feature = "unstable-msc3246")]
#[test]
fn plain_content_deserialization() {
let json_data = json!({
@ -200,62 +161,63 @@ fn plain_content_deserialization() {
"url": "mxc://notareal.hs/abcdef",
"name": "my_new_song.webm",
},
"m.audio": {
"waveform": [
"org.matrix.msc1767.audio_details": {
"duration": 14,
"org.matrix.msc3246.waveform": [
13,
34,
987,
937,
345,
648,
253,
234,
157,
255,
1,
366,
235,
201,
135,
125,
904,
783,
734,
250,
233,
231,
13,
34,
987,
937,
345,
648,
1,
366,
252,
255,
140,
187,
0,
143,
235,
125,
904,
783,
734,
247,
183,
134,
13,
34,
987,
937,
345,
648,
187,
237,
145,
48,
1,
366,
66,
235,
125,
904,
783,
734,
204,
183,
34,
13,
34,
987,
937,
345,
648,
187,
237,
45,
48,
1,
366,
166,
235,
125,
904,
783,
734,
104,
183,
234,
],
}
},
});
let content = from_json_value::<AudioEventContent>(json_data).unwrap();
@ -263,8 +225,9 @@ fn plain_content_deserialization() {
assert_eq!(content.text.find_html(), None);
assert_eq!(content.file.url, "mxc://notareal.hs/abcdef");
assert_eq!(content.file.name, "my_new_song.webm");
let waveform = content.audio.waveform.unwrap();
assert_eq!(waveform.amplitudes().len(), 52);
let audio_details = content.audio_details.unwrap();
assert_eq!(audio_details.duration, Duration::from_secs(14));
assert_eq!(audio_details.waveform.len(), 52);
}
#[test]
@ -287,9 +250,8 @@ fn encrypted_content_deserialization() {
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "v2"
"v": "v2",
},
"m.audio": {},
});
let content = from_json_value::<AudioEventContent>(json_data).unwrap();
@ -298,6 +260,7 @@ fn encrypted_content_deserialization() {
assert_eq!(content.file.url, "mxc://notareal.hs/abcdef");
assert_eq!(content.file.name, "my_file.txt");
assert!(content.file.encryption_info.is_some());
assert!(content.audio_details.is_none());
}
#[test]
@ -313,15 +276,15 @@ fn message_event_deserialization() {
"mimetype": "audio/opus",
"size": 123_774,
},
"m.audio": {
"duration": 5_300,
}
"org.matrix.msc1767.audio_details": {
"duration": 53,
},
},
"event_id": "$event:notareal.hs",
"origin_server_ts": 134_829_848,
"room_id": "!roomid:notareal.hs",
"sender": "@user:notareal.hs",
"type": "m.audio",
"type": "org.matrix.msc1767.audio",
});
let message_event = assert_matches!(
@ -342,6 +305,8 @@ fn message_event_deserialization() {
assert_eq!(content.file.name, "airplane_sound.opus");
assert_eq!(content.file.mimetype.as_deref(), Some("audio/opus"));
assert_eq!(content.file.size, Some(uint!(123_774)));
assert_eq!(content.audio.duration, Some(Duration::from_millis(5_300)));
assert_matches!(content.audio.waveform, None);
let audio_details = content.audio_details.unwrap();
assert_eq!(audio_details.duration, Duration::from_secs(53));
#[cfg(feature = "unstable-msc3246")]
assert!(audio_details.waveform.is_empty());
}

View File

@ -3,13 +3,12 @@
use std::time::Duration;
use assert_matches::assert_matches;
use assign::assign;
use js_int::uint;
use ruma_common::{
event_id,
events::{
audio::AudioContent, file::FileContentBlock, relation::InReplyTo, room::message::Relation,
voice::VoiceEventContent, AnyMessageLikeEvent, MessageLikeEvent,
audio::AudioDetailsContentBlock, file::FileContentBlock, relation::InReplyTo,
room::message::Relation, voice::VoiceEventContent, AnyMessageLikeEvent, MessageLikeEvent,
},
mxc_uri,
serde::CanBeEmpty,
@ -29,12 +28,7 @@ fn event_serialization() {
content.file.mimetype = Some("audio/opus".to_owned());
content.file.size = Some(uint!(897_774));
content.audio = assign!(
AudioContent::new(),
{
duration: Some(Duration::from_secs(23))
}
);
content.audio_details = Some(AudioDetailsContentBlock::new(Duration::from_secs(23)));
content.relates_to = Some(Relation::Reply {
in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()),
});
@ -51,8 +45,8 @@ fn event_serialization() {
"mimetype": "audio/opus",
"size": 897_774,
},
"m.audio": {
"duration": 23_000,
"org.matrix.msc1767.audio_details": {
"duration": 23,
},
"m.voice": {},
"m.relates_to": {
@ -77,8 +71,8 @@ fn message_event_deserialization() {
"mimetype": "audio/opus",
"size": 123_774,
},
"m.audio": {
"duration": 5_300,
"org.matrix.msc1767.audio_details": {
"duration": 53,
},
"m.voice": {},
},
@ -106,6 +100,7 @@ fn message_event_deserialization() {
assert_eq!(content.file.name, "voice_message.ogg");
assert_eq!(content.file.mimetype.as_deref(), Some("audio/opus"));
assert_eq!(content.file.size, Some(uint!(123_774)));
assert_eq!(content.audio.duration, Some(Duration::from_millis(5_300)));
assert_matches!(content.audio.waveform, None);
let audio_details = content.audio_details.unwrap();
assert_eq!(audio_details.duration, Duration::from_secs(53));
assert!(audio_details.waveform.is_empty());
}

View File

@ -153,6 +153,7 @@ unstable-msc3554 = ["ruma-common/unstable-msc3554"]
unstable-msc3575 = ["ruma-client-api?/unstable-msc3575"]
unstable-msc3618 = ["ruma-federation-api?/unstable-msc3618"]
unstable-msc3723 = ["ruma-federation-api?/unstable-msc3723"]
unstable-msc3927 = ["ruma-common/unstable-msc3927"]
unstable-msc3931 = ["ruma-common/unstable-msc3931"]
unstable-msc3932 = ["ruma-common/unstable-msc3932"]
unstable-msc3954 = ["ruma-common/unstable-msc3954"]
@ -194,6 +195,7 @@ __ci = [
"unstable-msc3575",
"unstable-msc3618",
"unstable-msc3723",
"unstable-msc3927",
"unstable-msc3932",
"unstable-msc3954",
"unstable-msc3955",