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