events: Reintroduce MSC3488 fallback behavior in LocationMessageEventContent

This commit is contained in:
Kévin Commaille 2023-06-23 09:50:43 +02:00 committed by Kévin Commaille
parent e017e65277
commit d0f11f0075
6 changed files with 296 additions and 4 deletions

View File

@ -86,6 +86,8 @@ use serde::{Deserialize, Serialize};
use super::room::message::Relation;
pub(super) mod historical_serde;
/// The payload for an extensible text message.
///
/// This is the new primary type introduced in [MSC1767] and should only be sent in rooms with a

View File

@ -0,0 +1,68 @@
//! Serde for old versions of MSC1767 still used in some types ([spec]).
//!
//! [spec]: https://github.com/matrix-org/matrix-spec-proposals/blob/d6046d8402e7a3c7a4fcbc9da16ea9bad5968992/proposals/1767-extensible-events.md
use serde::{Deserialize, Serialize};
use super::{TextContentBlock, TextRepresentation};
#[derive(Default, Serialize, Deserialize)]
pub(in crate::events) struct MessageContentBlockSerDeHelper {
/// Plain text short form.
#[serde(rename = "org.matrix.msc1767.text", skip_serializing_if = "Option::is_none")]
text: Option<String>,
/// HTML short form.
#[serde(rename = "org.matrix.msc1767.html", skip_serializing_if = "Option::is_none")]
html: Option<String>,
/// Long form.
#[serde(rename = "org.matrix.msc1767.message", skip_serializing_if = "Option::is_none")]
message: Option<Vec<TextRepresentation>>,
}
impl TryFrom<MessageContentBlockSerDeHelper> for TextContentBlock {
type Error = &'static str;
fn try_from(value: MessageContentBlockSerDeHelper) -> Result<Self, Self::Error> {
let MessageContentBlockSerDeHelper { text, html, message } = value;
if let Some(message) = message {
Ok(Self(message))
} else {
let message: Vec<_> = html
.map(TextRepresentation::html)
.into_iter()
.chain(text.map(TextRepresentation::plain))
.collect();
if !message.is_empty() {
Ok(Self(message))
} else {
Err("missing at least one of fields `org.matrix.msc1767.text`, `org.matrix.msc1767.html` or `org.matrix.msc1767.message`")
}
}
}
}
impl From<TextContentBlock> for MessageContentBlockSerDeHelper {
fn from(value: TextContentBlock) -> Self {
let has_shortcut =
|message: &TextRepresentation| matches!(&*message.mimetype, "text/plain" | "text/html");
if value.iter().all(has_shortcut) {
let mut helper = Self::default();
for message in value.0.into_iter() {
if message.mimetype == "text/plain" {
helper.text = Some(message.body);
} else if message.mimetype == "text/html" {
helper.html = Some(message.body);
}
}
helper
} else {
Self { message: Some(value.0), ..Default::default() }
}
}
}

View File

@ -49,3 +49,82 @@ impl<'de> Deserialize<'de> for MessageType {
})
}
}
#[cfg(feature = "unstable-msc3488")]
pub(in super::super) mod msc3488 {
use serde::{Deserialize, Serialize};
use crate::{
events::{
location::{AssetContent, LocationContent},
message::historical_serde::MessageContentBlockSerDeHelper,
room::message::{LocationInfo, LocationMessageEventContent},
},
MilliSecondsSinceUnixEpoch,
};
/// Deserialize helper type for `LocationMessageEventContent` with unstable fields from msc3488.
#[derive(Serialize, Deserialize)]
#[serde(tag = "msgtype", rename = "m.location")]
pub(in super::super) struct LocationMessageEventContentSerDeHelper {
pub body: String,
pub geo_uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<LocationInfo>>,
#[serde(flatten)]
pub message: MessageContentBlockSerDeHelper,
#[serde(rename = "org.matrix.msc3488.location", skip_serializing_if = "Option::is_none")]
pub location: Option<LocationContent>,
#[serde(rename = "org.matrix.msc3488.asset", skip_serializing_if = "Option::is_none")]
pub asset: Option<AssetContent>,
#[serde(rename = "org.matrix.msc3488.ts", skip_serializing_if = "Option::is_none")]
pub ts: Option<MilliSecondsSinceUnixEpoch>,
}
impl From<LocationMessageEventContent> for LocationMessageEventContentSerDeHelper {
fn from(value: LocationMessageEventContent) -> Self {
let LocationMessageEventContent { body, geo_uri, info, message, location, asset, ts } =
value;
Self {
body,
geo_uri,
info,
message: message.map(Into::into).unwrap_or_default(),
location,
asset,
ts,
}
}
}
impl From<LocationMessageEventContentSerDeHelper> for LocationMessageEventContent {
fn from(value: LocationMessageEventContentSerDeHelper) -> Self {
let LocationMessageEventContentSerDeHelper {
body,
geo_uri,
info,
message,
location,
asset,
ts,
} = value;
LocationMessageEventContent {
body,
geo_uri,
info,
message: message.try_into().ok(),
location,
asset,
ts,
}
}
}
}

View File

@ -1,11 +1,26 @@
use serde::{Deserialize, Serialize};
use crate::events::room::{MediaSource, ThumbnailInfo};
#[cfg(feature = "unstable-msc3488")]
use crate::{
events::{
location::{AssetContent, AssetType, LocationContent},
message::{TextContentBlock, TextRepresentation},
},
MilliSecondsSinceUnixEpoch,
};
/// The payload for a location message.
#[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 = "super::content_serde::msc3488::LocationMessageEventContentSerDeHelper",
into = "super::content_serde::msc3488::LocationMessageEventContentSerDeHelper"
)
)]
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".
@ -17,12 +32,84 @@ pub struct LocationMessageEventContent {
/// Info about the location being represented.
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<LocationInfo>>,
/// Extensible-event text representation of the message.
///
/// If present, this should be preferred over the `body` field.
#[cfg(feature = "unstable-msc3488")]
pub message: Option<TextContentBlock>,
/// Extensible-event location info of the message.
///
/// If present, this should be preferred over the `geo_uri` field.
#[cfg(feature = "unstable-msc3488")]
pub location: Option<LocationContent>,
/// Extensible-event asset this message refers to.
#[cfg(feature = "unstable-msc3488")]
pub asset: Option<AssetContent>,
/// Extensible-event timestamp this message refers to.
#[cfg(feature = "unstable-msc3488")]
pub ts: Option<MilliSecondsSinceUnixEpoch>,
}
impl LocationMessageEventContent {
/// Creates a new `LocationMessageEventContent` 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(vec![TextRepresentation::plain(&body)].into()),
#[cfg(feature = "unstable-msc3488")]
location: Some(LocationContent::new(geo_uri.clone())),
#[cfg(feature = "unstable-msc3488")]
asset: Some(AssetContent::default()),
#[cfg(feature = "unstable-msc3488")]
ts: None,
body,
geo_uri,
info: None,
}
}
/// Set the asset type of this `LocationMessageEventContent`.
#[cfg(feature = "unstable-msc3488")]
pub fn with_asset_type(mut self, asset: AssetType) -> Self {
self.asset = Some(AssetContent { type_: asset });
self
}
/// Set the timestamp of this `LocationMessageEventContent`.
#[cfg(feature = "unstable-msc3488")]
pub fn with_ts(mut self, ts: MilliSecondsSinceUnixEpoch) -> Self {
self.ts = Some(ts);
self
}
/// Get the `geo:` URI of this `LocationMessageEventContent`.
pub fn geo_uri(&self) -> &str {
#[cfg(feature = "unstable-msc3488")]
if let Some(uri) = self.location.as_ref().map(|l| &l.uri) {
return uri;
}
&self.geo_uri
}
/// Get the plain text representation of this `LocationMessageEventContent`.
pub fn plain_text_representation(&self) -> &str {
#[cfg(feature = "unstable-msc3488")]
if let Some(text) = self.message.as_ref().and_then(|m| m.find_plain()) {
return text;
}
&self.body
}
/// Get the asset type of this `LocationMessageEventContent`.
#[cfg(feature = "unstable-msc3488")]
pub fn asset_type(&self) -> AssetType {
self.asset.as_ref().map(|a| a.type_.clone()).unwrap_or_default()
}
}

View File

@ -9,7 +9,9 @@ use ruma_common::{
location::{AssetType, LocationContent, LocationEventContent, ZoomLevel, ZoomLevelError},
message::TextContentBlock,
relation::InReplyTo,
room::message::Relation,
room::message::{
LocationMessageEventContent, MessageType, Relation, RoomMessageEventContent,
},
AnyMessageLikeEvent, MessageLikeEvent,
},
owned_event_id, room_id,
@ -183,3 +185,54 @@ fn message_event_deserialization() {
assert_eq!(ev.sender, user_id!("@user:notareal.hs"));
assert!(ev.unsigned.is_empty());
}
#[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",
},
"org.matrix.msc3488.asset": {
"type": "m.self",
},
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::Location(content));
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");
assert_eq!(content.asset.unwrap().type_, AssetType::Self_);
}
#[test]
fn room_message_unstable_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",
},
"org.matrix.msc3488.asset": {
"type": "m.self",
},
})
);
}

View File

@ -9,8 +9,8 @@ use ruma_common::{
message::{
AudioMessageEventContent, EmoteMessageEventContent, FileMessageEventContent,
ForwardThread, ImageMessageEventContent, KeyVerificationRequestEventContent,
LocationMessageEventContent, MessageType, OriginalRoomMessageEvent,
RoomMessageEventContent, TextMessageEventContent, VideoMessageEventContent,
MessageType, OriginalRoomMessageEvent, RoomMessageEventContent,
TextMessageEventContent, VideoMessageEventContent,
},
EncryptedFileInit, JsonWebKeyInit, MediaSource,
},
@ -650,8 +650,11 @@ fn image_msgtype_deserialization() {
assert_eq!(url, "mxc://notareal.hs/file");
}
#[cfg(not(feature = "unstable-msc3488"))]
#[test]
fn location_msgtype_serialization() {
use ruma_common::events::room::message::LocationMessageEventContent;
let message_event_content =
RoomMessageEventContent::new(MessageType::Location(LocationMessageEventContent::new(
"Alice was at geo:51.5008,0.1247;u=35".to_owned(),