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