1pub mod v3 {
6 use ruma_common::{
11 api::{response, Metadata},
12 metadata, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName,
13 };
14
15 const METADATA: Metadata = 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 const METADATA: Metadata = METADATA;
77
78 fn try_into_http_request<T: Default + bytes::BufMut>(
79 self,
80 base_url: &str,
81 access_token: ruma_common::api::SendAccessToken<'_>,
82 considering: &'_ ruma_common::api::SupportedVersions,
83 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
84 use http::header::{self, HeaderValue};
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(METADATA.method)
104 .uri(METADATA.make_endpoint_url(
105 considering,
106 base_url,
107 &[&self.room_id_or_alias],
108 &query_string,
109 )?)
110 .header(header::CONTENT_TYPE, "application/json")
111 .header(
112 header::AUTHORIZATION,
113 HeaderValue::from_str(&format!(
114 "Bearer {}",
115 access_token
116 .get_required_for_endpoint()
117 .ok_or(ruma_common::api::error::IntoHttpError::NeedsAuthentication)?
118 ))?,
119 )
120 .body(ruma_common::serde::json_to_buf(&RequestBody { reason: self.reason })?)?;
121
122 Ok(http_request)
123 }
124 }
125
126 #[cfg(feature = "server")]
127 impl ruma_common::api::IncomingRequest for Request {
128 type EndpointError = crate::Error;
129 type OutgoingResponse = Response;
130
131 const METADATA: Metadata = METADATA;
132
133 fn try_from_http_request<B, S>(
134 request: http::Request<B>,
135 path_args: &[S],
136 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
137 where
138 B: AsRef<[u8]>,
139 S: AsRef<str>,
140 {
141 if request.method() != METADATA.method {
142 return Err(ruma_common::api::error::FromHttpRequestError::MethodMismatch {
143 expected: METADATA.method,
144 received: request.method().clone(),
145 });
146 }
147
148 let (room_id_or_alias,) =
149 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
150 _,
151 serde::de::value::Error,
152 >::new(
153 path_args.iter().map(::std::convert::AsRef::as_ref),
154 ))?;
155
156 let request_query: RequestQuery =
157 serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
158 let via = if request_query.via.is_empty() {
159 request_query.server_name
160 } else {
161 request_query.via
162 };
163
164 let body: RequestBody = serde_json::from_slice(request.body().as_ref())?;
165
166 Ok(Self { room_id_or_alias, reason: body.reason, via })
167 }
168 }
169
170 #[response(error = crate::Error)]
172 pub struct Response {
173 pub room_id: OwnedRoomId,
175 }
176
177 impl Request {
178 pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self {
180 Self { room_id_or_alias, reason: None, via: vec![] }
181 }
182 }
183
184 impl Response {
185 pub fn new(room_id: OwnedRoomId) -> Self {
187 Self { room_id }
188 }
189 }
190
191 #[cfg(all(test, any(feature = "client", feature = "server")))]
192 mod tests {
193 use ruma_common::{
194 api::{
195 IncomingRequest as _, MatrixVersion, OutgoingRequest, SendAccessToken,
196 SupportedVersions,
197 },
198 owned_room_id, owned_server_name,
199 };
200
201 use super::Request;
202
203 #[cfg(feature = "client")]
204 #[test]
205 fn serialize_request_via_and_server_name() {
206 let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
207 req.via = vec![owned_server_name!("f.oo")];
208 let supported =
209 SupportedVersions { versions: [MatrixVersion::V1_1].into(), features: Vec::new() };
210
211 let req = req
212 .try_into_http_request::<Vec<u8>>(
213 "https://matrix.org",
214 SendAccessToken::IfRequired("tok"),
215 &supported,
216 )
217 .unwrap();
218 assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
219 }
220
221 #[cfg(feature = "client")]
222 #[test]
223 fn serialize_request_only_via() {
224 let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
225 req.via = vec![owned_server_name!("f.oo")];
226 let supported =
227 SupportedVersions { versions: [MatrixVersion::V1_12].into(), features: Vec::new() };
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}