Skip to main content

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/v1.19/client-server-api/#post_matrixclientv3knockroomidoralias
9
10    use ruma_common::{
11        OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName,
12        api::{auth_scheme::AccessToken, error::Error, 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    #[doc(hidden)]
65    #[cfg_attr(feature = "client", derive(serde::Serialize, ruma_common::api::OutgoingBodyJson))]
66    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
67    pub struct RequestBody {
68        /// The reason for joining a room.
69        #[serde(skip_serializing_if = "Option::is_none")]
70        reason: Option<String>,
71    }
72
73    #[cfg(feature = "client")]
74    impl ruma_common::api::OutgoingRequest for Request {
75        type Body = RequestBody;
76        type EndpointError = Error;
77        type IncomingResponse = Response;
78
79        fn try_into_http_request_inner(
80            self,
81            base_url: &str,
82            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
83        ) -> Result<http::Request<Self::Body>, ruma_common::api::error::IntoHttpError> {
84            use ruma_common::api::Metadata;
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(Self::METHOD)
104                .uri(Self::make_endpoint_url(
105                    considering,
106                    base_url,
107                    &[&self.room_id_or_alias],
108                    &query_string,
109                )?)
110                .body(RequestBody { reason: self.reason })?;
111
112            Ok(http_request)
113        }
114    }
115
116    #[cfg(feature = "server")]
117    impl ruma_common::api::IncomingRequest for Request {
118        type EndpointError = Error;
119        type OutgoingResponse = Response;
120
121        fn try_from_http_request_inner(
122            request: http::Request<&[u8]>,
123            path_args: &[&str],
124        ) -> Result<Self, ruma_common::api::error::DeserializationError> {
125            let (room_id_or_alias,) =
126                serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
127                    _,
128                    serde::de::value::Error,
129                >::new(path_args.iter().copied()))?;
130
131            let request_query: RequestQuery =
132                serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
133            let via = if request_query.via.is_empty() {
134                request_query.server_name
135            } else {
136                request_query.via
137            };
138
139            let body: RequestBody = serde_json::from_slice(request.body())?;
140
141            Ok(Self { room_id_or_alias, reason: body.reason, via })
142        }
143    }
144
145    /// Response type for the `knock_room` endpoint.
146    #[response]
147    pub struct Response {
148        /// The room that the user knocked on.
149        pub room_id: OwnedRoomId,
150    }
151
152    impl Request {
153        /// Creates a new `Request` with the given room ID or alias.
154        pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self {
155            Self { room_id_or_alias, reason: None, via: vec![] }
156        }
157    }
158
159    impl Response {
160        /// Creates a new `Response` with the given room ID.
161        pub fn new(room_id: OwnedRoomId) -> Self {
162            Self { room_id }
163        }
164    }
165
166    #[cfg(all(test, feature = "client"))]
167    mod tests_client {
168        use std::borrow::Cow;
169
170        use ruma_common::{
171            api::{
172                MatrixVersion, OutgoingRequestExt as _, SupportedVersions,
173                auth_scheme::SendAccessToken,
174            },
175            owned_room_id, owned_server_name,
176        };
177
178        use super::Request;
179
180        #[test]
181        fn serialize_request_via_and_server_name() {
182            let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
183            req.via = vec![owned_server_name!("f.oo")];
184            let supported = SupportedVersions {
185                versions: [MatrixVersion::V1_1].into(),
186                features: Default::default(),
187            };
188
189            let req = req
190                .try_into_http_request::<Vec<u8>>(
191                    "https://matrix.org",
192                    SendAccessToken::IfRequired("tok"),
193                    Cow::Owned(supported),
194                )
195                .unwrap();
196            assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
197        }
198
199        #[test]
200        fn serialize_request_only_via() {
201            let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
202            req.via = vec![owned_server_name!("f.oo")];
203            let supported = SupportedVersions {
204                versions: [MatrixVersion::V1_12].into(),
205                features: Default::default(),
206            };
207
208            let req = req
209                .try_into_http_request::<Vec<u8>>(
210                    "https://matrix.org",
211                    SendAccessToken::IfRequired("tok"),
212                    Cow::Owned(supported),
213                )
214                .unwrap();
215            assert_eq!(req.uri().query(), Some("via=f.oo"));
216        }
217    }
218
219    #[cfg(all(test, feature = "server"))]
220    mod tests_server {
221        use ruma_common::{api::IncomingRequestExt as _, owned_server_name};
222
223        use super::Request;
224
225        #[test]
226        fn deserialize_request_wrong_method() {
227            Request::try_from_http_request(
228                http::Request::builder()
229                    .method(http::Method::GET)
230                    .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
231                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
232                    .unwrap(),
233                &["!foo:b.ar"],
234            )
235            .expect_err("Should not deserialize request with illegal method");
236        }
237
238        #[test]
239        fn deserialize_request_only_via() {
240            let req = Request::try_from_http_request(
241                http::Request::builder()
242                    .method(http::Method::POST)
243                    .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
244                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
245                    .unwrap(),
246                &["!foo:b.ar"],
247            )
248            .unwrap();
249
250            assert_eq!(req.room_id_or_alias, "!foo:b.ar");
251            assert_eq!(req.reason, Some("Let me in already!".to_owned()));
252            assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
253        }
254
255        #[test]
256        fn deserialize_request_only_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?server_name=f.oo")
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        #[test]
273        fn deserialize_request_via_and_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?via=f.oo&server_name=b.ar")
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}