Skip to main content

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