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        OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName,
12        api::{Metadata, auth_scheme::AccessToken, response},
13        metadata,
14    };
15
16    metadata! {
17        method: POST,
18        rate_limited: true,
19        authentication: AccessToken,
20        history: {
21            unstable => "/_matrix/client/unstable/xyz.amorgan.knock/knock/{room_id_or_alias}",
22            1.1 => "/_matrix/client/v3/knock/{room_id_or_alias}",
23        }
24    }
25
26    /// Request type for the `knock_room` endpoint.
27    #[derive(Clone, Debug)]
28    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
29    pub struct Request {
30        /// The room the user should knock on.
31        pub room_id_or_alias: OwnedRoomOrAliasId,
32
33        /// The reason for joining a room.
34        pub reason: Option<String>,
35
36        /// The servers to attempt to knock on the room through.
37        ///
38        /// One of the servers must be participating in the room.
39        ///
40        /// When serializing, this field is mapped to both `server_name` and `via`
41        /// with identical values.
42        ///
43        /// When deserializing, the value is read from `via` if it's not missing or
44        /// empty and `server_name` otherwise.
45        pub via: Vec<OwnedServerName>,
46    }
47
48    /// Data in the request's query string.
49    #[cfg_attr(feature = "client", derive(serde::Serialize))]
50    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
51    struct RequestQuery {
52        /// The servers to attempt to knock on the room through.
53        #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
54        via: Vec<OwnedServerName>,
55
56        /// The servers to attempt to knock on the room through.
57        ///
58        /// Deprecated in Matrix >1.11 in favour of `via`.
59        #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
60        server_name: Vec<OwnedServerName>,
61    }
62
63    /// Data in the request's body.
64    #[cfg_attr(feature = "client", derive(serde::Serialize))]
65    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
66    struct RequestBody {
67        /// The reason for joining a room.
68        #[serde(skip_serializing_if = "Option::is_none")]
69        reason: Option<String>,
70    }
71
72    #[cfg(feature = "client")]
73    impl ruma_common::api::OutgoingRequest for Request {
74        type EndpointError = crate::Error;
75        type IncomingResponse = Response;
76
77        fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
78            self,
79            base_url: &str,
80            access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
81            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
82        ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
83            use ruma_common::api::auth_scheme::AuthScheme;
84
85            // Only send `server_name` if the `via` parameter is not supported by the server.
86            // `via` was introduced in Matrix 1.12.
87            let server_name = if considering
88                .versions
89                .iter()
90                .rev()
91                .any(|version| version.is_superset_of(ruma_common::api::MatrixVersion::V1_12))
92            {
93                vec![]
94            } else {
95                self.via.clone()
96            };
97
98            let query_string =
99                serde_html_form::to_string(RequestQuery { server_name, via: self.via })?;
100
101            let mut http_request = http::Request::builder()
102                .method(Self::METHOD)
103                .uri(Self::make_endpoint_url(
104                    considering,
105                    base_url,
106                    &[&self.room_id_or_alias],
107                    &query_string,
108                )?)
109                .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
110                .body(ruma_common::serde::json_to_buf(&RequestBody { reason: self.reason })?)?;
111
112            Self::Authentication::add_authentication(&mut http_request, access_token).map_err(
113                |error| ruma_common::api::error::IntoHttpError::Authentication(error.into()),
114            )?;
115
116            Ok(http_request)
117        }
118    }
119
120    #[cfg(feature = "server")]
121    impl ruma_common::api::IncomingRequest for Request {
122        type EndpointError = crate::Error;
123        type OutgoingResponse = Response;
124
125        fn try_from_http_request<B, S>(
126            request: http::Request<B>,
127            path_args: &[S],
128        ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
129        where
130            B: AsRef<[u8]>,
131            S: AsRef<str>,
132        {
133            Self::check_request_method(request.method())?;
134
135            let (room_id_or_alias,) =
136                serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
137                    _,
138                    serde::de::value::Error,
139                >::new(
140                    path_args.iter().map(::std::convert::AsRef::as_ref),
141                ))?;
142
143            let request_query: RequestQuery =
144                serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
145            let via = if request_query.via.is_empty() {
146                request_query.server_name
147            } else {
148                request_query.via
149            };
150
151            let body: RequestBody = serde_json::from_slice(request.body().as_ref())?;
152
153            Ok(Self { room_id_or_alias, reason: body.reason, via })
154        }
155    }
156
157    /// Response type for the `knock_room` endpoint.
158    #[response(error = crate::Error)]
159    pub struct Response {
160        /// The room that the user knocked on.
161        pub room_id: OwnedRoomId,
162    }
163
164    impl Request {
165        /// Creates a new `Request` with the given room ID or alias.
166        pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self {
167            Self { room_id_or_alias, reason: None, via: vec![] }
168        }
169    }
170
171    impl Response {
172        /// Creates a new `Response` with the given room ID.
173        pub fn new(room_id: OwnedRoomId) -> Self {
174            Self { room_id }
175        }
176    }
177
178    #[cfg(all(test, any(feature = "client", feature = "server")))]
179    mod tests {
180        #[cfg(feature = "client")]
181        use std::borrow::Cow;
182
183        use ruma_common::{
184            api::{
185                IncomingRequest as _, MatrixVersion, OutgoingRequest, SupportedVersions,
186                auth_scheme::SendAccessToken,
187            },
188            owned_room_id, owned_server_name,
189        };
190
191        use super::Request;
192
193        #[cfg(feature = "client")]
194        #[test]
195        fn serialize_request_via_and_server_name() {
196            let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
197            req.via = vec![owned_server_name!("f.oo")];
198            let supported = SupportedVersions {
199                versions: [MatrixVersion::V1_1].into(),
200                features: Default::default(),
201            };
202
203            let req = req
204                .try_into_http_request::<Vec<u8>>(
205                    "https://matrix.org",
206                    SendAccessToken::IfRequired("tok"),
207                    Cow::Owned(supported),
208                )
209                .unwrap();
210            assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
211        }
212
213        #[cfg(feature = "client")]
214        #[test]
215        fn serialize_request_only_via() {
216            let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
217            req.via = vec![owned_server_name!("f.oo")];
218            let supported = SupportedVersions {
219                versions: [MatrixVersion::V1_12].into(),
220                features: Default::default(),
221            };
222
223            let req = req
224                .try_into_http_request::<Vec<u8>>(
225                    "https://matrix.org",
226                    SendAccessToken::IfRequired("tok"),
227                    Cow::Owned(supported),
228                )
229                .unwrap();
230            assert_eq!(req.uri().query(), Some("via=f.oo"));
231        }
232
233        #[cfg(feature = "server")]
234        #[test]
235        fn deserialize_request_wrong_method() {
236            Request::try_from_http_request(
237                http::Request::builder()
238                    .method(http::Method::GET)
239                    .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
240                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
241                    .unwrap(),
242                &["!foo:b.ar"],
243            )
244            .expect_err("Should not deserialize request with illegal method");
245        }
246
247        #[cfg(feature = "server")]
248        #[test]
249        fn deserialize_request_only_via() {
250            let req = Request::try_from_http_request(
251                http::Request::builder()
252                    .method(http::Method::POST)
253                    .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
254                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
255                    .unwrap(),
256                &["!foo:b.ar"],
257            )
258            .unwrap();
259
260            assert_eq!(req.room_id_or_alias, "!foo:b.ar");
261            assert_eq!(req.reason, Some("Let me in already!".to_owned()));
262            assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
263        }
264
265        #[cfg(feature = "server")]
266        #[test]
267        fn deserialize_request_only_server_name() {
268            let req = Request::try_from_http_request(
269                http::Request::builder()
270                    .method(http::Method::POST)
271                    .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?server_name=f.oo")
272                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
273                    .unwrap(),
274                &["!foo:b.ar"],
275            )
276            .unwrap();
277
278            assert_eq!(req.room_id_or_alias, "!foo:b.ar");
279            assert_eq!(req.reason, Some("Let me in already!".to_owned()));
280            assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
281        }
282
283        #[cfg(feature = "server")]
284        #[test]
285        fn deserialize_request_via_and_server_name() {
286            let req = Request::try_from_http_request(
287                http::Request::builder()
288                    .method(http::Method::POST)
289                    .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo&server_name=b.ar")
290                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
291                    .unwrap(),
292                &["!foo:b.ar"],
293            )
294            .unwrap();
295
296            assert_eq!(req.room_id_or_alias, "!foo:b.ar");
297            assert_eq!(req.reason, Some("Let me in already!".to_owned()));
298            assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
299        }
300    }
301}