ruma_client_api/membership/
join_room_by_id_or_alias.rs

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