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::{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    /// 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        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            // Only send `server_name` if the `via` parameter is not supported by the server.
85            // `via` was introduced in Matrix 1.12.
86            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 type for the `knock_room` endpoint.
157    #[response(error = crate::Error)]
158    pub struct Response {
159        /// The room that the user knocked on.
160        pub room_id: OwnedRoomId,
161    }
162
163    impl Request {
164        /// Creates a new `Request` with the given room ID or alias.
165        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        /// Creates a new `Response` with the given room ID.
172        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}