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}