ruma_common/identifiers/
room_id.rs

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