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::{response, Metadata},
12        metadata, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName,
13    };
14
15    use crate::membership::ThirdPartySigned;
16
17    const METADATA: Metadata = 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        const METADATA: Metadata = METADATA;
88
89        fn try_into_http_request<T: Default + bytes::BufMut>(
90            self,
91            base_url: &str,
92            access_token: ruma_common::api::SendAccessToken<'_>,
93            considering: &'_ ruma_common::api::SupportedVersions,
94        ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
95            use http::header::{self, HeaderValue};
96
97            // Only send `server_name` if the `via` parameter is not supported by the server.
98            // `via` was introduced in Matrix 1.12.
99            let server_name = if considering
100                .versions
101                .iter()
102                .rev()
103                .any(|version| version.is_superset_of(ruma_common::api::MatrixVersion::V1_12))
104            {
105                vec![]
106            } else {
107                self.via.clone()
108            };
109
110            let query_string =
111                serde_html_form::to_string(RequestQuery { server_name, via: self.via })?;
112
113            let http_request = http::Request::builder()
114                .method(METADATA.method)
115                .uri(METADATA.make_endpoint_url(
116                    considering,
117                    base_url,
118                    &[&self.room_id_or_alias],
119                    &query_string,
120                )?)
121                .header(header::CONTENT_TYPE, "application/json")
122                .header(
123                    header::AUTHORIZATION,
124                    HeaderValue::from_str(&format!(
125                        "Bearer {}",
126                        access_token
127                            .get_required_for_endpoint()
128                            .ok_or(ruma_common::api::error::IntoHttpError::NeedsAuthentication)?
129                    ))?,
130                )
131                .body(ruma_common::serde::json_to_buf(&RequestBody {
132                    third_party_signed: self.third_party_signed,
133                    reason: self.reason,
134                })?)?;
135
136            Ok(http_request)
137        }
138    }
139
140    #[cfg(feature = "server")]
141    impl ruma_common::api::IncomingRequest for Request {
142        type EndpointError = crate::Error;
143        type OutgoingResponse = Response;
144
145        const METADATA: Metadata = METADATA;
146
147        fn try_from_http_request<B, S>(
148            request: http::Request<B>,
149            path_args: &[S],
150        ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
151        where
152            B: AsRef<[u8]>,
153            S: AsRef<str>,
154        {
155            if request.method() != METADATA.method {
156                return Err(ruma_common::api::error::FromHttpRequestError::MethodMismatch {
157                    expected: METADATA.method,
158                    received: request.method().clone(),
159                });
160            }
161
162            let (room_id_or_alias,) =
163                serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
164                    _,
165                    serde::de::value::Error,
166                >::new(
167                    path_args.iter().map(::std::convert::AsRef::as_ref),
168                ))?;
169
170            let request_query: RequestQuery =
171                serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
172            let via = if request_query.via.is_empty() {
173                request_query.server_name
174            } else {
175                request_query.via
176            };
177
178            let body: RequestBody = serde_json::from_slice(request.body().as_ref())?;
179
180            Ok(Self {
181                room_id_or_alias,
182                reason: body.reason,
183                third_party_signed: body.third_party_signed,
184                via,
185            })
186        }
187    }
188
189    /// Response type for the `join_room_by_id_or_alias` endpoint.
190    #[response(error = crate::Error)]
191    pub struct Response {
192        /// The room that the user joined.
193        pub room_id: OwnedRoomId,
194    }
195
196    impl Request {
197        /// Creates a new `Request` with the given room ID or alias ID.
198        pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self {
199            Self { room_id_or_alias, via: vec![], third_party_signed: None, reason: None }
200        }
201    }
202
203    impl Response {
204        /// Creates a new `Response` with the given room ID.
205        pub fn new(room_id: OwnedRoomId) -> Self {
206            Self { room_id }
207        }
208    }
209
210    #[cfg(all(test, any(feature = "client", feature = "server")))]
211    mod tests {
212        use ruma_common::{
213            api::{
214                IncomingRequest as _, MatrixVersion, OutgoingRequest, SendAccessToken,
215                SupportedVersions,
216            },
217            owned_room_id, owned_server_name,
218        };
219
220        use super::Request;
221
222        #[cfg(feature = "client")]
223        #[test]
224        fn serialize_request_via_and_server_name() {
225            let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
226            req.via = vec![owned_server_name!("f.oo")];
227            let supported = SupportedVersions {
228                versions: [MatrixVersion::V1_1].into(),
229                features: Default::default(),
230            };
231
232            let req = req
233                .try_into_http_request::<Vec<u8>>(
234                    "https://matrix.org",
235                    SendAccessToken::IfRequired("tok"),
236                    &supported,
237                )
238                .unwrap();
239            assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
240        }
241
242        #[cfg(feature = "client")]
243        #[test]
244        fn serialize_request_only_via() {
245            let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
246            req.via = vec![owned_server_name!("f.oo")];
247            let supported = SupportedVersions {
248                versions: [MatrixVersion::V1_13].into(),
249                features: Default::default(),
250            };
251
252            let req = req
253                .try_into_http_request::<Vec<u8>>(
254                    "https://matrix.org",
255                    SendAccessToken::IfRequired("tok"),
256                    &supported,
257                )
258                .unwrap();
259            assert_eq!(req.uri().query(), Some("via=f.oo"));
260        }
261
262        #[cfg(feature = "server")]
263        #[test]
264        fn deserialize_request_wrong_method() {
265            Request::try_from_http_request(
266                http::Request::builder()
267                    .method(http::Method::GET)
268                    .uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?via=f.oo")
269                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
270                    .unwrap(),
271                &["!foo:b.ar"],
272            )
273            .expect_err("Should not deserialize request with illegal method");
274        }
275
276        #[cfg(feature = "server")]
277        #[test]
278        fn deserialize_request_only_via() {
279            let req = Request::try_from_http_request(
280                http::Request::builder()
281                    .method(http::Method::POST)
282                    .uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?via=f.oo")
283                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
284                    .unwrap(),
285                &["!foo:b.ar"],
286            )
287            .unwrap();
288
289            assert_eq!(req.room_id_or_alias, "!foo:b.ar");
290            assert_eq!(req.reason, Some("Let me in already!".to_owned()));
291            assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
292        }
293
294        #[cfg(feature = "server")]
295        #[test]
296        fn deserialize_request_only_server_name() {
297            let req = Request::try_from_http_request(
298                http::Request::builder()
299                    .method(http::Method::POST)
300                    .uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?server_name=f.oo")
301                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
302                    .unwrap(),
303                &["!foo:b.ar"],
304            )
305            .unwrap();
306
307            assert_eq!(req.room_id_or_alias, "!foo:b.ar");
308            assert_eq!(req.reason, Some("Let me in already!".to_owned()));
309            assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
310        }
311
312        #[cfg(feature = "server")]
313        #[test]
314        fn deserialize_request_via_and_server_name() {
315            let req = Request::try_from_http_request(
316                http::Request::builder()
317                    .method(http::Method::POST)
318                    .uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?via=f.oo&server_name=b.ar")
319                    .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
320                    .unwrap(),
321                &["!foo:b.ar"],
322            )
323            .unwrap();
324
325            assert_eq!(req.room_id_or_alias, "!foo:b.ar");
326            assert_eq!(req.reason, Some("Let me in already!".to_owned()));
327            assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
328        }
329    }
330}