Skip to main content

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/v1.18/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/v1.18/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
541impl From<MatrixUri> for MatrixToUri {
542    fn from(value: MatrixUri) -> Self {
543        Self { id: value.id, via: value.via }
544    }
545}
546
547impl From<MatrixToUri> for MatrixUri {
548    fn from(value: MatrixToUri) -> Self {
549        Self { id: value.id, via: value.via, action: None }
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use assert_matches2::assert_matches;
556    use ruma_identifiers_validation::{
557        Error,
558        error::{MatrixIdError, MatrixToError, MatrixUriError},
559    };
560
561    use super::{MatrixId, MatrixToUri, MatrixUri};
562    use crate::{
563        matrix_uri::UriAction, owned_event_id, owned_room_alias_id, owned_room_id,
564        owned_server_name, owned_user_id, room_alias_id, room_id, user_id,
565    };
566
567    #[test]
568    fn display_matrixtouri() {
569        assert_eq!(
570            user_id!("@jplatte:notareal.hs").matrix_to_uri().to_string(),
571            "https://matrix.to/#/@jplatte:notareal.hs"
572        );
573        assert_eq!(
574            room_alias_id!("#ruma:notareal.hs").matrix_to_uri().to_string(),
575            "https://matrix.to/#/%23ruma:notareal.hs"
576        );
577        assert_eq!(
578            room_id!("!ruma:notareal.hs").matrix_to_uri().to_string(),
579            "https://matrix.to/#/!ruma:notareal.hs"
580        );
581        assert_eq!(
582            room_id!("!ruma:notareal.hs")
583                .matrix_to_uri_via(vec![owned_server_name!("notareal.hs")])
584                .to_string(),
585            "https://matrix.to/#/!ruma:notareal.hs?via=notareal.hs"
586        );
587        #[allow(deprecated)]
588        let uri = room_alias_id!("#ruma:notareal.hs")
589            .matrix_to_event_uri(owned_event_id!("$event:notareal.hs"))
590            .to_string();
591        assert_eq!(uri, "https://matrix.to/#/%23ruma:notareal.hs/$event:notareal.hs");
592        assert_eq!(
593            room_id!("!ruma:notareal.hs")
594                .matrix_to_event_uri(owned_event_id!("$event:notareal.hs"))
595                .to_string(),
596            "https://matrix.to/#/!ruma:notareal.hs/$event:notareal.hs"
597        );
598        assert_eq!(
599            room_id!("!ruma:notareal.hs")
600                .matrix_to_event_uri_via(
601                    owned_event_id!("$event:notareal.hs"),
602                    vec![owned_server_name!("notareal.hs")]
603                )
604                .to_string(),
605            "https://matrix.to/#/!ruma:notareal.hs/$event:notareal.hs?via=notareal.hs"
606        );
607    }
608
609    #[test]
610    fn parse_valid_matrixid_with_sigil() {
611        assert_eq!(
612            MatrixId::parse_with_sigil("@user:imaginary.hs").expect("Failed to create MatrixId."),
613            MatrixId::User(owned_user_id!("@user:imaginary.hs"))
614        );
615        assert_eq!(
616            MatrixId::parse_with_sigil("!roomid:imaginary.hs").expect("Failed to create MatrixId."),
617            MatrixId::Room(owned_room_id!("!roomid:imaginary.hs"))
618        );
619        assert_eq!(
620            MatrixId::parse_with_sigil("#roomalias:imaginary.hs")
621                .expect("Failed to create MatrixId."),
622            MatrixId::RoomAlias(owned_room_alias_id!("#roomalias:imaginary.hs"))
623        );
624        assert_eq!(
625            MatrixId::parse_with_sigil("!roomid:imaginary.hs/$event:imaginary.hs")
626                .expect("Failed to create MatrixId."),
627            MatrixId::Event(
628                owned_room_id!("!roomid:imaginary.hs").into(),
629                owned_event_id!("$event:imaginary.hs")
630            )
631        );
632        assert_eq!(
633            MatrixId::parse_with_sigil("#roomalias:imaginary.hs/$event:imaginary.hs")
634                .expect("Failed to create MatrixId."),
635            MatrixId::Event(
636                owned_room_alias_id!("#roomalias:imaginary.hs").into(),
637                owned_event_id!("$event:imaginary.hs")
638            )
639        );
640        // Invert the order of the event and the room.
641        assert_eq!(
642            MatrixId::parse_with_sigil("$event:imaginary.hs/!roomid:imaginary.hs")
643                .expect("Failed to create MatrixId."),
644            MatrixId::Event(
645                owned_room_id!("!roomid:imaginary.hs").into(),
646                owned_event_id!("$event:imaginary.hs")
647            )
648        );
649        assert_eq!(
650            MatrixId::parse_with_sigil("$event:imaginary.hs/#roomalias:imaginary.hs")
651                .expect("Failed to create MatrixId."),
652            MatrixId::Event(
653                owned_room_alias_id!("#roomalias:imaginary.hs").into(),
654                owned_event_id!("$event:imaginary.hs")
655            )
656        );
657        // Starting with a slash
658        assert_eq!(
659            MatrixId::parse_with_sigil("/@user:imaginary.hs").expect("Failed to create MatrixId."),
660            MatrixId::User(owned_user_id!("@user:imaginary.hs"))
661        );
662        // Ending with a slash
663        assert_eq!(
664            MatrixId::parse_with_sigil("!roomid:imaginary.hs/")
665                .expect("Failed to create MatrixId."),
666            MatrixId::Room(owned_room_id!("!roomid:imaginary.hs"))
667        );
668        // Starting and ending with a slash
669        assert_eq!(
670            MatrixId::parse_with_sigil("/#roomalias:imaginary.hs/")
671                .expect("Failed to create MatrixId."),
672            MatrixId::RoomAlias(owned_room_alias_id!("#roomalias:imaginary.hs"))
673        );
674    }
675
676    #[test]
677    fn parse_matrixid_no_identifier() {
678        assert_eq!(MatrixId::parse_with_sigil("").unwrap_err(), MatrixIdError::NoIdentifier.into());
679        assert_eq!(
680            MatrixId::parse_with_sigil("/").unwrap_err(),
681            MatrixIdError::NoIdentifier.into()
682        );
683    }
684
685    #[test]
686    fn parse_matrixid_too_many_identifiers() {
687        assert_eq!(
688            MatrixId::parse_with_sigil(
689                "@user:imaginary.hs/#room:imaginary.hs/$event1:imaginary.hs"
690            )
691            .unwrap_err(),
692            MatrixIdError::TooManyIdentifiers.into()
693        );
694    }
695
696    #[test]
697    fn parse_matrixid_unknown_identifier_pair() {
698        assert_eq!(
699            MatrixId::parse_with_sigil("!roomid:imaginary.hs/@user:imaginary.hs").unwrap_err(),
700            MatrixIdError::UnknownIdentifierPair.into()
701        );
702        assert_eq!(
703            MatrixId::parse_with_sigil("#roomalias:imaginary.hs/notanidentifier").unwrap_err(),
704            MatrixIdError::UnknownIdentifierPair.into()
705        );
706        assert_eq!(
707            MatrixId::parse_with_sigil("$event:imaginary.hs/$otherevent:imaginary.hs").unwrap_err(),
708            MatrixIdError::UnknownIdentifierPair.into()
709        );
710        assert_eq!(
711            MatrixId::parse_with_sigil("notanidentifier/neitheristhis").unwrap_err(),
712            MatrixIdError::UnknownIdentifierPair.into()
713        );
714    }
715
716    #[test]
717    fn parse_matrixid_missing_room() {
718        assert_eq!(
719            MatrixId::parse_with_sigil("$event:imaginary.hs").unwrap_err(),
720            MatrixIdError::MissingRoom.into()
721        );
722    }
723
724    #[test]
725    fn parse_matrixid_unknown_identifier() {
726        assert_eq!(
727            MatrixId::parse_with_sigil("event:imaginary.hs").unwrap_err(),
728            MatrixIdError::UnknownIdentifier.into()
729        );
730        assert_eq!(
731            MatrixId::parse_with_sigil("notanidentifier").unwrap_err(),
732            MatrixIdError::UnknownIdentifier.into()
733        );
734    }
735
736    #[test]
737    fn parse_matrixtouri_valid_uris() {
738        let matrix_to = MatrixToUri::parse("https://matrix.to/#/%40jplatte%3Anotareal.hs")
739            .expect("Failed to create MatrixToUri.");
740        assert_eq!(*matrix_to.id(), owned_user_id!("@jplatte:notareal.hs").into());
741
742        let matrix_to = MatrixToUri::parse("https://matrix.to/#/%23ruma%3Anotareal.hs")
743            .expect("Failed to create MatrixToUri.");
744        assert_eq!(*matrix_to.id(), owned_room_alias_id!("#ruma:notareal.hs").into());
745
746        let matrix_to = MatrixToUri::parse(
747            "https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs&via=anotherunreal.hs",
748        )
749        .expect("Failed to create MatrixToUri.");
750        assert_eq!(*matrix_to.id(), owned_room_id!("!ruma:notareal.hs").into());
751        assert_eq!(
752            matrix_to.via(),
753            &[owned_server_name!("notareal.hs"), owned_server_name!("anotherunreal.hs"),]
754        );
755
756        let matrix_to =
757            MatrixToUri::parse("https://matrix.to/#/%23ruma%3Anotareal.hs/%24event%3Anotareal.hs")
758                .expect("Failed to create MatrixToUri.");
759        assert_eq!(
760            *matrix_to.id(),
761            (owned_room_alias_id!("#ruma:notareal.hs"), owned_event_id!("$event:notareal.hs"))
762                .into()
763        );
764
765        let matrix_to =
766            MatrixToUri::parse("https://matrix.to/#/%21ruma%3Anotareal.hs/%24event%3Anotareal.hs")
767                .expect("Failed to create MatrixToUri.");
768        assert_eq!(
769            *matrix_to.id(),
770            (owned_room_id!("!ruma:notareal.hs"), owned_event_id!("$event:notareal.hs")).into()
771        );
772        assert_eq!(matrix_to.via().len(), 0);
773    }
774
775    #[test]
776    fn parse_matrixtouri_valid_uris_not_urlencoded() {
777        let matrix_to = MatrixToUri::parse("https://matrix.to/#/@jplatte:notareal.hs")
778            .expect("Failed to create MatrixToUri.");
779        assert_eq!(*matrix_to.id(), owned_user_id!("@jplatte:notareal.hs").into());
780
781        let matrix_to = MatrixToUri::parse("https://matrix.to/#/#ruma:notareal.hs")
782            .expect("Failed to create MatrixToUri.");
783        assert_eq!(*matrix_to.id(), owned_room_alias_id!("#ruma:notareal.hs").into());
784
785        let matrix_to = MatrixToUri::parse("https://matrix.to/#/!ruma:notareal.hs?via=notareal.hs")
786            .expect("Failed to create MatrixToUri.");
787        assert_eq!(*matrix_to.id(), owned_room_id!("!ruma:notareal.hs").into());
788        assert_eq!(matrix_to.via(), &[owned_server_name!("notareal.hs")]);
789
790        let matrix_to =
791            MatrixToUri::parse("https://matrix.to/#/#ruma:notareal.hs/$event:notareal.hs")
792                .expect("Failed to create MatrixToUri.");
793        assert_eq!(
794            *matrix_to.id(),
795            (owned_room_alias_id!("#ruma:notareal.hs"), owned_event_id!("$event:notareal.hs"))
796                .into()
797        );
798
799        let matrix_to =
800            MatrixToUri::parse("https://matrix.to/#/!ruma:notareal.hs/$event:notareal.hs")
801                .expect("Failed to create MatrixToUri.");
802        assert_eq!(
803            *matrix_to.id(),
804            (owned_room_id!("!ruma:notareal.hs"), owned_event_id!("$event:notareal.hs")).into()
805        );
806        assert_eq!(matrix_to.via().len(), 0);
807    }
808
809    #[test]
810    fn parse_matrixtouri_wrong_base_url() {
811        assert_eq!(MatrixToUri::parse("").unwrap_err(), MatrixToError::WrongBaseUrl.into());
812        assert_eq!(
813            MatrixToUri::parse("https://notreal.to/#/").unwrap_err(),
814            MatrixToError::WrongBaseUrl.into()
815        );
816    }
817
818    #[test]
819    fn parse_matrixtouri_wrong_identifier() {
820        assert_matches!(
821            MatrixToUri::parse("https://matrix.to/#/notanidentifier").unwrap_err(),
822            Error::InvalidMatrixId(_)
823        );
824        assert_matches!(
825            MatrixToUri::parse("https://matrix.to/#/").unwrap_err(),
826            Error::InvalidMatrixId(_)
827        );
828        assert_matches!(
829            MatrixToUri::parse(
830                "https://matrix.to/#/%40jplatte%3Anotareal.hs/%24event%3Anotareal.hs"
831            )
832            .unwrap_err(),
833            Error::InvalidMatrixId(_)
834        );
835    }
836
837    #[test]
838    fn parse_matrixtouri_unknown_arguments() {
839        assert_eq!(
840            MatrixToUri::parse(
841                "https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs&custom=data"
842            )
843            .unwrap_err(),
844            MatrixToError::UnknownArgument.into()
845        );
846    }
847
848    #[test]
849    fn display_matrixuri() {
850        assert_eq!(
851            user_id!("@jplatte:notareal.hs").matrix_uri(false).to_string(),
852            "matrix:u/jplatte:notareal.hs"
853        );
854        assert_eq!(
855            user_id!("@jplatte:notareal.hs").matrix_uri(true).to_string(),
856            "matrix:u/jplatte:notareal.hs?action=chat"
857        );
858        assert_eq!(
859            room_alias_id!("#ruma:notareal.hs").matrix_uri(false).to_string(),
860            "matrix:r/ruma:notareal.hs"
861        );
862        assert_eq!(
863            room_alias_id!("#ruma:notareal.hs").matrix_uri(true).to_string(),
864            "matrix:r/ruma:notareal.hs?action=join"
865        );
866        assert_eq!(
867            room_id!("!ruma:notareal.hs").matrix_uri(false).to_string(),
868            "matrix:roomid/ruma:notareal.hs"
869        );
870        assert_eq!(
871            room_id!("!ruma:notareal.hs")
872                .matrix_uri_via(vec![owned_server_name!("notareal.hs")], false)
873                .to_string(),
874            "matrix:roomid/ruma:notareal.hs?via=notareal.hs"
875        );
876        assert_eq!(
877            room_id!("!ruma:notareal.hs")
878                .matrix_uri_via(
879                    vec![owned_server_name!("notareal.hs"), owned_server_name!("anotherunreal.hs")],
880                    true
881                )
882                .to_string(),
883            "matrix:roomid/ruma:notareal.hs?via=notareal.hs&via=anotherunreal.hs&action=join"
884        );
885        #[allow(deprecated)]
886        let uri = room_alias_id!("#ruma:notareal.hs")
887            .matrix_event_uri(owned_event_id!("$event:notareal.hs"))
888            .to_string();
889        assert_eq!(uri, "matrix:r/ruma:notareal.hs/e/event:notareal.hs");
890        assert_eq!(
891            room_id!("!ruma:notareal.hs")
892                .matrix_event_uri(owned_event_id!("$event:notareal.hs"))
893                .to_string(),
894            "matrix:roomid/ruma:notareal.hs/e/event:notareal.hs"
895        );
896        assert_eq!(
897            room_id!("!ruma:notareal.hs")
898                .matrix_event_uri_via(
899                    owned_event_id!("$event:notareal.hs"),
900                    vec![owned_server_name!("notareal.hs")]
901                )
902                .to_string(),
903            "matrix:roomid/ruma:notareal.hs/e/event:notareal.hs?via=notareal.hs"
904        );
905    }
906
907    #[test]
908    fn parse_valid_matrixid_with_type() {
909        assert_eq!(
910            MatrixId::parse_with_type("u/user:imaginary.hs").expect("Failed to create MatrixId."),
911            MatrixId::User(owned_user_id!("@user:imaginary.hs"))
912        );
913        assert_eq!(
914            MatrixId::parse_with_type("user/user:imaginary.hs")
915                .expect("Failed to create MatrixId."),
916            MatrixId::User(owned_user_id!("@user:imaginary.hs"))
917        );
918        assert_eq!(
919            MatrixId::parse_with_type("roomid/roomid:imaginary.hs")
920                .expect("Failed to create MatrixId."),
921            MatrixId::Room(owned_room_id!("!roomid:imaginary.hs"))
922        );
923        assert_eq!(
924            MatrixId::parse_with_type("r/roomalias:imaginary.hs")
925                .expect("Failed to create MatrixId."),
926            MatrixId::RoomAlias(owned_room_alias_id!("#roomalias:imaginary.hs"))
927        );
928        assert_eq!(
929            MatrixId::parse_with_type("room/roomalias:imaginary.hs")
930                .expect("Failed to create MatrixId."),
931            MatrixId::RoomAlias(owned_room_alias_id!("#roomalias:imaginary.hs"))
932        );
933        assert_eq!(
934            MatrixId::parse_with_type("roomid/roomid:imaginary.hs/e/event:imaginary.hs")
935                .expect("Failed to create MatrixId."),
936            MatrixId::Event(
937                owned_room_id!("!roomid:imaginary.hs").into(),
938                owned_event_id!("$event:imaginary.hs")
939            )
940        );
941        assert_eq!(
942            MatrixId::parse_with_type("r/roomalias:imaginary.hs/e/event:imaginary.hs")
943                .expect("Failed to create MatrixId."),
944            MatrixId::Event(
945                owned_room_alias_id!("#roomalias:imaginary.hs").into(),
946                owned_event_id!("$event:imaginary.hs")
947            )
948        );
949        assert_eq!(
950            MatrixId::parse_with_type("room/roomalias:imaginary.hs/event/event:imaginary.hs")
951                .expect("Failed to create MatrixId."),
952            MatrixId::Event(
953                owned_room_alias_id!("#roomalias:imaginary.hs").into(),
954                owned_event_id!("$event:imaginary.hs")
955            )
956        );
957        // Invert the order of the event and the room.
958        assert_eq!(
959            MatrixId::parse_with_type("e/event:imaginary.hs/roomid/roomid:imaginary.hs")
960                .expect("Failed to create MatrixId."),
961            MatrixId::Event(
962                owned_room_id!("!roomid:imaginary.hs").into(),
963                owned_event_id!("$event:imaginary.hs")
964            )
965        );
966        assert_eq!(
967            MatrixId::parse_with_type("e/event:imaginary.hs/r/roomalias:imaginary.hs")
968                .expect("Failed to create MatrixId."),
969            MatrixId::Event(
970                owned_room_alias_id!("#roomalias:imaginary.hs").into(),
971                owned_event_id!("$event:imaginary.hs")
972            )
973        );
974        // Starting with a slash
975        assert_eq!(
976            MatrixId::parse_with_type("/u/user:imaginary.hs").expect("Failed to create MatrixId."),
977            MatrixId::User(owned_user_id!("@user:imaginary.hs"))
978        );
979        // Ending with a slash
980        assert_eq!(
981            MatrixId::parse_with_type("roomid/roomid:imaginary.hs/")
982                .expect("Failed to create MatrixId."),
983            MatrixId::Room(owned_room_id!("!roomid:imaginary.hs"))
984        );
985        // Starting and ending with a slash
986        assert_eq!(
987            MatrixId::parse_with_type("/r/roomalias:imaginary.hs/")
988                .expect("Failed to create MatrixId."),
989            MatrixId::RoomAlias(owned_room_alias_id!("#roomalias:imaginary.hs"))
990        );
991    }
992
993    #[test]
994    fn parse_matrixid_type_no_identifier() {
995        assert_eq!(MatrixId::parse_with_type("").unwrap_err(), MatrixIdError::NoIdentifier.into());
996        assert_eq!(MatrixId::parse_with_type("/").unwrap_err(), MatrixIdError::NoIdentifier.into());
997    }
998
999    #[test]
1000    fn parse_matrixid_invalid_parts_number() {
1001        assert_eq!(
1002            MatrixId::parse_with_type("u/user:imaginary.hs/r/room:imaginary.hs/e").unwrap_err(),
1003            MatrixIdError::InvalidPartsNumber.into()
1004        );
1005    }
1006
1007    #[test]
1008    fn parse_matrixid_unknown_type() {
1009        assert_eq!(
1010            MatrixId::parse_with_type("notatype/fake:notareal.hs").unwrap_err(),
1011            MatrixIdError::UnknownType.into()
1012        );
1013    }
1014
1015    #[test]
1016    fn parse_matrixuri_valid_uris() {
1017        let matrix_uri =
1018            MatrixUri::parse("matrix:u/jplatte:notareal.hs").expect("Failed to create MatrixUri.");
1019        assert_eq!(*matrix_uri.id(), owned_user_id!("@jplatte:notareal.hs").into());
1020        assert_eq!(matrix_uri.action(), None);
1021
1022        let matrix_uri = MatrixUri::parse("matrix:u/jplatte:notareal.hs?action=chat")
1023            .expect("Failed to create MatrixUri.");
1024        assert_eq!(*matrix_uri.id(), owned_user_id!("@jplatte:notareal.hs").into());
1025        assert_eq!(matrix_uri.action(), Some(&UriAction::Chat));
1026
1027        let matrix_uri =
1028            MatrixUri::parse("matrix:r/ruma:notareal.hs").expect("Failed to create MatrixToUri.");
1029        assert_eq!(*matrix_uri.id(), owned_room_alias_id!("#ruma:notareal.hs").into());
1030
1031        let matrix_uri = MatrixUri::parse("matrix:roomid/ruma:notareal.hs?via=notareal.hs")
1032            .expect("Failed to create MatrixToUri.");
1033        assert_eq!(*matrix_uri.id(), owned_room_id!("!ruma:notareal.hs").into());
1034        assert_eq!(matrix_uri.via(), &[owned_server_name!("notareal.hs")]);
1035        assert_eq!(matrix_uri.action(), None);
1036
1037        let matrix_uri = MatrixUri::parse("matrix:r/ruma:notareal.hs/e/event:notareal.hs")
1038            .expect("Failed to create MatrixToUri.");
1039        assert_eq!(
1040            *matrix_uri.id(),
1041            (owned_room_alias_id!("#ruma:notareal.hs"), owned_event_id!("$event:notareal.hs"))
1042                .into()
1043        );
1044
1045        let matrix_uri = MatrixUri::parse("matrix:roomid/ruma:notareal.hs/e/event:notareal.hs")
1046            .expect("Failed to create MatrixToUri.");
1047        assert_eq!(
1048            *matrix_uri.id(),
1049            (owned_room_id!("!ruma:notareal.hs"), owned_event_id!("$event:notareal.hs")).into()
1050        );
1051        assert_eq!(matrix_uri.via().len(), 0);
1052        assert_eq!(matrix_uri.action(), None);
1053
1054        let matrix_uri =
1055            MatrixUri::parse("matrix:roomid/ruma:notareal.hs/e/event:notareal.hs?via=notareal.hs&action=join&via=anotherinexistant.hs")
1056                .expect("Failed to create MatrixToUri.");
1057        assert_eq!(
1058            *matrix_uri.id(),
1059            (owned_room_id!("!ruma:notareal.hs"), owned_event_id!("$event:notareal.hs")).into()
1060        );
1061        assert_eq!(
1062            matrix_uri.via(),
1063            &vec![owned_server_name!("notareal.hs"), owned_server_name!("anotherinexistant.hs")]
1064        );
1065        assert_eq!(matrix_uri.action(), Some(&UriAction::Join));
1066    }
1067
1068    #[test]
1069    fn parse_matrixuri_invalid_uri() {
1070        assert_eq!(
1071            MatrixUri::parse("").unwrap_err(),
1072            Error::InvalidMatrixToUri(MatrixToError::InvalidUrl)
1073        );
1074    }
1075
1076    #[test]
1077    fn parse_matrixuri_wrong_scheme() {
1078        assert_eq!(
1079            MatrixUri::parse("unknown:u/user:notareal.hs").unwrap_err(),
1080            MatrixUriError::WrongScheme.into()
1081        );
1082    }
1083
1084    #[test]
1085    fn parse_matrixuri_too_many_actions() {
1086        assert_eq!(
1087            MatrixUri::parse("matrix:u/user:notareal.hs?action=chat&action=join").unwrap_err(),
1088            MatrixUriError::TooManyActions.into()
1089        );
1090    }
1091
1092    #[test]
1093    fn parse_matrixuri_unknown_query_item() {
1094        assert_eq!(
1095            MatrixUri::parse("matrix:roomid/roomid:notareal.hs?via=notareal.hs&fake=data")
1096                .unwrap_err(),
1097            MatrixUriError::UnknownQueryItem.into()
1098        );
1099    }
1100
1101    #[test]
1102    fn parse_matrixuri_wrong_identifier() {
1103        assert_matches!(
1104            MatrixUri::parse("matrix:notanidentifier").unwrap_err(),
1105            Error::InvalidMatrixId(_)
1106        );
1107        assert_matches!(MatrixUri::parse("matrix:").unwrap_err(), Error::InvalidMatrixId(_));
1108        assert_matches!(
1109            MatrixUri::parse("matrix:u/jplatte:notareal.hs/e/event:notareal.hs").unwrap_err(),
1110            Error::InvalidMatrixId(_)
1111        );
1112    }
1113}