1pub mod v3 {
6 use ruma_common::{
11 OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName,
12 api::{auth_scheme::AccessToken, error::Error, 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 #[doc(hidden)]
65 #[cfg_attr(feature = "client", derive(serde::Serialize, ruma_common::api::OutgoingBodyJson))]
66 #[cfg_attr(feature = "server", derive(serde::Deserialize))]
67 pub struct RequestBody {
68 #[serde(skip_serializing_if = "Option::is_none")]
70 reason: Option<String>,
71 }
72
73 #[cfg(feature = "client")]
74 impl ruma_common::api::OutgoingRequest for Request {
75 type Body = RequestBody;
76 type EndpointError = Error;
77 type IncomingResponse = Response;
78
79 fn try_into_http_request_inner(
80 self,
81 base_url: &str,
82 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
83 ) -> Result<http::Request<Self::Body>, ruma_common::api::error::IntoHttpError> {
84 use ruma_common::api::Metadata;
85
86 let server_name = if considering
89 .versions
90 .iter()
91 .rev()
92 .any(|version| version.is_superset_of(ruma_common::api::MatrixVersion::V1_12))
93 {
94 vec![]
95 } else {
96 self.via.clone()
97 };
98
99 let query_string =
100 serde_html_form::to_string(RequestQuery { server_name, via: self.via })?;
101
102 let http_request = http::Request::builder()
103 .method(Self::METHOD)
104 .uri(Self::make_endpoint_url(
105 considering,
106 base_url,
107 &[&self.room_id_or_alias],
108 &query_string,
109 )?)
110 .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
111 .body(RequestBody { reason: self.reason })?;
112
113 Ok(http_request)
114 }
115 }
116
117 #[cfg(feature = "server")]
118 impl ruma_common::api::IncomingRequest for Request {
119 type EndpointError = Error;
120 type OutgoingResponse = Response;
121
122 fn try_from_http_request<B, S>(
123 request: http::Request<B>,
124 path_args: &[S],
125 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
126 where
127 B: AsRef<[u8]>,
128 S: AsRef<str>,
129 {
130 Self::check_request_method(request.method())?;
131
132 let (room_id_or_alias,) =
133 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
134 _,
135 serde::de::value::Error,
136 >::new(
137 path_args.iter().map(::std::convert::AsRef::as_ref),
138 ))?;
139
140 let request_query: RequestQuery =
141 serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
142 let via = if request_query.via.is_empty() {
143 request_query.server_name
144 } else {
145 request_query.via
146 };
147
148 let body: RequestBody = serde_json::from_slice(request.body().as_ref())?;
149
150 Ok(Self { room_id_or_alias, reason: body.reason, via })
151 }
152 }
153
154 #[response]
156 pub struct Response {
157 pub room_id: OwnedRoomId,
159 }
160
161 impl Request {
162 pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self {
164 Self { room_id_or_alias, reason: None, via: vec![] }
165 }
166 }
167
168 impl Response {
169 pub fn new(room_id: OwnedRoomId) -> Self {
171 Self { room_id }
172 }
173 }
174
175 #[cfg(all(test, feature = "client"))]
176 mod tests_client {
177 use std::borrow::Cow;
178
179 use ruma_common::{
180 api::{
181 MatrixVersion, OutgoingRequestExt as _, SupportedVersions,
182 auth_scheme::SendAccessToken,
183 },
184 owned_room_id, owned_server_name,
185 };
186
187 use super::Request;
188
189 #[test]
190 fn serialize_request_via_and_server_name() {
191 let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
192 req.via = vec![owned_server_name!("f.oo")];
193 let supported = SupportedVersions {
194 versions: [MatrixVersion::V1_1].into(),
195 features: Default::default(),
196 };
197
198 let req = req
199 .try_into_http_request::<Vec<u8>>(
200 "https://matrix.org",
201 SendAccessToken::IfRequired("tok"),
202 Cow::Owned(supported),
203 )
204 .unwrap();
205 assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
206 }
207
208 #[test]
209 fn serialize_request_only_via() {
210 let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
211 req.via = vec![owned_server_name!("f.oo")];
212 let supported = SupportedVersions {
213 versions: [MatrixVersion::V1_12].into(),
214 features: Default::default(),
215 };
216
217 let req = req
218 .try_into_http_request::<Vec<u8>>(
219 "https://matrix.org",
220 SendAccessToken::IfRequired("tok"),
221 Cow::Owned(supported),
222 )
223 .unwrap();
224 assert_eq!(req.uri().query(), Some("via=f.oo"));
225 }
226 }
227
228 #[cfg(all(test, feature = "server"))]
229 mod tests_server {
230 use ruma_common::{api::IncomingRequest as _, owned_server_name};
231
232 use super::Request;
233
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 #[test]
248 fn deserialize_request_only_via() {
249 let req = Request::try_from_http_request(
250 http::Request::builder()
251 .method(http::Method::POST)
252 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
253 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
254 .unwrap(),
255 &["!foo:b.ar"],
256 )
257 .unwrap();
258
259 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
260 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
261 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
262 }
263
264 #[test]
265 fn deserialize_request_only_server_name() {
266 let req = Request::try_from_http_request(
267 http::Request::builder()
268 .method(http::Method::POST)
269 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?server_name=f.oo")
270 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
271 .unwrap(),
272 &["!foo:b.ar"],
273 )
274 .unwrap();
275
276 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
277 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
278 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
279 }
280
281 #[test]
282 fn deserialize_request_via_and_server_name() {
283 let req = Request::try_from_http_request(
284 http::Request::builder()
285 .method(http::Method::POST)
286 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo&server_name=b.ar")
287 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
288 .unwrap(),
289 &["!foo:b.ar"],
290 )
291 .unwrap();
292
293 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
294 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
295 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
296 }
297 }
298}