diff --git a/crates/ruma-events/CHANGELOG.md b/crates/ruma-events/CHANGELOG.md index 1e566deb..257d0c27 100644 --- a/crates/ruma-events/CHANGELOG.md +++ b/crates/ruma-events/CHANGELOG.md @@ -6,7 +6,7 @@ Breaking changes: Improvements: -* Add unstable support for extensible text message events ([MSC1767](https://github.com/matrix-org/matrix-spec-proposals/pull/1767)) +* Add unstable support for extensible text, notice and emote message events ([MSC1767](https://github.com/matrix-org/matrix-spec-proposals/pull/1767)) # 0.26.0 diff --git a/crates/ruma-events/src/emote.rs b/crates/ruma-events/src/emote.rs new file mode 100644 index 00000000..c9eafc10 --- /dev/null +++ b/crates/ruma-events/src/emote.rs @@ -0,0 +1,54 @@ +//! Types for extensible emote message events ([MSC1767]). +//! +//! [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 + +use ruma_macros::EventContent; +use serde::{Deserialize, Serialize}; + +use crate::{ + message::{MessageContent, TextMessage}, + room::message::Relation, +}; + +/// The payload for an extensible emote message. +#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[ruma_event(type = "m.emote", kind = MessageLike)] +pub struct EmoteEventContent { + /// The message's text content. + #[serde(flatten)] + pub message: MessageContent, + + /// Information about related messages for [rich replies]. + /// + /// [rich replies]: https://spec.matrix.org/v1.2/client-server-api/#rich-replies + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub relates_to: Option, +} + +impl EmoteEventContent { + /// A convenience constructor to create a plain text message. + pub fn plain(body: impl Into) -> Self { + Self { message: MessageContent::plain(body), relates_to: None } + } + + /// A convenience constructor to create an HTML message. + pub fn html(body: impl Into, html_body: impl Into) -> Self { + Self { message: MessageContent::html(body, html_body), relates_to: None } + } + + /// A convenience constructor to create a Markdown message. + /// + /// Returns an HTML message if some Markdown formatting was detected, otherwise returns a plain + /// text message. + #[cfg(feature = "markdown")] + pub fn markdown(body: impl AsRef + Into) -> Self { + Self { message: MessageContent::markdown(body), relates_to: None } + } +} + +impl TextMessage for EmoteEventContent { + fn message(&self) -> &MessageContent { + &self.message + } +} diff --git a/crates/ruma-events/src/enums.rs b/crates/ruma-events/src/enums.rs index 4e5ce970..b202bebd 100644 --- a/crates/ruma-events/src/enums.rs +++ b/crates/ruma-events/src/enums.rs @@ -37,6 +37,8 @@ event_enum! { "m.call.invite", "m.call.hangup", "m.call.candidates", + #[cfg(feature = "unstable-msc1767")] + "m.emote", "m.key.verification.ready", "m.key.verification.start", "m.key.verification.cancel", @@ -46,6 +48,8 @@ event_enum! { "m.key.verification.done", #[cfg(feature = "unstable-msc1767")] "m.message", + #[cfg(feature = "unstable-msc1767")] + "m.notice", #[cfg(feature = "unstable-msc2677")] "m.reaction", "m.room.encrypted", @@ -356,6 +360,10 @@ impl AnyMessageLikeEventContent { Self::RoomMessage(ev) => ev.relates_to.clone().map(Into::into), #[cfg(feature = "unstable-msc1767")] Self::Message(ev) => ev.relates_to.clone().map(Into::into), + #[cfg(feature = "unstable-msc1767")] + Self::Notice(ev) => ev.relates_to.clone().map(Into::into), + #[cfg(feature = "unstable-msc1767")] + Self::Emote(ev) => ev.relates_to.clone().map(Into::into), Self::CallAnswer(_) | Self::CallInvite(_) | Self::CallHangup(_) diff --git a/crates/ruma-events/src/lib.rs b/crates/ruma-events/src/lib.rs index 23abdb14..0e6a858f 100644 --- a/crates/ruma-events/src/lib.rs +++ b/crates/ruma-events/src/lib.rs @@ -172,12 +172,16 @@ pub mod macros { pub mod call; pub mod direct; pub mod dummy; +#[cfg(feature = "unstable-msc1767")] +pub mod emote; pub mod forwarded_room_key; pub mod fully_read; pub mod ignored_user_list; pub mod key; #[cfg(feature = "unstable-msc1767")] pub mod message; +#[cfg(feature = "unstable-msc1767")] +pub mod notice; #[cfg(feature = "unstable-pdu")] pub mod pdu; pub mod policy; diff --git a/crates/ruma-events/src/notice.rs b/crates/ruma-events/src/notice.rs new file mode 100644 index 00000000..61c2831b --- /dev/null +++ b/crates/ruma-events/src/notice.rs @@ -0,0 +1,54 @@ +//! Types for extensible notice message events ([MSC1767]). +//! +//! [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 + +use ruma_macros::EventContent; +use serde::{Deserialize, Serialize}; + +use crate::{ + message::{MessageContent, TextMessage}, + room::message::Relation, +}; + +/// The payload for an extensible notice message. +#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[ruma_event(type = "m.notice", kind = MessageLike)] +pub struct NoticeEventContent { + /// The message's text content. + #[serde(flatten)] + pub message: MessageContent, + + /// Information about related messages for [rich replies]. + /// + /// [rich replies]: https://spec.matrix.org/v1.2/client-server-api/#rich-replies + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub relates_to: Option, +} + +impl NoticeEventContent { + /// A convenience constructor to create a plain text message. + pub fn plain(body: impl Into) -> Self { + Self { message: MessageContent::plain(body), relates_to: None } + } + + /// A convenience constructor to create an HTML message. + pub fn html(body: impl Into, html_body: impl Into) -> Self { + Self { message: MessageContent::html(body, html_body), relates_to: None } + } + + /// A convenience constructor to create a Markdown message. + /// + /// Returns an HTML message if some Markdown formatting was detected, otherwise returns a plain + /// text message. + #[cfg(feature = "markdown")] + pub fn markdown(body: impl AsRef + Into) -> Self { + Self { message: MessageContent::markdown(body), relates_to: None } + } +} + +impl TextMessage for NoticeEventContent { + fn message(&self) -> &MessageContent { + &self.message + } +} diff --git a/crates/ruma-events/tests/message.rs b/crates/ruma-events/tests/message.rs index 6f4bfdaf..8ac6e02b 100644 --- a/crates/ruma-events/tests/message.rs +++ b/crates/ruma-events/tests/message.rs @@ -5,7 +5,9 @@ use js_int::uint; use matches::assert_matches; use ruma_common::MilliSecondsSinceUnixEpoch; use ruma_events::{ + emote::EmoteEventContent, message::MessageEventContent, + notice::NoticeEventContent, room::message::{InReplyTo, Relation}, AnyMessageLikeEvent, MessageLikeEvent, Unsigned, }; @@ -219,3 +221,134 @@ fn message_event_deserialization() { && unsigned.is_empty() ); } + +#[test] +fn notice_event_serialization() { + let event = MessageLikeEvent { + content: NoticeEventContent::plain("Hello, I'm a robot!"), + event_id: event_id!("$event:notareal.hs").to_owned(), + sender: user_id!("@user:notareal.hs").to_owned(), + origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), + room_id: room_id!("!roomid:notareal.hs").to_owned(), + unsigned: Unsigned::default(), + }; + + assert_eq!( + to_json_value(&event).unwrap(), + json!({ + "content": { + "org.matrix.msc1767.text": "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", + }) + ); +} + +#[test] +fn notice_event_deserialization() { + let json_data = json!({ + "content": { + "org.matrix.msc1767.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().unwrap() == "Hello, I'm a robot!" + && message.find_html().unwrap() == "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 emote_event_serialization() { + let event = MessageLikeEvent { + content: EmoteEventContent::html( + "is testing some code…", + "is testing some code…", + ), + event_id: event_id!("$event:notareal.hs").to_owned(), + sender: user_id!("@user:notareal.hs").to_owned(), + origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), + room_id: room_id!("!roomid:notareal.hs").to_owned(), + unsigned: Unsigned::default(), + }; + + assert_eq!( + to_json_value(&event).unwrap(), + json!({ + "content": { + "org.matrix.msc1767.message": [ + { "body": "is testing some code…", "mimetype": "text/html" }, + { "body": "is testing some code…", "mimetype": "text/plain" }, + ] + }, + "event_id": "$event:notareal.hs", + "origin_server_ts": 134_829_848, + "room_id": "!roomid:notareal.hs", + "sender": "@user:notareal.hs", + "type": "m.emote", + }) + ); +} + +#[test] +fn emote_event_deserialization() { + let json_data = json!({ + "content": { + "org.matrix.msc1767.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().unwrap() == "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() + ); +}