ruma_identifiers/
matrix_uri.rs

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