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