From 7cbffe35dae0b4efcf2bfd3337f4f725cb821b0a Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 22 Sep 2021 13:49:39 +0200 Subject: [PATCH] identifiers: Add matrix.to URL formatting --- crates/ruma-identifiers/Cargo.toml | 1 + crates/ruma-identifiers/src/lib.rs | 2 + crates/ruma-identifiers/src/matrix_to.rs | 84 ++++++++++++++++++++ crates/ruma-identifiers/src/room_alias_id.rs | 12 ++- crates/ruma-identifiers/src/room_id.rs | 28 ++++++- crates/ruma-identifiers/src/user_id.rs | 18 ++++- 6 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 crates/ruma-identifiers/src/matrix_to.rs diff --git a/crates/ruma-identifiers/Cargo.toml b/crates/ruma-identifiers/Cargo.toml index 6215ac56..0ba8a0a7 100644 --- a/crates/ruma-identifiers/Cargo.toml +++ b/crates/ruma-identifiers/Cargo.toml @@ -26,6 +26,7 @@ serde = ["ruma-serde", "serde1"] [dependencies] either = { version = "1.6.1", optional = true } paste = "1.0.5" +percent-encoding = "2.1.0" rand = { version = "0.8.3", optional = true } ruma-identifiers-macros = { version = "=0.20.0", path = "../ruma-identifiers-macros" } ruma-identifiers-validation = { version = "0.5.0", path = "../ruma-identifiers-validation", default-features = false } diff --git a/crates/ruma-identifiers/src/lib.rs b/crates/ruma-identifiers/src/lib.rs index 2431e28b..c225125a 100644 --- a/crates/ruma-identifiers/src/lib.rs +++ b/crates/ruma-identifiers/src/lib.rs @@ -28,6 +28,7 @@ pub use crate::{ event_id::EventId, key_id::{DeviceSigningKeyId, KeyId, ServerSigningKeyId, SigningKeyId}, key_name::{KeyName, KeyNameBox}, + matrix_to::MatrixToRef, mxc_uri::MxcUri, room_alias_id::RoomAliasId, room_id::RoomId, @@ -54,6 +55,7 @@ mod device_key_id; mod event_id; mod key_id; mod key_name; +mod matrix_to; mod mxc_uri; mod room_alias_id; mod room_id; diff --git a/crates/ruma-identifiers/src/matrix_to.rs b/crates/ruma-identifiers/src/matrix_to.rs new file mode 100644 index 00000000..50cb8c06 --- /dev/null +++ b/crates/ruma-identifiers/src/matrix_to.rs @@ -0,0 +1,84 @@ +use std::fmt; + +use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; + +use crate::{EventId, ServerName}; + +const BASE_URL: &str = "https://matrix.to/#/"; +// Controls + Space + reserved characters from RFC 3986. In practice only the +// reserved characters will be encountered most likely, but better be safe. +// https://datatracker.ietf.org/doc/html/rfc3986/#page-13 +const TO_ENCODE: &AsciiSet = &CONTROLS + .add(b':') + .add(b'/') + .add(b'?') + .add(b'#') + .add(b'[') + .add(b']') + .add(b'@') + .add(b'!') + .add(b'$') + .add(b'&') + .add(b'\'') + .add(b'(') + .add(b')') + .add(b'*') + .add(b'+') + .add(b',') + .add(b';') + .add(b'='); + +/// A reference to a user, room or event. +/// +/// Turn it into a `matrix.to` URL through its `Display` implementation (i.e. by +/// interpolating it in a formatting macro or via `.to_string()`). +#[derive(Debug, PartialEq, Eq)] +pub struct MatrixToRef<'a> { + id: &'a str, + event_id: Option<&'a EventId>, + via: Vec<&'a ServerName>, +} + +impl<'a> MatrixToRef<'a> { + pub(crate) fn new(id: &'a str, via: Vec<&'a ServerName>) -> Self { + Self { id, event_id: None, via } + } + + pub(crate) fn event(id: &'a str, event_id: &'a EventId, via: Vec<&'a ServerName>) -> Self { + Self { id, event_id: Some(event_id), via } + } +} + +impl<'a> fmt::Display for MatrixToRef<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(BASE_URL)?; + write!(f, "{}", percent_encode(self.id.as_bytes(), TO_ENCODE))?; + + if let Some(ev_id) = self.event_id { + write!(f, "/{}", percent_encode(ev_id.as_bytes(), TO_ENCODE))?; + } + + let mut first = true; + for server_name in &self.via { + f.write_str(if first { "?via=" } else { "&via=" })?; + f.write_str(server_name.as_str())?; + + first = false; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::user_id; + + #[test] + fn matrix_to_ref() { + assert_eq!( + user_id!("@jplatte:notareal.hs").matrix_to_url().to_string(), + "https://matrix.to/#/%40jplatte%3Anotareal.hs" + ); + } +} diff --git a/crates/ruma-identifiers/src/room_alias_id.rs b/crates/ruma-identifiers/src/room_alias_id.rs index 41ec5117..769e4433 100644 --- a/crates/ruma-identifiers/src/room_alias_id.rs +++ b/crates/ruma-identifiers/src/room_alias_id.rs @@ -2,7 +2,7 @@ use std::{convert::TryInto, fmt, num::NonZeroU8}; -use crate::server_name::ServerName; +use crate::{server_name::ServerName, EventId, MatrixToRef}; /// A Matrix room alias ID. /// @@ -39,6 +39,16 @@ impl RoomAliasId { pub fn server_name(&self) -> &ServerName { self.full_id[self.colon_idx.get() as usize + 1..].try_into().unwrap() } + + /// Create a `matrix.to` reference for this room alias ID. + pub fn matrix_to_url(&self) -> MatrixToRef<'_> { + MatrixToRef::new(&self.full_id, Vec::new()) + } + + /// Create a `matrix.to` reference for an event scoped under this room alias ID. + pub fn matrix_to_event_url<'a>(&'a self, ev_id: &'a EventId) -> MatrixToRef<'a> { + MatrixToRef::event(&self.full_id, ev_id, Vec::new()) + } } /// Attempts to create a new Matrix room alias ID from a string representation. diff --git a/crates/ruma-identifiers/src/room_id.rs b/crates/ruma-identifiers/src/room_id.rs index f1015f61..e634e388 100644 --- a/crates/ruma-identifiers/src/room_id.rs +++ b/crates/ruma-identifiers/src/room_id.rs @@ -2,7 +2,7 @@ use std::{convert::TryInto, fmt, num::NonZeroU8}; -use crate::ServerName; +use crate::{EventId, MatrixToRef, ServerName}; /// A Matrix room ID. /// @@ -53,6 +53,32 @@ impl RoomId { pub fn server_name(&self) -> &ServerName { self.full_id[self.colon_idx.get() as usize + 1..].try_into().unwrap() } + + /// Create a `matrix.to` reference for this room ID. + /// + /// # Example + /// + /// ``` + /// use ruma_identifiers::{room_id, server_name}; + /// + /// assert_eq!( + /// room_id!("!somewhere:example.org") + /// .matrix_to_url([&*server_name!("example.org"), &*server_name!("alt.example.org")]) + /// .to_string(), + /// "https://matrix.to/#/%21somewhere%3Aexample.org?via=example.org&via=alt.example.org" + /// ); + /// ``` + pub fn matrix_to_url<'a>( + &'a self, + via: impl IntoIterator, + ) -> MatrixToRef<'a> { + MatrixToRef::new(&self.full_id, via.into_iter().collect()) + } + + /// Create a `matrix.to` reference for an event scoped under this room ID. + pub fn matrix_to_event_url<'a>(&'a self, ev_id: &'a EventId) -> MatrixToRef<'a> { + MatrixToRef::event(&self.full_id, ev_id, Vec::new()) + } } /// Attempts to create a new Matrix room ID from a string representation. diff --git a/crates/ruma-identifiers/src/user_id.rs b/crates/ruma-identifiers/src/user_id.rs index 8d163fa9..d118aaad 100644 --- a/crates/ruma-identifiers/src/user_id.rs +++ b/crates/ruma-identifiers/src/user_id.rs @@ -2,7 +2,7 @@ use std::{convert::TryInto, fmt, num::NonZeroU8}; -use crate::ServerName; +use crate::{MatrixToRef, ServerName}; /// A Matrix user ID. /// @@ -92,6 +92,22 @@ impl UserId { pub fn is_historical(&self) -> bool { self.is_historical } + + /// Create a `matrix.to` reference for this user ID. + /// + /// # Example + /// + /// ``` + /// use ruma_identifiers::user_id; + /// + /// let message = format!( + /// r#"Thanks for the update {display_name}."#, + /// link = user_id!("@jplatte:notareal.hs").matrix_to_url(), display_name = "jplatte", + /// ); + /// ``` + pub fn matrix_to_url(&self) -> MatrixToRef<'_> { + MatrixToRef::new(&self.full_id, Vec::new()) + } } /// Attempts to create a new Matrix user ID from a string representation.