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 .body(RequestBody { reason: self.reason })?;
111
112 Ok(http_request)
113 }
114 }
115
116 #[cfg(feature = "server")]
117 impl ruma_common::api::IncomingRequest for Request {
118 type EndpointError = Error;
119 type OutgoingResponse = Response;
120
121 fn try_from_http_request_inner(
122 request: http::Request<&[u8]>,
123 path_args: &[&str],
124 ) -> Result<Self, ruma_common::api::error::DeserializationError> {
125 let (room_id_or_alias,) =
126 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
127 _,
128 serde::de::value::Error,
129 >::new(path_args.iter().copied()))?;
130
131 let request_query: RequestQuery =
132 serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
133 let via = if request_query.via.is_empty() {
134 request_query.server_name
135 } else {
136 request_query.via
137 };
138
139 let body: RequestBody = serde_json::from_slice(request.body())?;
140
141 Ok(Self { room_id_or_alias, reason: body.reason, via })
142 }
143 }
144
145 #[response]
147 pub struct Response {
148 pub room_id: OwnedRoomId,
150 }
151
152 impl Request {
153 pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self {
155 Self { room_id_or_alias, reason: None, via: vec![] }
156 }
157 }
158
159 impl Response {
160 pub fn new(room_id: OwnedRoomId) -> Self {
162 Self { room_id }
163 }
164 }
165
166 #[cfg(all(test, feature = "client"))]
167 mod tests_client {
168 use std::borrow::Cow;
169
170 use ruma_common::{
171 api::{
172 MatrixVersion, OutgoingRequestExt as _, SupportedVersions,
173 auth_scheme::SendAccessToken,
174 },
175 owned_room_id, owned_server_name,
176 };
177
178 use super::Request;
179
180 #[test]
181 fn serialize_request_via_and_server_name() {
182 let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
183 req.via = vec![owned_server_name!("f.oo")];
184 let supported = SupportedVersions {
185 versions: [MatrixVersion::V1_1].into(),
186 features: Default::default(),
187 };
188
189 let req = req
190 .try_into_http_request::<Vec<u8>>(
191 "https://matrix.org",
192 SendAccessToken::IfRequired("tok"),
193 Cow::Owned(supported),
194 )
195 .unwrap();
196 assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
197 }
198
199 #[test]
200 fn serialize_request_only_via() {
201 let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
202 req.via = vec![owned_server_name!("f.oo")];
203 let supported = SupportedVersions {
204 versions: [MatrixVersion::V1_12].into(),
205 features: Default::default(),
206 };
207
208 let req = req
209 .try_into_http_request::<Vec<u8>>(
210 "https://matrix.org",
211 SendAccessToken::IfRequired("tok"),
212 Cow::Owned(supported),
213 )
214 .unwrap();
215 assert_eq!(req.uri().query(), Some("via=f.oo"));
216 }
217 }
218
219 #[cfg(all(test, feature = "server"))]
220 mod tests_server {
221 use ruma_common::{api::IncomingRequestExt as _, owned_server_name};
222
223 use super::Request;
224
225 #[test]
226 fn deserialize_request_wrong_method() {
227 Request::try_from_http_request(
228 http::Request::builder()
229 .method(http::Method::GET)
230 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
231 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
232 .unwrap(),
233 &["!foo:b.ar"],
234 )
235 .expect_err("Should not deserialize request with illegal method");
236 }
237
238 #[test]
239 fn deserialize_request_only_via() {
240 let req = Request::try_from_http_request(
241 http::Request::builder()
242 .method(http::Method::POST)
243 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
244 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
245 .unwrap(),
246 &["!foo:b.ar"],
247 )
248 .unwrap();
249
250 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
251 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
252 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
253 }
254
255 #[test]
256 fn deserialize_request_only_server_name() {
257 let req = Request::try_from_http_request(
258 http::Request::builder()
259 .method(http::Method::POST)
260 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?server_name=f.oo")
261 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
262 .unwrap(),
263 &["!foo:b.ar"],
264 )
265 .unwrap();
266
267 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
268 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
269 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
270 }
271
272 #[test]
273 fn deserialize_request_via_and_server_name() {
274 let req = Request::try_from_http_request(
275 http::Request::builder()
276 .method(http::Method::POST)
277 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo&server_name=b.ar")
278 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
279 .unwrap(),
280 &["!foo:b.ar"],
281 )
282 .unwrap();
283
284 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
285 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
286 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
287 }
288 }
289}