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