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}