From cf7d4b40e1e06e838db2e65268fba4cbb051dce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 10 Nov 2020 13:25:41 +0100 Subject: [PATCH] events: Add support for the new relation types --- ruma-events/src/room.rs | 1 + ruma-events/src/room/message.rs | 103 ++++++++++++----- ruma-events/src/room/relationships.rs | 160 ++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 26 deletions(-) create mode 100644 ruma-events/src/room/relationships.rs diff --git a/ruma-events/src/room.rs b/ruma-events/src/room.rs index 5e864d5b..f9e51e5d 100644 --- a/ruma-events/src/room.rs +++ b/ruma-events/src/room.rs @@ -22,6 +22,7 @@ pub mod name; pub mod pinned_events; pub mod power_levels; pub mod redaction; +pub(crate) mod relationships; pub mod server_acl; pub mod third_party_invite; pub mod tombstone; diff --git a/ruma-events/src/room/message.rs b/ruma-events/src/room/message.rs index 8dfc948e..45795197 100644 --- a/ruma-events/src/room/message.rs +++ b/ruma-events/src/room/message.rs @@ -3,10 +3,18 @@ use js_int::UInt; use ruma_common::StringEnum; use ruma_events_macros::MessageEventContent; -use ruma_identifiers::EventId; use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; -use super::{EncryptedFile, ImageInfo, ThumbnailInfo}; +use super::{ + relationships::{RelatesToJsonRepr, RelationJsonRepr}, + EncryptedFile, ImageInfo, ThumbnailInfo, +}; + +pub use super::relationships::{Annotation, InReplyTo}; + +#[cfg(feature = "unstable-pre-spec")] +pub use super::relationships::{Reference, Replacement}; pub mod feedback; @@ -60,6 +68,65 @@ pub enum MessageEventContent { Video(VideoMessageEventContent), } +/// 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)] +#[serde(from = "RelatesToJsonRepr", into = "RelatesToJsonRepr")] +pub enum Relation { + /// A reference to another event. + #[cfg(feature = "unstable-pre-spec")] + Reference(Reference), + + /// An annotation to an event. + Annotation(Annotation), + + /// An event that replaces another event. + #[cfg(feature = "unstable-pre-spec")] + Replacement(Replacement), + + /// An `m.in_reply_to` relation indicating that the event is a reply to + /// another event. + Reply { + /// Information about another message being replied to. + in_reply_to: InReplyTo, + }, + + /// Custom, unsupported relation. + Custom(JsonValue), +} + +impl From for RelatesToJsonRepr { + fn from(value: Relation) -> Self { + match value { + Relation::Annotation(r) => RelatesToJsonRepr::Relation(RelationJsonRepr::Annotation(r)), + #[cfg(feature = "unstable-pre-spec")] + Relation::Reference(r) => RelatesToJsonRepr::Relation(RelationJsonRepr::Reference(r)), + #[cfg(feature = "unstable-pre-spec")] + Relation::Replacement(r) => { + RelatesToJsonRepr::Relation(RelationJsonRepr::Replacement(r)) + } + Relation::Reply { in_reply_to } => RelatesToJsonRepr::Reply { in_reply_to }, + Relation::Custom(c) => RelatesToJsonRepr::Custom(c), + } + } +} + +impl From for Relation { + fn from(value: RelatesToJsonRepr) -> Self { + match value { + RelatesToJsonRepr::Relation(r) => match r { + RelationJsonRepr::Annotation(a) => Self::Annotation(a), + #[cfg(feature = "unstable-pre-spec")] + RelationJsonRepr::Reference(r) => Self::Reference(r), + #[cfg(feature = "unstable-pre-spec")] + RelationJsonRepr::Replacement(r) => Self::Replacement(r), + }, + RelatesToJsonRepr::Reply { in_reply_to } => Self::Reply { in_reply_to }, + RelatesToJsonRepr::Custom(v) => Self::Custom(v), + } + } +} + impl MessageEventContent { /// A convenience constructor to create a plain text message. pub fn text_plain(body: impl Into) -> Self { @@ -245,7 +312,7 @@ pub struct NoticeMessageEventContent { /// 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, + pub relates_to: Option, } impl NoticeMessageEventContent { @@ -365,7 +432,7 @@ pub struct TextMessageEventContent { /// 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, + pub relates_to: Option, } impl TextMessageEventContent { @@ -450,22 +517,6 @@ pub struct VideoInfo { pub thumbnail_file: Option>, } -/// Information about related messages for -/// [rich replies](https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies). -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct RelatesTo { - /// Information about another message being replied to. - #[serde(rename = "m.in_reply_to")] - pub in_reply_to: Option, -} - -/// Information about the event a "rich reply" is replying to. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct InReplyTo { - /// The event being replied to. - pub event_id: EventId, -} - #[cfg(test)] mod tests { use std::time::{Duration, UNIX_EPOCH}; @@ -475,9 +526,11 @@ mod tests { use ruma_identifiers::{event_id, room_id, user_id}; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; - use super::{AudioMessageEventContent, FormattedBody, MessageEventContent, MessageFormat}; + use super::{ + AudioMessageEventContent, FormattedBody, MessageEventContent, MessageFormat, Relation, + }; use crate::{ - room::message::{InReplyTo, RelatesTo, TextMessageEventContent}, + room::{message::TextMessageEventContent, relationships::InReplyTo}, MessageEvent, Unsigned, }; @@ -575,10 +628,8 @@ mod tests { let message_event_content = MessageEventContent::Text(TextMessageEventContent { body: "> <@test:example.com> test\n\ntest reply".to_owned(), formatted: None, - relates_to: Some(RelatesTo { - in_reply_to: Some(InReplyTo { - event_id: event_id!("$15827405538098VGFWH:example.com"), - }), + relates_to: Some(Relation::Reply { + in_reply_to: InReplyTo { event_id: event_id!("$15827405538098VGFWH:example.com") }, }), }); diff --git a/ruma-events/src/room/relationships.rs b/ruma-events/src/room/relationships.rs new file mode 100644 index 00000000..1feed9fb --- /dev/null +++ b/ruma-events/src/room/relationships.rs @@ -0,0 +1,160 @@ +//! Types for event relationships. +//! +//! Events in Matrix can relate to one another in a couple of ways, this module +//! adds types to parse the relationship of an event if any exists. +//! +//! MSC for all the relates_to types except replies: +//! https://github.com/matrix-org/matrix-doc/pull/2674 + +use ruma_identifiers::EventId; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +/// Enum modeling the different ways relationships can be expressed in a +/// `m.relates_to` field of an event. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub(crate) enum RelatesToJsonRepr { + /// A relation which contains subtypes indicating the type of the + /// relationship with the `rel_type` field. + Relation(RelationJsonRepr), + + /// An `m.in_reply_to` relationship indicating that the event is a reply to + /// another event. + Reply { + /// Information about another message being replied to. + #[serde(rename = "m.in_reply_to")] + in_reply_to: InReplyTo, + }, + + /// Custom, unsupported relationship. + Custom(JsonValue), +} + +/// A relation, which associates new information to an existing event. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "rel_type")] +pub(crate) enum RelationJsonRepr { + /// An annotation to an event. + #[serde(rename = "m.annotation")] + Annotation(Annotation), + + /// A reference to another event. + #[cfg(feature = "unstable-pre-spec")] + #[serde(rename = "m.reference")] + Reference(Reference), + + /// An event that replaces another event. + #[cfg(feature = "unstable-pre-spec")] + #[serde(rename = "m.replace")] + Replacement(Replacement), +} + +/// Information about the event a "rich reply" is replying to. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct InReplyTo { + /// The event being replied to. + pub event_id: EventId, +} + +/// A reference to another event. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg(feature = "unstable-pre-spec")] +pub struct Reference { + /// The event we are referencing. + pub event_id: EventId, +} + +/// An annotation for an event. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Annotation { + /// The event that is being annotated. + pub event_id: EventId, + + /// The annotation. + pub key: String, +} + +/// An event replacing another event. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg(feature = "unstable-pre-spec")] +pub struct Replacement { + /// The event this event is replacing. + pub event_id: EventId, +} + +#[cfg(test)] +mod test { + use crate::room::message::Relation; + use matches::assert_matches; + use ruma_identifiers::event_id; + use serde_json::{from_value as from_json_value, json}; + + #[test] + fn reply_deserialize() { + let event_id = event_id!("$1598361704261elfgc:localhost"); + + let json = json!({ + "m.in_reply_to": { + "event_id": event_id, + } + }); + + assert_matches!( + from_json_value::(json).unwrap(), + Relation::Reply { in_reply_to } + if in_reply_to.event_id == event_id + ); + } + + #[test] + #[cfg(feature = "unstable-pre-spec")] + fn reference_deserialize() { + let event_id = event_id!("$1598361704261elfgc:localhost"); + + let json = json!({ + "rel_type": "m.reference", + "event_id": event_id, + }); + + assert_matches!( + from_json_value::(json).unwrap(), + Relation::Reference(reference) + if reference.event_id == event_id + ); + } + + #[test] + #[cfg(feature = "unstable-pre-spec")] + fn replacement_deserialization() { + let event_id = event_id!("$1598361704261elfgc:localhost"); + + let json = json!({ + "rel_type": "m.replace", + "event_id": event_id, + }); + + assert_matches!( + from_json_value::(json).unwrap(), + Relation::Replacement(replacement) + if replacement.event_id == event_id + ); + } + + #[test] + fn annotation_deserialize() { + let event_id = event_id!("$1598361704261elfgc:localhost"); + + let json = json!({ + "rel_type": "m.annotation", + "event_id": event_id, + "key": "🦛", + }); + + assert_matches!( + from_json_value::(json).unwrap(), + Relation::Annotation(annotation) + if annotation.event_id == event_id && annotation.key == "🦛" + ); + } +}