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_versions: &'_ [ruma_common::api::MatrixVersion],
83 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
84 use http::header::{self, HeaderValue};
85
86 let query_string = serde_html_form::to_string(RequestQuery {
87 server_name: self.via.clone(),
88 via: self.via,
89 })?;
90
91 let http_request = http::Request::builder()
92 .method(METADATA.method)
93 .uri(METADATA.make_endpoint_url(
94 considering_versions,
95 base_url,
96 &[&self.room_id_or_alias],
97 &query_string,
98 )?)
99 .header(header::CONTENT_TYPE, "application/json")
100 .header(
101 header::AUTHORIZATION,
102 HeaderValue::from_str(&format!(
103 "Bearer {}",
104 access_token
105 .get_required_for_endpoint()
106 .ok_or(ruma_common::api::error::IntoHttpError::NeedsAuthentication)?
107 ))?,
108 )
109 .body(ruma_common::serde::json_to_buf(&RequestBody { reason: self.reason })?)?;
110
111 Ok(http_request)
112 }
113 }
114
115 #[cfg(feature = "server")]
116 impl ruma_common::api::IncomingRequest for Request {
117 type EndpointError = crate::Error;
118 type OutgoingResponse = Response;
119
120 const METADATA: Metadata = METADATA;
121
122 fn try_from_http_request<B, S>(
123 request: http::Request<B>,
124 path_args: &[S],
125 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
126 where
127 B: AsRef<[u8]>,
128 S: AsRef<str>,
129 {
130 if request.method() != METADATA.method {
131 return Err(ruma_common::api::error::FromHttpRequestError::MethodMismatch {
132 expected: METADATA.method,
133 received: request.method().clone(),
134 });
135 }
136
137 let (room_id_or_alias,) =
138 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
139 _,
140 serde::de::value::Error,
141 >::new(
142 path_args.iter().map(::std::convert::AsRef::as_ref),
143 ))?;
144
145 let request_query: RequestQuery =
146 serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
147 let via = if request_query.via.is_empty() {
148 request_query.server_name
149 } else {
150 request_query.via
151 };
152
153 let body: RequestBody = serde_json::from_slice(request.body().as_ref())?;
154
155 Ok(Self { room_id_or_alias, reason: body.reason, via })
156 }
157 }
158
159 #[response(error = crate::Error)]
161 pub struct Response {
162 pub room_id: OwnedRoomId,
164 }
165
166 impl Request {
167 pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self {
169 Self { room_id_or_alias, reason: None, via: vec![] }
170 }
171 }
172
173 impl Response {
174 pub fn new(room_id: OwnedRoomId) -> Self {
176 Self { room_id }
177 }
178 }
179
180 #[cfg(all(test, any(feature = "client", feature = "server")))]
181 mod tests {
182 use ruma_common::{
183 api::{IncomingRequest as _, MatrixVersion, OutgoingRequest, SendAccessToken},
184 owned_room_id, owned_server_name,
185 };
186
187 use super::Request;
188
189 #[cfg(feature = "client")]
190 #[test]
191 fn serialize_request() {
192 let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
193 req.via = vec![owned_server_name!("f.oo")];
194 let req = req
195 .try_into_http_request::<Vec<u8>>(
196 "https://matrix.org",
197 SendAccessToken::IfRequired("tok"),
198 &[MatrixVersion::V1_1],
199 )
200 .unwrap();
201 assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
202 }
203
204 #[cfg(feature = "server")]
205 #[test]
206 fn deserialize_request_wrong_method() {
207 Request::try_from_http_request(
208 http::Request::builder()
209 .method(http::Method::GET)
210 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
211 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
212 .unwrap(),
213 &["!foo:b.ar"],
214 )
215 .expect_err("Should not deserialize request with illegal method");
216 }
217
218 #[cfg(feature = "server")]
219 #[test]
220 fn deserialize_request_only_via() {
221 let req = Request::try_from_http_request(
222 http::Request::builder()
223 .method(http::Method::POST)
224 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
225 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
226 .unwrap(),
227 &["!foo:b.ar"],
228 )
229 .unwrap();
230
231 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
232 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
233 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
234 }
235
236 #[cfg(feature = "server")]
237 #[test]
238 fn deserialize_request_only_server_name() {
239 let req = Request::try_from_http_request(
240 http::Request::builder()
241 .method(http::Method::POST)
242 .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?server_name=f.oo")
243 .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
244 .unwrap(),
245 &["!foo:b.ar"],
246 )
247 .unwrap();
248
249 assert_eq!(req.room_id_or_alias, "!foo:b.ar");
250 assert_eq!(req.reason, Some("Let me in already!".to_owned()));
251 assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
252 }
253
254 #[cfg(feature = "server")]
255 #[test]
256 fn deserialize_request_via_and_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?via=f.oo&server_name=b.ar")
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}