ruma_client_api/knock/
knock_room.rs

1//! `POST /_matrix/client/*/knock/{roomIdOrAlias}`
2//!
3//! Knock on a room.
4
5pub mod v3 {
6    //! `/v3/` ([spec])
7    //!
8    //! [spec]: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3knockroomidoralias
9
10    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    /// Request type for the `knock_room` endpoint.
26    #[derive(Clone, Debug)]
27    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
28    pub struct Request {
29        /// The room the user should knock on.
30        pub room_id_or_alias: OwnedRoomOrAliasId,
31
32        /// The reason for joining a room.
33        pub reason: Option<String>,
34
35        /// The servers to attempt to knock on the room through.
36        ///
37        /// One of the servers must be participating in the room.
38        ///
39        /// When serializing, this field is mapped to both `server_name` and `via`
40        /// with identical values.
41        ///
42        /// When deserializing, the value is read from `via` if it's not missing or
43        /// empty and `server_name` otherwise.
44        pub via: Vec<OwnedServerName>,
45    }
46
47    /// Data in the request's query string.
48    #[cfg_attr(feature = "client", derive(serde::Serialize))]
49    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
50    struct RequestQuery {
51        /// The servers to attempt to knock on the room through.
52        #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
53        via: Vec<OwnedServerName>,
54
55        /// The servers to attempt to knock on the room through.
56        ///
57        /// Deprecated in Matrix >1.11 in favour of `via`.
58        #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
59        server_name: Vec<OwnedServerName>,
60    }
61
62    /// Data in the request's body.
63    #[cfg_attr(feature = "client", derive(serde::Serialize))]
64    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
65    struct RequestBody {
66        /// The reason for joining a room.
67        #[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            // Only send `server_name` if the `via` parameter is not supported by the server.
87            // `via` was introduced in Matrix 1.12.
88            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 type for the `knock_room` endpoint.
171    #[response(error = crate::Error)]
172    pub struct Response {
173        /// The room that the user knocked on.
174        pub room_id: OwnedRoomId,
175    }
176
177    impl Request {
178        /// Creates a new `Request` with the given room ID or alias.
179        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        /// Creates a new `Response` with the given room ID.
186        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 = SupportedVersions {
209                versions: [MatrixVersion::V1_1].into(),
210                features: Default::default(),
211            };
212
213            let req = req
214                .try_into_http_request::<Vec<u8>>(
215                    "https://matrix.org",
216                    SendAccessToken::IfRequired("tok"),
217                    &supported,
218                )
219                .unwrap();
220            assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
221        }
222
223        #[cfg(feature = "client")]
224        #[test]
225        fn serialize_request_only_via() {
226            let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
227            req.via = vec![owned_server_name!("f.oo")];
228            let supported = SupportedVersions {
229                versions: [MatrixVersion::V1_12].into(),
230                features: Default::default(),
231            };
232
233            let req = req
234                .try_into_http_request::<Vec<u8>>(
235                    "https://matrix.org",
236                    SendAccessToken::IfRequired("tok"),
237                    &supported,
238                )
239                .unwrap();
240            assert_eq!(req.uri().query(), Some("via=f.oo"));
241        }
242
243        #[cfg(feature = "server")]
244        #[test]
245        fn deserialize_request_wrong_method() {
246            Request::try_from_http_request(
247                http::Request::builder()
248                    .method(http::Method::GET)
249                    .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
250                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
251                    .unwrap(),
252                &["!foo:b.ar"],
253            )
254            .expect_err("Should not deserialize request with illegal method");
255        }
256
257        #[cfg(feature = "server")]
258        #[test]
259        fn deserialize_request_only_via() {
260            let req = Request::try_from_http_request(
261                http::Request::builder()
262                    .method(http::Method::POST)
263                    .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
264                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
265                    .unwrap(),
266                &["!foo:b.ar"],
267            )
268            .unwrap();
269
270            assert_eq!(req.room_id_or_alias, "!foo:b.ar");
271            assert_eq!(req.reason, Some("Let me in already!".to_owned()));
272            assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
273        }
274
275        #[cfg(feature = "server")]
276        #[test]
277        fn deserialize_request_only_server_name() {
278            let req = Request::try_from_http_request(
279                http::Request::builder()
280                    .method(http::Method::POST)
281                    .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?server_name=f.oo")
282                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
283                    .unwrap(),
284                &["!foo:b.ar"],
285            )
286            .unwrap();
287
288            assert_eq!(req.room_id_or_alias, "!foo:b.ar");
289            assert_eq!(req.reason, Some("Let me in already!".to_owned()));
290            assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
291        }
292
293        #[cfg(feature = "server")]
294        #[test]
295        fn deserialize_request_via_and_server_name() {
296            let req = Request::try_from_http_request(
297                http::Request::builder()
298                    .method(http::Method::POST)
299                    .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo&server_name=b.ar")
300                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
301                    .unwrap(),
302                &["!foo:b.ar"],
303            )
304            .unwrap();
305
306            assert_eq!(req.room_id_or_alias, "!foo:b.ar");
307            assert_eq!(req.reason, Some("Let me in already!".to_owned()));
308            assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
309        }
310    }
311}