common: Add support for extensible location events
According to MSC3488
This commit is contained in:
parent
5af2e38506
commit
195ddf8112
@ -34,6 +34,7 @@ unstable-msc2675 = []
|
||||
unstable-msc2676 = []
|
||||
unstable-msc2677 = []
|
||||
unstable-msc3246 = ["unstable-msc3551", "thiserror"]
|
||||
unstable-msc3488 = ["unstable-msc1767"]
|
||||
unstable-msc3551 = ["unstable-msc1767"]
|
||||
unstable-msc3552 = ["unstable-msc1767", "unstable-msc3551"]
|
||||
unstable-msc3553 = ["unstable-msc3552"]
|
||||
|
@ -159,6 +159,8 @@ pub mod ignored_user_list;
|
||||
#[cfg(feature = "unstable-msc3552")]
|
||||
pub mod image;
|
||||
pub mod key;
|
||||
#[cfg(feature = "unstable-msc3488")]
|
||||
pub mod location;
|
||||
#[cfg(feature = "unstable-msc1767")]
|
||||
pub mod message;
|
||||
#[cfg(feature = "unstable-msc1767")]
|
||||
|
@ -52,6 +52,8 @@ event_enum! {
|
||||
"m.key.verification.key",
|
||||
"m.key.verification.mac",
|
||||
"m.key.verification.done",
|
||||
#[cfg(feature = "unstable-msc3488")]
|
||||
"m.location",
|
||||
#[cfg(feature = "unstable-msc1767")]
|
||||
"m.message",
|
||||
#[cfg(feature = "unstable-msc1767")]
|
||||
@ -374,6 +376,8 @@ impl AnyMessageLikeEventContent {
|
||||
Self::Emote(ev) => ev.relates_to.clone().map(Into::into),
|
||||
#[cfg(feature = "unstable-msc3246")]
|
||||
Self::Audio(ev) => ev.relates_to.clone().map(Into::into),
|
||||
#[cfg(feature = "unstable-msc3488")]
|
||||
Self::Location(ev) => ev.relates_to.clone().map(Into::into),
|
||||
#[cfg(feature = "unstable-msc3551")]
|
||||
Self::File(ev) => ev.relates_to.clone().map(Into::into),
|
||||
#[cfg(feature = "unstable-msc3552")]
|
||||
|
173
crates/ruma-common/src/events/location.rs
Normal file
173
crates/ruma-common/src/events/location.rs
Normal file
@ -0,0 +1,173 @@
|
||||
//! Types for extensible location message events ([MSC3488]).
|
||||
//!
|
||||
//! [MSC3488]: https://github.com/matrix-org/matrix-spec-proposals/pull/3488
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use js_int::UInt;
|
||||
use ruma_macros::{EventContent, StringEnum};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod zoomlevel_serde;
|
||||
|
||||
use super::{message::MessageContent, room::message::Relation};
|
||||
use crate::{MilliSecondsSinceUnixEpoch, PrivOwnedStr};
|
||||
|
||||
/// The payload for an extensible location message.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
#[ruma_event(type = "m.location", kind = MessageLike)]
|
||||
pub struct LocationEventContent {
|
||||
/// The text representation of the message.
|
||||
#[serde(flatten)]
|
||||
pub message: MessageContent,
|
||||
|
||||
/// The location info of the message.
|
||||
#[serde(rename = "org.matrix.msc3488.location")]
|
||||
pub location: LocationContent,
|
||||
|
||||
/// The asset this message refers to.
|
||||
#[serde(
|
||||
default,
|
||||
rename = "org.matrix.msc3488.asset",
|
||||
skip_serializing_if = "ruma_common::serde::is_default"
|
||||
)]
|
||||
pub asset: AssetContent,
|
||||
|
||||
/// The timestamp this message refers to.
|
||||
#[serde(rename = "org.matrix.msc3488.ts", skip_serializing_if = "Option::is_none")]
|
||||
pub ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||
|
||||
/// Information about related messages.
|
||||
#[serde(flatten, skip_serializing_if = "Option::is_none")]
|
||||
pub relates_to: Option<Relation>,
|
||||
}
|
||||
|
||||
impl LocationEventContent {
|
||||
/// Creates a new `LocationEventContent` with the given plain text representation and location.
|
||||
pub fn plain(message: impl Into<String>, location: LocationContent) -> Self {
|
||||
Self {
|
||||
message: MessageContent::plain(message),
|
||||
location,
|
||||
asset: Default::default(),
|
||||
ts: None,
|
||||
relates_to: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `LocationEventContent` with the given text representation and location.
|
||||
pub fn with_message(message: MessageContent, location: LocationContent) -> Self {
|
||||
Self { message, location, asset: Default::default(), ts: None, relates_to: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// Location content.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct LocationContent {
|
||||
/// A `geo:` URI representing the location.
|
||||
///
|
||||
/// See [RFC 5870](https://datatracker.ietf.org/doc/html/rfc5870) for more details.
|
||||
pub uri: String,
|
||||
|
||||
/// The description of the location.
|
||||
///
|
||||
/// It should be used to label the location on a map.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// A zoom level to specify the displayed area size.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub zoom_level: Option<ZoomLevel>,
|
||||
}
|
||||
|
||||
impl LocationContent {
|
||||
/// Creates a new `LocationContent` with the given geo URI.
|
||||
pub fn new(uri: String) -> Self {
|
||||
Self { uri, description: None, zoom_level: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// An error encountered when trying to convert to a `ZoomLevel`.
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ZoomLevelError {
|
||||
/// The value is higher than [`ZoomLevel::MAX`].
|
||||
#[error("value too high")]
|
||||
TooHigh,
|
||||
}
|
||||
|
||||
/// A zoom level.
|
||||
///
|
||||
/// This is an integer between 0 and 20 as defined in the [OpenStreetMap Wiki].
|
||||
///
|
||||
/// [OpenStreetMap Wiki]: https://wiki.openstreetmap.org/wiki/Zoom_levels
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ZoomLevel(UInt);
|
||||
|
||||
impl ZoomLevel {
|
||||
/// The smallest value of a `ZoomLevel`, 0.
|
||||
pub const MIN: u8 = 0;
|
||||
|
||||
/// The largest value of a `ZoomLevel`, 20.
|
||||
pub const MAX: u8 = 20;
|
||||
|
||||
/// Creates a new `ZoomLevel` with the given value.
|
||||
pub fn new(value: u8) -> Option<Self> {
|
||||
if value > Self::MAX {
|
||||
None
|
||||
} else {
|
||||
Some(Self(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of this `ZoomLevel`.
|
||||
pub fn value(&self) -> UInt {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for ZoomLevel {
|
||||
type Error = ZoomLevelError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
Self::new(value).ok_or(ZoomLevelError::TooHigh)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asset content.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct AssetContent {
|
||||
/// The type of asset being referred to.
|
||||
#[serde(rename = "type")]
|
||||
pub type_: AssetType,
|
||||
}
|
||||
|
||||
impl AssetContent {
|
||||
/// Creates a new default `AssetContent`.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of an asset.
|
||||
///
|
||||
/// This type can hold an arbitrary string. To check for formats that are not available as a
|
||||
/// documented variant here, use its string representation, obtained through `.as_str()`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, StringEnum)]
|
||||
#[non_exhaustive]
|
||||
pub enum AssetType {
|
||||
/// The asset is the sender of the event.
|
||||
#[ruma_enum(rename = "m.self")]
|
||||
Self_,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
impl Default for AssetType {
|
||||
fn default() -> Self {
|
||||
Self::Self_
|
||||
}
|
||||
}
|
20
crates/ruma-common/src/events/location/zoomlevel_serde.rs
Normal file
20
crates/ruma-common/src/events/location/zoomlevel_serde.rs
Normal file
@ -0,0 +1,20 @@
|
||||
//! `Serialize` and `Deserialize` implementations for extensible events (MSC1767).
|
||||
|
||||
use js_int::UInt;
|
||||
use serde::{de, Deserialize};
|
||||
|
||||
use super::{ZoomLevel, ZoomLevelError};
|
||||
|
||||
impl<'de> Deserialize<'de> for ZoomLevel {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let uint = UInt::deserialize(deserializer)?;
|
||||
if uint > Self::MAX.into() {
|
||||
Err(de::Error::custom(ZoomLevelError::TooHigh))
|
||||
} else {
|
||||
Ok(Self(uint))
|
||||
}
|
||||
}
|
||||
}
|
230
crates/ruma-common/tests/events/location.rs
Normal file
230
crates/ruma-common/tests/events/location.rs
Normal file
@ -0,0 +1,230 @@
|
||||
#![cfg(feature = "unstable-msc3488")]
|
||||
|
||||
use assign::assign;
|
||||
use js_int::uint;
|
||||
use matches::assert_matches;
|
||||
use ruma_common::{
|
||||
event_id,
|
||||
events::{
|
||||
location::{
|
||||
AssetContent, AssetType, LocationContent, LocationEventContent, ZoomLevel,
|
||||
ZoomLevelError,
|
||||
},
|
||||
message::MessageContent,
|
||||
room::message::{InReplyTo, Relation},
|
||||
AnyMessageLikeEvent, MessageLikeEvent, Unsigned,
|
||||
},
|
||||
room_id, user_id, MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
||||
|
||||
#[test]
|
||||
fn plain_content_serialization() {
|
||||
let event_content = LocationEventContent::plain(
|
||||
"Alice was at geo:51.5008,0.1247;u=35",
|
||||
LocationContent::new("geo:51.5008,0.1247;u=35".to_owned()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
to_json_value(&event_content).unwrap(),
|
||||
json!({
|
||||
"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 event_serialization() {
|
||||
let event = MessageLikeEvent {
|
||||
content: assign!(
|
||||
LocationEventContent::with_message(
|
||||
MessageContent::html(
|
||||
"Alice was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
|
||||
"Alice was at <strong>geo:51.5008,0.1247;u=35</strong> as of <em>Sat Nov 13 18:50:58 2021</em>",
|
||||
),
|
||||
assign!(
|
||||
LocationContent::new("geo:51.5008,0.1247;u=35".to_owned()),
|
||||
{
|
||||
description: Some("Alice's whereabouts".into()),
|
||||
zoom_level: Some(ZoomLevel::new(4).unwrap())
|
||||
}
|
||||
)
|
||||
),
|
||||
{
|
||||
ts: Some(MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))),
|
||||
relates_to: Some(Relation::Reply {
|
||||
in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()),
|
||||
}),
|
||||
}
|
||||
),
|
||||
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": "Alice was at <strong>geo:51.5008,0.1247;u=35</strong> as of <em>Sat Nov 13 18:50:58 2021</em>",
|
||||
"mimetype": "text/html",
|
||||
},
|
||||
{
|
||||
"body": "Alice was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
|
||||
"mimetype": "text/plain",
|
||||
},
|
||||
],
|
||||
"org.matrix.msc3488.location": {
|
||||
"uri": "geo:51.5008,0.1247;u=35",
|
||||
"description": "Alice's whereabouts",
|
||||
"zoom_level": 4,
|
||||
},
|
||||
"org.matrix.msc3488.ts": 1_636_829_458,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$replyevent:example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
"event_id": "$event:notareal.hs",
|
||||
"origin_server_ts": 134_829_848,
|
||||
"room_id": "!roomid:notareal.hs",
|
||||
"sender": "@user:notareal.hs",
|
||||
"type": "m.location",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_content_deserialization() {
|
||||
let json_data = json!({
|
||||
"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",
|
||||
},
|
||||
});
|
||||
|
||||
assert_matches!(
|
||||
from_json_value::<LocationEventContent>(json_data)
|
||||
.unwrap(),
|
||||
LocationEventContent {
|
||||
message,
|
||||
location: LocationContent {
|
||||
uri,
|
||||
description: None,
|
||||
zoom_level: None,
|
||||
..
|
||||
},
|
||||
asset: AssetContent {
|
||||
type_: AssetType::Self_,
|
||||
..
|
||||
},
|
||||
ts: None,
|
||||
..
|
||||
}
|
||||
if message.find_plain() == Some("Alice was at geo:51.5008,0.1247;u=35")
|
||||
&& message.find_html().is_none()
|
||||
&& uri == "geo:51.5008,0.1247;u=35"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoomlevel_deserialization_pass() {
|
||||
let json_data = json!({
|
||||
"uri": "geo:51.5008,0.1247;u=35",
|
||||
"zoom_level": 16,
|
||||
});
|
||||
|
||||
assert_matches!(
|
||||
from_json_value::<LocationContent>(json_data).unwrap(),
|
||||
LocationContent {
|
||||
zoom_level: Some(zoom_level),
|
||||
..
|
||||
} if zoom_level.value() == uint!(16)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoomlevel_deserialization_too_high() {
|
||||
let json_data = json!({
|
||||
"uri": "geo:51.5008,0.1247;u=35",
|
||||
"zoom_level": 30,
|
||||
});
|
||||
|
||||
assert_matches!(
|
||||
from_json_value::<LocationContent>(json_data),
|
||||
Err(err)
|
||||
if err.is_data()
|
||||
&& format!("{}", err) == format!("{}", ZoomLevelError::TooHigh)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_event_deserialization() {
|
||||
let json_data = json!({
|
||||
"content": {
|
||||
"org.matrix.msc1767.message": [
|
||||
{ "body": "Alice was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021" },
|
||||
],
|
||||
"org.matrix.msc3488.location": {
|
||||
"uri": "geo:51.5008,0.1247;u=35",
|
||||
"description": "Alice's whereabouts",
|
||||
"zoom_level": 4,
|
||||
},
|
||||
"org.matrix.msc3488.ts": 1_636_829_458,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$replyevent:example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
"event_id": "$event:notareal.hs",
|
||||
"origin_server_ts": 134_829_848,
|
||||
"room_id": "!roomid:notareal.hs",
|
||||
"sender": "@user:notareal.hs",
|
||||
"type": "m.location",
|
||||
});
|
||||
|
||||
assert_matches!(
|
||||
from_json_value::<AnyMessageLikeEvent>(json_data).unwrap(),
|
||||
AnyMessageLikeEvent::Location(MessageLikeEvent {
|
||||
content: LocationEventContent {
|
||||
message,
|
||||
location: LocationContent {
|
||||
uri,
|
||||
description: Some(description),
|
||||
zoom_level: Some(zoom_level),
|
||||
..
|
||||
},
|
||||
asset: AssetContent {
|
||||
type_: AssetType::Self_,
|
||||
..
|
||||
},
|
||||
ts: Some(ts),
|
||||
..
|
||||
},
|
||||
event_id,
|
||||
origin_server_ts,
|
||||
room_id,
|
||||
sender,
|
||||
unsigned
|
||||
}) if event_id == event_id!("$event:notareal.hs")
|
||||
&& message.find_plain() == Some("Alice was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021")
|
||||
&& message.find_html().is_none()
|
||||
&& uri == "geo:51.5008,0.1247;u=35"
|
||||
&& description == "Alice's whereabouts"
|
||||
&& zoom_level.value() == uint!(4)
|
||||
&& ts == MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))
|
||||
&& origin_server_ts == MilliSecondsSinceUnixEpoch(uint!(134_829_848))
|
||||
&& room_id == room_id!("!roomid:notareal.hs")
|
||||
&& sender == user_id!("@user:notareal.hs")
|
||||
&& unsigned.is_empty()
|
||||
);
|
||||
}
|
@ -11,6 +11,7 @@ mod event_enums;
|
||||
mod file;
|
||||
mod image;
|
||||
mod initial_state;
|
||||
mod location;
|
||||
mod message;
|
||||
mod message_event;
|
||||
mod pdu;
|
||||
|
@ -118,6 +118,7 @@ unstable-msc2675 = ["ruma-common/unstable-msc2675"]
|
||||
unstable-msc2676 = ["ruma-common/unstable-msc2676"]
|
||||
unstable-msc2677 = ["ruma-common/unstable-msc2677"]
|
||||
unstable-msc3246 = ["ruma-common/unstable-msc3246"]
|
||||
unstable-msc3488 = ["ruma-common/unstable-msc3488"]
|
||||
unstable-msc3551 = ["ruma-common/unstable-msc3551"]
|
||||
unstable-msc3552 = ["ruma-common/unstable-msc3552"]
|
||||
unstable-msc3553 = ["ruma-common/unstable-msc3553"]
|
||||
@ -134,6 +135,7 @@ __ci = [
|
||||
"unstable-msc2676",
|
||||
"unstable-msc2677",
|
||||
"unstable-msc3246",
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3551",
|
||||
"unstable-msc3552",
|
||||
"unstable-msc3553",
|
||||
|
Loading…
x
Reference in New Issue
Block a user