1pub mod v3 {
6 use ruma_common::{
11 OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName,
12 api::{Metadata, auth_scheme::AccessToken, response},
13 metadata,
14 };
15
16 metadata! {
17 method: POST,
18 rate_limited: true,
19 authentication: AccessToken,
20 history: {
21 unstable => "/_matrix/client/unstable/xyz.amorgan.knock/knock/{room_id_or_alias}",
22 1.1 => "/_matrix/client/v3/knock/{room_id_or_alias}",
23 }
24 }
25
26 #[derive(Clone, Debug)]
28 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
29 pub struct Request {
30 pub room_id_or_alias: OwnedRoomOrAliasId,
32
33 pub reason: Option<String>,
35
36 pub via: Vec<OwnedServerName>,
46 }
47
48 #[cfg_attr(feature = "client", derive(serde::Serialize))]
50 #[cfg_attr(feature = "server", derive(serde::Deserialize))]
51 struct RequestQuery {
52 #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
54 via: Vec<OwnedServerName>,
55
56 #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
60 server_name: Vec<OwnedServerName>,
61 }
62
63 #[cfg_attr(feature = "client", derive(serde::Serialize))]
65 #[cfg_attr(feature = "server", derive(serde::Deserialize))]
66 struct RequestBody {
67 #[serde(skip_serializing_if = "Option::is_none")]
69 reason: Option<String>,
70 }
71
72 #[cfg(feature = "client")]
73 impl ruma_common::api::OutgoingRequest for Request {
74 type EndpointError = crate::Error;
75 type IncomingResponse = Response;
76
77 fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
78 self,
79 base_url: &str,
80 access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
81 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
82 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
83 use ruma_common::api::auth_scheme::AuthScheme;
84
85 let server_name = if considering
88 .versions
89 .iter()
90 .rev()
91 .any(|version| version.is_superset_of(ruma_common::api::MatrixVersion::V1_12))
92 {
93 vec![]
94 } else {
95 self.via.clone()
96 };
97
98 let query_string =
99 serde_html_form::to_string(RequestQuery { server_name, via: self.via })?;
100
101 let mut http_request = http::Request::builder()
102 .method(Self::METHOD)
103 .uri(Self::make_endpoint_url(
104 considering,
105 base_url,
106 &[&self.room_id_or_alias],
107 &query_string,
108 )?)
109 .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
110 .body(ruma_common::serde::json_to_buf(&RequestBody { reason: self.reason })?)?;
111
112 Self::Authentication::add_authentication(&mut http_request, access_token).map_err(
113 |error| ruma_common::api::error::IntoHttpError::Authentication(error.into()),
114 )?;
115
116 Ok(http_request)
117 }
118 }
119
120 #[cfg(feature = "server")]
121 impl ruma_common::api::IncomingRequest for Request {
122 type EndpointError = crate::Error;
123 type OutgoingResponse = Response;
124
125 fn try_from_http_request<B, S>(
126 request: http::Request<B>,
127 path_args: &[S],
128 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
129 where
130 B: AsRef<[u8]>,
131 S: AsRef<str>,
132 {
133 Self::check_request_method(request.method())?;
134
135 let (room_id_or_alias,) =
136 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
137 _,
138 serde::de::value::Error,
139 >::new(
140 path_args.iter().map(::std::convert::AsRef::as_ref),
141 ))?;
142
143 let request_query: RequestQuery =
144 serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
145 let via = if request_query.via.is_empty() {
146 request_query.server_name
147 } else {
148 request_query.via
149 };
150
151 let body: RequestBody = serde_json::from_slice(request.body().as_ref())?;
152
153 Ok(Self { room_id_or_alias, reason: body.reason, via })
154 }
155 }
156
157 #[response(error = crate::Error)]
159 pub struct Response {
160 pub room_id: OwnedRoomId,
162 }
163
164 impl Request {
165 pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self {
167 Self { room_id_or_alias, reason: None, via: vec![] }
168 }
169 }
170
171 impl Response {
172 pub fn new(room_id: OwnedRoomId) -> Self {
174 Self { room_id }
175 }
176 }
177
178 #[cfg(all(test, any(feature = "client", feature = "server")))]
179 mod tests {
180 #[cfg(feature = "client")]
181 use std::borrow::Cow;
182
183 use ruma_common::{
184 api::{
185 IncomingRequest as _, MatrixVersion, OutgoingRequest, SupportedVersions,
186 auth_scheme::SendAccessToken,
187 },
188 owned_room_id, owned_server_name,
189 };
190
191 use super::Request;
192
193 #[cfg(feature = "client")]
194 #[test]
195 fn serialize_request_via_and_server_name() {
196 let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
197 req.via = vec![owned_server_name!("f.oo")];
198 let supported = SupportedVersions {
199 versions: [MatrixVersion::V1_1].into(),
200 features: Default::default(),
201 };
202
203 let req = req
204 .try_into_http_request::<Vec<u8>>(
205 "https://matrix.org",
206 SendAccessToken::IfRequired("tok"),
207 Cow::Owned(supported),
208 )
209 .unwrap();
210 assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
211 }
212
213 #[cfg(feature = "client")]
214 #[test]
215 fn serialize_request_only_via() {
216 let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
217 req.via = vec![owned_server_name!("f.oo")];
218 let supported = SupportedVersions {
219 versions: [MatrixVersion::V1_12].into(),
220 features: Default::default(),
221 };
222
223 let req = req
224 .try_into_http_request::<Vec<u8>>(
225 "https://matrix.org",
226 SendAccessToken::IfRequired("tok"),
227 Cow::Owned(supported),
228 )
229 .unwrap();
230 assert_eq!(req.uri().query(), Some("via=f.oo"));
231 }
232
233 #[cfg(feature = "server")]
234 #[test]
235 fn deserialize_request_wrong_method() {
236 Request::try_from_http_request(
237 http::Request::builder()
238 .method(http::Method::GET)
239 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
240 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
241 .unwrap(),
242 &["!foo:b.ar"],
243 )
244 .expect_err("Should not deserialize request with illegal method");
245 }
246
247 #[cfg(feature = "server")]
248 #[test]
249 fn deserialize_request_only_via() {
250 let req = Request::try_from_http_request(
251 http::Request::builder()
252 .method(http::Method::POST)
253 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
254 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
255 .unwrap(),
256 &["!foo:b.ar"],
257 )
258 .unwrap();
259
260 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
261 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
262 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
263 }
264
265 #[cfg(feature = "server")]
266 #[test]
267 fn deserialize_request_only_server_name() {
268 let req = Request::try_from_http_request(
269 http::Request::builder()
270 .method(http::Method::POST)
271 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?server_name=f.oo")
272 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
273 .unwrap(),
274 &["!foo:b.ar"],
275 )
276 .unwrap();
277
278 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
279 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
280 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
281 }
282
283 #[cfg(feature = "server")]
284 #[test]
285 fn deserialize_request_via_and_server_name() {
286 let req = Request::try_from_http_request(
287 http::Request::builder()
288 .method(http::Method::POST)
289 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo&server_name=b.ar")
290 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
291 .unwrap(),
292 &["!foo:b.ar"],
293 )
294 .unwrap();
295
296 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
297 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
298 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
299 }
300 }
301}