diff --git a/ruma-client/examples/message_log.rs b/ruma-client/examples/message_log.rs index b6928454..b05bce1e 100644 --- a/ruma-client/examples/message_log.rs +++ b/ruma-client/examples/message_log.rs @@ -5,7 +5,7 @@ use http::Uri; use ruma::{ api::client::r0::{filter::FilterDefinition, sync::sync_events}, events::{ - room::message::{MessageEventContent, TextMessageEventContent}, + room::message::{MessageEventContent, MessageType, TextMessageEventContent}, AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent, }, presence::PresenceState, @@ -40,9 +40,13 @@ async fn log_messages(homeserver_url: Uri, username: &str, password: &str) -> an if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage( SyncMessageEvent { content: - MessageEventContent::Text(TextMessageEventContent { - body: msg_body, .. - }), + MessageEventContent { + msgtype: + MessageType::Text(TextMessageEventContent { + body: msg_body, .. + }), + .. + }, sender, .. }, diff --git a/ruma-events/src/room/message.rs b/ruma-events/src/room/message.rs index 44ab325d..c5ec5359 100644 --- a/ruma-events/src/room/message.rs +++ b/ruma-events/src/room/message.rs @@ -34,8 +34,63 @@ pub type MessageEvent = OuterMessageEvent; #[derive(Clone, Debug, Serialize, MessageEventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.message")] -#[serde(untagged)] -pub enum MessageEventContent { +pub struct MessageEventContent { + /// A key which identifies the type of message being sent. + /// + /// This also holds the specific content of each message. + #[serde(flatten)] + pub msgtype: MessageType, + + /// Information about related messages for + /// [rich replies](https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies). + #[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")] + pub relates_to: Option, + + /// New content of an edited message. + /// + /// This should only be set if `relates_to` is `Some(Relation::Replacement(_))`. + #[cfg(feature = "unstable-pre-spec")] + #[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")] + pub new_content: Option>, +} + +impl MessageEventContent { + /// Create a `MessageEventContent` with the given `MessageType`. + pub fn new(msgtype: MessageType) -> Self { + Self { + msgtype, + relates_to: None, + #[cfg(feature = "unstable-pre-spec")] + new_content: None, + } + } + + /// A constructor to create a plain text message. + pub fn text_plain(body: impl Into) -> Self { + Self::new(MessageType::Text(TextMessageEventContent::plain(body))) + } + + /// A constructor to create an html message. + pub fn text_html(body: impl Into, html_body: impl Into) -> Self { + Self::new(MessageType::Text(TextMessageEventContent::html(body, html_body))) + } + + /// A constructor to create a plain text notice. + pub fn notice_plain(body: impl Into) -> Self { + Self::new(MessageType::Notice(NoticeMessageEventContent::plain(body))) + } + + /// A constructor to create an html notice. + pub fn notice_html(body: impl Into, html_body: impl Into) -> Self { + Self::new(MessageType::Notice(NoticeMessageEventContent::html(body, html_body))) + } +} + +/// The content that is specific to each message type variant. +#[derive(Clone, Debug, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[serde(tag = "msgtype")] +pub enum MessageType { /// An audio message. Audio(AudioMessageEventContent), @@ -72,6 +127,12 @@ pub enum MessageEventContent { _Custom(CustomEventContent), } +impl From for MessageEventContent { + fn from(msgtype: MessageType) -> Self { + Self::new(msgtype) + } +} + /// Enum modeling the different ways relationships can be expressed in a /// `m.relates_to` field of an m.room.message event. #[derive(Clone, Debug, Deserialize, Serialize)] @@ -132,28 +193,6 @@ impl From for Relation { } } -impl MessageEventContent { - /// A convenience constructor to create a plain text message. - pub fn text_plain(body: impl Into) -> Self { - Self::Text(TextMessageEventContent::plain(body)) - } - - /// A convenience constructor to create an html message. - pub fn text_html(body: impl Into, html_body: impl Into) -> Self { - Self::Text(TextMessageEventContent::html(body, html_body)) - } - - /// A convenience constructor to create an plain text notice. - pub fn notice_plain(body: impl Into) -> Self { - Self::Notice(NoticeMessageEventContent::plain(body)) - } - - /// A convenience constructor to create an html notice. - pub fn notice_html(body: impl Into, html_body: impl Into) -> Self { - Self::Notice(NoticeMessageEventContent::html(body, html_body)) - } -} - /// The payload for an audio message. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "msgtype", rename = "m.audio")] @@ -321,30 +360,12 @@ pub struct NoticeMessageEventContent { /// Formatted form of the message `body`. #[serde(flatten)] pub formatted: Option, - - /// Information about related messages for - /// [rich replies](https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies). - #[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")] - pub relates_to: Option, - - /// New content of an edited message. - /// - /// This should only be set if `relates_to` is `Some(Relation::Replacement(_))`. - #[cfg(feature = "unstable-pre-spec")] - #[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")] - pub new_content: 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, - relates_to: None, - #[cfg(feature = "unstable-pre-spec")] - new_content: None, - } + Self { body: body.into(), formatted: None } } /// A convenience constructor to create an html notice. @@ -453,30 +474,12 @@ pub struct TextMessageEventContent { /// Formatted form of the message `body`. #[serde(flatten)] pub formatted: Option, - - /// Information about related messages for - /// [rich replies](https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies). - #[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")] - pub relates_to: Option, - - /// New content of an edited message. - /// - /// This should only be set if `relates_to` is `Some(Relation::Replacement(_))`. - #[cfg(feature = "unstable-pre-spec")] - #[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")] - pub new_content: 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, - relates_to: None, - #[cfg(feature = "unstable-pre-spec")] - new_content: None, - } + Self { body: body.into(), formatted: None } } /// A convenience constructor to create an html message. diff --git a/ruma-events/src/room/message/content_serde.rs b/ruma-events/src/room/message/content_serde.rs index c618ed61..4011d2bb 100644 --- a/ruma-events/src/room/message/content_serde.rs +++ b/ruma-events/src/room/message/content_serde.rs @@ -1,15 +1,23 @@ -//! `Deserialize` implementation for MessageEventContent +//! `Deserialize` implementation for MessageEventContent and MessageType. use serde::{de, Deserialize}; use serde_json::value::RawValue as RawJsonValue; -use crate::{from_raw_json_value, room::message::MessageEventContent}; +use crate::{ + from_raw_json_value, + room::message::{MessageEventContent, MessageType, Relation}, +}; -/// Helper struct to determine the msgtype from a `serde_json::value::RawValue` +/// Helper struct to determine the msgtype, relates_to and new_content fields +/// from a `serde_json::value::RawValue` #[derive(Debug, Deserialize)] -struct MessageDeHelper { - /// The message type field - msgtype: String, +struct MessageContentDeHelper { + #[serde(rename = "m.relates_to")] + relates_to: Option, + + #[cfg(feature = "unstable-pre-spec")] + #[serde(rename = "m.new_content")] + new_content: Option>, } impl<'de> de::Deserialize<'de> for MessageEventContent { @@ -18,7 +26,31 @@ impl<'de> de::Deserialize<'de> for MessageEventContent { D: de::Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; - let MessageDeHelper { msgtype } = from_raw_json_value(&json)?; + let helper = from_raw_json_value::(&json)?; + + Ok(Self { + msgtype: from_raw_json_value(&json)?, + relates_to: helper.relates_to, + #[cfg(feature = "unstable-pre-spec")] + new_content: helper.new_content, + }) + } +} + +/// Helper struct to determine the msgtype from a `serde_json::value::RawValue` +#[derive(Debug, Deserialize)] +struct MessageTypeDeHelper { + /// The message type field + msgtype: String, +} + +impl<'de> de::Deserialize<'de> for MessageType { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let json = Box::::deserialize(deserializer)?; + let MessageTypeDeHelper { msgtype } = from_raw_json_value(&json)?; Ok(match msgtype.as_ref() { "m.audio" => Self::Audio(from_raw_json_value(&json)?), diff --git a/ruma-events/tests/enums.rs b/ruma-events/tests/enums.rs index 94ee7313..73c9439b 100644 --- a/ruma-events/tests/enums.rs +++ b/ruma-events/tests/enums.rs @@ -5,7 +5,7 @@ use serde_json::{from_value as from_json_value, json, Value as JsonValue}; use ruma_events::{ room::{ aliases::AliasesEventContent, - message::{MessageEventContent, TextMessageEventContent}, + message::{MessageEventContent, MessageType, TextMessageEventContent}, power_levels::PowerLevelsEventContent, }, AnyEvent, AnyMessageEvent, AnyRoomEvent, AnyStateEvent, AnyStateEventContent, @@ -137,12 +137,14 @@ fn message_event_sync_deserialization() { from_json_value::(json_data), Ok(AnySyncRoomEvent::Message( AnySyncMessageEvent::RoomMessage(SyncMessageEvent { - content: MessageEventContent::Text(TextMessageEventContent { - body, - formatted: Some(formatted), - relates_to: None, + content: MessageEventContent { + msgtype: MessageType::Text(TextMessageEventContent { + body, + formatted: Some(formatted), + .. + }), .. - }), + }, .. }) )) @@ -177,12 +179,14 @@ fn message_room_event_deserialization() { from_json_value::(json_data), Ok(AnyRoomEvent::Message( AnyMessageEvent::RoomMessage(MessageEvent { - content: MessageEventContent::Text(TextMessageEventContent { - body, - formatted: Some(formatted), - relates_to: None, + content: MessageEventContent { + msgtype: MessageType::Text(TextMessageEventContent { + body, + formatted: Some(formatted), + .. + }), .. - }), + }, .. }) )) @@ -217,12 +221,14 @@ fn message_event_deserialization() { from_json_value::(json_data), Ok(AnyEvent::Message( AnyMessageEvent::RoomMessage(MessageEvent { - content: MessageEventContent::Text(TextMessageEventContent { - body, - formatted: Some(formatted), - relates_to: None, + content: MessageEventContent { + msgtype: MessageType::Text(TextMessageEventContent { + body, + formatted: Some(formatted), + .. + }), .. - }), + }, .. }) )) diff --git a/ruma-events/tests/room_message.rs b/ruma-events/tests/room_message.rs index a19d08a3..aaa4cadf 100644 --- a/ruma-events/tests/room_message.rs +++ b/ruma-events/tests/room_message.rs @@ -11,7 +11,7 @@ use ruma_events::{ room::{ message::{ AudioMessageEventContent, CustomEventContent, MessageEvent, MessageEventContent, - Relation, TextMessageEventContent, + MessageType, Relation, TextMessageEventContent, }, relationships::InReplyTo, }, @@ -26,12 +26,12 @@ use serde_json::{from_value as from_json_value, json, to_value as to_json_value} #[test] fn serialization() { let ev = MessageEvent { - content: MessageEventContent::Audio(AudioMessageEventContent { + content: MessageEventContent::new(MessageType::Audio(AudioMessageEventContent { body: "test".into(), info: None, url: Some("http://example.com/audio.mp3".into()), file: None, - }), + })), event_id: event_id!("$143273582443PhrSn:example.org"), origin_server_ts: UNIX_EPOCH + Duration::from_millis(10_000), room_id: room_id!("!testroomid:example.org"), @@ -58,12 +58,13 @@ fn serialization() { #[test] fn content_serialization() { - let message_event_content = MessageEventContent::Audio(AudioMessageEventContent { - body: "test".into(), - info: None, - url: Some("http://example.com/audio.mp3".into()), - file: None, - }); + let message_event_content = + MessageEventContent::new(MessageType::Audio(AudioMessageEventContent { + body: "test".into(), + info: None, + url: Some("http://example.com/audio.mp3".into()), + file: None, + })); assert_eq!( to_json_value(&message_event_content).unwrap(), @@ -81,7 +82,7 @@ fn custom_content_serialization() { "custom_field".into() => json!("baba"), "another_one".into() => json!("abab"), }; - let custom_event_content = MessageEventContent::_Custom(CustomEventContent { + let custom_event_content = MessageType::_Custom(CustomEventContent { msgtype: "my_custom_msgtype".into(), data: json_data, }); @@ -110,11 +111,11 @@ fn custom_content_deserialization() { }; assert_matches!( - from_json_value::>(json_data) + from_json_value::>(json_data) .unwrap() .deserialize() .unwrap(), - MessageEventContent::_Custom(CustomEventContent { + MessageType::_Custom(CustomEventContent { msgtype, data }) if msgtype == "my_custom_msgtype" @@ -124,10 +125,8 @@ fn custom_content_deserialization() { #[test] fn formatted_body_serialization() { - let message_event_content = MessageEventContent::Text(TextMessageEventContent::html( - "Hello, World!", - "Hello, World!", - )); + let message_event_content = + MessageEventContent::text_html("Hello, World!", "Hello, World!"); assert_eq!( to_json_value(&message_event_content).unwrap(), @@ -142,9 +141,8 @@ fn formatted_body_serialization() { #[test] fn plain_text_content_serialization() { - let message_event_content = MessageEventContent::Text(TextMessageEventContent::plain( - "> <@test:example.com> test\n\ntest reply", - )); + let message_event_content = + MessageEventContent::text_plain("> <@test:example.com> test\n\ntest reply"); assert_eq!( to_json_value(&message_event_content).unwrap(), @@ -157,13 +155,12 @@ fn plain_text_content_serialization() { #[test] fn relates_to_content_serialization() { - let message_event_content = MessageEventContent::Text( - assign!(TextMessageEventContent::plain("> <@test:example.com> test\n\ntest reply"), { + let message_event_content = + assign!(MessageEventContent::text_plain("> <@test:example.com> test\n\ntest reply"), { relates_to: Some(Relation::Reply { in_reply_to: InReplyTo { event_id: event_id!("$15827405538098VGFWH:example.com") }, }), - }), - ); + }); let json_data = json!({ "body": "> <@test:example.com> test\n\ntest reply", @@ -195,12 +192,15 @@ fn edit_deserialization_061() { assert_matches!( from_json_value::(json_data).unwrap(), - MessageEventContent::Text(TextMessageEventContent { - body, - formatted: None, + MessageEventContent { + msgtype: MessageType::Text(TextMessageEventContent { + body, + formatted: None, + .. + }), relates_to: Some(Relation::Custom(_)), .. - }) if body == "s/foo/bar" + } if body == "s/foo/bar" ); } @@ -225,23 +225,27 @@ fn edit_deserialization_future() { assert_matches!( from_json_value::(json_data).unwrap(), - MessageEventContent::Text(TextMessageEventContent { - body, - formatted: None, + MessageEventContent { + msgtype: MessageType::Text(TextMessageEventContent { + body, + formatted: None, + .. + }), relates_to: Some(Relation::Replacement(Replacement { event_id })), new_content: Some(new_content), .. - }) if body == "s/foo/bar" + } if body == "s/foo/bar" && event_id == ev_id && matches!( &*new_content, - MessageEventContent::Text(TextMessageEventContent { - body, - formatted: None, - relates_to: None, - new_content: None, + MessageEventContent { + msgtype: MessageType::Text(TextMessageEventContent { + body, + formatted: None, + .. + }), .. - }) if body == "bar" + } if body == "bar" ) ); } @@ -266,12 +270,15 @@ fn verification_request_deserialization() { assert_matches!( from_json_value::(json_data).unwrap(), - MessageEventContent::VerificationRequest(KeyVerificationRequestEventContent { - body, - to, - from_device, - methods, - }) if body == "@example:localhost is requesting to verify your key, ..." + MessageEventContent { + msgtype: MessageType::VerificationRequest(KeyVerificationRequestEventContent { + body, + to, + from_device, + methods, + }), + .. + } if body == "@example:localhost is requesting to verify your key, ..." && to == user_id && from_device == device_id && methods.contains(&VerificationMethod::MSasV1) @@ -299,7 +306,7 @@ fn verification_request_serialization() { "methods": methods }); - let content = MessageEventContent::VerificationRequest(KeyVerificationRequestEventContent { + let content = MessageType::VerificationRequest(KeyVerificationRequestEventContent { to: user_id, from_device: device_id, body, @@ -322,12 +329,15 @@ fn content_deserialization() { .unwrap() .deserialize() .unwrap(), - MessageEventContent::Audio(AudioMessageEventContent { - body, - info: None, - url: Some(url), - file: None, - }) if body == "test" && url == "http://example.com/audio.mp3" + MessageEventContent { + msgtype: MessageType::Audio(AudioMessageEventContent { + body, + info: None, + url: Some(url), + file: None, + }), + .. + } if body == "test" && url == "http://example.com/audio.mp3" ); }