From 12523cd741b977b1f5cceac5310fc4192fab45a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= <76261501+zecakeh@users.noreply.github.com> Date: Mon, 21 Mar 2022 13:16:44 +0100 Subject: [PATCH] common: Add support for transitional extensible text messages --- crates/ruma-common/src/events/emote.rs | 19 +- crates/ruma-common/src/events/message.rs | 27 +- .../src/events/message/content_serde.rs | 39 +- crates/ruma-common/src/events/notice.rs | 19 +- crates/ruma-common/src/events/room/message.rs | 156 +++++++- crates/ruma-common/tests/events/enums.rs | 9 +- crates/ruma-common/tests/events/message.rs | 364 +++++++++++++++++- crates/ruma-common/tests/events/relations.rs | 73 ++++ .../ruma-common/tests/events/room_message.rs | 80 +++- 9 files changed, 754 insertions(+), 32 deletions(-) diff --git a/crates/ruma-common/src/events/emote.rs b/crates/ruma-common/src/events/emote.rs index d9ba718b..a692f185 100644 --- a/crates/ruma-common/src/events/emote.rs +++ b/crates/ruma-common/src/events/emote.rs @@ -5,7 +5,10 @@ use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; -use super::{message::MessageContent, room::message::Relation}; +use super::{ + message::MessageContent, + room::message::{EmoteMessageEventContent, Relation}, +}; /// The payload for an extensible emote message. #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] @@ -42,4 +45,18 @@ impl EmoteEventContent { pub fn markdown(body: impl AsRef + Into) -> Self { Self { message: MessageContent::markdown(body), relates_to: None } } + + /// Create a new `MessageEventContent` from the given `EmoteMessageEventContent` and optional + /// relation. + pub fn from_emote_room_message( + content: EmoteMessageEventContent, + relates_to: Option, + ) -> Self { + let EmoteMessageEventContent { body, formatted, message, .. } = content; + if let Some(message) = message { + Self { message, relates_to } + } else { + Self { message: MessageContent::from_room_message_content(body, formatted), relates_to } + } + } } diff --git a/crates/ruma-common/src/events/message.rs b/crates/ruma-common/src/events/message.rs index 84572223..3464f696 100644 --- a/crates/ruma-common/src/events/message.rs +++ b/crates/ruma-common/src/events/message.rs @@ -9,7 +9,7 @@ mod content_serde; use content_serde::MessageContentSerDeHelper; -use super::room::message::Relation; +use super::room::message::{FormattedBody, MessageFormat, Relation, TextMessageEventContent}; /// The payload for an extensible text message. #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] @@ -46,6 +46,20 @@ impl MessageEventContent { pub fn markdown(body: impl AsRef + Into) -> Self { Self { message: MessageContent::markdown(body), relates_to: None } } + + /// Create a new `MessageEventContent` from the given `TextMessageEventContent` and optional + /// relation. + pub fn from_text_room_message( + content: TextMessageEventContent, + relates_to: Option, + ) -> Self { + let TextMessageEventContent { body, formatted, message, .. } = content; + if let Some(message) = message { + Self { message, relates_to } + } else { + Self { message: MessageContent::from_room_message_content(body, formatted), relates_to } + } + } } /// Text message content. @@ -78,6 +92,17 @@ impl MessageContent { Self(message) } + /// Create a new `MessageContent` from the given body and optional formatted body. + pub fn from_room_message_content(body: String, formatted: Option) -> Self { + if let Some(FormattedBody { body: html_body, .. }) = + formatted.filter(|formatted| formatted.format == MessageFormat::Html) + { + Self::html(body, html_body) + } else { + Self::plain(body) + } + } + /// Get the plain text representation of this message. pub fn find_plain(&self) -> Option<&str> { self.variants() diff --git a/crates/ruma-common/src/events/message/content_serde.rs b/crates/ruma-common/src/events/message/content_serde.rs index aff2abac..401e3102 100644 --- a/crates/ruma-common/src/events/message/content_serde.rs +++ b/crates/ruma-common/src/events/message/content_serde.rs @@ -9,31 +9,46 @@ use super::{MessageContent, Text}; #[derive(Error, Debug)] pub enum MessageContentSerdeError { - #[error("missing field `{0}`")] - MissingField(String), + #[error("missing field `m.message` or `m.text`")] + MissingMessage, } #[derive(Debug, Default, Deserialize)] pub(crate) struct MessageContentSerDeHelper { - /// Plain text short form. - #[serde(rename = "org.matrix.msc1767.text", skip_serializing_if = "Option::is_none")] - text: Option, + /// Plain text short form, stable name. + #[serde(rename = "m.text")] + text_stable: Option, - /// Long form. - #[serde(rename = "org.matrix.msc1767.message", default, skip_serializing_if = "Vec::is_empty")] - message: Vec, + /// Plain text short form, unstable name. + #[serde(rename = "org.matrix.msc1767.text")] + text_unstable: Option, + + /// Long form, stable name. + #[serde(rename = "m.message")] + message_stable: Option>, + + /// Long form, unstable name. + #[serde(rename = "org.matrix.msc1767.message")] + message_unstable: Option>, } impl TryFrom for MessageContent { type Error = MessageContentSerdeError; fn try_from(helper: MessageContentSerDeHelper) -> Result { - if !helper.message.is_empty() { - Ok(Self(helper.message)) - } else if let Some(text) = helper.text { + let MessageContentSerDeHelper { + text_stable, + text_unstable, + message_stable, + message_unstable, + } = helper; + + if let Some(message) = message_stable.or(message_unstable) { + Ok(Self(message)) + } else if let Some(text) = text_stable.or(text_unstable) { Ok(Self::plain(text)) } else { - Err(MessageContentSerdeError::MissingField("m.message".into())) + Err(MessageContentSerdeError::MissingMessage) } } } diff --git a/crates/ruma-common/src/events/notice.rs b/crates/ruma-common/src/events/notice.rs index 28e9a88d..dd619503 100644 --- a/crates/ruma-common/src/events/notice.rs +++ b/crates/ruma-common/src/events/notice.rs @@ -5,7 +5,10 @@ use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; -use super::{message::MessageContent, room::message::Relation}; +use super::{ + message::MessageContent, + room::message::{NoticeMessageEventContent, Relation}, +}; /// The payload for an extensible notice message. #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] @@ -42,4 +45,18 @@ impl NoticeEventContent { pub fn markdown(body: impl AsRef + Into) -> Self { Self { message: MessageContent::markdown(body), relates_to: None } } + + /// Create a new `MessageEventContent` from the given `NoticeMessageEventContent` and optional + /// relation. + pub fn from_notice_room_message( + content: NoticeMessageEventContent, + relates_to: Option, + ) -> Self { + let NoticeMessageEventContent { body, formatted, message, .. } = content; + if let Some(message) = message { + Self { message, relates_to } + } else { + Self { message: MessageContent::from_room_message_content(body, formatted), relates_to } + } + } } diff --git a/crates/ruma-common/src/events/room/message.rs b/crates/ruma-common/src/events/room/message.rs index 2b4586d1..36a4f25d 100644 --- a/crates/ruma-common/src/events/room/message.rs +++ b/crates/ruma-common/src/events/room/message.rs @@ -10,6 +10,12 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value as JsonValue; use super::{EncryptedFile, ImageInfo, ThumbnailInfo}; +#[cfg(feature = "unstable-msc1767")] +use crate::events::{ + emote::EmoteEventContent, + message::{MessageContent, MessageEventContent}, + notice::NoticeEventContent, +}; use crate::{ events::key::verification::VerificationMethod, serde::{JsonObject, StringEnum}, @@ -182,6 +188,33 @@ impl RoomMessageEventContent { } } +#[cfg(feature = "unstable-msc1767")] +impl From for RoomMessageEventContent { + fn from(content: EmoteEventContent) -> Self { + let EmoteEventContent { message, relates_to, .. } = content; + + Self { msgtype: MessageType::Emote(message.into()), relates_to } + } +} + +#[cfg(feature = "unstable-msc1767")] +impl From for RoomMessageEventContent { + fn from(content: MessageEventContent) -> Self { + let MessageEventContent { message, relates_to, .. } = content; + + Self { msgtype: MessageType::Text(message.into()), relates_to } + } +} + +#[cfg(feature = "unstable-msc1767")] +impl From for RoomMessageEventContent { + fn from(content: NoticeEventContent) -> Self { + let NoticeEventContent { message, relates_to, .. } = content; + + Self { msgtype: MessageType::Notice(message.into()), relates_to } + } +} + /// The content that is specific to each message type variant. #[derive(Clone, Debug, Serialize)] #[serde(untagged)] @@ -507,17 +540,37 @@ pub struct EmoteMessageEventContent { /// Formatted form of the message `body`. #[serde(flatten)] pub formatted: Option, + + /// Extensible-event representation of the message. + /// + /// If present, this should be preferred over the other fields. + #[cfg(feature = "unstable-msc1767")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, } impl EmoteMessageEventContent { /// A convenience constructor to create a plain-text emote. pub fn plain(body: impl Into) -> Self { - Self { body: body.into(), formatted: None } + let body = body.into(); + Self { + #[cfg(feature = "unstable-msc1767")] + message: Some(MessageContent::plain(body.clone())), + body, + formatted: None, + } } /// A convenience constructor to create an html emote message. pub fn html(body: impl Into, html_body: impl Into) -> Self { - Self { formatted: Some(FormattedBody::html(html_body)), ..Self::plain(body) } + let body = body.into(); + let html_body = html_body.into(); + Self { + #[cfg(feature = "unstable-msc1767")] + message: Some(MessageContent::html(body.clone(), html_body.clone())), + body, + formatted: Some(FormattedBody::html(html_body)), + } } /// A convenience constructor to create a markdown emote. @@ -526,7 +579,22 @@ impl EmoteMessageEventContent { /// plain-text emote. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { - Self { formatted: FormattedBody::markdown(&body), ..Self::plain(body) } + if let Some(formatted) = FormattedBody::markdown(&body) { + Self::html(body, formatted.body) + } else { + Self::plain(body) + } + } +} + +#[cfg(feature = "unstable-msc1767")] +impl From for EmoteMessageEventContent { + fn from(message: MessageContent) -> Self { + let body = + if let Some(body) = message.find_plain() { body } else { &message.variants()[0].body }; + let formatted = message.find_html().map(FormattedBody::html); + + Self { body: body.to_owned(), formatted, message: Some(message) } } } @@ -712,17 +780,37 @@ pub struct NoticeMessageEventContent { /// Formatted form of the message `body`. #[serde(flatten)] pub formatted: Option, + + /// Extensible-event representation of the message. + /// + /// If present, this should be preferred over the other fields. + #[cfg(feature = "unstable-msc1767")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, } impl NoticeMessageEventContent { /// A convenience constructor to create a plain text notice. pub fn plain(body: impl Into) -> Self { - Self { body: body.into(), formatted: None } + let body = body.into(); + Self { + #[cfg(feature = "unstable-msc1767")] + message: Some(MessageContent::plain(body.clone())), + body, + formatted: None, + } } /// A convenience constructor to create an html notice. pub fn html(body: impl Into, html_body: impl Into) -> Self { - Self { formatted: Some(FormattedBody::html(html_body)), ..Self::plain(body) } + let body = body.into(); + let html_body = html_body.into(); + Self { + #[cfg(feature = "unstable-msc1767")] + message: Some(MessageContent::html(body.clone(), html_body.clone())), + body, + formatted: Some(FormattedBody::html(html_body)), + } } /// A convenience constructor to create a markdown notice. @@ -731,7 +819,22 @@ impl NoticeMessageEventContent { /// text notice. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { - Self { formatted: FormattedBody::markdown(&body), ..Self::plain(body) } + if let Some(formatted) = FormattedBody::markdown(&body) { + Self::html(body, formatted.body) + } else { + Self::plain(body) + } + } +} + +#[cfg(feature = "unstable-msc1767")] +impl From for NoticeMessageEventContent { + fn from(message: MessageContent) -> Self { + let body = + if let Some(body) = message.find_plain() { body } else { &message.variants()[0].body }; + let formatted = message.find_html().map(FormattedBody::html); + + Self { body: body.to_owned(), formatted, message: Some(message) } } } @@ -879,17 +982,37 @@ pub struct TextMessageEventContent { /// Formatted form of the message `body`. #[serde(flatten)] pub formatted: Option, + + /// Extensible-event representation of the message. + /// + /// If present, this should be preferred over the other fields. + #[cfg(feature = "unstable-msc1767")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, } impl TextMessageEventContent { /// A convenience constructor to create a plain text message. pub fn plain(body: impl Into) -> Self { - Self { body: body.into(), formatted: None } + let body = body.into(); + Self { + #[cfg(feature = "unstable-msc1767")] + message: Some(MessageContent::plain(body.clone())), + body, + formatted: None, + } } /// A convenience constructor to create an html message. pub fn html(body: impl Into, html_body: impl Into) -> Self { - Self { formatted: Some(FormattedBody::html(html_body)), ..Self::plain(body) } + let body = body.into(); + let html_body = html_body.into(); + Self { + #[cfg(feature = "unstable-msc1767")] + message: Some(MessageContent::html(body.clone(), html_body.clone())), + body, + formatted: Some(FormattedBody::html(html_body)), + } } /// A convenience constructor to create a markdown message. @@ -898,7 +1021,22 @@ impl TextMessageEventContent { /// text message. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { - Self { formatted: FormattedBody::markdown(&body), ..Self::plain(body) } + if let Some(formatted) = FormattedBody::markdown(&body) { + Self::html(body, formatted.body) + } else { + Self::plain(body) + } + } +} + +#[cfg(feature = "unstable-msc1767")] +impl From for TextMessageEventContent { + fn from(message: MessageContent) -> Self { + let body = + if let Some(body) = message.find_plain() { body } else { &message.variants()[0].body }; + let formatted = message.find_html().map(FormattedBody::html); + + Self { body: body.to_owned(), formatted, message: Some(message) } } } diff --git a/crates/ruma-common/tests/events/enums.rs b/crates/ruma-common/tests/events/enums.rs index 1e12d9e0..1c2c28ea 100644 --- a/crates/ruma-common/tests/events/enums.rs +++ b/crates/ruma-common/tests/events/enums.rs @@ -211,10 +211,17 @@ fn message_event_serialization() { unsigned: MessageLikeUnsigned::default(), }; + #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( serde_json::to_string(&event).expect("Failed to serialize message event"), r#"{"type":"m.room.message","content":{"msgtype":"m.text","body":"test"},"event_id":"$1234:example.com","sender":"@test:example.com","origin_server_ts":0,"room_id":"!roomid:example.com"}"# - ) + ); + + #[cfg(feature = "unstable-msc1767")] + assert_eq!( + serde_json::to_string(&event).expect("Failed to serialize message event"), + r#"{"type":"m.room.message","content":{"msgtype":"m.text","body":"test","org.matrix.msc1767.text":"test"},"event_id":"$1234:example.com","sender":"@test:example.com","origin_server_ts":0,"room_id":"!roomid:example.com"}"# + ); } #[test] diff --git a/crates/ruma-common/tests/events/message.rs b/crates/ruma-common/tests/events/message.rs index a49ffa44..523d7dff 100644 --- a/crates/ruma-common/tests/events/message.rs +++ b/crates/ruma-common/tests/events/message.rs @@ -9,7 +9,10 @@ use ruma_common::{ emote::EmoteEventContent, message::MessageEventContent, notice::NoticeEventContent, - room::message::{InReplyTo, Relation}, + room::message::{ + EmoteMessageEventContent, InReplyTo, MessageType, NoticeMessageEventContent, Relation, + RoomMessageEventContent, TextMessageEventContent, + }, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, }, room_id, user_id, MilliSecondsSinceUnixEpoch, @@ -133,7 +136,7 @@ fn message_event_serialization() { } #[test] -fn plain_text_content_deserialization() { +fn plain_text_content_unstable_deserialization() { let json_data = json!({ "org.matrix.msc1767.text": "This is my body", }); @@ -148,7 +151,22 @@ fn plain_text_content_deserialization() { } #[test] -fn html_text_content_deserialization() { +fn plain_text_content_stable_deserialization() { + let json_data = json!({ + "m.text": "This is my body", + }); + + assert_matches!( + from_json_value::(json_data) + .unwrap(), + MessageEventContent { message, .. } + if message.find_plain() == Some("This is my body") + && message.find_html().is_none() + ); +} + +#[test] +fn html_text_content_unstable_deserialization() { let json_data = json!({ "org.matrix.msc1767.message": [ { "body": "Hello, New World!", "mimetype": "text/html"}, @@ -165,6 +183,24 @@ fn html_text_content_deserialization() { ); } +#[test] +fn html_text_content_stable_deserialization() { + let json_data = json!({ + "m.message": [ + { "body": "Hello, New World!", "mimetype": "text/html"}, + { "body": "Hello, New World!" }, + ] + }); + + assert_matches!( + from_json_value::(json_data) + .unwrap(), + MessageEventContent { message, .. } + if message.find_plain() == Some("Hello, New World!") + && message.find_html() == Some("Hello, New World!") + ); +} + #[test] fn relates_to_content_deserialization() { let json_data = json!({ @@ -224,6 +260,120 @@ fn message_event_deserialization() { ); } +#[test] +fn room_message_plain_text_stable_deserialization() { + let json_data = json!({ + "body": "test", + "msgtype": "m.text", + "m.text": "test", + }); + + assert_matches!( + from_json_value::(json_data) + .unwrap(), + RoomMessageEventContent { + msgtype: MessageType::Text(TextMessageEventContent { + body, + formatted: None, + message: Some(message), + .. + }), + .. + } if body == "test" + && message.variants().len() == 1 + && message.variants()[0].body == "test" + ); +} + +#[test] +fn room_message_plain_text_unstable_deserialization() { + let json_data = json!({ + "body": "test", + "msgtype": "m.text", + "org.matrix.msc1767.text": "test", + }); + + assert_matches!( + from_json_value::(json_data) + .unwrap(), + RoomMessageEventContent { + msgtype: MessageType::Text(TextMessageEventContent { + body, + formatted: None, + message: Some(message), + .. + }), + .. + } if body == "test" + && message.variants().len() == 1 + && message.variants()[0].body == "test" + ); +} + +#[test] +fn room_message_html_text_stable_deserialization() { + let json_data = json!({ + "body": "test", + "formatted_body": "

test

", + "format": "org.matrix.custom.html", + "msgtype": "m.text", + "m.message": [ + { "body": "

test

", "mimetype": "text/html" }, + { "body": "test", "mimetype": "text/plain" }, + ], + }); + + assert_matches!( + from_json_value::(json_data) + .unwrap(), + RoomMessageEventContent { + msgtype: MessageType::Text(TextMessageEventContent { + body, + formatted: Some(formatted), + message: Some(message), + .. + }), + .. + } if body == "test" + && formatted.body == "

test

" + && message.variants().len() == 2 + && message.variants()[0].body == "

test

" + && message.variants()[1].body == "test" + ); +} + +#[test] +fn room_message_html_text_unstable_deserialization() { + let json_data = json!({ + "body": "test", + "formatted_body": "

test

", + "format": "org.matrix.custom.html", + "msgtype": "m.text", + "org.matrix.msc1767.message": [ + { "body": "

test

", "mimetype": "text/html" }, + { "body": "test", "mimetype": "text/plain" }, + ], + }); + + assert_matches!( + from_json_value::(json_data) + .unwrap(), + RoomMessageEventContent { + msgtype: MessageType::Text(TextMessageEventContent { + body, + formatted: Some(formatted), + message: Some(message), + .. + }), + .. + } if body == "test" + && formatted.body == "

test

" + && message.variants().len() == 2 + && message.variants()[0].body == "

test

" + && message.variants()[1].body == "test" + ); +} + #[test] fn notice_event_serialization() { let event = MessageLikeEvent { @@ -251,7 +401,60 @@ fn notice_event_serialization() { } #[test] -fn notice_event_deserialization() { +fn room_message_notice_serialization() { + let message_event_content = + RoomMessageEventContent::notice_plain("> <@test:example.com> test\n\ntest reply"); + + assert_eq!( + to_json_value(&message_event_content).unwrap(), + json!({ + "body": "> <@test:example.com> test\n\ntest reply", + "msgtype": "m.notice", + "org.matrix.msc1767.text": "> <@test:example.com> test\n\ntest reply", + }) + ); +} + +#[test] +fn notice_event_stable_deserialization() { + let json_data = json!({ + "content": { + "m.message": [ + { "body": "Hello, I'm a robot!", "mimetype": "text/html"}, + { "body": "Hello, I'm a robot!" }, + ] + }, + "event_id": "$event:notareal.hs", + "origin_server_ts": 134_829_848, + "room_id": "!roomid:notareal.hs", + "sender": "@user:notareal.hs", + "type": "m.notice", + }); + + assert_matches!( + from_json_value::(json_data).unwrap(), + AnyMessageLikeEvent::Notice(MessageLikeEvent { + content: NoticeEventContent { + message, + .. + }, + event_id, + origin_server_ts, + room_id, + sender, + unsigned + }) if event_id == event_id!("$event:notareal.hs") + && message.find_plain() == Some("Hello, I'm a robot!") + && message.find_html() == Some("Hello, I'm a robot!") + && origin_server_ts == MilliSecondsSinceUnixEpoch(uint!(134_829_848)) + && room_id == room_id!("!roomid:notareal.hs") + && sender == user_id!("@user:notareal.hs") + && unsigned.is_empty() + ); +} + +#[test] +fn notice_event_unstable_deserialization() { let json_data = json!({ "content": { "org.matrix.msc1767.message": [ @@ -288,6 +491,56 @@ fn notice_event_deserialization() { ); } +#[test] +fn room_message_notice_stable_deserialization() { + let json_data = json!({ + "body": "test", + "msgtype": "m.notice", + "m.text": "test", + }); + + assert_matches!( + from_json_value::(json_data) + .unwrap(), + RoomMessageEventContent { + msgtype: MessageType::Notice(NoticeMessageEventContent { + body, + formatted: None, + message: Some(message), + .. + }), + .. + } if body == "test" + && message.variants().len() == 1 + && message.variants()[0].body == "test" + ); +} + +#[test] +fn room_message_notice_unstable_deserialization() { + let json_data = json!({ + "body": "test", + "msgtype": "m.notice", + "org.matrix.msc1767.text": "test", + }); + + assert_matches!( + from_json_value::(json_data) + .unwrap(), + RoomMessageEventContent { + msgtype: MessageType::Notice(NoticeMessageEventContent { + body, + formatted: None, + message: Some(message), + .. + }), + .. + } if body == "test" + && message.variants().len() == 1 + && message.variants()[0].body == "test" + ); +} + #[test] fn emote_event_serialization() { let event = MessageLikeEvent { @@ -321,7 +574,58 @@ fn emote_event_serialization() { } #[test] -fn emote_event_deserialization() { +fn room_message_emote_serialization() { + let message_event_content = RoomMessageEventContent::new(MessageType::Emote( + EmoteMessageEventContent::plain("> <@test:example.com> test\n\ntest reply"), + )); + + assert_eq!( + to_json_value(&message_event_content).unwrap(), + json!({ + "body": "> <@test:example.com> test\n\ntest reply", + "msgtype": "m.emote", + "org.matrix.msc1767.text": "> <@test:example.com> test\n\ntest reply", + }) + ); +} + +#[test] +fn emote_event_stable_deserialization() { + let json_data = json!({ + "content": { + "m.text": "is testing some code…", + }, + "event_id": "$event:notareal.hs", + "origin_server_ts": 134_829_848, + "room_id": "!roomid:notareal.hs", + "sender": "@user:notareal.hs", + "type": "m.emote", + }); + + assert_matches!( + from_json_value::(json_data).unwrap(), + AnyMessageLikeEvent::Emote(MessageLikeEvent { + content: EmoteEventContent { + message, + .. + }, + event_id, + origin_server_ts, + room_id, + sender, + unsigned + }) if event_id == event_id!("$event:notareal.hs") + && message.find_plain() == Some("is testing some code…") + && message.find_html().is_none() + && origin_server_ts == MilliSecondsSinceUnixEpoch(uint!(134_829_848)) + && room_id == room_id!("!roomid:notareal.hs") + && sender == user_id!("@user:notareal.hs") + && unsigned.is_empty() + ); +} + +#[test] +fn emote_event_unstable_deserialization() { let json_data = json!({ "content": { "org.matrix.msc1767.text": "is testing some code…", @@ -355,6 +659,56 @@ fn emote_event_deserialization() { ); } +#[test] +fn room_message_emote_stable_deserialization() { + let json_data = json!({ + "body": "test", + "msgtype": "m.emote", + "m.text": "test", + }); + + assert_matches!( + from_json_value::(json_data) + .unwrap(), + RoomMessageEventContent { + msgtype: MessageType::Emote(EmoteMessageEventContent { + body, + formatted: None, + message: Some(message), + .. + }), + .. + } if body == "test" + && message.variants().len() == 1 + && message.variants()[0].body == "test" + ); +} + +#[test] +fn room_message_emote_unstable_deserialization() { + let json_data = json!({ + "body": "test", + "msgtype": "m.emote", + "org.matrix.msc1767.text": "test", + }); + + assert_matches!( + from_json_value::(json_data) + .unwrap(), + RoomMessageEventContent { + msgtype: MessageType::Emote(EmoteMessageEventContent { + body, + formatted: None, + message: Some(message), + .. + }), + .. + } if body == "test" + && message.variants().len() == 1 + && message.variants()[0].body == "test" + ); +} + #[test] #[cfg(feature = "unstable-msc3554")] fn lang_serialization() { diff --git a/crates/ruma-common/tests/events/relations.rs b/crates/ruma-common/tests/events/relations.rs index 13f85d61..598c99d0 100644 --- a/crates/ruma-common/tests/events/relations.rs +++ b/crates/ruma-common/tests/events/relations.rs @@ -36,6 +36,7 @@ fn reply_serialize() { relates_to: Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id!("$1598361704261elfgc").to_owned()) }), }); + #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(content).unwrap(), json!({ @@ -48,6 +49,21 @@ fn reply_serialize() { }, }) ); + + #[cfg(feature = "unstable-msc1767")] + assert_eq!( + to_json_value(content).unwrap(), + json!({ + "msgtype": "m.text", + "body": "This is a reply", + "org.matrix.msc1767.text": "This is a reply", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$1598361704261elfgc", + }, + }, + }) + ); } #[test] @@ -67,6 +83,7 @@ fn replacement_serialize() { } ); + #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(content).unwrap(), json!({ @@ -82,6 +99,25 @@ fn replacement_serialize() { }, }) ); + + #[cfg(feature = "unstable-msc1767")] + assert_eq!( + to_json_value(content).unwrap(), + json!({ + "msgtype": "m.text", + "body": "", + "org.matrix.msc1767.text": "", + "m.new_content": { + "body": "This is the new content.", + "msgtype": "m.text", + "org.matrix.msc1767.text": "This is the new content.", + }, + "m.relates_to": { + "rel_type": "m.replace", + "event_id": "$1598361704261elfgc", + }, + }) + ); } #[test] @@ -130,6 +166,7 @@ fn thread_plain_serialize() { } ); + #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(content).unwrap(), json!({ @@ -144,6 +181,23 @@ fn thread_plain_serialize() { }, }) ); + + #[cfg(feature = "unstable-msc1767")] + assert_eq!( + to_json_value(content).unwrap(), + json!({ + "msgtype": "m.text", + "body": "", + "org.matrix.msc1767.text": "", + "m.relates_to": { + "rel_type": "io.element.thread", + "event_id": "$1598361704261elfgc", + "m.in_reply_to": { + "event_id": "$latesteventid", + }, + }, + }) + ); } #[test] @@ -163,6 +217,7 @@ fn thread_reply_serialize() { } ); + #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(content).unwrap(), json!({ @@ -178,6 +233,24 @@ fn thread_reply_serialize() { }, }) ); + + #[cfg(feature = "unstable-msc1767")] + assert_eq!( + to_json_value(content).unwrap(), + json!({ + "msgtype": "m.text", + "body": "", + "org.matrix.msc1767.text": "", + "m.relates_to": { + "rel_type": "io.element.thread", + "event_id": "$1598361704261elfgc", + "m.in_reply_to": { + "event_id": "$repliedtoeventid", + }, + "io.element.show_reply": true, + }, + }) + ); } #[test] diff --git a/crates/ruma-common/tests/events/room_message.rs b/crates/ruma-common/tests/events/room_message.rs index e31260e4..f8e4f8d9 100644 --- a/crates/ruma-common/tests/events/room_message.rs +++ b/crates/ruma-common/tests/events/room_message.rs @@ -1,15 +1,20 @@ use std::borrow::Cow; +#[cfg(not(feature = "unstable-msc1767"))] use assign::assign; use js_int::uint; use matches::assert_matches; +#[cfg(not(feature = "unstable-msc1767"))] +use ruma_common::events::room::message::InReplyTo; +#[cfg(any(feature = "unstable-msc2676", not(feature = "unstable-msc1767")))] +use ruma_common::events::room::message::Relation; use ruma_common::{ event_id, events::{ key::verification::VerificationMethod, room::message::{ - AudioMessageEventContent, InReplyTo, KeyVerificationRequestEventContent, MessageType, - Relation, RoomMessageEvent, RoomMessageEventContent, TextMessageEventContent, + AudioMessageEventContent, KeyVerificationRequestEventContent, MessageType, + RoomMessageEvent, RoomMessageEventContent, TextMessageEventContent, }, MessageLikeUnsigned, }, @@ -126,6 +131,7 @@ fn formatted_body_serialization() { let message_event_content = RoomMessageEventContent::text_html("Hello, World!", "Hello, World!"); + #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ @@ -135,6 +141,21 @@ fn formatted_body_serialization() { "formatted_body": "Hello, World!", }) ); + + #[cfg(feature = "unstable-msc1767")] + assert_eq!( + to_json_value(&message_event_content).unwrap(), + json!({ + "body": "Hello, World!", + "msgtype": "m.text", + "format": "org.matrix.custom.html", + "formatted_body": "Hello, World!", + "org.matrix.msc1767.message": [ + { "body": "Hello, World!", "mimetype": "text/html" }, + { "body": "Hello, World!", "mimetype": "text/plain" }, + ], + }) + ); } #[test] @@ -142,6 +163,7 @@ fn plain_text_content_serialization() { let message_event_content = RoomMessageEventContent::text_plain("> <@test:example.com> test\n\ntest reply"); + #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ @@ -149,6 +171,16 @@ fn plain_text_content_serialization() { "msgtype": "m.text" }) ); + + #[cfg(feature = "unstable-msc1767")] + assert_eq!( + to_json_value(&message_event_content).unwrap(), + json!({ + "body": "> <@test:example.com> test\n\ntest reply", + "msgtype": "m.text", + "org.matrix.msc1767.text": "> <@test:example.com> test\n\ntest reply", + }) + ); } #[test] @@ -158,6 +190,7 @@ fn markdown_content_serialization() { TextMessageEventContent::markdown("Testing **bold** and _italic_!"), )); + #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(&formatted_message).unwrap(), json!({ @@ -168,10 +201,26 @@ fn markdown_content_serialization() { }) ); + #[cfg(feature = "unstable-msc1767")] + assert_eq!( + to_json_value(&formatted_message).unwrap(), + json!({ + "body": "Testing **bold** and _italic_!", + "formatted_body": "

Testing bold and italic!

\n", + "format": "org.matrix.custom.html", + "msgtype": "m.text", + "org.matrix.msc1767.message": [ + { "body": "

Testing bold and italic!

\n", "mimetype": "text/html" }, + { "body": "Testing **bold** and _italic_!", "mimetype": "text/plain" }, + ], + }) + ); + let plain_message_simple = RoomMessageEventContent::new(MessageType::Text( TextMessageEventContent::markdown("Testing a simple phrase…"), )); + #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(&plain_message_simple).unwrap(), json!({ @@ -180,10 +229,21 @@ fn markdown_content_serialization() { }) ); + #[cfg(feature = "unstable-msc1767")] + assert_eq!( + to_json_value(&plain_message_simple).unwrap(), + json!({ + "body": "Testing a simple phrase…", + "msgtype": "m.text", + "org.matrix.msc1767.text": "Testing a simple phrase…", + }) + ); + let plain_message_paragraphs = RoomMessageEventContent::new(MessageType::Text( TextMessageEventContent::markdown("Testing\n\nSeveral\n\nParagraphs."), )); + #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(&plain_message_paragraphs).unwrap(), json!({ @@ -193,9 +253,25 @@ fn markdown_content_serialization() { "msgtype": "m.text" }) ); + + #[cfg(feature = "unstable-msc1767")] + assert_eq!( + to_json_value(&plain_message_paragraphs).unwrap(), + json!({ + "body": "Testing\n\nSeveral\n\nParagraphs.", + "formatted_body": "

Testing

\n

Several

\n

Paragraphs.

\n", + "format": "org.matrix.custom.html", + "msgtype": "m.text", + "org.matrix.msc1767.message": [ + { "body": "

Testing

\n

Several

\n

Paragraphs.

\n", "mimetype": "text/html" }, + { "body": "Testing\n\nSeveral\n\nParagraphs.", "mimetype": "text/plain" }, + ], + }) + ); } #[test] +#[cfg(not(feature = "unstable-msc1767"))] fn relates_to_content_serialization() { let message_event_content = assign!(RoomMessageEventContent::text_plain("> <@test:example.com> test\n\ntest reply"), {