Move new_content and relates_to fields to MessageEventContent struct

MessageEventContent used to be an enum, it now has a msgtype field where
the enum (now called MessageType) sits.
This commit is contained in:
Devin Ragotzy 2021-02-12 18:29:07 -05:00 committed by GitHub
parent 798cd49e9e
commit 12c294422b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 194 additions and 139 deletions

View File

@ -5,7 +5,7 @@ use http::Uri;
use ruma::{ use ruma::{
api::client::r0::{filter::FilterDefinition, sync::sync_events}, api::client::r0::{filter::FilterDefinition, sync::sync_events},
events::{ events::{
room::message::{MessageEventContent, TextMessageEventContent}, room::message::{MessageEventContent, MessageType, TextMessageEventContent},
AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent, AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent,
}, },
presence::PresenceState, presence::PresenceState,
@ -40,9 +40,13 @@ async fn log_messages(homeserver_url: Uri, username: &str, password: &str) -> an
if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage( if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(
SyncMessageEvent { SyncMessageEvent {
content: content:
MessageEventContent::Text(TextMessageEventContent { MessageEventContent {
msgtype:
MessageType::Text(TextMessageEventContent {
body: msg_body, .. body: msg_body, ..
}), }),
..
},
sender, sender,
.. ..
}, },

View File

@ -34,8 +34,63 @@ pub type MessageEvent = OuterMessageEvent<MessageEventContent>;
#[derive(Clone, Debug, Serialize, MessageEventContent)] #[derive(Clone, Debug, Serialize, MessageEventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.room.message")] #[ruma_event(type = "m.room.message")]
#[serde(untagged)] pub struct MessageEventContent {
pub enum 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<Relation>,
/// 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<Box<MessageEventContent>>,
}
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<String>) -> Self {
Self::new(MessageType::Text(TextMessageEventContent::plain(body)))
}
/// A constructor to create an html message.
pub fn text_html(body: impl Into<String>, html_body: impl Into<String>) -> 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<String>) -> Self {
Self::new(MessageType::Notice(NoticeMessageEventContent::plain(body)))
}
/// A constructor to create an html notice.
pub fn notice_html(body: impl Into<String>, html_body: impl Into<String>) -> 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. /// An audio message.
Audio(AudioMessageEventContent), Audio(AudioMessageEventContent),
@ -72,6 +127,12 @@ pub enum MessageEventContent {
_Custom(CustomEventContent), _Custom(CustomEventContent),
} }
impl From<MessageType> for MessageEventContent {
fn from(msgtype: MessageType) -> Self {
Self::new(msgtype)
}
}
/// Enum modeling the different ways relationships can be expressed in a /// Enum modeling the different ways relationships can be expressed in a
/// `m.relates_to` field of an m.room.message event. /// `m.relates_to` field of an m.room.message event.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
@ -132,28 +193,6 @@ impl From<RelatesToJsonRepr> for Relation {
} }
} }
impl MessageEventContent {
/// A convenience constructor to create a plain text message.
pub fn text_plain(body: impl Into<String>) -> Self {
Self::Text(TextMessageEventContent::plain(body))
}
/// A convenience constructor to create an html message.
pub fn text_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self::Text(TextMessageEventContent::html(body, html_body))
}
/// A convenience constructor to create an plain text notice.
pub fn notice_plain(body: impl Into<String>) -> Self {
Self::Notice(NoticeMessageEventContent::plain(body))
}
/// A convenience constructor to create an html notice.
pub fn notice_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self::Notice(NoticeMessageEventContent::html(body, html_body))
}
}
/// The payload for an audio message. /// The payload for an audio message.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "msgtype", rename = "m.audio")] #[serde(tag = "msgtype", rename = "m.audio")]
@ -321,30 +360,12 @@ pub struct NoticeMessageEventContent {
/// Formatted form of the message `body`. /// Formatted form of the message `body`.
#[serde(flatten)] #[serde(flatten)]
pub formatted: Option<FormattedBody>, pub formatted: Option<FormattedBody>,
/// 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<Relation>,
/// 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<Box<MessageEventContent>>,
} }
impl NoticeMessageEventContent { impl NoticeMessageEventContent {
/// A convenience constructor to create a plain text notice. /// A convenience constructor to create a plain text notice.
pub fn plain(body: impl Into<String>) -> Self { pub fn plain(body: impl Into<String>) -> Self {
Self { Self { body: body.into(), formatted: None }
body: body.into(),
formatted: None,
relates_to: None,
#[cfg(feature = "unstable-pre-spec")]
new_content: None,
}
} }
/// A convenience constructor to create an html notice. /// A convenience constructor to create an html notice.
@ -453,30 +474,12 @@ pub struct TextMessageEventContent {
/// Formatted form of the message `body`. /// Formatted form of the message `body`.
#[serde(flatten)] #[serde(flatten)]
pub formatted: Option<FormattedBody>, pub formatted: Option<FormattedBody>,
/// 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<Relation>,
/// 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<Box<MessageEventContent>>,
} }
impl TextMessageEventContent { impl TextMessageEventContent {
/// A convenience constructor to create a plain text message. /// A convenience constructor to create a plain text message.
pub fn plain(body: impl Into<String>) -> Self { pub fn plain(body: impl Into<String>) -> Self {
Self { Self { body: body.into(), formatted: None }
body: body.into(),
formatted: None,
relates_to: None,
#[cfg(feature = "unstable-pre-spec")]
new_content: None,
}
} }
/// A convenience constructor to create an html message. /// A convenience constructor to create an html message.

View File

@ -1,15 +1,23 @@
//! `Deserialize` implementation for MessageEventContent //! `Deserialize` implementation for MessageEventContent and MessageType.
use serde::{de, Deserialize}; use serde::{de, Deserialize};
use serde_json::value::RawValue as RawJsonValue; 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)] #[derive(Debug, Deserialize)]
struct MessageDeHelper { struct MessageContentDeHelper {
/// The message type field #[serde(rename = "m.relates_to")]
msgtype: String, relates_to: Option<Relation>,
#[cfg(feature = "unstable-pre-spec")]
#[serde(rename = "m.new_content")]
new_content: Option<Box<MessageEventContent>>,
} }
impl<'de> de::Deserialize<'de> for MessageEventContent { impl<'de> de::Deserialize<'de> for MessageEventContent {
@ -18,7 +26,31 @@ impl<'de> de::Deserialize<'de> for MessageEventContent {
D: de::Deserializer<'de>, D: de::Deserializer<'de>,
{ {
let json = Box::<RawJsonValue>::deserialize(deserializer)?; let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let MessageDeHelper { msgtype } = from_raw_json_value(&json)?; let helper = from_raw_json_value::<MessageContentDeHelper, D::Error>(&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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let MessageTypeDeHelper { msgtype } = from_raw_json_value(&json)?;
Ok(match msgtype.as_ref() { Ok(match msgtype.as_ref() {
"m.audio" => Self::Audio(from_raw_json_value(&json)?), "m.audio" => Self::Audio(from_raw_json_value(&json)?),

View File

@ -5,7 +5,7 @@ use serde_json::{from_value as from_json_value, json, Value as JsonValue};
use ruma_events::{ use ruma_events::{
room::{ room::{
aliases::AliasesEventContent, aliases::AliasesEventContent,
message::{MessageEventContent, TextMessageEventContent}, message::{MessageEventContent, MessageType, TextMessageEventContent},
power_levels::PowerLevelsEventContent, power_levels::PowerLevelsEventContent,
}, },
AnyEvent, AnyMessageEvent, AnyRoomEvent, AnyStateEvent, AnyStateEventContent, AnyEvent, AnyMessageEvent, AnyRoomEvent, AnyStateEvent, AnyStateEventContent,
@ -137,13 +137,15 @@ fn message_event_sync_deserialization() {
from_json_value::<AnySyncRoomEvent>(json_data), from_json_value::<AnySyncRoomEvent>(json_data),
Ok(AnySyncRoomEvent::Message( Ok(AnySyncRoomEvent::Message(
AnySyncMessageEvent::RoomMessage(SyncMessageEvent { AnySyncMessageEvent::RoomMessage(SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { content: MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent {
body, body,
formatted: Some(formatted), formatted: Some(formatted),
relates_to: None,
.. ..
}), }),
.. ..
},
..
}) })
)) ))
if body == "baba" && formatted.body == "<strong>baba</strong>" if body == "baba" && formatted.body == "<strong>baba</strong>"
@ -177,13 +179,15 @@ fn message_room_event_deserialization() {
from_json_value::<AnyRoomEvent>(json_data), from_json_value::<AnyRoomEvent>(json_data),
Ok(AnyRoomEvent::Message( Ok(AnyRoomEvent::Message(
AnyMessageEvent::RoomMessage(MessageEvent { AnyMessageEvent::RoomMessage(MessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { content: MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent {
body, body,
formatted: Some(formatted), formatted: Some(formatted),
relates_to: None,
.. ..
}), }),
.. ..
},
..
}) })
)) ))
if body == "baba" && formatted.body == "<strong>baba</strong>" if body == "baba" && formatted.body == "<strong>baba</strong>"
@ -217,13 +221,15 @@ fn message_event_deserialization() {
from_json_value::<AnyEvent>(json_data), from_json_value::<AnyEvent>(json_data),
Ok(AnyEvent::Message( Ok(AnyEvent::Message(
AnyMessageEvent::RoomMessage(MessageEvent { AnyMessageEvent::RoomMessage(MessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { content: MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent {
body, body,
formatted: Some(formatted), formatted: Some(formatted),
relates_to: None,
.. ..
}), }),
.. ..
},
..
}) })
)) ))
if body == "baba" && formatted.body == "<strong>baba</strong>" if body == "baba" && formatted.body == "<strong>baba</strong>"

View File

@ -11,7 +11,7 @@ use ruma_events::{
room::{ room::{
message::{ message::{
AudioMessageEventContent, CustomEventContent, MessageEvent, MessageEventContent, AudioMessageEventContent, CustomEventContent, MessageEvent, MessageEventContent,
Relation, TextMessageEventContent, MessageType, Relation, TextMessageEventContent,
}, },
relationships::InReplyTo, relationships::InReplyTo,
}, },
@ -26,12 +26,12 @@ use serde_json::{from_value as from_json_value, json, to_value as to_json_value}
#[test] #[test]
fn serialization() { fn serialization() {
let ev = MessageEvent { let ev = MessageEvent {
content: MessageEventContent::Audio(AudioMessageEventContent { content: MessageEventContent::new(MessageType::Audio(AudioMessageEventContent {
body: "test".into(), body: "test".into(),
info: None, info: None,
url: Some("http://example.com/audio.mp3".into()), url: Some("http://example.com/audio.mp3".into()),
file: None, file: None,
}), })),
event_id: event_id!("$143273582443PhrSn:example.org"), event_id: event_id!("$143273582443PhrSn:example.org"),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(10_000), origin_server_ts: UNIX_EPOCH + Duration::from_millis(10_000),
room_id: room_id!("!testroomid:example.org"), room_id: room_id!("!testroomid:example.org"),
@ -58,12 +58,13 @@ fn serialization() {
#[test] #[test]
fn content_serialization() { fn content_serialization() {
let message_event_content = MessageEventContent::Audio(AudioMessageEventContent { let message_event_content =
MessageEventContent::new(MessageType::Audio(AudioMessageEventContent {
body: "test".into(), body: "test".into(),
info: None, info: None,
url: Some("http://example.com/audio.mp3".into()), url: Some("http://example.com/audio.mp3".into()),
file: None, file: None,
}); }));
assert_eq!( assert_eq!(
to_json_value(&message_event_content).unwrap(), to_json_value(&message_event_content).unwrap(),
@ -81,7 +82,7 @@ fn custom_content_serialization() {
"custom_field".into() => json!("baba"), "custom_field".into() => json!("baba"),
"another_one".into() => json!("abab"), "another_one".into() => json!("abab"),
}; };
let custom_event_content = MessageEventContent::_Custom(CustomEventContent { let custom_event_content = MessageType::_Custom(CustomEventContent {
msgtype: "my_custom_msgtype".into(), msgtype: "my_custom_msgtype".into(),
data: json_data, data: json_data,
}); });
@ -110,11 +111,11 @@ fn custom_content_deserialization() {
}; };
assert_matches!( assert_matches!(
from_json_value::<Raw<MessageEventContent>>(json_data) from_json_value::<Raw<MessageType>>(json_data)
.unwrap() .unwrap()
.deserialize() .deserialize()
.unwrap(), .unwrap(),
MessageEventContent::_Custom(CustomEventContent { MessageType::_Custom(CustomEventContent {
msgtype, msgtype,
data data
}) if msgtype == "my_custom_msgtype" }) if msgtype == "my_custom_msgtype"
@ -124,10 +125,8 @@ fn custom_content_deserialization() {
#[test] #[test]
fn formatted_body_serialization() { fn formatted_body_serialization() {
let message_event_content = MessageEventContent::Text(TextMessageEventContent::html( let message_event_content =
"Hello, World!", MessageEventContent::text_html("Hello, World!", "Hello, <em>World</em>!");
"Hello, <em>World</em>!",
));
assert_eq!( assert_eq!(
to_json_value(&message_event_content).unwrap(), to_json_value(&message_event_content).unwrap(),
@ -142,9 +141,8 @@ fn formatted_body_serialization() {
#[test] #[test]
fn plain_text_content_serialization() { fn plain_text_content_serialization() {
let message_event_content = MessageEventContent::Text(TextMessageEventContent::plain( let message_event_content =
"> <@test:example.com> test\n\ntest reply", MessageEventContent::text_plain("> <@test:example.com> test\n\ntest reply");
));
assert_eq!( assert_eq!(
to_json_value(&message_event_content).unwrap(), to_json_value(&message_event_content).unwrap(),
@ -157,13 +155,12 @@ fn plain_text_content_serialization() {
#[test] #[test]
fn relates_to_content_serialization() { fn relates_to_content_serialization() {
let message_event_content = MessageEventContent::Text( let message_event_content =
assign!(TextMessageEventContent::plain("> <@test:example.com> test\n\ntest reply"), { assign!(MessageEventContent::text_plain("> <@test:example.com> test\n\ntest reply"), {
relates_to: Some(Relation::Reply { relates_to: Some(Relation::Reply {
in_reply_to: InReplyTo { event_id: event_id!("$15827405538098VGFWH:example.com") }, in_reply_to: InReplyTo { event_id: event_id!("$15827405538098VGFWH:example.com") },
}), }),
}), });
);
let json_data = json!({ let json_data = json!({
"body": "> <@test:example.com> test\n\ntest reply", "body": "> <@test:example.com> test\n\ntest reply",
@ -195,12 +192,15 @@ fn edit_deserialization_061() {
assert_matches!( assert_matches!(
from_json_value::<MessageEventContent>(json_data).unwrap(), from_json_value::<MessageEventContent>(json_data).unwrap(),
MessageEventContent::Text(TextMessageEventContent { MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent {
body, body,
formatted: None, formatted: None,
..
}),
relates_to: Some(Relation::Custom(_)), 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!( assert_matches!(
from_json_value::<MessageEventContent>(json_data).unwrap(), from_json_value::<MessageEventContent>(json_data).unwrap(),
MessageEventContent::Text(TextMessageEventContent { MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent {
body, body,
formatted: None, formatted: None,
..
}),
relates_to: Some(Relation::Replacement(Replacement { event_id })), relates_to: Some(Relation::Replacement(Replacement { event_id })),
new_content: Some(new_content), new_content: Some(new_content),
.. ..
}) if body == "s/foo/bar" } if body == "s/foo/bar"
&& event_id == ev_id && event_id == ev_id
&& matches!( && matches!(
&*new_content, &*new_content,
MessageEventContent::Text(TextMessageEventContent { MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent {
body, body,
formatted: None, formatted: None,
relates_to: None,
new_content: None,
.. ..
}) if body == "bar" }),
..
} if body == "bar"
) )
); );
} }
@ -266,12 +270,15 @@ fn verification_request_deserialization() {
assert_matches!( assert_matches!(
from_json_value::<MessageEventContent>(json_data).unwrap(), from_json_value::<MessageEventContent>(json_data).unwrap(),
MessageEventContent::VerificationRequest(KeyVerificationRequestEventContent { MessageEventContent {
msgtype: MessageType::VerificationRequest(KeyVerificationRequestEventContent {
body, body,
to, to,
from_device, from_device,
methods, methods,
}) if body == "@example:localhost is requesting to verify your key, ..." }),
..
} if body == "@example:localhost is requesting to verify your key, ..."
&& to == user_id && to == user_id
&& from_device == device_id && from_device == device_id
&& methods.contains(&VerificationMethod::MSasV1) && methods.contains(&VerificationMethod::MSasV1)
@ -299,7 +306,7 @@ fn verification_request_serialization() {
"methods": methods "methods": methods
}); });
let content = MessageEventContent::VerificationRequest(KeyVerificationRequestEventContent { let content = MessageType::VerificationRequest(KeyVerificationRequestEventContent {
to: user_id, to: user_id,
from_device: device_id, from_device: device_id,
body, body,
@ -322,12 +329,15 @@ fn content_deserialization() {
.unwrap() .unwrap()
.deserialize() .deserialize()
.unwrap(), .unwrap(),
MessageEventContent::Audio(AudioMessageEventContent { MessageEventContent {
msgtype: MessageType::Audio(AudioMessageEventContent {
body, body,
info: None, info: None,
url: Some(url), url: Some(url),
file: None, file: None,
}) if body == "test" && url == "http://example.com/audio.mp3" }),
..
} if body == "test" && url == "http://example.com/audio.mp3"
); );
} }