ruma_common/identifiers/
matrix_uri.rs

1//! Matrix URIs.
2
3use std::{fmt, str::FromStr};
4
5use percent_encoding::{percent_decode_str, percent_encode};
6use ruma_identifiers_validation::{
7    error::{MatrixIdError, MatrixToError, MatrixUriError},
8    Error,
9};
10use url::Url;
11
12use super::{
13    EventId, OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName,
14    OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId, UserId,
15};
16use crate::{percent_encode::PATH_PERCENT_ENCODE_SET, PrivOwnedStr, ServerName};
17
18const MATRIX_TO_BASE_URL: &str = "https://matrix.to/#/";
19const MATRIX_SCHEME: &str = "matrix";
20
21/// All Matrix Identifiers that can be represented as a Matrix URI.
22#[derive(Clone, Debug, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum MatrixId {
25    /// A room ID.
26    Room(OwnedRoomId),
27
28    /// A room alias.
29    RoomAlias(OwnedRoomAliasId),
30
31    /// A user ID.
32    User(OwnedUserId),
33
34    /// An event ID.
35    ///
36    /// Constructing this variant from an `OwnedRoomAliasId` is deprecated, because room aliases
37    /// are mutable, so the URI might break after a while.
38    Event(OwnedRoomOrAliasId, OwnedEventId),
39}
40
41impl MatrixId {
42    /// Try parsing a `&str` with sigils into a `MatrixId`.
43    ///
44    /// The identifiers are expected to start with a sigil and to be percent
45    /// encoded. Slashes at the beginning and the end are stripped.
46    ///
47    /// For events, the room ID or alias and the event ID should be separated by
48    /// a slash and they can be in any order.
49    pub(crate) fn parse_with_sigil(s: &str) -> Result<Self, Error> {
50        let s = if let Some(stripped) = s.strip_prefix('/') { stripped } else { s };
51        let s = if let Some(stripped) = s.strip_suffix('/') { stripped } else { s };
52        if s.is_empty() {
53            return Err(MatrixIdError::NoIdentifier.into());
54        }
55
56        if s.matches('/').count() > 1 {
57            return Err(MatrixIdError::TooManyIdentifiers.into());
58        }
59
60        if let Some((first_raw, second_raw)) = s.split_once('/') {
61            let first = percent_decode_str(first_raw).decode_utf8()?;
62            let second = percent_decode_str(second_raw).decode_utf8()?;
63
64            match first.as_bytes()[0] {
65                b'!' | b'#' if second.as_bytes()[0] == b'$' => {
66                    let room_id = <&RoomOrAliasId>::try_from(first.as_ref())?;
67                    let event_id = <&EventId>::try_from(second.as_ref())?;
68                    Ok((room_id, event_id).into())
69                }
70                b'$' if matches!(second.as_bytes()[0], b'!' | b'#') => {
71                    let room_id = <&RoomOrAliasId>::try_from(second.as_ref())?;
72                    let event_id = <&EventId>::try_from(first.as_ref())?;
73                    Ok((room_id, event_id).into())
74                }
75                _ => Err(MatrixIdError::UnknownIdentifierPair.into()),
76            }
77        } else {
78            let id = percent_decode_str(s).decode_utf8()?;
79
80            match id.as_bytes()[0] {
81                b'@' => Ok(<&UserId>::try_from(id.as_ref())?.into()),
82                b'!' => Ok(<&RoomId>::try_from(id.as_ref())?.into()),
83                b'#' => Ok(<&RoomAliasId>::try_from(id.as_ref())?.into()),
84                b'$' => Err(MatrixIdError::MissingRoom.into()),
85                _ => Err(MatrixIdError::UnknownIdentifier.into()),
86            }
87        }
88    }
89
90    /// Try parsing a `&str` with types into a `MatrixId`.
91    ///
92    /// The identifiers are expected to be in the format
93    /// `type/identifier_without_sigil` and the identifier part is expected to
94    /// be percent encoded. Slashes at the beginning and the end are stripped.
95    ///
96    /// For events, the room ID or alias and the event ID should be separated by
97    /// a slash and they can be in any order.
98    pub(crate) fn parse_with_type(s: &str) -> Result<Self, Error> {
99        let s = if let Some(stripped) = s.strip_prefix('/') { stripped } else { s };
100        let s = if let Some(stripped) = s.strip_suffix('/') { stripped } else { s };
101        if s.is_empty() {
102            return Err(MatrixIdError::NoIdentifier.into());
103        }
104
105        if ![1, 3].contains(&s.matches('/').count()) {
106            return Err(MatrixIdError::InvalidPartsNumber.into());
107        }
108
109        let mut id = String::new();
110        let mut split = s.split('/');
111        while let (Some(type_), Some(id_without_sigil)) = (split.next(), split.next()) {
112            let sigil = match type_ {
113                "u" | "user" => '@',
114                "r" | "room" => '#',
115                "e" | "event" => '$',
116                "roomid" => '!',
117                _ => return Err(MatrixIdError::UnknownType.into()),
118            };
119            id = format!("{id}/{sigil}{id_without_sigil}");
120        }
121
122        Self::parse_with_sigil(&id)
123    }
124
125    /// Construct a string with sigils from `self`.
126    ///
127    /// The identifiers will start with a sigil and be percent encoded.
128    ///
129    /// For events, the room ID or alias and the event ID will be separated by
130    /// a slash.
131    pub(crate) fn to_string_with_sigil(&self) -> String {
132        match self {
133            Self::Room(room_id) => {
134                percent_encode(room_id.as_bytes(), PATH_PERCENT_ENCODE_SET).to_string()
135            }
136            Self::RoomAlias(room_alias) => {
137                percent_encode(room_alias.as_bytes(), PATH_PERCENT_ENCODE_SET).to_string()
138            }
139            Self::User(user_id) => {
140                percent_encode(user_id.as_bytes(), PATH_PERCENT_ENCODE_SET).to_string()
141            }
142            Self::Event(room_id, event_id) => format!(
143                "{}/{}",
144                percent_encode(room_id.as_bytes(), PATH_PERCENT_ENCODE_SET),
145                percent_encode(event_id.as_bytes(), PATH_PERCENT_ENCODE_SET),
146            ),
147        }
148    }
149
150    /// Construct a string with types from `self`.
151    ///
152    /// The identifiers will be in the format `type/identifier_without_sigil`
153    /// and the identifier part will be percent encoded.
154    ///
155    /// For events, the room ID or alias and the event ID will be separated by
156    /// a slash.
157    pub(crate) fn to_string_with_type(&self) -> String {
158        match self {
159            Self::Room(room_id) => {
160                format!(
161                    "roomid/{}",
162                    percent_encode(&room_id.as_bytes()[1..], PATH_PERCENT_ENCODE_SET)
163                )
164            }
165            Self::RoomAlias(room_alias) => {
166                format!(
167                    "r/{}",
168                    percent_encode(&room_alias.as_bytes()[1..], PATH_PERCENT_ENCODE_SET)
169                )
170            }
171            Self::User(user_id) => {
172                format!("u/{}", percent_encode(&user_id.as_bytes()[1..], PATH_PERCENT_ENCODE_SET))
173            }
174            Self::Event(room_id, event_id) => {
175                let room_type = if room_id.is_room_id() { "roomid" } else { "r" };
176                format!(
177                    "{}/{}/e/{}",
178                    room_type,
179                    percent_encode(&room_id.as_bytes()[1..], PATH_PERCENT_ENCODE_SET),
180                    percent_encode(&event_id.as_bytes()[1..], PATH_PERCENT_ENCODE_SET),
181                )
182            }
183        }
184    }
185}
186
187impl From<OwnedRoomId> for MatrixId {
188    fn from(room_id: OwnedRoomId) -> Self {
189        Self::Room(room_id)
190    }
191}
192
193impl From<&RoomId> for MatrixId {
194    fn from(room_id: &RoomId) -> Self {
195        room_id.to_owned().into()
196    }
197}
198
199impl From<OwnedRoomAliasId> for MatrixId {
200    fn from(room_alias: OwnedRoomAliasId) -> Self {
201        Self::RoomAlias(room_alias)
202    }
203}
204
205impl From<&RoomAliasId> for MatrixId {
206    fn from(room_alias: &RoomAliasId) -> Self {
207        room_alias.to_owned().into()
208    }
209}
210
211impl From<OwnedUserId> for MatrixId {
212    fn from(user_id: OwnedUserId) -> Self {
213        Self::User(user_id)
214    }
215}
216
217impl From<&UserId> for MatrixId {
218    fn from(user_id: &UserId) -> Self {
219        user_id.to_owned().into()
220    }
221}
222
223impl From<(OwnedRoomOrAliasId, OwnedEventId)> for MatrixId {
224    fn from(ids: (OwnedRoomOrAliasId, OwnedEventId)) -> Self {
225        Self::Event(ids.0, ids.1)
226    }
227}
228
229impl From<(&RoomOrAliasId, &EventId)> for MatrixId {
230    fn from(ids: (&RoomOrAliasId, &EventId)) -> Self {
231        (ids.0.to_owned(), ids.1.to_owned()).into()
232    }
233}
234
235impl From<(OwnedRoomId, OwnedEventId)> for MatrixId {
236    fn from(ids: (OwnedRoomId, OwnedEventId)) -> Self {
237        Self::Event(ids.0.into(), ids.1)
238    }
239}
240
241impl From<(&RoomId, &EventId)> for MatrixId {
242    fn from(ids: (&RoomId, &EventId)) -> Self {
243        (ids.0.to_owned(), ids.1.to_owned()).into()
244    }
245}
246
247impl From<(OwnedRoomAliasId, OwnedEventId)> for MatrixId {
248    fn from(ids: (OwnedRoomAliasId, OwnedEventId)) -> Self {
249        Self::Event(ids.0.into(), ids.1)
250    }
251}
252
253impl From<(&RoomAliasId, &EventId)> for MatrixId {
254    fn from(ids: (&RoomAliasId, &EventId)) -> Self {
255        (ids.0.to_owned(), ids.1.to_owned()).into()
256    }
257}
258
259/// The [`matrix.to` URI] representation of a user, room or event.
260///
261/// Get the URI through its `Display` implementation (i.e. by interpolating it
262/// in a formatting macro or via `.to_string()`).
263///
264/// [`matrix.to` URI]: https://spec.matrix.org/latest/appendices/#matrixto-navigation
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct MatrixToUri {
267    id: MatrixId,
268    via: Vec<OwnedServerName>,
269}
270
271impl MatrixToUri {
272    pub(crate) fn new(id: MatrixId, via: Vec<OwnedServerName>) -> Self {
273        Self { id, via }
274    }
275
276    /// The identifier represented by this `matrix.to` URI.
277    pub fn id(&self) -> &MatrixId {
278        &self.id
279    }
280
281    /// Matrix servers usable to route a `RoomId`.
282    pub fn via(&self) -> &[OwnedServerName] {
283        &self.via
284    }
285
286    /// Try parsing a `&str` into a `MatrixToUri`.
287    pub fn parse(s: &str) -> Result<Self, Error> {
288        // We do not rely on parsing with `url::Url` because the meaningful part
289        // of the URI is in its fragment part.
290        //
291        // Even if the fragment part looks like parts of a URI, non-url-encoded
292        // room aliases (starting with `#`) could be detected as fragments,
293        // messing up the URI parsing.
294        //
295        // A matrix.to URI looks like this: https://matrix.to/#/{MatrixId}?{query};
296        // where the MatrixId should be percent-encoded, but might not, and the query
297        // should also be percent-encoded.
298
299        let s = s.strip_prefix(MATRIX_TO_BASE_URL).ok_or(MatrixToError::WrongBaseUrl)?;
300        let s = s.strip_suffix('/').unwrap_or(s);
301
302        // Separate the identifiers and the query.
303        let mut parts = s.split('?');
304
305        let ids_part = parts.next().expect("a split iterator yields at least one value");
306        let id = MatrixId::parse_with_sigil(ids_part)?;
307
308        // Parse the query for routing arguments.
309        let via = parts
310            .next()
311            .map(|query| {
312                // `form_urlencoded` takes care of percent-decoding the query.
313                let query_parts = form_urlencoded::parse(query.as_bytes());
314
315                query_parts
316                    .map(|(key, value)| {
317                        (key == "via")
318                            .then(|| ServerName::parse(&value))
319                            .unwrap_or_else(|| Err(MatrixToError::UnknownArgument.into()))
320                    })
321                    .collect::<Result<Vec<_>, _>>()
322            })
323            .transpose()?
324            .unwrap_or_default();
325
326        // That would mean there are two `?` in the URL which is not valid.
327        if parts.next().is_some() {
328            return Err(MatrixToError::InvalidUrl.into());
329        }
330
331        Ok(Self { id, via })
332    }
333}
334
335impl fmt::Display for MatrixToUri {
336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        f.write_str(MATRIX_TO_BASE_URL)?;
338        write!(f, "{}", self.id().to_string_with_sigil())?;
339
340        let mut first = true;
341        for server_name in &self.via {
342            f.write_str(if first { "?via=" } else { "&via=" })?;
343            f.write_str(server_name.as_str())?;
344
345            first = false;
346        }
347
348        Ok(())
349    }
350}
351
352impl TryFrom<&str> for MatrixToUri {
353    type Error = Error;
354
355    fn try_from(s: &str) -> Result<Self, Self::Error> {
356        Self::parse(s)
357    }
358}
359
360impl FromStr for MatrixToUri {
361    type Err = Error;
362
363    fn from_str(s: &str) -> Result<Self, Self::Err> {
364        Self::parse(s)
365    }
366}
367
368/// The intent of a Matrix URI.
369#[derive(Clone, Debug, PartialEq, Eq)]
370#[non_exhaustive]
371pub enum UriAction {
372    /// Join the room referenced by the URI.
373    ///
374    /// The client should prompt for confirmation prior to joining the room, if
375    /// the user isn’t already part of the room.
376    Join,
377
378    /// Start a direct chat with the user referenced by the URI.
379    ///
380    /// Clients supporting a form of Canonical DMs should reuse existing DMs
381    /// instead of creating new ones if available. The client should prompt for
382    /// confirmation prior to creating the DM, if the user isn’t being
383    /// redirected to an existing canonical DM.
384    Chat,
385
386    #[doc(hidden)]
387    _Custom(PrivOwnedStr),
388}
389
390impl UriAction {
391    /// Creates a string slice from this `UriAction`.
392    pub fn as_str(&self) -> &str {
393        self.as_ref()
394    }
395
396    fn from<T>(s: T) -> Self
397    where
398        T: AsRef<str> + Into<Box<str>>,
399    {
400        match s.as_ref() {
401            "join" => UriAction::Join,
402            "chat" => UriAction::Chat,
403            _ => UriAction::_Custom(PrivOwnedStr(s.into())),
404        }
405    }
406}
407
408impl AsRef<str> for UriAction {
409    fn as_ref(&self) -> &str {
410        match self {
411            UriAction::Join => "join",
412            UriAction::Chat => "chat",
413            UriAction::_Custom(s) => s.0.as_ref(),
414        }
415    }
416}
417
418impl fmt::Display for UriAction {
419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420        write!(f, "{}", self.as_ref())?;
421        Ok(())
422    }
423}
424
425impl From<&str> for UriAction {
426    fn from(s: &str) -> Self {
427        Self::from(s)
428    }
429}
430
431impl From<String> for UriAction {
432    fn from(s: String) -> Self {
433        Self::from(s)
434    }
435}
436
437impl From<Box<str>> for UriAction {
438    fn from(s: Box<str>) -> Self {
439        Self::from(s)
440    }
441}
442
443/// The [`matrix:` URI] representation of a user, room or event.
444///
445/// Get the URI through its `Display` implementation (i.e. by interpolating it
446/// in a formatting macro or via `.to_string()`).
447///
448/// [`matrix:` URI]: https://spec.matrix.org/latest/appendices/#matrix-uri-scheme
449#[derive(Debug, Clone, PartialEq, Eq)]
450pub struct MatrixUri {
451    id: MatrixId,
452    via: Vec<OwnedServerName>,
453    action: Option<UriAction>,
454}
455
456impl MatrixUri {
457    pub(crate) fn new(id: MatrixId, via: Vec<OwnedServerName>, action: Option<UriAction>) -> Self {
458        Self { id, via, action }
459    }
460
461    /// The identifier represented by this `matrix:` URI.
462    pub fn id(&self) -> &MatrixId {
463        &self.id
464    }
465
466    /// Matrix servers usable to route a `RoomId`.
467    pub fn via(&self) -> &[OwnedServerName] {
468        &self.via
469    }
470
471    /// The intent of this URI.
472    pub fn action(&self) -> Option<&UriAction> {
473        self.action.as_ref()
474    }
475
476    /// Try parsing a `&str` into a `MatrixUri`.
477    pub fn parse(s: &str) -> Result<Self, Error> {
478        let url = Url::parse(s).map_err(|_| MatrixToError::InvalidUrl)?;
479
480        if url.scheme() != MATRIX_SCHEME {
481            return Err(MatrixUriError::WrongScheme.into());
482        }
483
484        let id = MatrixId::parse_with_type(url.path())?;
485
486        let mut via = vec![];
487        let mut action = None;
488
489        for (key, value) in url.query_pairs() {
490            if key.as_ref() == "via" {
491                via.push(value.parse()?);
492            } else if key.as_ref() == "action" {
493                if action.is_some() {
494                    return Err(MatrixUriError::TooManyActions.into());
495                };
496
497                action = Some(value.as_ref().into());
498            } else {
499                return Err(MatrixUriError::UnknownQueryItem.into());
500            }
501        }
502
503        Ok(Self { id, via, action })
504    }
505}
506
507impl fmt::Display for MatrixUri {
508    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509        write!(f, "{MATRIX_SCHEME}:{}", self.id().to_string_with_type())?;
510
511        let mut first = true;
512        for server_name in &self.via {
513            f.write_str(if first { "?via=" } else { "&via=" })?;
514            f.write_str(server_name.as_str())?;
515
516            first = false;
517        }
518
519        if let Some(action) = self.action() {
520            f.write_str(if first { "?action=" } else { "&action=" })?;
521            f.write_str(action.as_str())?;
522        }
523
524        Ok(())
525    }
526}
527
528impl TryFrom<&str> for MatrixUri {
529    type Error = Error;
530
531    fn try_from(s: &str) -> Result<Self, Self::Error> {
532        Self::parse(s)
533    }
534}
535
536impl FromStr for MatrixUri {
537    type Err = Error;
538
539    fn from_str(s: &str) -> Result<Self, Self::Err> {
540        Self::parse(s)
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use assert_matches2::assert_matches;
547    use ruma_identifiers_validation::{
548        error::{MatrixIdError, MatrixToError, MatrixUriError},
549        Error,
550    };
551
552    use super::{MatrixId, MatrixToUri, MatrixUri};
553    use crate::{
554        event_id, matrix_uri::UriAction, room_alias_id, room_id, server_name, user_id,
555        RoomOrAliasId,
556    };
557
558    #[test]
559    fn display_matrixtouri() {
560        assert_eq!(
561            user_id!("@jplatte:notareal.hs").matrix_to_uri().to_string(),
562            "https://matrix.to/#/@jplatte:notareal.hs"
563        );
564        assert_eq!(
565            room_alias_id!("#ruma:notareal.hs").matrix_to_uri().to_string(),
566            "https://matrix.to/#/%23ruma:notareal.hs"
567        );
568        assert_eq!(
569            room_id!("!ruma:notareal.hs").matrix_to_uri().to_string(),
570            "https://matrix.to/#/!ruma:notareal.hs"
571        );
572        assert_eq!(
573            room_id!("!ruma:notareal.hs")
574                .matrix_to_uri_via(vec![server_name!("notareal.hs")])
575                .to_string(),
576            "https://matrix.to/#/!ruma:notareal.hs?via=notareal.hs"
577        );
578        #[allow(deprecated)]
579        let uri = room_alias_id!("#ruma:notareal.hs")
580            .matrix_to_event_uri(event_id!("$event:notareal.hs"))
581            .to_string();
582        assert_eq!(uri, "https://matrix.to/#/%23ruma:notareal.hs/$event:notareal.hs");
583        assert_eq!(
584            room_id!("!ruma:notareal.hs")
585                .matrix_to_event_uri(event_id!("$event:notareal.hs"))
586                .to_string(),
587            "https://matrix.to/#/!ruma:notareal.hs/$event:notareal.hs"
588        );
589        assert_eq!(
590            room_id!("!ruma:notareal.hs")
591                .matrix_to_event_uri_via(
592                    event_id!("$event:notareal.hs"),
593                    vec![server_name!("notareal.hs")]
594                )
595                .to_string(),
596            "https://matrix.to/#/!ruma:notareal.hs/$event:notareal.hs?via=notareal.hs"
597        );
598    }
599
600    #[test]
601    fn parse_valid_matrixid_with_sigil() {
602        assert_eq!(
603            MatrixId::parse_with_sigil("@user:imaginary.hs").expect("Failed to create MatrixId."),
604            MatrixId::User(user_id!("@user:imaginary.hs").into())
605        );
606        assert_eq!(
607            MatrixId::parse_with_sigil("!roomid:imaginary.hs").expect("Failed to create MatrixId."),
608            MatrixId::Room(room_id!("!roomid:imaginary.hs").into())
609        );
610        assert_eq!(
611            MatrixId::parse_with_sigil("#roomalias:imaginary.hs")
612                .expect("Failed to create MatrixId."),
613            MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into())
614        );
615        assert_eq!(
616            MatrixId::parse_with_sigil("!roomid:imaginary.hs/$event:imaginary.hs")
617                .expect("Failed to create MatrixId."),
618            MatrixId::Event(
619                <&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(),
620                event_id!("$event:imaginary.hs").into()
621            )
622        );
623        assert_eq!(
624            MatrixId::parse_with_sigil("#roomalias:imaginary.hs/$event:imaginary.hs")
625                .expect("Failed to create MatrixId."),
626            MatrixId::Event(
627                <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(),
628                event_id!("$event:imaginary.hs").into()
629            )
630        );
631        // Invert the order of the event and the room.
632        assert_eq!(
633            MatrixId::parse_with_sigil("$event:imaginary.hs/!roomid:imaginary.hs")
634                .expect("Failed to create MatrixId."),
635            MatrixId::Event(
636                <&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(),
637                event_id!("$event:imaginary.hs").into()
638            )
639        );
640        assert_eq!(
641            MatrixId::parse_with_sigil("$event:imaginary.hs/#roomalias:imaginary.hs")
642                .expect("Failed to create MatrixId."),
643            MatrixId::Event(
644                <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(),
645                event_id!("$event:imaginary.hs").into()
646            )
647        );
648        // Starting with a slash
649        assert_eq!(
650            MatrixId::parse_with_sigil("/@user:imaginary.hs").expect("Failed to create MatrixId."),
651            MatrixId::User(user_id!("@user:imaginary.hs").into())
652        );
653        // Ending with a slash
654        assert_eq!(
655            MatrixId::parse_with_sigil("!roomid:imaginary.hs/")
656                .expect("Failed to create MatrixId."),
657            MatrixId::Room(room_id!("!roomid:imaginary.hs").into())
658        );
659        // Starting and ending with a slash
660        assert_eq!(
661            MatrixId::parse_with_sigil("/#roomalias:imaginary.hs/")
662                .expect("Failed to create MatrixId."),
663            MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into())
664        );
665    }
666
667    #[test]
668    fn parse_matrixid_no_identifier() {
669        assert_eq!(MatrixId::parse_with_sigil("").unwrap_err(), MatrixIdError::NoIdentifier.into());
670        assert_eq!(
671            MatrixId::parse_with_sigil("/").unwrap_err(),
672            MatrixIdError::NoIdentifier.into()
673        );
674    }
675
676    #[test]
677    fn parse_matrixid_too_many_identifiers() {
678        assert_eq!(
679            MatrixId::parse_with_sigil(
680                "@user:imaginary.hs/#room:imaginary.hs/$event1:imaginary.hs"
681            )
682            .unwrap_err(),
683            MatrixIdError::TooManyIdentifiers.into()
684        );
685    }
686
687    #[test]
688    fn parse_matrixid_unknown_identifier_pair() {
689        assert_eq!(
690            MatrixId::parse_with_sigil("!roomid:imaginary.hs/@user:imaginary.hs").unwrap_err(),
691            MatrixIdError::UnknownIdentifierPair.into()
692        );
693        assert_eq!(
694            MatrixId::parse_with_sigil("#roomalias:imaginary.hs/notanidentifier").unwrap_err(),
695            MatrixIdError::UnknownIdentifierPair.into()
696        );
697        assert_eq!(
698            MatrixId::parse_with_sigil("$event:imaginary.hs/$otherevent:imaginary.hs").unwrap_err(),
699            MatrixIdError::UnknownIdentifierPair.into()
700        );
701        assert_eq!(
702            MatrixId::parse_with_sigil("notanidentifier/neitheristhis").unwrap_err(),
703            MatrixIdError::UnknownIdentifierPair.into()
704        );
705    }
706
707    #[test]
708    fn parse_matrixid_missing_room() {
709        assert_eq!(
710            MatrixId::parse_with_sigil("$event:imaginary.hs").unwrap_err(),
711            MatrixIdError::MissingRoom.into()
712        );
713    }
714
715    #[test]
716    fn parse_matrixid_unknown_identifier() {
717        assert_eq!(
718            MatrixId::parse_with_sigil("event:imaginary.hs").unwrap_err(),
719            MatrixIdError::UnknownIdentifier.into()
720        );
721        assert_eq!(
722            MatrixId::parse_with_sigil("notanidentifier").unwrap_err(),
723            MatrixIdError::UnknownIdentifier.into()
724        );
725    }
726
727    #[test]
728    fn parse_matrixtouri_valid_uris() {
729        let matrix_to = MatrixToUri::parse("https://matrix.to/#/%40jplatte%3Anotareal.hs")
730            .expect("Failed to create MatrixToUri.");
731        assert_eq!(matrix_to.id(), &user_id!("@jplatte:notareal.hs").into());
732
733        let matrix_to = MatrixToUri::parse("https://matrix.to/#/%23ruma%3Anotareal.hs")
734            .expect("Failed to create MatrixToUri.");
735        assert_eq!(matrix_to.id(), &room_alias_id!("#ruma:notareal.hs").into());
736
737        let matrix_to = MatrixToUri::parse(
738            "https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs&via=anotherunreal.hs",
739        )
740        .expect("Failed to create MatrixToUri.");
741        assert_eq!(matrix_to.id(), &room_id!("!ruma:notareal.hs").into());
742        assert_eq!(
743            matrix_to.via(),
744            &[server_name!("notareal.hs").to_owned(), server_name!("anotherunreal.hs").to_owned(),]
745        );
746
747        let matrix_to =
748            MatrixToUri::parse("https://matrix.to/#/%23ruma%3Anotareal.hs/%24event%3Anotareal.hs")
749                .expect("Failed to create MatrixToUri.");
750        assert_eq!(
751            matrix_to.id(),
752            &(room_alias_id!("#ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
753        );
754
755        let matrix_to =
756            MatrixToUri::parse("https://matrix.to/#/%21ruma%3Anotareal.hs/%24event%3Anotareal.hs")
757                .expect("Failed to create MatrixToUri.");
758        assert_eq!(
759            matrix_to.id(),
760            &(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
761        );
762        assert_eq!(matrix_to.via().len(), 0);
763    }
764
765    #[test]
766    fn parse_matrixtouri_valid_uris_not_urlencoded() {
767        let matrix_to = MatrixToUri::parse("https://matrix.to/#/@jplatte:notareal.hs")
768            .expect("Failed to create MatrixToUri.");
769        assert_eq!(matrix_to.id(), &user_id!("@jplatte:notareal.hs").into());
770
771        let matrix_to = MatrixToUri::parse("https://matrix.to/#/#ruma:notareal.hs")
772            .expect("Failed to create MatrixToUri.");
773        assert_eq!(matrix_to.id(), &room_alias_id!("#ruma:notareal.hs").into());
774
775        let matrix_to = MatrixToUri::parse("https://matrix.to/#/!ruma:notareal.hs?via=notareal.hs")
776            .expect("Failed to create MatrixToUri.");
777        assert_eq!(matrix_to.id(), &room_id!("!ruma:notareal.hs").into());
778        assert_eq!(matrix_to.via(), &[server_name!("notareal.hs").to_owned()]);
779
780        let matrix_to =
781            MatrixToUri::parse("https://matrix.to/#/#ruma:notareal.hs/$event:notareal.hs")
782                .expect("Failed to create MatrixToUri.");
783        assert_eq!(
784            matrix_to.id(),
785            &(room_alias_id!("#ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
786        );
787
788        let matrix_to =
789            MatrixToUri::parse("https://matrix.to/#/!ruma:notareal.hs/$event:notareal.hs")
790                .expect("Failed to create MatrixToUri.");
791        assert_eq!(
792            matrix_to.id(),
793            &(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
794        );
795        assert_eq!(matrix_to.via().len(), 0);
796    }
797
798    #[test]
799    fn parse_matrixtouri_wrong_base_url() {
800        assert_eq!(MatrixToUri::parse("").unwrap_err(), MatrixToError::WrongBaseUrl.into());
801        assert_eq!(
802            MatrixToUri::parse("https://notreal.to/#/").unwrap_err(),
803            MatrixToError::WrongBaseUrl.into()
804        );
805    }
806
807    #[test]
808    fn parse_matrixtouri_wrong_identifier() {
809        assert_matches!(
810            MatrixToUri::parse("https://matrix.to/#/notanidentifier").unwrap_err(),
811            Error::InvalidMatrixId(_)
812        );
813        assert_matches!(
814            MatrixToUri::parse("https://matrix.to/#/").unwrap_err(),
815            Error::InvalidMatrixId(_)
816        );
817        assert_matches!(
818            MatrixToUri::parse(
819                "https://matrix.to/#/%40jplatte%3Anotareal.hs/%24event%3Anotareal.hs"
820            )
821            .unwrap_err(),
822            Error::InvalidMatrixId(_)
823        );
824    }
825
826    #[test]
827    fn parse_matrixtouri_unknown_arguments() {
828        assert_eq!(
829            MatrixToUri::parse(
830                "https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs&custom=data"
831            )
832            .unwrap_err(),
833            MatrixToError::UnknownArgument.into()
834        );
835    }
836
837    #[test]
838    fn display_matrixuri() {
839        assert_eq!(
840            user_id!("@jplatte:notareal.hs").matrix_uri(false).to_string(),
841            "matrix:u/jplatte:notareal.hs"
842        );
843        assert_eq!(
844            user_id!("@jplatte:notareal.hs").matrix_uri(true).to_string(),
845            "matrix:u/jplatte:notareal.hs?action=chat"
846        );
847        assert_eq!(
848            room_alias_id!("#ruma:notareal.hs").matrix_uri(false).to_string(),
849            "matrix:r/ruma:notareal.hs"
850        );
851        assert_eq!(
852            room_alias_id!("#ruma:notareal.hs").matrix_uri(true).to_string(),
853            "matrix:r/ruma:notareal.hs?action=join"
854        );
855        assert_eq!(
856            room_id!("!ruma:notareal.hs").matrix_uri(false).to_string(),
857            "matrix:roomid/ruma:notareal.hs"
858        );
859        assert_eq!(
860            room_id!("!ruma:notareal.hs")
861                .matrix_uri_via(vec![server_name!("notareal.hs")], false)
862                .to_string(),
863            "matrix:roomid/ruma:notareal.hs?via=notareal.hs"
864        );
865        assert_eq!(
866            room_id!("!ruma:notareal.hs")
867                .matrix_uri_via(
868                    vec![server_name!("notareal.hs"), server_name!("anotherunreal.hs")],
869                    true
870                )
871                .to_string(),
872            "matrix:roomid/ruma:notareal.hs?via=notareal.hs&via=anotherunreal.hs&action=join"
873        );
874        #[allow(deprecated)]
875        let uri = room_alias_id!("#ruma:notareal.hs")
876            .matrix_event_uri(event_id!("$event:notareal.hs"))
877            .to_string();
878        assert_eq!(uri, "matrix:r/ruma:notareal.hs/e/event:notareal.hs");
879        assert_eq!(
880            room_id!("!ruma:notareal.hs")
881                .matrix_event_uri(event_id!("$event:notareal.hs"))
882                .to_string(),
883            "matrix:roomid/ruma:notareal.hs/e/event:notareal.hs"
884        );
885        assert_eq!(
886            room_id!("!ruma:notareal.hs")
887                .matrix_event_uri_via(
888                    event_id!("$event:notareal.hs"),
889                    vec![server_name!("notareal.hs")]
890                )
891                .to_string(),
892            "matrix:roomid/ruma:notareal.hs/e/event:notareal.hs?via=notareal.hs"
893        );
894    }
895
896    #[test]
897    fn parse_valid_matrixid_with_type() {
898        assert_eq!(
899            MatrixId::parse_with_type("u/user:imaginary.hs").expect("Failed to create MatrixId."),
900            MatrixId::User(user_id!("@user:imaginary.hs").into())
901        );
902        assert_eq!(
903            MatrixId::parse_with_type("user/user:imaginary.hs")
904                .expect("Failed to create MatrixId."),
905            MatrixId::User(user_id!("@user:imaginary.hs").into())
906        );
907        assert_eq!(
908            MatrixId::parse_with_type("roomid/roomid:imaginary.hs")
909                .expect("Failed to create MatrixId."),
910            MatrixId::Room(room_id!("!roomid:imaginary.hs").into())
911        );
912        assert_eq!(
913            MatrixId::parse_with_type("r/roomalias:imaginary.hs")
914                .expect("Failed to create MatrixId."),
915            MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into())
916        );
917        assert_eq!(
918            MatrixId::parse_with_type("room/roomalias:imaginary.hs")
919                .expect("Failed to create MatrixId."),
920            MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into())
921        );
922        assert_eq!(
923            MatrixId::parse_with_type("roomid/roomid:imaginary.hs/e/event:imaginary.hs")
924                .expect("Failed to create MatrixId."),
925            MatrixId::Event(
926                <&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(),
927                event_id!("$event:imaginary.hs").into()
928            )
929        );
930        assert_eq!(
931            MatrixId::parse_with_type("r/roomalias:imaginary.hs/e/event:imaginary.hs")
932                .expect("Failed to create MatrixId."),
933            MatrixId::Event(
934                <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(),
935                event_id!("$event:imaginary.hs").into()
936            )
937        );
938        assert_eq!(
939            MatrixId::parse_with_type("room/roomalias:imaginary.hs/event/event:imaginary.hs")
940                .expect("Failed to create MatrixId."),
941            MatrixId::Event(
942                <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(),
943                event_id!("$event:imaginary.hs").into()
944            )
945        );
946        // Invert the order of the event and the room.
947        assert_eq!(
948            MatrixId::parse_with_type("e/event:imaginary.hs/roomid/roomid:imaginary.hs")
949                .expect("Failed to create MatrixId."),
950            MatrixId::Event(
951                <&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(),
952                event_id!("$event:imaginary.hs").into()
953            )
954        );
955        assert_eq!(
956            MatrixId::parse_with_type("e/event:imaginary.hs/r/roomalias:imaginary.hs")
957                .expect("Failed to create MatrixId."),
958            MatrixId::Event(
959                <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(),
960                event_id!("$event:imaginary.hs").into()
961            )
962        );
963        // Starting with a slash
964        assert_eq!(
965            MatrixId::parse_with_type("/u/user:imaginary.hs").expect("Failed to create MatrixId."),
966            MatrixId::User(user_id!("@user:imaginary.hs").into())
967        );
968        // Ending with a slash
969        assert_eq!(
970            MatrixId::parse_with_type("roomid/roomid:imaginary.hs/")
971                .expect("Failed to create MatrixId."),
972            MatrixId::Room(room_id!("!roomid:imaginary.hs").into())
973        );
974        // Starting and ending with a slash
975        assert_eq!(
976            MatrixId::parse_with_type("/r/roomalias:imaginary.hs/")
977                .expect("Failed to create MatrixId."),
978            MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into())
979        );
980    }
981
982    #[test]
983    fn parse_matrixid_type_no_identifier() {
984        assert_eq!(MatrixId::parse_with_type("").unwrap_err(), MatrixIdError::NoIdentifier.into());
985        assert_eq!(MatrixId::parse_with_type("/").unwrap_err(), MatrixIdError::NoIdentifier.into());
986    }
987
988    #[test]
989    fn parse_matrixid_invalid_parts_number() {
990        assert_eq!(
991            MatrixId::parse_with_type("u/user:imaginary.hs/r/room:imaginary.hs/e").unwrap_err(),
992            MatrixIdError::InvalidPartsNumber.into()
993        );
994    }
995
996    #[test]
997    fn parse_matrixid_unknown_type() {
998        assert_eq!(
999            MatrixId::parse_with_type("notatype/fake:notareal.hs").unwrap_err(),
1000            MatrixIdError::UnknownType.into()
1001        );
1002    }
1003
1004    #[test]
1005    fn parse_matrixuri_valid_uris() {
1006        let matrix_uri =
1007            MatrixUri::parse("matrix:u/jplatte:notareal.hs").expect("Failed to create MatrixUri.");
1008        assert_eq!(matrix_uri.id(), &user_id!("@jplatte:notareal.hs").into());
1009        assert_eq!(matrix_uri.action(), None);
1010
1011        let matrix_uri = MatrixUri::parse("matrix:u/jplatte:notareal.hs?action=chat")
1012            .expect("Failed to create MatrixUri.");
1013        assert_eq!(matrix_uri.id(), &user_id!("@jplatte:notareal.hs").into());
1014        assert_eq!(matrix_uri.action(), Some(&UriAction::Chat));
1015
1016        let matrix_uri =
1017            MatrixUri::parse("matrix:r/ruma:notareal.hs").expect("Failed to create MatrixToUri.");
1018        assert_eq!(matrix_uri.id(), &room_alias_id!("#ruma:notareal.hs").into());
1019
1020        let matrix_uri = MatrixUri::parse("matrix:roomid/ruma:notareal.hs?via=notareal.hs")
1021            .expect("Failed to create MatrixToUri.");
1022        assert_eq!(matrix_uri.id(), &room_id!("!ruma:notareal.hs").into());
1023        assert_eq!(matrix_uri.via(), &[server_name!("notareal.hs").to_owned()]);
1024        assert_eq!(matrix_uri.action(), None);
1025
1026        let matrix_uri = MatrixUri::parse("matrix:r/ruma:notareal.hs/e/event:notareal.hs")
1027            .expect("Failed to create MatrixToUri.");
1028        assert_eq!(
1029            matrix_uri.id(),
1030            &(room_alias_id!("#ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
1031        );
1032
1033        let matrix_uri = MatrixUri::parse("matrix:roomid/ruma:notareal.hs/e/event:notareal.hs")
1034            .expect("Failed to create MatrixToUri.");
1035        assert_eq!(
1036            matrix_uri.id(),
1037            &(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
1038        );
1039        assert_eq!(matrix_uri.via().len(), 0);
1040        assert_eq!(matrix_uri.action(), None);
1041
1042        let matrix_uri =
1043            MatrixUri::parse("matrix:roomid/ruma:notareal.hs/e/event:notareal.hs?via=notareal.hs&action=join&via=anotherinexistant.hs")
1044                .expect("Failed to create MatrixToUri.");
1045        assert_eq!(
1046            matrix_uri.id(),
1047            &(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into()
1048        );
1049        assert_eq!(
1050            matrix_uri.via(),
1051            &vec![
1052                server_name!("notareal.hs").to_owned(),
1053                server_name!("anotherinexistant.hs").to_owned()
1054            ]
1055        );
1056        assert_eq!(matrix_uri.action(), Some(&UriAction::Join));
1057    }
1058
1059    #[test]
1060    fn parse_matrixuri_invalid_uri() {
1061        assert_eq!(
1062            MatrixUri::parse("").unwrap_err(),
1063            Error::InvalidMatrixToUri(MatrixToError::InvalidUrl)
1064        );
1065    }
1066
1067    #[test]
1068    fn parse_matrixuri_wrong_scheme() {
1069        assert_eq!(
1070            MatrixUri::parse("unknown:u/user:notareal.hs").unwrap_err(),
1071            MatrixUriError::WrongScheme.into()
1072        );
1073    }
1074
1075    #[test]
1076    fn parse_matrixuri_too_many_actions() {
1077        assert_eq!(
1078            MatrixUri::parse("matrix:u/user:notareal.hs?action=chat&action=join").unwrap_err(),
1079            MatrixUriError::TooManyActions.into()
1080        );
1081    }
1082
1083    #[test]
1084    fn parse_matrixuri_unknown_query_item() {
1085        assert_eq!(
1086            MatrixUri::parse("matrix:roomid/roomid:notareal.hs?via=notareal.hs&fake=data")
1087                .unwrap_err(),
1088            MatrixUriError::UnknownQueryItem.into()
1089        );
1090    }
1091
1092    #[test]
1093    fn parse_matrixuri_wrong_identifier() {
1094        assert_matches!(
1095            MatrixUri::parse("matrix:notanidentifier").unwrap_err(),
1096            Error::InvalidMatrixId(_)
1097        );
1098        assert_matches!(MatrixUri::parse("matrix:").unwrap_err(), Error::InvalidMatrixId(_));
1099        assert_matches!(
1100            MatrixUri::parse("matrix:u/jplatte:notareal.hs/e/event:notareal.hs").unwrap_err(),
1101            Error::InvalidMatrixId(_)
1102        );
1103    }
1104}