ruma_common/identifiers/
room_id.rs

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