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