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