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