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.19/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.19/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!(matrix_to.via(), &["notareal.hs", "anotherunreal.hs"]);
752
753        let matrix_to =
754            MatrixToUri::parse("https://matrix.to/#/%23ruma%3Anotareal.hs/%24event%3Anotareal.hs")
755                .expect("Failed to create MatrixToUri.");
756        assert_eq!(
757            *matrix_to.id(),
758            (owned_room_alias_id!("#ruma:notareal.hs"), owned_event_id!("$event:notareal.hs"))
759                .into()
760        );
761
762        let matrix_to =
763            MatrixToUri::parse("https://matrix.to/#/%21ruma%3Anotareal.hs/%24event%3Anotareal.hs")
764                .expect("Failed to create MatrixToUri.");
765        assert_eq!(
766            *matrix_to.id(),
767            (owned_room_id!("!ruma:notareal.hs"), owned_event_id!("$event:notareal.hs")).into()
768        );
769        assert_eq!(matrix_to.via().len(), 0);
770    }
771
772    #[test]
773    fn parse_matrixtouri_valid_uris_not_urlencoded() {
774        let matrix_to = MatrixToUri::parse("https://matrix.to/#/@jplatte:notareal.hs")
775            .expect("Failed to create MatrixToUri.");
776        assert_eq!(*matrix_to.id(), owned_user_id!("@jplatte:notareal.hs").into());
777
778        let matrix_to = MatrixToUri::parse("https://matrix.to/#/#ruma:notareal.hs")
779            .expect("Failed to create MatrixToUri.");
780        assert_eq!(*matrix_to.id(), owned_room_alias_id!("#ruma:notareal.hs").into());
781
782        let matrix_to = MatrixToUri::parse("https://matrix.to/#/!ruma:notareal.hs?via=notareal.hs")
783            .expect("Failed to create MatrixToUri.");
784        assert_eq!(*matrix_to.id(), owned_room_id!("!ruma:notareal.hs").into());
785        assert_eq!(matrix_to.via(), &["notareal.hs"]);
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_alias_id!("#ruma:notareal.hs"), owned_event_id!("$event:notareal.hs"))
793                .into()
794        );
795
796        let matrix_to =
797            MatrixToUri::parse("https://matrix.to/#/!ruma:notareal.hs/$event:notareal.hs")
798                .expect("Failed to create MatrixToUri.");
799        assert_eq!(
800            *matrix_to.id(),
801            (owned_room_id!("!ruma:notareal.hs"), owned_event_id!("$event:notareal.hs")).into()
802        );
803        assert_eq!(matrix_to.via().len(), 0);
804    }
805
806    #[test]
807    fn parse_matrixtouri_wrong_base_url() {
808        assert_eq!(MatrixToUri::parse("").unwrap_err(), MatrixToError::WrongBaseUrl.into());
809        assert_eq!(
810            MatrixToUri::parse("https://notreal.to/#/").unwrap_err(),
811            MatrixToError::WrongBaseUrl.into()
812        );
813    }
814
815    #[test]
816    fn parse_matrixtouri_wrong_identifier() {
817        assert_matches!(
818            MatrixToUri::parse("https://matrix.to/#/notanidentifier").unwrap_err(),
819            Error::InvalidMatrixId(_)
820        );
821        assert_matches!(
822            MatrixToUri::parse("https://matrix.to/#/").unwrap_err(),
823            Error::InvalidMatrixId(_)
824        );
825        assert_matches!(
826            MatrixToUri::parse(
827                "https://matrix.to/#/%40jplatte%3Anotareal.hs/%24event%3Anotareal.hs"
828            )
829            .unwrap_err(),
830            Error::InvalidMatrixId(_)
831        );
832    }
833
834    #[test]
835    fn parse_matrixtouri_unknown_arguments() {
836        assert_eq!(
837            MatrixToUri::parse(
838                "https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs&custom=data"
839            )
840            .unwrap_err(),
841            MatrixToError::UnknownArgument.into()
842        );
843    }
844
845    #[test]
846    fn display_matrixuri() {
847        assert_eq!(
848            user_id!("@jplatte:notareal.hs").matrix_uri(false).to_string(),
849            "matrix:u/jplatte:notareal.hs"
850        );
851        assert_eq!(
852            user_id!("@jplatte:notareal.hs").matrix_uri(true).to_string(),
853            "matrix:u/jplatte:notareal.hs?action=chat"
854        );
855        assert_eq!(
856            room_alias_id!("#ruma:notareal.hs").matrix_uri(false).to_string(),
857            "matrix:r/ruma:notareal.hs"
858        );
859        assert_eq!(
860            room_alias_id!("#ruma:notareal.hs").matrix_uri(true).to_string(),
861            "matrix:r/ruma:notareal.hs?action=join"
862        );
863        assert_eq!(
864            room_id!("!ruma:notareal.hs").matrix_uri(false).to_string(),
865            "matrix:roomid/ruma:notareal.hs"
866        );
867        assert_eq!(
868            room_id!("!ruma:notareal.hs")
869                .matrix_uri_via(vec![owned_server_name!("notareal.hs")], false)
870                .to_string(),
871            "matrix:roomid/ruma:notareal.hs?via=notareal.hs"
872        );
873        assert_eq!(
874            room_id!("!ruma:notareal.hs")
875                .matrix_uri_via(
876                    vec![owned_server_name!("notareal.hs"), owned_server_name!("anotherunreal.hs")],
877                    true
878                )
879                .to_string(),
880            "matrix:roomid/ruma:notareal.hs?via=notareal.hs&via=anotherunreal.hs&action=join"
881        );
882        #[allow(deprecated)]
883        let uri = room_alias_id!("#ruma:notareal.hs")
884            .matrix_event_uri(owned_event_id!("$event:notareal.hs"))
885            .to_string();
886        assert_eq!(uri, "matrix:r/ruma:notareal.hs/e/event:notareal.hs");
887        assert_eq!(
888            room_id!("!ruma:notareal.hs")
889                .matrix_event_uri(owned_event_id!("$event:notareal.hs"))
890                .to_string(),
891            "matrix:roomid/ruma:notareal.hs/e/event:notareal.hs"
892        );
893        assert_eq!(
894            room_id!("!ruma:notareal.hs")
895                .matrix_event_uri_via(
896                    owned_event_id!("$event:notareal.hs"),
897                    vec![owned_server_name!("notareal.hs")]
898                )
899                .to_string(),
900            "matrix:roomid/ruma:notareal.hs/e/event:notareal.hs?via=notareal.hs"
901        );
902    }
903
904    #[test]
905    fn parse_valid_matrixid_with_type() {
906        assert_eq!(
907            MatrixId::parse_with_type("u/user:imaginary.hs").expect("Failed to create MatrixId."),
908            MatrixId::User(owned_user_id!("@user:imaginary.hs"))
909        );
910        assert_eq!(
911            MatrixId::parse_with_type("user/user:imaginary.hs")
912                .expect("Failed to create MatrixId."),
913            MatrixId::User(owned_user_id!("@user:imaginary.hs"))
914        );
915        assert_eq!(
916            MatrixId::parse_with_type("roomid/roomid:imaginary.hs")
917                .expect("Failed to create MatrixId."),
918            MatrixId::Room(owned_room_id!("!roomid:imaginary.hs"))
919        );
920        assert_eq!(
921            MatrixId::parse_with_type("r/roomalias:imaginary.hs")
922                .expect("Failed to create MatrixId."),
923            MatrixId::RoomAlias(owned_room_alias_id!("#roomalias:imaginary.hs"))
924        );
925        assert_eq!(
926            MatrixId::parse_with_type("room/roomalias:imaginary.hs")
927                .expect("Failed to create MatrixId."),
928            MatrixId::RoomAlias(owned_room_alias_id!("#roomalias:imaginary.hs"))
929        );
930        assert_eq!(
931            MatrixId::parse_with_type("roomid/roomid:imaginary.hs/e/event:imaginary.hs")
932                .expect("Failed to create MatrixId."),
933            MatrixId::Event(
934                owned_room_id!("!roomid:imaginary.hs").into(),
935                owned_event_id!("$event:imaginary.hs")
936            )
937        );
938        assert_eq!(
939            MatrixId::parse_with_type("r/roomalias:imaginary.hs/e/event:imaginary.hs")
940                .expect("Failed to create MatrixId."),
941            MatrixId::Event(
942                owned_room_alias_id!("#roomalias:imaginary.hs").into(),
943                owned_event_id!("$event:imaginary.hs")
944            )
945        );
946        assert_eq!(
947            MatrixId::parse_with_type("room/roomalias:imaginary.hs/event/event:imaginary.hs")
948                .expect("Failed to create MatrixId."),
949            MatrixId::Event(
950                owned_room_alias_id!("#roomalias:imaginary.hs").into(),
951                owned_event_id!("$event:imaginary.hs")
952            )
953        );
954        // Invert the order of the event and the room.
955        assert_eq!(
956            MatrixId::parse_with_type("e/event:imaginary.hs/roomid/roomid:imaginary.hs")
957                .expect("Failed to create MatrixId."),
958            MatrixId::Event(
959                owned_room_id!("!roomid:imaginary.hs").into(),
960                owned_event_id!("$event:imaginary.hs")
961            )
962        );
963        assert_eq!(
964            MatrixId::parse_with_type("e/event:imaginary.hs/r/roomalias:imaginary.hs")
965                .expect("Failed to create MatrixId."),
966            MatrixId::Event(
967                owned_room_alias_id!("#roomalias:imaginary.hs").into(),
968                owned_event_id!("$event:imaginary.hs")
969            )
970        );
971        // Starting with a slash
972        assert_eq!(
973            MatrixId::parse_with_type("/u/user:imaginary.hs").expect("Failed to create MatrixId."),
974            MatrixId::User(owned_user_id!("@user:imaginary.hs"))
975        );
976        // Ending with a slash
977        assert_eq!(
978            MatrixId::parse_with_type("roomid/roomid:imaginary.hs/")
979                .expect("Failed to create MatrixId."),
980            MatrixId::Room(owned_room_id!("!roomid:imaginary.hs"))
981        );
982        // Starting and ending with a slash
983        assert_eq!(
984            MatrixId::parse_with_type("/r/roomalias:imaginary.hs/")
985                .expect("Failed to create MatrixId."),
986            MatrixId::RoomAlias(owned_room_alias_id!("#roomalias:imaginary.hs"))
987        );
988    }
989
990    #[test]
991    fn parse_matrixid_type_no_identifier() {
992        assert_eq!(MatrixId::parse_with_type("").unwrap_err(), MatrixIdError::NoIdentifier.into());
993        assert_eq!(MatrixId::parse_with_type("/").unwrap_err(), MatrixIdError::NoIdentifier.into());
994    }
995
996    #[test]
997    fn parse_matrixid_invalid_parts_number() {
998        assert_eq!(
999            MatrixId::parse_with_type("u/user:imaginary.hs/r/room:imaginary.hs/e").unwrap_err(),
1000            MatrixIdError::InvalidPartsNumber.into()
1001        );
1002    }
1003
1004    #[test]
1005    fn parse_matrixid_unknown_type() {
1006        assert_eq!(
1007            MatrixId::parse_with_type("notatype/fake:notareal.hs").unwrap_err(),
1008            MatrixIdError::UnknownType.into()
1009        );
1010    }
1011
1012    #[test]
1013    fn parse_matrixuri_valid_uris() {
1014        let matrix_uri =
1015            MatrixUri::parse("matrix:u/jplatte:notareal.hs").expect("Failed to create MatrixUri.");
1016        assert_eq!(*matrix_uri.id(), owned_user_id!("@jplatte:notareal.hs").into());
1017        assert_eq!(matrix_uri.action(), None);
1018
1019        let matrix_uri = MatrixUri::parse("matrix:u/jplatte:notareal.hs?action=chat")
1020            .expect("Failed to create MatrixUri.");
1021        assert_eq!(*matrix_uri.id(), owned_user_id!("@jplatte:notareal.hs").into());
1022        assert_eq!(matrix_uri.action(), Some(&UriAction::Chat));
1023
1024        let matrix_uri =
1025            MatrixUri::parse("matrix:r/ruma:notareal.hs").expect("Failed to create MatrixToUri.");
1026        assert_eq!(*matrix_uri.id(), owned_room_alias_id!("#ruma:notareal.hs").into());
1027
1028        let matrix_uri = MatrixUri::parse("matrix:roomid/ruma:notareal.hs?via=notareal.hs")
1029            .expect("Failed to create MatrixToUri.");
1030        assert_eq!(*matrix_uri.id(), owned_room_id!("!ruma:notareal.hs").into());
1031        assert_eq!(matrix_uri.via(), &["notareal.hs"]);
1032        assert_eq!(matrix_uri.action(), None);
1033
1034        let matrix_uri = MatrixUri::parse("matrix:r/ruma:notareal.hs/e/event:notareal.hs")
1035            .expect("Failed to create MatrixToUri.");
1036        assert_eq!(
1037            *matrix_uri.id(),
1038            (owned_room_alias_id!("#ruma:notareal.hs"), owned_event_id!("$event:notareal.hs"))
1039                .into()
1040        );
1041
1042        let matrix_uri = MatrixUri::parse("matrix:roomid/ruma:notareal.hs/e/event:notareal.hs")
1043            .expect("Failed to create MatrixToUri.");
1044        assert_eq!(
1045            *matrix_uri.id(),
1046            (owned_room_id!("!ruma:notareal.hs"), owned_event_id!("$event:notareal.hs")).into()
1047        );
1048        assert_eq!(matrix_uri.via().len(), 0);
1049        assert_eq!(matrix_uri.action(), None);
1050
1051        let matrix_uri =
1052            MatrixUri::parse("matrix:roomid/ruma:notareal.hs/e/event:notareal.hs?via=notareal.hs&action=join&via=anotherinexistant.hs")
1053                .expect("Failed to create MatrixToUri.");
1054        assert_eq!(
1055            *matrix_uri.id(),
1056            (owned_room_id!("!ruma:notareal.hs"), owned_event_id!("$event:notareal.hs")).into()
1057        );
1058        assert_eq!(matrix_uri.via(), &["notareal.hs", "anotherinexistant.hs"]);
1059        assert_eq!(matrix_uri.action(), Some(&UriAction::Join));
1060    }
1061
1062    #[test]
1063    fn parse_matrixuri_invalid_uri() {
1064        assert_eq!(
1065            MatrixUri::parse("").unwrap_err(),
1066            Error::InvalidMatrixToUri(MatrixToError::InvalidUrl)
1067        );
1068    }
1069
1070    #[test]
1071    fn parse_matrixuri_wrong_scheme() {
1072        assert_eq!(
1073            MatrixUri::parse("unknown:u/user:notareal.hs").unwrap_err(),
1074            MatrixUriError::WrongScheme.into()
1075        );
1076    }
1077
1078    #[test]
1079    fn parse_matrixuri_too_many_actions() {
1080        assert_eq!(
1081            MatrixUri::parse("matrix:u/user:notareal.hs?action=chat&action=join").unwrap_err(),
1082            MatrixUriError::TooManyActions.into()
1083        );
1084    }
1085
1086    #[test]
1087    fn parse_matrixuri_unknown_query_item() {
1088        assert_eq!(
1089            MatrixUri::parse("matrix:roomid/roomid:notareal.hs?via=notareal.hs&fake=data")
1090                .unwrap_err(),
1091            MatrixUriError::UnknownQueryItem.into()
1092        );
1093    }
1094
1095    #[test]
1096    fn parse_matrixuri_wrong_identifier() {
1097        assert_matches!(
1098            MatrixUri::parse("matrix:notanidentifier").unwrap_err(),
1099            Error::InvalidMatrixId(_)
1100        );
1101        assert_matches!(MatrixUri::parse("matrix:").unwrap_err(), Error::InvalidMatrixId(_));
1102        assert_matches!(
1103            MatrixUri::parse("matrix:u/jplatte:notareal.hs/e/event:notareal.hs").unwrap_err(),
1104            Error::InvalidMatrixId(_)
1105        );
1106    }
1107}