ruma_common/identifiers/
room_id.rs

1//! Matrix room identifiers.
2
3use ruma_macros::IdZst;
4
5use super::{
6    matrix_uri::UriAction, MatrixToUri, MatrixUri, OwnedEventId, OwnedServerName, ServerName,
7};
8use crate::RoomOrAliasId;
9
10/// A Matrix [room ID].
11///
12/// A `RoomId` is generated randomly or converted from a string slice, and can be converted back
13/// into a string as needed.
14///
15/// ```
16/// # use ruma_common::RoomId;
17/// assert_eq!(<&RoomId>::try_from("!n8f893n9:example.com").unwrap(), "!n8f893n9:example.com");
18/// ```
19///
20/// [room ID]: https://spec.matrix.org/latest/appendices/#room-ids
21#[repr(transparent)]
22#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)]
23#[ruma_id(validate = ruma_identifiers_validation::room_id::validate)]
24pub struct RoomId(str);
25
26impl RoomId {
27    /// Attempts to generate a `RoomId` for the given origin server with a localpart consisting of
28    /// 18 random ASCII characters.
29    ///
30    /// Fails if the given homeserver cannot be parsed as a valid host.
31    #[cfg(feature = "rand")]
32    #[allow(clippy::new_ret_no_self)]
33    pub fn new(server_name: &ServerName) -> OwnedRoomId {
34        Self::from_borrowed(&format!("!{}:{server_name}", super::generate_localpart(18))).to_owned()
35    }
36
37    /// Returns the server name of the room ID.
38    pub fn server_name(&self) -> Option<&ServerName> {
39        <&RoomOrAliasId>::from(self).server_name()
40    }
41
42    /// Create a `matrix.to` URI for this room ID.
43    ///
44    /// Note that it is recommended to provide servers that should know the room to be able to find
45    /// it with its room ID. For that use [`RoomId::matrix_to_uri_via()`].
46    ///
47    /// # Example
48    ///
49    /// ```
50    /// use ruma_common::{room_id, server_name};
51    ///
52    /// assert_eq!(
53    ///     room_id!("!somewhere:example.org").matrix_to_uri().to_string(),
54    ///     "https://matrix.to/#/!somewhere:example.org"
55    /// );
56    /// ```
57    pub fn matrix_to_uri(&self) -> MatrixToUri {
58        MatrixToUri::new(self.into(), vec![])
59    }
60
61    /// Create a `matrix.to` URI for this room ID with a list of servers that should know it.
62    ///
63    /// To get the list of servers, it is recommended to use the [routing algorithm] from the spec.
64    ///
65    /// If you don't have a list of servers, you can use [`RoomId::matrix_to_uri()`] instead.
66    ///
67    /// # Example
68    ///
69    /// ```
70    /// use ruma_common::{room_id, server_name};
71    ///
72    /// assert_eq!(
73    ///     room_id!("!somewhere:example.org")
74    ///         .matrix_to_uri_via([&*server_name!("example.org"), &*server_name!("alt.example.org")])
75    ///         .to_string(),
76    ///     "https://matrix.to/#/!somewhere:example.org?via=example.org&via=alt.example.org"
77    /// );
78    /// ```
79    ///
80    /// [routing algorithm]: https://spec.matrix.org/latest/appendices/#routing
81    pub fn matrix_to_uri_via<T>(&self, via: T) -> MatrixToUri
82    where
83        T: IntoIterator,
84        T::Item: Into<OwnedServerName>,
85    {
86        MatrixToUri::new(self.into(), via.into_iter().map(Into::into).collect())
87    }
88
89    /// Create a `matrix.to` URI for an event scoped under this room ID.
90    ///
91    /// Note that it is recommended to provide servers that should know the room to be able to find
92    /// it with its room ID. For that use [`RoomId::matrix_to_event_uri_via()`].
93    pub fn matrix_to_event_uri(&self, ev_id: impl Into<OwnedEventId>) -> MatrixToUri {
94        MatrixToUri::new((self.to_owned(), ev_id.into()).into(), vec![])
95    }
96
97    /// Create a `matrix.to` URI for an event scoped under this room ID with a list of servers that
98    /// should know it.
99    ///
100    /// To get the list of servers, it is recommended to use the [routing algorithm] from the spec.
101    ///
102    /// If you don't have a list of servers, you can use [`RoomId::matrix_to_event_uri()`] instead.
103    ///
104    /// [routing algorithm]: https://spec.matrix.org/latest/appendices/#routing
105    pub fn matrix_to_event_uri_via<T>(&self, ev_id: impl Into<OwnedEventId>, via: T) -> MatrixToUri
106    where
107        T: IntoIterator,
108        T::Item: Into<OwnedServerName>,
109    {
110        MatrixToUri::new(
111            (self.to_owned(), ev_id.into()).into(),
112            via.into_iter().map(Into::into).collect(),
113        )
114    }
115
116    /// Create a `matrix:` URI for this room ID.
117    ///
118    /// If `join` is `true`, a click on the URI should join the room.
119    ///
120    /// Note that it is recommended to provide servers that should know the room to be able to find
121    /// it with its room ID. For that use [`RoomId::matrix_uri_via()`].
122    ///
123    /// # Example
124    ///
125    /// ```
126    /// use ruma_common::{room_id, server_name};
127    ///
128    /// assert_eq!(
129    ///     room_id!("!somewhere:example.org").matrix_uri(false).to_string(),
130    ///     "matrix:roomid/somewhere:example.org"
131    /// );
132    /// ```
133    pub fn matrix_uri(&self, join: bool) -> MatrixUri {
134        MatrixUri::new(self.into(), vec![], Some(UriAction::Join).filter(|_| join))
135    }
136
137    /// Create a `matrix:` URI for this room ID with a list of servers that should know it.
138    ///
139    /// To get the list of servers, it is recommended to use the [routing algorithm] from the spec.
140    ///
141    /// If you don't have a list of servers, you can use [`RoomId::matrix_uri()`] instead.
142    ///
143    /// If `join` is `true`, a click on the URI should join the room.
144    ///
145    /// # Example
146    ///
147    /// ```
148    /// use ruma_common::{room_id, server_name};
149    ///
150    /// assert_eq!(
151    ///     room_id!("!somewhere:example.org")
152    ///         .matrix_uri_via(
153    ///             [&*server_name!("example.org"), &*server_name!("alt.example.org")],
154    ///             true
155    ///         )
156    ///         .to_string(),
157    ///     "matrix:roomid/somewhere:example.org?via=example.org&via=alt.example.org&action=join"
158    /// );
159    /// ```
160    ///
161    /// [routing algorithm]: https://spec.matrix.org/latest/appendices/#routing
162    pub fn matrix_uri_via<T>(&self, via: T, join: bool) -> MatrixUri
163    where
164        T: IntoIterator,
165        T::Item: Into<OwnedServerName>,
166    {
167        MatrixUri::new(
168            self.into(),
169            via.into_iter().map(Into::into).collect(),
170            Some(UriAction::Join).filter(|_| join),
171        )
172    }
173
174    /// Create a `matrix:` URI for an event scoped under this room ID.
175    ///
176    /// Note that it is recommended to provide servers that should know the room to be able to find
177    /// it with its room ID. For that use [`RoomId::matrix_event_uri_via()`].
178    pub fn matrix_event_uri(&self, ev_id: impl Into<OwnedEventId>) -> MatrixUri {
179        MatrixUri::new((self.to_owned(), ev_id.into()).into(), vec![], None)
180    }
181
182    /// Create a `matrix:` URI for an event scoped under this room ID with a list of servers that
183    /// should know it.
184    ///
185    /// To get the list of servers, it is recommended to use the [routing algorithm] from the spec.
186    ///
187    /// If you don't have a list of servers, you can use [`RoomId::matrix_event_uri()`] instead.
188    ///
189    /// [routing algorithm]: https://spec.matrix.org/latest/appendices/#routing
190    pub fn matrix_event_uri_via<T>(&self, ev_id: impl Into<OwnedEventId>, via: T) -> MatrixUri
191    where
192        T: IntoIterator,
193        T::Item: Into<OwnedServerName>,
194    {
195        MatrixUri::new(
196            (self.to_owned(), ev_id.into()).into(),
197            via.into_iter().map(Into::into).collect(),
198            None,
199        )
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::{OwnedRoomId, RoomId};
206    use crate::{server_name, IdParseError};
207
208    #[test]
209    fn valid_room_id() {
210        let room_id =
211            <&RoomId>::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.");
212        assert_eq!(room_id, "!29fhd83h92h0:example.com");
213    }
214
215    #[test]
216    fn empty_localpart() {
217        let room_id = <&RoomId>::try_from("!:example.com").expect("Failed to create RoomId.");
218        assert_eq!(room_id, "!:example.com");
219        assert_eq!(room_id.server_name(), Some(server_name!("example.com")));
220    }
221
222    #[cfg(feature = "rand")]
223    #[test]
224    fn generate_random_valid_room_id() {
225        let room_id = RoomId::new(server_name!("example.com"));
226        let id_str = room_id.as_str();
227
228        assert!(id_str.starts_with('!'));
229        assert_eq!(id_str.len(), 31);
230    }
231
232    #[test]
233    fn serialize_valid_room_id() {
234        assert_eq!(
235            serde_json::to_string(
236                <&RoomId>::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.")
237            )
238            .expect("Failed to convert RoomId to JSON."),
239            r#""!29fhd83h92h0:example.com""#
240        );
241    }
242
243    #[test]
244    fn deserialize_valid_room_id() {
245        assert_eq!(
246            serde_json::from_str::<OwnedRoomId>(r#""!29fhd83h92h0:example.com""#)
247                .expect("Failed to convert JSON to RoomId"),
248            <&RoomId>::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.")
249        );
250    }
251
252    #[test]
253    fn valid_room_id_with_explicit_standard_port() {
254        let room_id =
255            <&RoomId>::try_from("!29fhd83h92h0:example.com:443").expect("Failed to create RoomId.");
256        assert_eq!(room_id, "!29fhd83h92h0:example.com:443");
257        assert_eq!(room_id.server_name(), Some(server_name!("example.com:443")));
258    }
259
260    #[test]
261    fn valid_room_id_with_non_standard_port() {
262        assert_eq!(
263            <&RoomId>::try_from("!29fhd83h92h0:example.com:5000")
264                .expect("Failed to create RoomId."),
265            "!29fhd83h92h0:example.com:5000"
266        );
267    }
268
269    #[test]
270    fn missing_room_id_sigil() {
271        assert_eq!(
272            <&RoomId>::try_from("carl:example.com").unwrap_err(),
273            IdParseError::MissingLeadingSigil
274        );
275    }
276
277    #[test]
278    fn missing_server_name() {
279        let room_id = <&RoomId>::try_from("!29fhd83h92h0").expect("Failed to create RoomId.");
280        assert_eq!(room_id, "!29fhd83h92h0");
281        assert_eq!(room_id.server_name(), None);
282    }
283
284    #[test]
285    fn invalid_room_id_host() {
286        let room_id = <&RoomId>::try_from("!29fhd83h92h0:/").expect("Failed to create RoomId.");
287        assert_eq!(room_id, "!29fhd83h92h0:/");
288        assert_eq!(room_id.server_name(), None);
289    }
290
291    #[test]
292    fn invalid_room_id_port() {
293        let room_id = <&RoomId>::try_from("!29fhd83h92h0:example.com:notaport")
294            .expect("Failed to create RoomId.");
295        assert_eq!(room_id, "!29fhd83h92h0:example.com:notaport");
296        assert_eq!(room_id.server_name(), None);
297    }
298}