1pub mod v3 {
6 use ruma_common::{
11 api::{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>(
77 self,
78 base_url: &str,
79 access_token: ruma_common::api::SendAccessToken<'_>,
80 considering: &'_ ruma_common::api::SupportedVersions,
81 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
82 use http::header::{self, HeaderValue};
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 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(header::CONTENT_TYPE, "application/json")
109 .header(
110 header::AUTHORIZATION,
111 HeaderValue::from_str(&format!(
112 "Bearer {}",
113 access_token
114 .get_required_for_endpoint()
115 .ok_or(ruma_common::api::error::IntoHttpError::NeedsAuthentication)?
116 ))?,
117 )
118 .body(ruma_common::serde::json_to_buf(&RequestBody { reason: self.reason })?)?;
119
120 Ok(http_request)
121 }
122 }
123
124 #[cfg(feature = "server")]
125 impl ruma_common::api::IncomingRequest for Request {
126 type EndpointError = crate::Error;
127 type OutgoingResponse = Response;
128
129 fn try_from_http_request<B, S>(
130 request: http::Request<B>,
131 path_args: &[S],
132 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
133 where
134 B: AsRef<[u8]>,
135 S: AsRef<str>,
136 {
137 if request.method() != Self::METHOD {
138 return Err(ruma_common::api::error::FromHttpRequestError::MethodMismatch {
139 expected: Self::METHOD,
140 received: request.method().clone(),
141 });
142 }
143
144 let (room_id_or_alias,) =
145 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
146 _,
147 serde::de::value::Error,
148 >::new(
149 path_args.iter().map(::std::convert::AsRef::as_ref),
150 ))?;
151
152 let request_query: RequestQuery =
153 serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
154 let via = if request_query.via.is_empty() {
155 request_query.server_name
156 } else {
157 request_query.via
158 };
159
160 let body: RequestBody = serde_json::from_slice(request.body().as_ref())?;
161
162 Ok(Self { room_id_or_alias, reason: body.reason, via })
163 }
164 }
165
166 #[response(error = crate::Error)]
168 pub struct Response {
169 pub room_id: OwnedRoomId,
171 }
172
173 impl Request {
174 pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self {
176 Self { room_id_or_alias, reason: None, via: vec![] }
177 }
178 }
179
180 impl Response {
181 pub fn new(room_id: OwnedRoomId) -> Self {
183 Self { room_id }
184 }
185 }
186
187 #[cfg(all(test, any(feature = "client", feature = "server")))]
188 mod tests {
189 use ruma_common::{
190 api::{
191 IncomingRequest as _, MatrixVersion, OutgoingRequest, SendAccessToken,
192 SupportedVersions,
193 },
194 owned_room_id, owned_server_name,
195 };
196
197 use super::Request;
198
199 #[cfg(feature = "client")]
200 #[test]
201 fn serialize_request_via_and_server_name() {
202 let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
203 req.via = vec![owned_server_name!("f.oo")];
204 let supported = SupportedVersions {
205 versions: [MatrixVersion::V1_1].into(),
206 features: Default::default(),
207 };
208
209 let req = req
210 .try_into_http_request::<Vec<u8>>(
211 "https://matrix.org",
212 SendAccessToken::IfRequired("tok"),
213 &supported,
214 )
215 .unwrap();
216 assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
217 }
218
219 #[cfg(feature = "client")]
220 #[test]
221 fn serialize_request_only_via() {
222 let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
223 req.via = vec![owned_server_name!("f.oo")];
224 let supported = SupportedVersions {
225 versions: [MatrixVersion::V1_12].into(),
226 features: Default::default(),
227 };
228
229 let req = req
230 .try_into_http_request::<Vec<u8>>(
231 "https://matrix.org",
232 SendAccessToken::IfRequired("tok"),
233 &supported,
234 )
235 .unwrap();
236 assert_eq!(req.uri().query(), Some("via=f.oo"));
237 }
238
239 #[cfg(feature = "server")]
240 #[test]
241 fn deserialize_request_wrong_method() {
242 Request::try_from_http_request(
243 http::Request::builder()
244 .method(http::Method::GET)
245 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
246 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
247 .unwrap(),
248 &["!foo:b.ar"],
249 )
250 .expect_err("Should not deserialize request with illegal method");
251 }
252
253 #[cfg(feature = "server")]
254 #[test]
255 fn deserialize_request_only_via() {
256 let req = Request::try_from_http_request(
257 http::Request::builder()
258 .method(http::Method::POST)
259 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
260 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
261 .unwrap(),
262 &["!foo:b.ar"],
263 )
264 .unwrap();
265
266 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
267 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
268 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
269 }
270
271 #[cfg(feature = "server")]
272 #[test]
273 fn deserialize_request_only_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?server_name=f.oo")
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 #[cfg(feature = "server")]
290 #[test]
291 fn deserialize_request_via_and_server_name() {
292 let req = Request::try_from_http_request(
293 http::Request::builder()
294 .method(http::Method::POST)
295 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo&server_name=b.ar")
296 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
297 .unwrap(),
298 &["!foo:b.ar"],
299 )
300 .unwrap();
301
302 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
303 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
304 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
305 }
306 }
307}