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