1000 lines
34 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Matrix URIs.
use std::{fmt, str::FromStr};
use percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS};
use ruma_identifiers_validation::{
error::{MatrixIdError, MatrixToError, MatrixUriError},
Error,
};
use url::Url;
use super::{
EventId, OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName,
OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId, ServerName, UserId,
};
use crate::PrivOwnedStr;
const MATRIX_TO_BASE_URL: &str = "https://matrix.to/#/";
const MATRIX_SCHEME: &str = "matrix";
// Controls + Space + non-path characters from RFC 3986. In practice only the
// non-path characters will be encountered most likely, but better be safe.
// https://datatracker.ietf.org/doc/html/rfc3986/#page-23
const NON_PATH: &AsciiSet = &CONTROLS.add(b'/').add(b'?').add(b'#').add(b'[').add(b']');
// 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 RESERVED: &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'=');
/// All Matrix Identifiers that can be represented as a Matrix URI.
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum MatrixId {
/// A room ID.
Room(OwnedRoomId),
/// A room alias.
RoomAlias(OwnedRoomAliasId),
/// A user ID.
User(OwnedUserId),
/// An event ID.
Event(OwnedRoomOrAliasId, OwnedEventId),
}
impl MatrixId {
/// Try parsing a `&str` with sigils into a `MatrixId`.
///
/// The identifiers are expected to start with a sigil and to be percent
/// encoded. Slashes at the beginning and the end are stripped.
///
/// For events, the room ID or alias and the event ID should be separated by
/// a slash and they can be in any order.
pub(crate) fn parse_with_sigil(s: &str) -> Result<Self, Error> {
let s = if let Some(stripped) = s.strip_prefix('/') { stripped } else { s };
let s = if let Some(stripped) = s.strip_suffix('/') { stripped } else { s };
if s.is_empty() {
return Err(MatrixIdError::NoIdentifier.into());
}
if s.matches('/').count() > 1 {
return Err(MatrixIdError::TooManyIdentifiers.into());
}
if let Some((first_raw, second_raw)) = s.split_once('/') {
let first = percent_decode_str(first_raw).decode_utf8()?;
let second = percent_decode_str(second_raw).decode_utf8()?;
match first.as_bytes()[0] {
b'!' | b'#' if second.as_bytes()[0] == b'$' => {
let room_id = <&RoomOrAliasId>::try_from(first.as_ref())?;
let event_id = <&EventId>::try_from(second.as_ref())?;
Ok((room_id, event_id).into())
}
b'$' if matches!(second.as_bytes()[0], b'!' | b'#') => {
let room_id = <&RoomOrAliasId>::try_from(second.as_ref())?;
let event_id = <&EventId>::try_from(first.as_ref())?;
Ok((room_id, event_id).into())
}
_ => Err(MatrixIdError::UnknownIdentifierPair.into()),
}
} else {
let id = percent_decode_str(s).decode_utf8()?;
match id.as_bytes()[0] {
b'@' => Ok(<&UserId>::try_from(id.as_ref())?.into()),
b'!' => Ok(<&RoomId>::try_from(id.as_ref())?.into()),
b'#' => Ok(<&RoomAliasId>::try_from(id.as_ref())?.into()),
b'$' => Err(MatrixIdError::MissingRoom.into()),
_ => Err(MatrixIdError::UnknownIdentifier.into()),
}
}
}
/// Try parsing a `&str` with types into a `MatrixId`.
///
/// The identifiers are expected to be in the format
/// `type/identifier_without_sigil` and the identifier part is expected to
/// be percent encoded. Slashes at the beginning and the end are stripped.
///
/// For events, the room ID or alias and the event ID should be separated by
/// a slash and they can be in any order.
pub(crate) fn parse_with_type(s: &str) -> Result<Self, Error> {
let s = if let Some(stripped) = s.strip_prefix('/') { stripped } else { s };
let s = if let Some(stripped) = s.strip_suffix('/') { stripped } else { s };
if s.is_empty() {
return Err(MatrixIdError::NoIdentifier.into());
}
if ![1, 3].contains(&s.matches('/').count()) {
return Err(MatrixIdError::InvalidPartsNumber.into());
}
let mut id = String::new();
let mut split = s.split('/');
while let (Some(type_), Some(id_without_sigil)) = (split.next(), split.next()) {
let sigil = match type_ {
"u" | "user" => '@',
"r" | "room" => '#',
"e" | "event" => '$',
"roomid" => '!',
_ => return Err(MatrixIdError::UnknownType.into()),
};
id = format!("{id}/{sigil}{id_without_sigil}");
}
Self::parse_with_sigil(&id)
}
/// Construct a string with sigils from `self`.
///
/// The identifiers will start with a sigil and be percent encoded.
///
/// For events, the room ID or alias and the event ID will be separated by
/// a slash.
pub(crate) fn to_string_with_sigil(&self) -> String {
match self {
Self::Room(room_id) => percent_encode(room_id.as_bytes(), RESERVED).to_string(),
Self::RoomAlias(room_alias) => {
percent_encode(room_alias.as_bytes(), RESERVED).to_string()
}
Self::User(user_id) => percent_encode(user_id.as_bytes(), RESERVED).to_string(),
Self::Event(room_id, event_id) => format!(
"{}/{}",
percent_encode(room_id.as_bytes(), RESERVED),
percent_encode(event_id.as_bytes(), RESERVED),
),
}
}
/// Construct a string with types from `self`.
///
/// The identifiers will be in the format `type/identifier_without_sigil`
/// and the identifier part will be percent encoded.
///
/// For events, the room ID or alias and the event ID will be separated by
/// a slash.
pub(crate) fn to_string_with_type(&self) -> String {
match self {
Self::Room(room_id) => {
format!("roomid/{}", percent_encode(&room_id.as_bytes()[1..], NON_PATH))
}
Self::RoomAlias(room_alias) => {
format!("r/{}", percent_encode(&room_alias.as_bytes()[1..], NON_PATH))
}
Self::User(user_id) => {
format!("u/{}", percent_encode(&user_id.as_bytes()[1..], NON_PATH))
}
Self::Event(room_id, event_id) => {
let room_type = if room_id.is_room_id() { "roomid" } else { "r" };
format!(
"{}/{}/e/{}",
room_type,
percent_encode(&room_id.as_bytes()[1..], NON_PATH),
percent_encode(&event_id.as_bytes()[1..], NON_PATH),
)
}
}
}
}
impl From<&RoomId> for MatrixId {
fn from(room_id: &RoomId) -> Self {
Self::Room(room_id.into())
}
}
impl From<&RoomAliasId> for MatrixId {
fn from(room_alias: &RoomAliasId) -> Self {
Self::RoomAlias(room_alias.into())
}
}
impl From<&UserId> for MatrixId {
fn from(user_id: &UserId) -> Self {
Self::User(user_id.into())
}
}
impl From<(&RoomOrAliasId, &EventId)> for MatrixId {
fn from(ids: (&RoomOrAliasId, &EventId)) -> Self {
Self::Event(ids.0.into(), ids.1.into())
}
}
impl From<(&RoomId, &EventId)> for MatrixId {
fn from(ids: (&RoomId, &EventId)) -> Self {
Self::Event(<&RoomOrAliasId>::from(ids.0).into(), ids.1.into())
}
}
impl From<(&RoomAliasId, &EventId)> for MatrixId {
fn from(ids: (&RoomAliasId, &EventId)) -> Self {
Self::Event(<&RoomOrAliasId>::from(ids.0).into(), ids.1.into())
}
}
/// The [`matrix.to` URI] representation of a user, room or event.
///
/// Get the URI through its `Display` implementation (i.e. by interpolating it
/// in a formatting macro or via `.to_string()`).
///
/// [`matrix.to` URI]: https://spec.matrix.org/v1.2/appendices/#matrixto-navigation
#[derive(Debug, PartialEq, Eq)]
pub struct MatrixToUri {
id: MatrixId,
via: Vec<OwnedServerName>,
}
impl MatrixToUri {
pub(crate) fn new(id: MatrixId, via: Vec<&ServerName>) -> Self {
Self { id, via: via.into_iter().map(ToOwned::to_owned).collect() }
}
/// The identifier represented by this `matrix.to` URI.
pub fn id(&self) -> &MatrixId {
&self.id
}
/// Matrix servers usable to route a `RoomId`.
pub fn via(&self) -> &[OwnedServerName] {
&self.via
}
/// Try parsing a `&str` into a `MatrixToUri`.
pub fn parse(s: &str) -> Result<Self, Error> {
let without_base = if let Some(stripped) = s.strip_prefix(MATRIX_TO_BASE_URL) {
stripped
} else {
return Err(MatrixToError::WrongBaseUrl.into());
};
let url = Url::parse(MATRIX_TO_BASE_URL.trim_end_matches("#/"))
.expect("matrix.to base URL is valid")
.join(without_base)
.map_err(|_| MatrixToError::InvalidUrl)?;
let id = MatrixId::parse_with_sigil(url.path())?;
let mut via = vec![];
for (key, value) in url.query_pairs() {
if key.as_ref() == "via" {
via.push(value.parse()?);
} else {
return Err(MatrixToError::UnknownArgument.into());
}
}
Ok(Self { id, via })
}
}
impl fmt::Display for MatrixToUri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(MATRIX_TO_BASE_URL)?;
write!(f, "{}", self.id().to_string_with_sigil())?;
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(())
}
}
impl TryFrom<&str> for MatrixToUri {
type Error = Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::parse(s)
}
}
impl FromStr for MatrixToUri {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
/// The intent of a Matrix URI.
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum UriAction {
/// Join the room referenced by the URI.
///
/// The client should prompt for confirmation prior to joining the room, if
/// the user isnt already part of the room.
Join,
/// Start a direct chat with the user referenced by the URI.
///
/// Clients supporting a form of Canonical DMs should reuse existing DMs
/// instead of creating new ones if available. The client should prompt for
/// confirmation prior to creating the DM, if the user isnt being
/// redirected to an existing canonical DM.
Chat,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
impl UriAction {
/// Creates a string slice from this `UriAction`.
pub fn as_str(&self) -> &str {
self.as_ref()
}
fn from<T>(s: T) -> Self
where
T: AsRef<str> + Into<Box<str>>,
{
match s.as_ref() {
"join" => UriAction::Join,
"chat" => UriAction::Chat,
_ => UriAction::_Custom(PrivOwnedStr(s.into())),
}
}
}
impl AsRef<str> for UriAction {
fn as_ref(&self) -> &str {
match self {
UriAction::Join => "join",
UriAction::Chat => "chat",
UriAction::_Custom(s) => s.0.as_ref(),
}
}
}
impl fmt::Display for UriAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_ref())?;
Ok(())
}
}
impl From<&str> for UriAction {
fn from(s: &str) -> Self {
Self::from(s)
}
}
impl From<String> for UriAction {
fn from(s: String) -> Self {
Self::from(s)
}
}
impl From<Box<str>> for UriAction {
fn from(s: Box<str>) -> Self {
Self::from(s)
}
}
/// The [`matrix:` URI] representation of a user, room or event.
///
/// Get the URI through its `Display` implementation (i.e. by interpolating it
/// in a formatting macro or via `.to_string()`).
///
/// [`matrix:` URI]: https://spec.matrix.org/v1.2/appendices/#matrix-uri-scheme
#[derive(Debug, PartialEq, Eq)]
pub struct MatrixUri {
id: MatrixId,
via: Vec<OwnedServerName>,
action: Option<UriAction>,
}
impl MatrixUri {
pub(crate) fn new(id: MatrixId, via: Vec<&ServerName>, action: Option<UriAction>) -> Self {
Self { id, via: via.into_iter().map(ToOwned::to_owned).collect(), action }
}
/// The identifier represented by this `matrix:` URI.
pub fn id(&self) -> &MatrixId {
&self.id
}
/// Matrix servers usable to route a `RoomId`.
pub fn via(&self) -> &[OwnedServerName] {
&self.via
}
/// The intent of this URI.
pub fn action(&self) -> Option<&UriAction> {
self.action.as_ref()
}
/// Try parsing a `&str` into a `MatrixUri`.
pub fn parse(s: &str) -> Result<Self, Error> {
let url = Url::parse(s).map_err(|_| MatrixToError::InvalidUrl)?;
if url.scheme() != MATRIX_SCHEME {
return Err(MatrixUriError::WrongScheme.into());
}
let id = MatrixId::parse_with_type(url.path())?;
let mut via = vec![];
let mut action = None;
for (key, value) in url.query_pairs() {
if key.as_ref() == "via" {
via.push(value.parse()?);
} else if key.as_ref() == "action" {
if action.is_some() {
return Err(MatrixUriError::TooManyActions.into());
};
action = Some(value.as_ref().into());
} else {
return Err(MatrixUriError::UnknownQueryItem.into());
}
}
Ok(Self { id, via, action })
}
}
impl fmt::Display for MatrixUri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{MATRIX_SCHEME}:{}", self.id().to_string_with_type())?;
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;
}
if let Some(action) = self.action() {
f.write_str(if first { "?action=" } else { "&action=" })?;
f.write_str(action.as_str())?;
}
Ok(())
}
}
impl TryFrom<&str> for MatrixUri {
type Error = Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::parse(s)
}
}
impl FromStr for MatrixUri {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use ruma_identifiers_validation::{
error::{MatrixIdError, MatrixToError, MatrixUriError},
Error,
};
use super::{MatrixId, MatrixToUri, MatrixUri};
use crate::{
event_id, matrix_uri::UriAction, room_alias_id, room_id, server_name, user_id,
RoomOrAliasId,
};
#[test]
fn display_matrixtouri() {
assert_eq!(
user_id!("@jplatte:notareal.hs").matrix_to_uri().to_string(),
"https://matrix.to/#/%40jplatte%3Anotareal.hs"
);
assert_eq!(
room_alias_id!("#ruma:notareal.hs").matrix_to_uri().to_string(),
"https://matrix.to/#/%23ruma%3Anotareal.hs"
);
assert_eq!(
room_id!("!ruma:notareal.hs")
.matrix_to_uri(vec![server_name!("notareal.hs")])
.to_string(),
"https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs"
);
assert_eq!(
room_alias_id!("#ruma:notareal.hs")
.matrix_to_event_uri(event_id!("$event:notareal.hs"))
.to_string(),
"https://matrix.to/#/%23ruma%3Anotareal.hs/%24event%3Anotareal.hs"
);
assert_eq!(
room_id!("!ruma:notareal.hs")
.matrix_to_event_uri(event_id!("$event:notareal.hs"))
.to_string(),
"https://matrix.to/#/%21ruma%3Anotareal.hs/%24event%3Anotareal.hs"
);
}
#[test]
fn parse_valid_matrixid_with_sigil() {
assert_eq!(
MatrixId::parse_with_sigil("@user:imaginary.hs").expect("Failed to create MatrixId."),
MatrixId::User(user_id!("@user:imaginary.hs").into())
);
assert_eq!(
MatrixId::parse_with_sigil("!roomid:imaginary.hs").expect("Failed to create MatrixId."),
MatrixId::Room(room_id!("!roomid:imaginary.hs").into())
);
assert_eq!(
MatrixId::parse_with_sigil("#roomalias:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into())
);
assert_eq!(
MatrixId::parse_with_sigil("!roomid:imaginary.hs/$event:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::Event(
<&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(),
event_id!("$event:imaginary.hs").into()
)
);
assert_eq!(
MatrixId::parse_with_sigil("#roomalias:imaginary.hs/$event:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::Event(
<&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(),
event_id!("$event:imaginary.hs").into()
)
);
// Invert the order of the event and the room.
assert_eq!(
MatrixId::parse_with_sigil("$event:imaginary.hs/!roomid:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::Event(
<&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(),
event_id!("$event:imaginary.hs").into()
)
);
assert_eq!(
MatrixId::parse_with_sigil("$event:imaginary.hs/#roomalias:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::Event(
<&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(),
event_id!("$event:imaginary.hs").into()
)
);
// Starting with a slash
assert_eq!(
MatrixId::parse_with_sigil("/@user:imaginary.hs").expect("Failed to create MatrixId."),
MatrixId::User(user_id!("@user:imaginary.hs").into())
);
// Ending with a slash
assert_eq!(
MatrixId::parse_with_sigil("!roomid:imaginary.hs/")
.expect("Failed to create MatrixId."),
MatrixId::Room(room_id!("!roomid:imaginary.hs").into())
);
// Starting and ending with a slash
assert_eq!(
MatrixId::parse_with_sigil("/#roomalias:imaginary.hs/")
.expect("Failed to create MatrixId."),
MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into())
);
}
#[test]
fn parse_matrixid_no_identifier() {
assert_eq!(MatrixId::parse_with_sigil("").unwrap_err(), MatrixIdError::NoIdentifier.into());
assert_eq!(
MatrixId::parse_with_sigil("/").unwrap_err(),
MatrixIdError::NoIdentifier.into()
);
}
#[test]
fn parse_matrixid_too_many_identifiers() {
assert_eq!(
MatrixId::parse_with_sigil(
"@user:imaginary.hs/#room:imaginary.hs/$event1:imaginary.hs"
)
.unwrap_err(),
MatrixIdError::TooManyIdentifiers.into()
);
}
#[test]
fn parse_matrixid_unknown_identifier_pair() {
assert_eq!(
MatrixId::parse_with_sigil("!roomid:imaginary.hs/@user:imaginary.hs").unwrap_err(),
MatrixIdError::UnknownIdentifierPair.into()
);
assert_eq!(
MatrixId::parse_with_sigil("#roomalias:imaginary.hs/notanidentifier").unwrap_err(),
MatrixIdError::UnknownIdentifierPair.into()
);
assert_eq!(
MatrixId::parse_with_sigil("$event:imaginary.hs/$otherevent:imaginary.hs").unwrap_err(),
MatrixIdError::UnknownIdentifierPair.into()
);
assert_eq!(
MatrixId::parse_with_sigil("notanidentifier/neitheristhis").unwrap_err(),
MatrixIdError::UnknownIdentifierPair.into()
);
}
#[test]
fn parse_matrixid_missing_room() {
assert_eq!(
MatrixId::parse_with_sigil("$event:imaginary.hs").unwrap_err(),
MatrixIdError::MissingRoom.into()
);
}
#[test]
fn parse_matrixid_unknown_identifier() {
assert_eq!(
MatrixId::parse_with_sigil("event:imaginary.hs").unwrap_err(),
MatrixIdError::UnknownIdentifier.into()
);
assert_eq!(
MatrixId::parse_with_sigil("notanidentifier").unwrap_err(),
MatrixIdError::UnknownIdentifier.into()
);
}
#[test]
fn parse_matrixtouri_valid_uris() {
let matrix_to = MatrixToUri::parse("https://matrix.to/#/%40jplatte%3Anotareal.hs")
.expect("Failed to create MatrixToUri.");
assert_eq!(matrix_to.id(), &user_id!("@jplatte:notareal.hs").into());
let matrix_to = MatrixToUri::parse("https://matrix.to/#/%23ruma%3Anotareal.hs")
.expect("Failed to create MatrixToUri.");
assert_eq!(matrix_to.id(), &room_alias_id!("#ruma:notareal.hs").into());
let matrix_to =
MatrixToUri::parse("https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs")
.expect("Failed to create MatrixToUri.");
assert_eq!(matrix_to.id(), &room_id!("!ruma:notareal.hs").into());
assert_eq!(matrix_to.via(), &vec![server_name!("notareal.hs").to_owned()]);
let matrix_to =
MatrixToUri::parse("https://matrix.to/#/%23ruma%3Anotareal.hs/%24event%3Anotareal.hs")
.expect("Failed to create MatrixToUri.");
assert_eq!(
matrix_to.id(),
&(room_alias_id!("#ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
);
let matrix_to =
MatrixToUri::parse("https://matrix.to/#/%21ruma%3Anotareal.hs/%24event%3Anotareal.hs")
.expect("Failed to create MatrixToUri.");
assert_eq!(
matrix_to.id(),
&(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
);
assert!(matrix_to.via().is_empty());
}
#[test]
fn parse_matrixtouri_wrong_base_url() {
assert_eq!(MatrixToUri::parse("").unwrap_err(), MatrixToError::WrongBaseUrl.into());
assert_eq!(
MatrixToUri::parse("https://notreal.to/#/").unwrap_err(),
MatrixToError::WrongBaseUrl.into()
);
}
#[test]
fn parse_matrixtouri_wrong_identifier() {
assert_matches!(
MatrixToUri::parse("https://matrix.to/#/notanidentifier").unwrap_err(),
Error::InvalidMatrixId(_)
);
assert_matches!(
MatrixToUri::parse("https://matrix.to/#/").unwrap_err(),
Error::InvalidMatrixId(_)
);
assert_matches!(
MatrixToUri::parse(
"https://matrix.to/#/%40jplatte%3Anotareal.hs/%24event%3Anotareal.hs"
)
.unwrap_err(),
Error::InvalidMatrixId(_)
);
}
#[test]
fn parse_matrixtouri_unknown_arguments() {
assert_eq!(
MatrixToUri::parse(
"https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs&custom=data"
)
.unwrap_err(),
MatrixToError::UnknownArgument.into()
);
}
#[test]
fn display_matrixuri() {
assert_eq!(
user_id!("@jplatte:notareal.hs").matrix_uri(false).to_string(),
"matrix:u/jplatte:notareal.hs"
);
assert_eq!(
user_id!("@jplatte:notareal.hs").matrix_uri(true).to_string(),
"matrix:u/jplatte:notareal.hs?action=chat"
);
assert_eq!(
room_alias_id!("#ruma:notareal.hs").matrix_uri(false).to_string(),
"matrix:r/ruma:notareal.hs"
);
assert_eq!(
room_alias_id!("#ruma:notareal.hs").matrix_uri(true).to_string(),
"matrix:r/ruma:notareal.hs?action=join"
);
assert_eq!(
room_id!("!ruma:notareal.hs")
.matrix_uri(vec![server_name!("notareal.hs")], false)
.to_string(),
"matrix:roomid/ruma:notareal.hs?via=notareal.hs"
);
assert_eq!(
room_id!("!ruma:notareal.hs")
.matrix_uri(
vec![server_name!("notareal.hs"), server_name!("anotherunreal.hs")],
true
)
.to_string(),
"matrix:roomid/ruma:notareal.hs?via=notareal.hs&via=anotherunreal.hs&action=join"
);
assert_eq!(
room_alias_id!("#ruma:notareal.hs")
.matrix_event_uri(event_id!("$event:notareal.hs"))
.to_string(),
"matrix:r/ruma:notareal.hs/e/event:notareal.hs"
);
assert_eq!(
room_id!("!ruma:notareal.hs")
.matrix_event_uri(event_id!("$event:notareal.hs"), vec![])
.to_string(),
"matrix:roomid/ruma:notareal.hs/e/event:notareal.hs"
);
}
#[test]
fn parse_valid_matrixid_with_type() {
assert_eq!(
MatrixId::parse_with_type("u/user:imaginary.hs").expect("Failed to create MatrixId."),
MatrixId::User(user_id!("@user:imaginary.hs").into())
);
assert_eq!(
MatrixId::parse_with_type("user/user:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::User(user_id!("@user:imaginary.hs").into())
);
assert_eq!(
MatrixId::parse_with_type("roomid/roomid:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::Room(room_id!("!roomid:imaginary.hs").into())
);
assert_eq!(
MatrixId::parse_with_type("r/roomalias:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into())
);
assert_eq!(
MatrixId::parse_with_type("room/roomalias:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into())
);
assert_eq!(
MatrixId::parse_with_type("roomid/roomid:imaginary.hs/e/event:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::Event(
<&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(),
event_id!("$event:imaginary.hs").into()
)
);
assert_eq!(
MatrixId::parse_with_type("r/roomalias:imaginary.hs/e/event:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::Event(
<&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(),
event_id!("$event:imaginary.hs").into()
)
);
assert_eq!(
MatrixId::parse_with_type("room/roomalias:imaginary.hs/event/event:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::Event(
<&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(),
event_id!("$event:imaginary.hs").into()
)
);
// Invert the order of the event and the room.
assert_eq!(
MatrixId::parse_with_type("e/event:imaginary.hs/roomid/roomid:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::Event(
<&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(),
event_id!("$event:imaginary.hs").into()
)
);
assert_eq!(
MatrixId::parse_with_type("e/event:imaginary.hs/r/roomalias:imaginary.hs")
.expect("Failed to create MatrixId."),
MatrixId::Event(
<&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(),
event_id!("$event:imaginary.hs").into()
)
);
// Starting with a slash
assert_eq!(
MatrixId::parse_with_type("/u/user:imaginary.hs").expect("Failed to create MatrixId."),
MatrixId::User(user_id!("@user:imaginary.hs").into())
);
// Ending with a slash
assert_eq!(
MatrixId::parse_with_type("roomid/roomid:imaginary.hs/")
.expect("Failed to create MatrixId."),
MatrixId::Room(room_id!("!roomid:imaginary.hs").into())
);
// Starting and ending with a slash
assert_eq!(
MatrixId::parse_with_type("/r/roomalias:imaginary.hs/")
.expect("Failed to create MatrixId."),
MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into())
);
}
#[test]
fn parse_matrixid_type_no_identifier() {
assert_eq!(MatrixId::parse_with_type("").unwrap_err(), MatrixIdError::NoIdentifier.into());
assert_eq!(MatrixId::parse_with_type("/").unwrap_err(), MatrixIdError::NoIdentifier.into());
}
#[test]
fn parse_matrixid_invalid_parts_number() {
assert_eq!(
MatrixId::parse_with_type("u/user:imaginary.hs/r/room:imaginary.hs/e").unwrap_err(),
MatrixIdError::InvalidPartsNumber.into()
);
}
#[test]
fn parse_matrixid_unknown_type() {
assert_eq!(
MatrixId::parse_with_type("notatype/fake:notareal.hs").unwrap_err(),
MatrixIdError::UnknownType.into()
);
}
#[test]
fn parse_matrixuri_valid_uris() {
let matrix_uri =
MatrixUri::parse("matrix:u/jplatte:notareal.hs").expect("Failed to create MatrixUri.");
assert_eq!(matrix_uri.id(), &user_id!("@jplatte:notareal.hs").into());
assert!(matrix_uri.action().is_none());
let matrix_uri = MatrixUri::parse("matrix:u/jplatte:notareal.hs?action=chat")
.expect("Failed to create MatrixUri.");
assert_eq!(matrix_uri.id(), &user_id!("@jplatte:notareal.hs").into());
assert_eq!(matrix_uri.action(), Some(&UriAction::Chat));
let matrix_uri =
MatrixUri::parse("matrix:r/ruma:notareal.hs").expect("Failed to create MatrixToUri.");
assert_eq!(matrix_uri.id(), &room_alias_id!("#ruma:notareal.hs").into());
let matrix_uri = MatrixUri::parse("matrix:roomid/ruma:notareal.hs?via=notareal.hs")
.expect("Failed to create MatrixToUri.");
assert_eq!(matrix_uri.id(), &room_id!("!ruma:notareal.hs").into());
assert_eq!(matrix_uri.via(), &vec![server_name!("notareal.hs").to_owned()]);
assert!(matrix_uri.action().is_none());
let matrix_uri = MatrixUri::parse("matrix:r/ruma:notareal.hs/e/event:notareal.hs")
.expect("Failed to create MatrixToUri.");
assert_eq!(
matrix_uri.id(),
&(room_alias_id!("#ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
);
let matrix_uri = MatrixUri::parse("matrix:roomid/ruma:notareal.hs/e/event:notareal.hs")
.expect("Failed to create MatrixToUri.");
assert_eq!(
matrix_uri.id(),
&(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
);
assert!(matrix_uri.via().is_empty());
assert!(matrix_uri.action().is_none());
let matrix_uri =
MatrixUri::parse("matrix:roomid/ruma:notareal.hs/e/event:notareal.hs?via=notareal.hs&action=join&via=anotherinexistant.hs")
.expect("Failed to create MatrixToUri.");
assert_eq!(
matrix_uri.id(),
&(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
);
assert_eq!(
matrix_uri.via(),
&vec![
server_name!("notareal.hs").to_owned(),
server_name!("anotherinexistant.hs").to_owned()
]
);
assert_eq!(matrix_uri.action(), Some(&UriAction::Join));
}
#[test]
fn parse_matrixuri_invalid_uri() {
assert_eq!(
MatrixUri::parse("").unwrap_err(),
Error::InvalidMatrixToUri(MatrixToError::InvalidUrl)
);
}
#[test]
fn parse_matrixuri_wrong_scheme() {
assert_eq!(
MatrixUri::parse("unknown:u/user:notareal.hs").unwrap_err(),
MatrixUriError::WrongScheme.into()
);
}
#[test]
fn parse_matrixuri_too_many_actions() {
assert_eq!(
MatrixUri::parse("matrix:u/user:notareal.hs?action=chat&action=join").unwrap_err(),
MatrixUriError::TooManyActions.into()
);
}
#[test]
fn parse_matrixuri_unknown_query_item() {
assert_eq!(
MatrixUri::parse("matrix:roomid/roomid:notareal.hs?via=notareal.hs&fake=data")
.unwrap_err(),
MatrixUriError::UnknownQueryItem.into()
);
}
#[test]
fn parse_matrixuri_wrong_identifier() {
assert_matches!(
MatrixUri::parse("matrix:notanidentifier").unwrap_err(),
Error::InvalidMatrixId(_)
);
assert_matches!(MatrixUri::parse("matrix:").unwrap_err(), Error::InvalidMatrixId(_));
assert_matches!(
MatrixUri::parse("matrix:u/jplatte:notareal.hs/e/event:notareal.hs").unwrap_err(),
Error::InvalidMatrixId(_)
);
}
}