From aba6328d1cb2213e79b6f514c39733271e12222d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sat, 26 Mar 2022 16:05:42 +0100 Subject: [PATCH] events: Add support for transitional extensible location messages According to MSC3488 --- crates/ruma-common/src/events/location.rs | 33 ++++++- crates/ruma-common/src/events/room/message.rs | 88 ++++++++++++++++++- .../src/events/room/message/content_serde.rs | 76 ++++++++++++++++ crates/ruma-common/tests/events/location.rs | 76 +++++++++++++++- 4 files changed, 269 insertions(+), 4 deletions(-) diff --git a/crates/ruma-common/src/events/location.rs b/crates/ruma-common/src/events/location.rs index a7141353..83ee7c01 100644 --- a/crates/ruma-common/src/events/location.rs +++ b/crates/ruma-common/src/events/location.rs @@ -10,10 +10,25 @@ use serde::{Deserialize, Serialize}; mod zoomlevel_serde; -use super::{message::MessageContent, room::message::Relation}; +use super::{ + message::MessageContent, + room::message::{LocationMessageEventContent, Relation}, +}; use crate::{MilliSecondsSinceUnixEpoch, PrivOwnedStr}; /// The payload for an extensible location message. +/// +/// This is the new primary type introduced in [MSC3488] and should not be sent before the end of +/// the transition period. See the documentation of the [`message`] module for more information. +/// +/// `LocationEventContent` can be converted to a [`RoomMessageEventContent`] with a +/// [`MessageType::Location`]. You can convert it back with +/// [`LocationEventContent::from_location_room_message()`]. +/// +/// [MSC3488]: https://github.com/matrix-org/matrix-spec-proposals/pull/3488 +/// [`message`]: super::message +/// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent +/// [`MessageType::Location`]: super::room::message::MessageType::Location #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.location", kind = MessageLike)] @@ -55,6 +70,22 @@ impl LocationEventContent { pub fn with_message(message: MessageContent, location: LocationContent) -> Self { Self { message, location, asset: Default::default(), ts: None, relates_to: None } } + + /// Create a new `LocationEventContent` from the given `LocationMessageEventContent` and + /// optional relation. + pub fn from_location_room_message( + content: LocationMessageEventContent, + relates_to: Option, + ) -> Self { + let LocationMessageEventContent { body, geo_uri, message, location, asset, ts, .. } = + content; + + let message = message.unwrap_or_else(|| MessageContent::plain(body)); + let location = location.unwrap_or_else(|| LocationContent::new(geo_uri)); + let asset = asset.unwrap_or_default(); + + Self { message, location, asset, ts, relates_to } + } } /// Location content. diff --git a/crates/ruma-common/src/events/room/message.rs b/crates/ruma-common/src/events/room/message.rs index 13889068..51f692fa 100644 --- a/crates/ruma-common/src/events/room/message.rs +++ b/crates/ruma-common/src/events/room/message.rs @@ -31,6 +31,11 @@ use crate::{ serde::{JsonObject, StringEnum}, DeviceId, EventId, MxcUri, PrivOwnedStr, UserId, }; +#[cfg(feature = "unstable-msc3488")] +use crate::{ + events::location::{AssetContent, LocationContent, LocationEventContent}, + MilliSecondsSinceUnixEpoch, +}; mod content_serde; pub mod feedback; @@ -249,6 +254,20 @@ impl From for RoomMessageEventContent { } } +#[cfg(feature = "unstable-msc3488")] +impl From for RoomMessageEventContent { + fn from(content: LocationEventContent) -> Self { + let LocationEventContent { message, location, asset, ts, relates_to } = content; + + Self { + msgtype: MessageType::Location(LocationMessageEventContent::from_extensible_content( + message, location, asset, ts, + )), + relates_to, + } + } +} + #[cfg(feature = "unstable-msc1767")] impl From for RoomMessageEventContent { fn from(content: MessageEventContent) -> Self { @@ -1073,6 +1092,10 @@ impl ImageMessageEventContent { #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "msgtype", rename = "m.location")] +#[cfg_attr( + feature = "unstable-msc3488", + serde(from = "content_serde::LocationMessageEventContentDeHelper") +)] pub struct LocationMessageEventContent { /// A description of the location e.g. "Big Ben, London, UK", or some kind of content /// description for accessibility, e.g. "location attachment". @@ -1084,12 +1107,75 @@ pub struct LocationMessageEventContent { /// Info about the location being represented. #[serde(skip_serializing_if = "Option::is_none")] pub info: Option>, + + /// Extensible-event text representation of the message. + /// + /// If present, this should be preferred over the `body` field. + #[cfg(feature = "unstable-msc3488")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Extensible-event location info of the message. + /// + /// If present, this should be preferred over the `geo_uri` field. + #[cfg(feature = "unstable-msc3488")] + #[serde(rename = "org.matrix.msc3488.location", skip_serializing_if = "Option::is_none")] + pub location: Option, + + /// Extensible-event asset this message refers to. + #[cfg(feature = "unstable-msc3488")] + #[serde(rename = "org.matrix.msc3488.asset", skip_serializing_if = "Option::is_none")] + pub asset: Option, + + /// Extensible-event timestamp this message refers to. + #[cfg(feature = "unstable-msc3488")] + #[serde(rename = "org.matrix.msc3488.ts", skip_serializing_if = "Option::is_none")] + pub ts: Option, } impl LocationMessageEventContent { /// Creates a new `RoomLocationMessageEventContent` with the given body and geo URI. pub fn new(body: String, geo_uri: String) -> Self { - Self { body, geo_uri, info: None } + Self { + #[cfg(feature = "unstable-msc3488")] + message: Some(MessageContent::plain(body.clone())), + #[cfg(feature = "unstable-msc3488")] + location: Some(LocationContent::new(geo_uri.clone())), + #[cfg(feature = "unstable-msc3488")] + asset: None, + #[cfg(feature = "unstable-msc3488")] + ts: None, + body, + geo_uri, + info: None, + } + } + + /// Create a new `LocationMessageEventContent` with the given message, location info, asset and + /// timestamp. + #[cfg(feature = "unstable-msc3488")] + pub fn from_extensible_content( + message: MessageContent, + location: LocationContent, + asset: AssetContent, + ts: Option, + ) -> Self { + let body = if let Some(body) = message.find_plain() { + body.to_owned() + } else { + message[0].body.clone() + }; + let geo_uri = location.uri.clone(); + + Self { + message: Some(message), + location: Some(location), + asset: Some(asset), + ts, + body, + geo_uri, + info: None, + } } } diff --git a/crates/ruma-common/src/events/room/message/content_serde.rs b/crates/ruma-common/src/events/room/message/content_serde.rs index e07949c2..64720368 100644 --- a/crates/ruma-common/src/events/room/message/content_serde.rs +++ b/crates/ruma-common/src/events/room/message/content_serde.rs @@ -5,6 +5,11 @@ use serde_json::value::RawValue as RawJsonValue; #[cfg(feature = "unstable-msc3245")] use super::VoiceContent; +#[cfg(feature = "unstable-msc3488")] +use super::{ + AssetContent, LocationContent, LocationInfo, LocationMessageEventContent, + MilliSecondsSinceUnixEpoch, +}; #[cfg(feature = "unstable-msc3246")] use super::{AudioContent, AudioInfo, AudioMessageEventContent}; #[cfg(feature = "unstable-msc3551")] @@ -278,6 +283,77 @@ impl From for ImageMessageEventContent { } } +/// Helper struct for deserializing `LocationMessageEventContent` with stable and unstable field +/// names. +/// +/// It's not possible to use the `alias` attribute of serde because of +/// https://github.com/serde-rs/serde/issues/1504. +#[derive(Clone, Debug, Deserialize)] +#[cfg(feature = "unstable-msc3488")] +pub struct LocationMessageEventContentDeHelper { + /// A description of the location. + pub body: String, + + /// A geo URI representing the location. + pub geo_uri: String, + + /// Info about the location being represented. + #[serde(skip_serializing_if = "Option::is_none")] + pub info: Option>, + + /// Extensible-event text representation of the message. + #[serde(flatten)] + pub message: Option, + + /// Extensible-event location info of the message, with stable name. + #[serde(rename = "m.location")] + pub location_stable: Option, + + /// Extensible-event location info of the message, with unstable name. + #[serde(rename = "org.matrix.msc3488.location")] + pub location_unstable: Option, + + /// Extensible-event asset this message refers to, with stable name. + #[serde(rename = "m.asset")] + pub asset_stable: Option, + + /// Extensible-event asset this message refers to, with unstable name. + #[serde(rename = "org.matrix.msc3488.asset")] + pub asset_unstable: Option, + + /// Extensible-event timestamp this message refers to, with stable name. + #[serde(rename = "m.ts")] + pub ts_stable: Option, + + /// Extensible-event timestamp this message refers to, with unstable name. + #[serde(rename = "org.matrix.msc3488.ts")] + pub ts_unstable: Option, +} + +#[cfg(feature = "unstable-msc3488")] +impl From for LocationMessageEventContent { + fn from(helper: LocationMessageEventContentDeHelper) -> Self { + let LocationMessageEventContentDeHelper { + body, + geo_uri, + info, + message, + location_stable, + location_unstable, + asset_stable, + asset_unstable, + ts_stable, + ts_unstable, + } = helper; + + let location = location_stable.or(location_unstable); + let asset = asset_stable.or(asset_unstable); + let ts = ts_stable.or(ts_unstable); + + Self { body, geo_uri, info, message, location, asset, ts } + } +} + /// Helper struct for deserializing `VideoMessageEventContent` with stable and unstable field names. /// /// It's not possible to use the `alias` attribute of serde because of diff --git a/crates/ruma-common/tests/events/location.rs b/crates/ruma-common/tests/events/location.rs index fde850f0..17f14b12 100644 --- a/crates/ruma-common/tests/events/location.rs +++ b/crates/ruma-common/tests/events/location.rs @@ -11,7 +11,9 @@ use ruma_common::{ ZoomLevelError, }, message::MessageContent, - room::message::{InReplyTo, Relation}, + room::message::{ + InReplyTo, LocationMessageEventContent, MessageType, Relation, RoomMessageEventContent, + }, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, }, room_id, user_id, MilliSecondsSinceUnixEpoch, @@ -105,7 +107,7 @@ fn event_serialization() { #[test] fn plain_content_deserialization() { let json_data = json!({ - "org.matrix.msc1767.text": "Alice was at geo:51.5008,0.1247;u=35", + "m.text": "Alice was at geo:51.5008,0.1247;u=35", "m.location": { "uri": "geo:51.5008,0.1247;u=35", }, @@ -228,3 +230,73 @@ fn message_event_deserialization() { && unsigned.is_empty() ); } + +#[test] +fn room_message_serialization() { + let message_event_content = + RoomMessageEventContent::new(MessageType::Location(LocationMessageEventContent::new( + "Alice was at geo:51.5008,0.1247;u=35".to_owned(), + "geo:51.5008,0.1247;u=35".to_owned(), + ))); + + assert_eq!( + to_json_value(&message_event_content).unwrap(), + json!({ + "body": "Alice was at geo:51.5008,0.1247;u=35", + "geo_uri": "geo:51.5008,0.1247;u=35", + "msgtype": "m.location", + "org.matrix.msc1767.text": "Alice was at geo:51.5008,0.1247;u=35", + "org.matrix.msc3488.location": { + "uri": "geo:51.5008,0.1247;u=35", + }, + }) + ); +} + +#[test] +fn room_message_stable_deserialization() { + let json_data = json!({ + "body": "Alice was at geo:51.5008,0.1247;u=35", + "geo_uri": "geo:51.5008,0.1247;u=35", + "msgtype": "m.location", + "m.text": "Alice was at geo:51.5008,0.1247;u=35", + "m.location": { + "uri": "geo:51.5008,0.1247;u=35", + }, + }); + + let event_content = from_json_value::(json_data).unwrap(); + assert_matches!(event_content.msgtype, MessageType::Location(_)); + if let MessageType::Location(content) = event_content.msgtype { + assert_eq!(content.body, "Alice was at geo:51.5008,0.1247;u=35"); + assert_eq!(content.geo_uri, "geo:51.5008,0.1247;u=35"); + let message = content.message.unwrap(); + assert_eq!(message.len(), 1); + assert_eq!(message[0].body, "Alice was at geo:51.5008,0.1247;u=35"); + assert_eq!(content.location.unwrap().uri, "geo:51.5008,0.1247;u=35"); + } +} + +#[test] +fn room_message_unstable_deserialization() { + let json_data = json!({ + "body": "Alice was at geo:51.5008,0.1247;u=35", + "geo_uri": "geo:51.5008,0.1247;u=35", + "msgtype": "m.location", + "org.matrix.msc1767.text": "Alice was at geo:51.5008,0.1247;u=35", + "org.matrix.msc3488.location": { + "uri": "geo:51.5008,0.1247;u=35", + }, + }); + + let event_content = from_json_value::(json_data).unwrap(); + assert_matches!(event_content.msgtype, MessageType::Location(_)); + if let MessageType::Location(content) = event_content.msgtype { + assert_eq!(content.body, "Alice was at geo:51.5008,0.1247;u=35"); + assert_eq!(content.geo_uri, "geo:51.5008,0.1247;u=35"); + let message = content.message.unwrap(); + assert_eq!(message.len(), 1); + assert_eq!(message[0].body, "Alice was at geo:51.5008,0.1247;u=35"); + assert_eq!(content.location.unwrap().uri, "geo:51.5008,0.1247;u=35"); + } +}