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