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