diff --git a/crates/ruma-events/src/reaction.rs b/crates/ruma-events/src/reaction.rs index 9e8ad21d..5c26b30e 100644 --- a/crates/ruma-events/src/reaction.rs +++ b/crates/ruma-events/src/reaction.rs @@ -52,3 +52,31 @@ impl Relation { Self { event_id, emoji } } } + +#[cfg(test)] +mod tests { + use matches::assert_matches; + use ruma_identifiers::event_id; + use serde_json::{from_value as from_json_value, json}; + + use super::{ReactionEventContent, Relation}; + + #[test] + fn deserialize() { + let ev_id = event_id!("$1598361704261elfgc:localhost"); + + let json = json!({ + "m.relates_to": { + "rel_type": "m.annotation", + "event_id": ev_id, + "key": "🦛", + } + }); + + assert_matches!( + from_json_value::(json).unwrap(), + ReactionEventContent { relates_to: Relation { event_id, emoji } } + if event_id == ev_id && emoji == "🦛" + ); + } +} diff --git a/crates/ruma-events/src/room/encrypted.rs b/crates/ruma-events/src/room/encrypted.rs index 7699f98b..5fe65d9e 100644 --- a/crates/ruma-events/src/room/encrypted.rs +++ b/crates/ruma-events/src/room/encrypted.rs @@ -7,7 +7,10 @@ use ruma_events_macros::EventContent; use ruma_identifiers::DeviceIdBox; use serde::{Deserialize, Serialize}; -use crate::{room::message::Relation, MessageEvent}; +use crate::{ + room::relationships::{relation_serde, Relation}, + MessageEvent, +}; /// An event that has been encrypted. pub type EncryptedEvent = MessageEvent; @@ -35,22 +38,23 @@ pub struct EncryptedEventContent { #[serde(flatten)] pub scheme: EncryptedEventScheme, - /// 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, + /// Information about related messages for [rich replies]. + /// + /// [rich replies]: https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies + #[serde(flatten, with = "relation_serde", skip_serializing_if = "Option::is_none")] + pub relation: Option, } impl EncryptedEventContent { /// Creates a new `EncryptedEventContent` with the given scheme and relation. pub fn new(scheme: EncryptedEventScheme, relates_to: Option) -> Self { - Self { scheme, relates_to } + Self { scheme, relation: relates_to } } } impl From for EncryptedEventContent { fn from(scheme: EncryptedEventScheme) -> Self { - Self { scheme, relates_to: None } + Self { scheme, relation: None } } } @@ -150,7 +154,7 @@ mod tests { use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{EncryptedEventContent, EncryptedEventScheme, MegolmV1AesSha2Content}; - use crate::room::message::{InReplyTo, Relation}; + use crate::room::relationships::{InReplyTo, Relation}; use ruma_identifiers::event_id; #[test] @@ -162,7 +166,7 @@ mod tests { device_id: "device_id".into(), session_id: "session_id".into(), }), - relates_to: Some(Relation::Reply { + relation: Some(Relation::Reply { in_reply_to: InReplyTo { event_id: event_id!("$h29iv0s8:example.com") }, }), }; @@ -218,7 +222,7 @@ mod tests { ); assert_matches!( - content.relates_to, + content.relation, Some(Relation::Reply { in_reply_to }) if in_reply_to.event_id == event_id!("$h29iv0s8:example.com") ); diff --git a/crates/ruma-events/src/room/message.rs b/crates/ruma-events/src/room/message.rs index 07d03326..177e236e 100644 --- a/crates/ruma-events/src/room/message.rs +++ b/crates/ruma-events/src/room/message.rs @@ -12,15 +12,13 @@ use ruma_serde::StringEnum; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value as JsonValue; -#[cfg(feature = "unstable-pre-spec")] -use super::relationships::{Annotation, Reference, RelationJsonRepr, Replacement}; -use super::{relationships::RelatesToJsonRepr, EncryptedFile, ImageInfo, ThumbnailInfo}; +use super::{ + relationships::{relation_serde, InReplyTo, Relation}, + EncryptedFile, ImageInfo, ThumbnailInfo, +}; #[cfg(feature = "unstable-pre-spec")] use crate::key::verification::VerificationMethod; -// FIXME: Do we want to keep re-exporting this? -pub use super::relationships::InReplyTo; - mod content_serde; pub mod feedback; @@ -42,29 +40,17 @@ pub struct MessageEventContent { #[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. + /// Information about related messages for [rich replies]. /// - /// This should only be set if `relates_to` is `Some(Relation::Replacement(_))`. - #[cfg(feature = "unstable-pre-spec")] - #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] - #[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")] - pub new_content: Option>, + /// [rich replies]: https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies + #[serde(flatten, with = "relation_serde", skip_serializing_if = "Option::is_none")] + pub relates_to: 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, - } + Self { msgtype, relates_to: None } } /// A constructor to create a plain text message. @@ -276,70 +262,6 @@ impl From for MessageEventContent { } } -/// 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")] - #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] - Reference(Reference), - - /// An annotation to an event. - #[cfg(feature = "unstable-pre-spec")] - #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] - Annotation(Annotation), - - /// An event that replaces another event. - #[cfg(feature = "unstable-pre-spec")] - #[cfg_attr(docsrs, doc(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. - #[doc(hidden)] - _Custom(JsonValue), -} - -impl From for RelatesToJsonRepr { - fn from(value: Relation) -> Self { - match value { - #[cfg(feature = "unstable-pre-spec")] - 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 { - #[cfg(feature = "unstable-pre-spec")] - RelatesToJsonRepr::Relation(r) => match r { - RelationJsonRepr::Annotation(a) => Self::Annotation(a), - RelationJsonRepr::Reference(r) => Self::Reference(r), - RelationJsonRepr::Replacement(r) => Self::Replacement(r), - }, - RelatesToJsonRepr::Reply { in_reply_to } => Self::Reply { in_reply_to }, - RelatesToJsonRepr::Custom(v) => Self::_Custom(v), - } - } -} - /// The payload for an audio message. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] @@ -1189,3 +1111,36 @@ fn formatted_or_plain_body<'a>(formatted: &'a Option, body: &'a s body } } + +#[cfg(test)] +mod tests { + use matches::assert_matches; + use ruma_identifiers::event_id; + use serde_json::{from_value as from_json_value, json}; + + use super::{MessageEventContent, MessageType, Relation}; + use crate::room::relationships::InReplyTo; + + #[test] + fn deserialize_reply() { + let ev_id = event_id!("$1598361704261elfgc:localhost"); + + let json = json!({ + "msgtype": "m.text", + "body": "", + "m.relates_to": { + "m.in_reply_to": { + "event_id": ev_id, + }, + }, + }); + + assert_matches!( + from_json_value::(json).unwrap(), + MessageEventContent { + msgtype: MessageType::Text(_), + relates_to: Some(Relation::Reply { in_reply_to: InReplyTo { event_id } }), + } if event_id == ev_id + ); + } +} diff --git a/crates/ruma-events/src/room/message/content_serde.rs b/crates/ruma-events/src/room/message/content_serde.rs index 55bcd8b8..6aa8d9f4 100644 --- a/crates/ruma-events/src/room/message/content_serde.rs +++ b/crates/ruma-events/src/room/message/content_serde.rs @@ -5,35 +5,20 @@ use serde_json::value::RawValue as RawJsonValue; use crate::{ from_raw_json_value, - room::message::{MessageEventContent, MessageType, Relation}, + room::message::{MessageEventContent, MessageType}, }; -/// Helper struct to determine the msgtype, relates_to and new_content fields -/// from a `serde_json::value::RawValue` -#[derive(Debug, Deserialize)] -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> Deserialize<'de> for MessageEventContent { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; - let helper = from_raw_json_value::(&json)?; + let mut deserializer = serde_json::Deserializer::from_str(json.get()); + let relation = + super::relation_serde::deserialize(&mut deserializer).map_err(de::Error::custom)?; - Ok(Self { - msgtype: from_raw_json_value(&json)?, - relates_to: helper.relates_to, - #[cfg(feature = "unstable-pre-spec")] - new_content: helper.new_content, - }) + Ok(Self { msgtype: from_raw_json_value(&json)?, relates_to: relation }) } } diff --git a/crates/ruma-events/src/room/relationships.rs b/crates/ruma-events/src/room/relationships.rs index 05d14629..d0685dba 100644 --- a/crates/ruma-events/src/room/relationships.rs +++ b/crates/ruma-events/src/room/relationships.rs @@ -8,45 +8,57 @@ use ruma_identifiers::EventId; use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; + +#[cfg(feature = "unstable-pre-spec")] +use crate::room::message::MessageEventContent; + +pub(crate) mod relation_serde; /// 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. - #[cfg(feature = "unstable-pre-spec")] - 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)] -#[cfg(feature = "unstable-pre-spec")] -#[serde(tag = "rel_type")] -pub(crate) enum RelationJsonRepr { - /// An annotation to an event. - #[serde(rename = "m.annotation")] - Annotation(Annotation), - +/// `m.room.message` or `m.room.encrypted` event. +#[derive(Clone, Debug)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub enum Relation { /// A reference to another event. - #[serde(rename = "m.reference")] + #[cfg(feature = "unstable-pre-spec")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] Reference(Reference), + /// An annotation to an event. + #[cfg(feature = "unstable-pre-spec")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] + Annotation(Annotation), + /// An event that replaces another event. - #[serde(rename = "m.replace")] + #[cfg(feature = "unstable-pre-spec")] + #[cfg_attr(docsrs, doc(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, + }, +} + +/// The event this relation belongs to replaces another event. +#[derive(Clone, Debug)] +#[cfg(feature = "unstable-pre-spec")] +#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] +pub struct Replacement { + /// The ID of the event being replacing. + pub event_id: EventId, + + /// New content. + pub new_content: Box, +} + +#[cfg(feature = "unstable-pre-spec")] +impl Replacement { + /// Creates a new `Replacement` with the given event ID and new content. + pub fn new(event_id: EventId, new_content: Box) -> Self { + Self { event_id, new_content } + } } /// Information about the event a "rich reply" is replying to. @@ -99,99 +111,3 @@ impl Annotation { Self { event_id, key } } } - -/// An event replacing another event. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg(feature = "unstable-pre-spec")] -#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct Replacement { - /// The event this event is replacing. - pub event_id: EventId, -} - -#[cfg(feature = "unstable-pre-spec")] -impl Replacement { - /// Creates a new `Replacement` with the given event ID. - pub fn new(event_id: EventId) -> Self { - Self { event_id } - } -} - -#[cfg(test)] -mod tests { - use matches::assert_matches; - use ruma_identifiers::event_id; - use serde_json::{from_value as from_json_value, json}; - - use crate::room::message::Relation; - - #[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] - #[cfg(feature = "unstable-pre-spec")] - 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 == "🦛" - ); - } -} diff --git a/crates/ruma-events/src/room/relationships/relation_serde.rs b/crates/ruma-events/src/room/relationships/relation_serde.rs new file mode 100644 index 00000000..84317611 --- /dev/null +++ b/crates/ruma-events/src/room/relationships/relation_serde.rs @@ -0,0 +1,162 @@ +#[cfg(feature = "unstable-pre-spec")] +use ruma_identifiers::EventId; +use serde::{ser::SerializeStruct as _, Deserialize, Deserializer, Serialize, Serializer}; + +#[cfg(feature = "unstable-pre-spec")] +use super::{Annotation, Reference, Replacement}; +use super::{InReplyTo, Relation}; +#[cfg(feature = "unstable-pre-spec")] +use crate::room::message::MessageEventContent; + +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + fn convert_relation(ev: EventWithRelatesToJsonRepr) -> Option { + if let Some(in_reply_to) = ev.relates_to.in_reply_to { + return Some(Relation::Reply { in_reply_to }); + } + + #[cfg(feature = "unstable-pre-spec")] + if let Some(relation) = ev.relates_to.relation { + let relation = match relation { + RelationJsonRepr::Annotation(a) => Relation::Annotation(a), + RelationJsonRepr::Reference(r) => Relation::Reference(r), + RelationJsonRepr::Replacement(ReplacementJsonRepr { event_id }) => { + let new_content = ev.new_content?; + Relation::Replacement(Replacement { event_id, new_content }) + } + // FIXME: Maybe we should log this, though at this point we don't even have access + // to the rel_type of the unknown relation. + RelationJsonRepr::Unknown => return None, + }; + + return Some(relation); + } + + None + } + + EventWithRelatesToJsonRepr::deserialize(deserializer).map(convert_relation) +} + +pub fn serialize(relation: &Option, serializer: S) -> Result +where + S: Serializer, +{ + let relation = match relation { + Some(rel) => rel, + // FIXME: If this crate ends up depending on tracing, emit a warning here. + // This code path should not be reachable due to the skip_serializing_if serde attribute + // that should be applied together with `with = "relation_serde"`. + None => return serializer.serialize_struct("NoRelation", 0)?.end(), + }; + + let json_repr = match relation { + #[cfg(feature = "unstable-pre-spec")] + Relation::Annotation(r) => EventWithRelatesToJsonRepr::new(RelatesToJsonRepr { + relation: Some(RelationJsonRepr::Annotation(r.clone())), + ..Default::default() + }), + #[cfg(feature = "unstable-pre-spec")] + Relation::Reference(r) => EventWithRelatesToJsonRepr::new(RelatesToJsonRepr { + relation: Some(RelationJsonRepr::Reference(r.clone())), + ..Default::default() + }), + #[cfg(feature = "unstable-pre-spec")] + Relation::Replacement(Replacement { event_id, new_content }) => { + EventWithRelatesToJsonRepr { + relates_to: RelatesToJsonRepr { + relation: Some(RelationJsonRepr::Replacement(ReplacementJsonRepr { + event_id: event_id.clone(), + })), + ..Default::default() + }, + new_content: Some(new_content.clone()), + } + } + Relation::Reply { in_reply_to } => EventWithRelatesToJsonRepr::new(RelatesToJsonRepr { + in_reply_to: Some(in_reply_to.clone()), + ..Default::default() + }), + }; + + json_repr.serialize(serializer) +} + +#[derive(Deserialize, Serialize)] +struct EventWithRelatesToJsonRepr { + #[serde(rename = "m.relates_to", default, skip_serializing_if = "RelatesToJsonRepr::is_empty")] + relates_to: RelatesToJsonRepr, + + #[cfg(feature = "unstable-pre-spec")] + #[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")] + new_content: Option>, +} + +impl EventWithRelatesToJsonRepr { + fn new(relates_to: RelatesToJsonRepr) -> Self { + Self { + relates_to, + #[cfg(feature = "unstable-pre-spec")] + new_content: None, + } + } +} + +/// Enum modeling the different ways relationships can be expressed in a `m.relates_to` field of an +/// event. +#[derive(Default, Deserialize, Serialize)] +struct RelatesToJsonRepr { + #[serde(rename = "m.in_reply_to", skip_serializing_if = "Option::is_none")] + in_reply_to: Option, + + #[cfg(feature = "unstable-pre-spec")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + relation: Option, +} + +impl RelatesToJsonRepr { + fn is_empty(&self) -> bool { + #[cfg(not(feature = "unstable-pre-spec"))] + { + self.in_reply_to.is_none() + } + + #[cfg(feature = "unstable-pre-spec")] + { + self.in_reply_to.is_none() && self.relation.is_none() + } + } +} + +/// A relation, which associates new information to an existing event. +#[derive(Clone, Deserialize, Serialize)] +#[cfg(feature = "unstable-pre-spec")] +#[serde(tag = "rel_type")] +enum RelationJsonRepr { + /// An annotation to an event. + #[serde(rename = "m.annotation")] + Annotation(Annotation), + + /// A reference to another event. + #[serde(rename = "m.reference")] + Reference(Reference), + + /// An event that replaces another event. + #[serde(rename = "m.replace")] + Replacement(ReplacementJsonRepr), + + /// An unknown relation type. + /// + /// Not available in the public API, but exists here so deserialization + /// doesn't fail with new / custom `rel_type`s. + #[serde(other)] + Unknown, +} + +#[derive(Clone, Deserialize, Serialize)] +#[cfg(feature = "unstable-pre-spec")] +struct ReplacementJsonRepr { + event_id: EventId, +} diff --git a/crates/ruma-events/tests/room_message.rs b/crates/ruma-events/tests/room_message.rs index ba2d9477..38a72a87 100644 --- a/crates/ruma-events/tests/room_message.rs +++ b/crates/ruma-events/tests/room_message.rs @@ -11,10 +11,10 @@ use ruma_events::{ use ruma_events::{ room::{ message::{ - AudioMessageEventContent, MessageEvent, MessageEventContent, MessageType, Relation, + AudioMessageEventContent, MessageEvent, MessageEventContent, MessageType, TextMessageEventContent, }, - relationships::InReplyTo, + relationships::{InReplyTo, Relation}, }, Unsigned, }; @@ -244,7 +244,7 @@ fn edit_deserialization_061() { formatted: None, .. }), - relates_to: Some(Relation::_Custom(_)), + relates_to: None, .. } if body == "s/foo/bar" ); @@ -277,8 +277,7 @@ fn edit_deserialization_future() { formatted: None, .. }), - relates_to: Some(Relation::Replacement(Replacement { event_id })), - new_content: Some(new_content), + relates_to: Some(Relation::Replacement(Replacement { event_id, new_content })), .. } if body == "s/foo/bar" && event_id == ev_id