Skip to main content

ruma_common/identifiers/
room_id.rs

1//! Matrix room identifiers.
2
3use ruma_macros::IdDst;
4use tracing::warn;
5
6use super::{
7    IdParseError, MatrixToUri, MatrixUri, OwnedEventId, OwnedServerName, ServerName,
8    matrix_uri::UriAction,
9};
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/v1.19/appendices/#room-ids
22#[repr(transparent)]
23#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)]
24#[ruma_id(validate = ruma_identifiers_validation::room_id::validate, smallvec_inline_bytes = 48)]
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        find_server_name(self.as_str())
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/v1.19/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/v1.19/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![], join.then_some(UriAction::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/v1.19/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            join.then_some(UriAction::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/v1.19/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/// Find the server name from the given room ID string and return it as a `ServerName`.
243///
244/// This function expects the server name to be the part of the string after the first colon, and
245/// this part of the string is validated.
246///
247/// Returns `None` if there is no colon in the string or if the server name is invalid. If the
248/// server name is invalid a warning is logged.
249pub(super) fn find_server_name(s: &str) -> Option<&ServerName> {
250    let server_name = super::find_server_name_str(s)?;
251
252    server_name
253        .try_into()
254        .inspect_err(|e| {
255            warn!(server_name, "Room ID contains colon but no valid server name afterwards: {e}",);
256        })
257        .ok()
258}
259
260#[cfg(test)]
261mod tests {
262    use super::{OwnedRoomId, RoomId};
263    use crate::{IdParseError, server_name};
264
265    #[test]
266    fn valid_room_id() {
267        let room_id =
268            <&RoomId>::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.");
269        assert_eq!(room_id, "!29fhd83h92h0:example.com");
270    }
271
272    #[test]
273    fn empty_localpart() {
274        let room_id = <&RoomId>::try_from("!:example.com").expect("Failed to create RoomId.");
275        assert_eq!(room_id, "!:example.com");
276        assert_eq!(room_id.server_name(), Some(server_name!("example.com")));
277    }
278
279    #[cfg(feature = "rand")]
280    #[test]
281    fn generate_random_valid_room_id() {
282        let room_id = RoomId::new_v1(server_name!("example.com"));
283        let id_str = room_id.as_str();
284
285        assert!(id_str.starts_with('!'));
286        assert_eq!(id_str.len(), 31);
287    }
288
289    #[test]
290    fn serialize_valid_room_id() {
291        assert_eq!(
292            serde_json::to_string(
293                <&RoomId>::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.")
294            )
295            .expect("Failed to convert RoomId to JSON."),
296            r#""!29fhd83h92h0:example.com""#
297        );
298    }
299
300    #[test]
301    fn deserialize_valid_room_id() {
302        assert_eq!(
303            serde_json::from_str::<OwnedRoomId>(r#""!29fhd83h92h0:example.com""#)
304                .expect("Failed to convert JSON to RoomId"),
305            "!29fhd83h92h0:example.com"
306        );
307    }
308
309    #[test]
310    fn valid_room_id_with_explicit_standard_port() {
311        let room_id =
312            <&RoomId>::try_from("!29fhd83h92h0:example.com:443").expect("Failed to create RoomId.");
313        assert_eq!(room_id, "!29fhd83h92h0:example.com:443");
314        assert_eq!(room_id.server_name(), Some(server_name!("example.com:443")));
315    }
316
317    #[test]
318    fn valid_room_id_with_non_standard_port() {
319        assert_eq!(
320            <&RoomId>::try_from("!29fhd83h92h0:example.com:5000")
321                .expect("Failed to create RoomId."),
322            "!29fhd83h92h0:example.com:5000"
323        );
324    }
325
326    #[test]
327    fn missing_room_id_sigil() {
328        assert_eq!(
329            <&RoomId>::try_from("carl:example.com").unwrap_err(),
330            IdParseError::MissingLeadingSigil
331        );
332    }
333
334    #[test]
335    fn missing_server_name() {
336        let room_id = <&RoomId>::try_from("!29fhd83h92h0").expect("Failed to create RoomId.");
337        assert_eq!(room_id, "!29fhd83h92h0");
338        assert_eq!(room_id.server_name(), None);
339    }
340
341    #[test]
342    fn invalid_room_id_host() {
343        let room_id = <&RoomId>::try_from("!29fhd83h92h0:/").expect("Failed to create RoomId.");
344        assert_eq!(room_id, "!29fhd83h92h0:/");
345        assert_eq!(room_id.server_name(), None);
346    }
347
348    #[test]
349    fn invalid_room_id_port() {
350        let room_id = <&RoomId>::try_from("!29fhd83h92h0:example.com:notaport")
351            .expect("Failed to create RoomId.");
352        assert_eq!(room_id, "!29fhd83h92h0:example.com:notaport");
353        assert_eq!(room_id.server_name(), None);
354    }
355
356    #[test]
357    fn room_id_from_reference_hash() {
358        let reference_hash = "Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg";
359        let room_id = RoomId::new_v2(reference_hash).unwrap();
360        let id_str = room_id.as_str();
361
362        assert!(id_str.starts_with('!'));
363        assert_eq!(&id_str[1..], reference_hash);
364    }
365
366    #[test]
367    fn zeroize() {
368        let room_id = <&RoomId>::try_from("!room_id").expect("Failed to create RoomId.").to_owned();
369        assert_eq!(room_id, "!room_id");
370
371        room_id.zeroize();
372    }
373}