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