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