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