ruma_client_api/profile/
get_profile_field.rs

1//! `GET /_matrix/client/*/profile/{userId}/{key_name}`
2//!
3//! Get a field in the profile of the user.
4
5pub mod v3 {
6    //! `/v3/` ([spec])
7    //!
8    //! Although this endpoint has a similar format to [`get_avatar_url`] and [`get_display_name`],
9    //! it will only work with homeservers advertising support for the proper unstable feature or
10    //! a version compatible with Matrix 1.16.
11    //!
12    //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3profileuseridkeyname
13    //! [`get_avatar_url`]: crate::profile::get_avatar_url
14    //! [`get_display_name`]: crate::profile::get_display_name
15
16    use std::marker::PhantomData;
17
18    use ruma_common::{
19        api::{auth_scheme::NoAuthentication, path_builder::VersionHistory, Metadata},
20        metadata, OwnedUserId,
21    };
22
23    use crate::profile::{
24        profile_field_serde::StaticProfileFieldVisitor, ProfileFieldName, ProfileFieldValue,
25        StaticProfileField,
26    };
27
28    metadata! {
29        method: GET,
30        rate_limited: false,
31        authentication: NoAuthentication,
32        // History valid for fields that existed in Matrix 1.0, i.e. `displayname` and `avatar_url`.
33        history: {
34            unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}",
35            1.0 => "/_matrix/client/r0/profile/{user_id}/{field}",
36            1.1 => "/_matrix/client/v3/profile/{user_id}/{field}",
37        }
38    }
39
40    /// Request type for the `get_profile_field` endpoint.
41    #[derive(Clone, Debug)]
42    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
43    pub struct Request {
44        /// The user whose profile will be fetched.
45        pub user_id: OwnedUserId,
46
47        /// The profile field to get.
48        pub field: ProfileFieldName,
49    }
50
51    impl Request {
52        /// Creates a new `Request` with the given user ID and field.
53        pub fn new(user_id: OwnedUserId, field: ProfileFieldName) -> Self {
54            Self { user_id, field }
55        }
56
57        /// Creates a new request with the given user ID and statically-known field.
58        pub fn new_static<F: StaticProfileField>(user_id: OwnedUserId) -> RequestStatic<F> {
59            RequestStatic::new(user_id)
60        }
61    }
62
63    #[cfg(feature = "client")]
64    impl ruma_common::api::OutgoingRequest for Request {
65        type EndpointError = crate::Error;
66        type IncomingResponse = Response;
67
68        fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
69            self,
70            base_url: &str,
71            access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
72            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
73        ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
74            use ruma_common::api::{auth_scheme::AuthScheme, path_builder::PathBuilder};
75
76            let url = if self.field.existed_before_extended_profiles() {
77                Self::make_endpoint_url(considering, base_url, &[&self.user_id, &self.field], "")?
78            } else {
79                crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
80                    considering,
81                    base_url,
82                    &[&self.user_id, &self.field],
83                    "",
84                )?
85            };
86
87            let mut http_request =
88                http::Request::builder().method(Self::METHOD).uri(url).body(T::default())?;
89
90            Self::Authentication::add_authentication(&mut http_request, access_token)?;
91
92            Ok(http_request)
93        }
94    }
95
96    #[cfg(feature = "server")]
97    impl ruma_common::api::IncomingRequest for Request {
98        type EndpointError = crate::Error;
99        type OutgoingResponse = Response;
100
101        fn try_from_http_request<B, S>(
102            request: http::Request<B>,
103            path_args: &[S],
104        ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
105        where
106            B: AsRef<[u8]>,
107            S: AsRef<str>,
108        {
109            Self::check_request_method(request.method())?;
110
111            let (user_id, field) =
112                serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
113                    _,
114                    serde::de::value::Error,
115                >::new(
116                    path_args.iter().map(::std::convert::AsRef::as_ref),
117                ))?;
118
119            Ok(Self { user_id, field })
120        }
121    }
122
123    /// Request type for the `get_profile_field` endpoint, using a statically-known field.
124    #[derive(Debug)]
125    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
126    pub struct RequestStatic<F: StaticProfileField> {
127        /// The user whose profile will be fetched.
128        pub user_id: OwnedUserId,
129
130        /// The profile field to get.
131        field: PhantomData<F>,
132    }
133
134    impl<F: StaticProfileField> RequestStatic<F> {
135        /// Creates a new request with the given user ID.
136        pub fn new(user_id: OwnedUserId) -> Self {
137            Self { user_id, field: PhantomData }
138        }
139    }
140
141    impl<F: StaticProfileField> Clone for RequestStatic<F> {
142        fn clone(&self) -> Self {
143            Self { user_id: self.user_id.clone(), field: self.field }
144        }
145    }
146
147    impl<F: StaticProfileField> Metadata for RequestStatic<F> {
148        const METHOD: http::Method = Request::METHOD;
149        const RATE_LIMITED: bool = Request::RATE_LIMITED;
150        type Authentication = <Request as Metadata>::Authentication;
151        type PathBuilder = <Request as Metadata>::PathBuilder;
152        const PATH_BUILDER: VersionHistory = Request::PATH_BUILDER;
153    }
154
155    #[cfg(feature = "client")]
156    impl<F: StaticProfileField> ruma_common::api::OutgoingRequest for RequestStatic<F> {
157        type EndpointError = crate::Error;
158        type IncomingResponse = ResponseStatic<F>;
159
160        fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
161            self,
162            base_url: &str,
163            access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
164            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
165        ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
166            Request::new(self.user_id, F::NAME.into()).try_into_http_request(
167                base_url,
168                access_token,
169                considering,
170            )
171        }
172    }
173
174    /// Response type for the `get_profile_field` endpoint.
175    #[derive(Debug, Clone, Default)]
176    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
177    pub struct Response {
178        /// The value of the profile field.
179        pub value: Option<ProfileFieldValue>,
180    }
181
182    impl Response {
183        /// Creates a `Response` with the given value.
184        pub fn new(value: ProfileFieldValue) -> Self {
185            Self { value: Some(value) }
186        }
187    }
188
189    #[cfg(feature = "client")]
190    impl ruma_common::api::IncomingResponse for Response {
191        type EndpointError = crate::Error;
192
193        fn try_from_http_response<T: AsRef<[u8]>>(
194            response: http::Response<T>,
195        ) -> Result<Self, ruma_common::api::error::FromHttpResponseError<Self::EndpointError>>
196        {
197            use ruma_common::api::EndpointError;
198
199            use crate::profile::profile_field_serde::deserialize_profile_field_value_option;
200
201            if response.status().as_u16() >= 400 {
202                return Err(ruma_common::api::error::FromHttpResponseError::Server(
203                    Self::EndpointError::from_http_response(response),
204                ));
205            }
206
207            let mut de = serde_json::Deserializer::from_slice(response.body().as_ref());
208            let value = deserialize_profile_field_value_option(&mut de)?;
209            de.end()?;
210
211            Ok(Self { value })
212        }
213    }
214
215    #[cfg(feature = "server")]
216    impl ruma_common::api::OutgoingResponse for Response {
217        fn try_into_http_response<T: Default + bytes::BufMut>(
218            self,
219        ) -> Result<http::Response<T>, ruma_common::api::error::IntoHttpError> {
220            use ruma_common::serde::JsonObject;
221
222            let body = self
223                .value
224                .as_ref()
225                .map(|value| ruma_common::serde::json_to_buf(value))
226                .unwrap_or_else(||
227                   // Send an empty object.
228                    ruma_common::serde::json_to_buf(&JsonObject::new()))?;
229
230            Ok(http::Response::builder()
231                .status(http::StatusCode::OK)
232                .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
233                .body(body)?)
234        }
235    }
236
237    /// Response type for the `get_profile_field` endpoint, using a statically-known field.
238    #[derive(Debug)]
239    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
240    pub struct ResponseStatic<F: StaticProfileField> {
241        /// The value of the profile field, if it is set.
242        pub value: Option<F::Value>,
243    }
244
245    impl<F: StaticProfileField> Clone for ResponseStatic<F>
246    where
247        F::Value: Clone,
248    {
249        fn clone(&self) -> Self {
250            Self { value: self.value.clone() }
251        }
252    }
253
254    #[cfg(feature = "client")]
255    impl<F: StaticProfileField> ruma_common::api::IncomingResponse for ResponseStatic<F> {
256        type EndpointError = crate::Error;
257
258        fn try_from_http_response<T: AsRef<[u8]>>(
259            response: http::Response<T>,
260        ) -> Result<Self, ruma_common::api::error::FromHttpResponseError<Self::EndpointError>>
261        {
262            use ruma_common::api::EndpointError;
263            use serde::de::Deserializer;
264
265            if response.status().as_u16() >= 400 {
266                return Err(ruma_common::api::error::FromHttpResponseError::Server(
267                    Self::EndpointError::from_http_response(response),
268                ));
269            }
270
271            let value = serde_json::Deserializer::from_slice(response.into_body().as_ref())
272                .deserialize_map(StaticProfileFieldVisitor(PhantomData::<F>))?;
273
274            Ok(Self { value })
275        }
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use ruma_common::{owned_mxc_uri, owned_user_id};
282    use serde_json::{
283        from_slice as from_json_slice, json, to_vec as to_json_vec, Value as JsonValue,
284    };
285
286    use super::v3::{Request, RequestStatic, Response};
287    use crate::profile::{ProfileFieldName, ProfileFieldValue};
288
289    #[test]
290    #[cfg(feature = "client")]
291    fn serialize_request() {
292        use std::borrow::Cow;
293
294        use ruma_common::api::{auth_scheme::SendAccessToken, OutgoingRequest, SupportedVersions};
295
296        // Profile field that existed in Matrix 1.0.
297        let avatar_url_request =
298            Request::new(owned_user_id!("@alice:localhost"), ProfileFieldName::AvatarUrl);
299
300        // Matrix 1.11
301        let http_request = avatar_url_request
302            .clone()
303            .try_into_http_request::<Vec<u8>>(
304                "http://localhost/",
305                SendAccessToken::None,
306                Cow::Owned(SupportedVersions::from_parts(
307                    &["v1.11".to_owned()],
308                    &Default::default(),
309                )),
310            )
311            .unwrap();
312        assert_eq!(
313            http_request.uri().path(),
314            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
315        );
316
317        // Matrix 1.16
318        let http_request = avatar_url_request
319            .try_into_http_request::<Vec<u8>>(
320                "http://localhost/",
321                SendAccessToken::None,
322                Cow::Owned(SupportedVersions::from_parts(
323                    &["v1.16".to_owned()],
324                    &Default::default(),
325                )),
326            )
327            .unwrap();
328        assert_eq!(
329            http_request.uri().path(),
330            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
331        );
332
333        // Profile field that didn't exist in Matrix 1.0.
334        let custom_field_request =
335            Request::new(owned_user_id!("@alice:localhost"), "dev.ruma.custom_field".into());
336
337        // Matrix 1.11
338        let http_request = custom_field_request
339            .clone()
340            .try_into_http_request::<Vec<u8>>(
341                "http://localhost/",
342                SendAccessToken::None,
343                Cow::Owned(SupportedVersions::from_parts(
344                    &["v1.11".to_owned()],
345                    &Default::default(),
346                )),
347            )
348            .unwrap();
349        assert_eq!(
350            http_request.uri().path(),
351            "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
352        );
353
354        // Matrix 1.16
355        let http_request = custom_field_request
356            .try_into_http_request::<Vec<u8>>(
357                "http://localhost/",
358                SendAccessToken::None,
359                Cow::Owned(SupportedVersions::from_parts(
360                    &["v1.16".to_owned()],
361                    &Default::default(),
362                )),
363            )
364            .unwrap();
365        assert_eq!(
366            http_request.uri().path(),
367            "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
368        );
369    }
370
371    #[test]
372    #[cfg(feature = "server")]
373    fn deserialize_request() {
374        use ruma_common::api::IncomingRequest;
375
376        let request = Request::try_from_http_request(
377            http::Request::get(
378                "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
379            )
380            .body(Vec::<u8>::new())
381            .unwrap(),
382            &["@alice:localhost", "displayname"],
383        )
384        .unwrap();
385
386        assert_eq!(request.user_id, "@alice:localhost");
387        assert_eq!(request.field, ProfileFieldName::DisplayName);
388    }
389
390    #[test]
391    #[cfg(feature = "server")]
392    fn serialize_response() {
393        use ruma_common::api::OutgoingResponse;
394
395        let response =
396            Response::new(ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")));
397
398        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
399
400        assert_eq!(
401            from_json_slice::<JsonValue>(http_response.body().as_ref()).unwrap(),
402            json!({
403                "avatar_url": "mxc://localhost/abcdef",
404            })
405        );
406    }
407
408    #[test]
409    #[cfg(feature = "client")]
410    fn deserialize_response() {
411        use ruma_common::api::IncomingResponse;
412
413        let body = to_json_vec(&json!({
414            "custom_field": "value",
415        }))
416        .unwrap();
417
418        let response = Response::try_from_http_response(http::Response::new(body)).unwrap();
419        let value = response.value.unwrap();
420        assert_eq!(value.field_name().as_str(), "custom_field");
421        assert_eq!(value.value().as_str().unwrap(), "value");
422
423        let empty_body = to_json_vec(&json!({})).unwrap();
424
425        let response = Response::try_from_http_response(http::Response::new(empty_body)).unwrap();
426        assert!(response.value.is_none());
427    }
428
429    /// Mock a response from the homeserver to a request of type `R` and return the given `value` as
430    /// a typed response.
431    #[cfg(feature = "client")]
432    fn get_static_response<R: ruma_common::api::OutgoingRequest>(
433        value: Option<ProfileFieldValue>,
434    ) -> Result<R::IncomingResponse, ruma_common::api::error::FromHttpResponseError<R::EndpointError>>
435    {
436        use ruma_common::api::IncomingResponse;
437
438        let body =
439            value.map(|value| to_json_vec(&value).unwrap()).unwrap_or_else(|| b"{}".to_vec());
440        R::IncomingResponse::try_from_http_response(http::Response::new(body))
441    }
442
443    #[test]
444    #[cfg(feature = "client")]
445    fn static_request_and_valid_response() {
446        use crate::profile::AvatarUrl;
447
448        let response = get_static_response::<RequestStatic<AvatarUrl>>(Some(
449            ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
450        ))
451        .unwrap();
452        assert_eq!(response.value.unwrap(), "mxc://localhost/abcdef");
453
454        let response = get_static_response::<RequestStatic<AvatarUrl>>(None).unwrap();
455        assert!(response.value.is_none());
456    }
457
458    #[test]
459    #[cfg(feature = "client")]
460    fn static_request_and_invalid_response() {
461        use crate::profile::AvatarUrl;
462
463        get_static_response::<RequestStatic<AvatarUrl>>(Some(ProfileFieldValue::DisplayName(
464            "Alice".to_owned(),
465        )))
466        .unwrap_err();
467    }
468}