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 url = if field_existed_before_extended_profiles(&self.field) {
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 http_request =
89                http::Request::builder().method(Self::METHOD).uri(url).body(EmptyBody)?;
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 Body = EmptyBody;
157        type EndpointError = Error;
158        type IncomingResponse = ResponseStatic<F>;
159
160        fn try_into_http_request_inner(
161            self,
162            base_url: &str,
163            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
164        ) -> Result<http::Request<EmptyBody>, ruma_common::api::error::IntoHttpError> {
165            Request::new(self.user_id, F::NAME.into())
166                .try_into_http_request_inner(base_url, considering)
167        }
168    }
169
170    /// Response type for the `get_profile_field` endpoint.
171    #[derive(Debug, Clone, Default)]
172    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
173    pub struct Response {
174        /// The value of the profile field.
175        pub value: Option<ProfileFieldValue>,
176    }
177
178    impl Response {
179        /// Creates a `Response` with the given value.
180        pub fn new(value: ProfileFieldValue) -> Self {
181            Self { value: Some(value) }
182        }
183    }
184
185    #[doc(hidden)]
186    #[derive(ruma_common::serde::_FakeDeriveSerde)]
187    #[cfg_attr(feature = "server", derive(ruma_common::api::OutgoingBodyJson))]
188    #[serde(transparent)]
189    pub struct ResponseBody(Option<ProfileFieldValue>);
190
191    #[cfg(feature = "server")]
192    impl serde::Serialize for ResponseBody {
193        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
194        where
195            S: serde::Serializer,
196        {
197            use ruma_common::serde::JsonObject;
198
199            if let Some(value) = &self.0 {
200                value.serialize(serializer)
201            } else {
202                JsonObject::new().serialize(serializer)
203            }
204        }
205    }
206
207    #[cfg(feature = "client")]
208    impl<'de> serde::Deserialize<'de> for ResponseBody {
209        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
210        where
211            D: serde::Deserializer<'de>,
212        {
213            use ruma_common::profile::ProfileFieldValueVisitor;
214
215            let value = deserializer.deserialize_map(ProfileFieldValueVisitor::new(None))?;
216
217            Ok(Self(value))
218        }
219    }
220
221    #[cfg(feature = "client")]
222    impl ruma_common::api::IncomingResponse for Response {
223        type EndpointError = Error;
224
225        fn try_from_http_response_inner(
226            response: http::Response<&[u8]>,
227        ) -> Result<Self, ruma_common::api::error::DeserializationError> {
228            let ResponseBody(value) = serde_json::from_slice(response.body())?;
229            Ok(Self { value })
230        }
231    }
232
233    #[cfg(feature = "server")]
234    impl ruma_common::api::OutgoingResponse for Response {
235        type Body = ResponseBody;
236
237        fn try_into_http_response_inner(
238            self,
239        ) -> Result<http::Response<Self::Body>, ruma_common::api::error::IntoHttpError> {
240            Ok(http::Response::builder()
241                .status(http::StatusCode::OK)
242                .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
243                .body(ResponseBody(self.value))?)
244        }
245    }
246
247    /// Response type for the `get_profile_field` endpoint, using a statically-known field.
248    #[derive(Debug)]
249    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
250    pub struct ResponseStatic<F: StaticProfileField> {
251        /// The value of the profile field, if it is set.
252        pub value: Option<F::Value>,
253    }
254
255    impl<F: StaticProfileField> Clone for ResponseStatic<F>
256    where
257        F::Value: Clone,
258    {
259        fn clone(&self) -> Self {
260            Self { value: self.value.clone() }
261        }
262    }
263
264    #[cfg(feature = "client")]
265    impl<F: StaticProfileField> ruma_common::api::IncomingResponse for ResponseStatic<F> {
266        type EndpointError = Error;
267
268        fn try_from_http_response_inner(
269            response: http::Response<&[u8]>,
270        ) -> Result<Self, ruma_common::api::error::DeserializationError> {
271            use serde::de::Deserializer;
272
273            use crate::profile::profile_field_serde::StaticProfileFieldVisitor;
274
275            let value = serde_json::Deserializer::from_slice(response.into_body())
276                .deserialize_map(StaticProfileFieldVisitor(PhantomData::<F>))?;
277
278            Ok(Self { value })
279        }
280    }
281}
282
283#[cfg(all(test, feature = "client"))]
284mod tests_client {
285    use ruma_common::{
286        owned_mxc_uri, owned_user_id,
287        profile::{ProfileFieldName, ProfileFieldValue},
288    };
289    use serde_json::{json, to_vec as to_json_vec};
290
291    use super::v3::{Request, RequestStatic, Response};
292
293    #[test]
294    fn serialize_request() {
295        use std::borrow::Cow;
296
297        use ruma_common::api::{
298            OutgoingRequestExt as _, SupportedVersions, auth_scheme::SendAccessToken,
299        };
300
301        // Profile field that existed in Matrix 1.0.
302        let avatar_url_request =
303            Request::new(owned_user_id!("@alice:localhost"), ProfileFieldName::AvatarUrl);
304
305        // Matrix 1.11
306        let http_request = avatar_url_request
307            .clone()
308            .try_into_http_request::<Vec<u8>>(
309                "http://localhost/",
310                SendAccessToken::None,
311                Cow::Owned(SupportedVersions::from_parts(
312                    &["v1.11".to_owned()],
313                    &Default::default(),
314                )),
315            )
316            .unwrap();
317        assert_eq!(
318            http_request.uri().path(),
319            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
320        );
321
322        // Matrix 1.16
323        let http_request = avatar_url_request
324            .try_into_http_request::<Vec<u8>>(
325                "http://localhost/",
326                SendAccessToken::None,
327                Cow::Owned(SupportedVersions::from_parts(
328                    &["v1.16".to_owned()],
329                    &Default::default(),
330                )),
331            )
332            .unwrap();
333        assert_eq!(
334            http_request.uri().path(),
335            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
336        );
337
338        // Profile field that didn't exist in Matrix 1.0.
339        let custom_field_request =
340            Request::new(owned_user_id!("@alice:localhost"), "dev.ruma.custom_field".into());
341
342        // Matrix 1.11
343        let http_request = custom_field_request
344            .clone()
345            .try_into_http_request::<Vec<u8>>(
346                "http://localhost/",
347                SendAccessToken::None,
348                Cow::Owned(SupportedVersions::from_parts(
349                    &["v1.11".to_owned()],
350                    &Default::default(),
351                )),
352            )
353            .unwrap();
354        assert_eq!(
355            http_request.uri().path(),
356            "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
357        );
358
359        // Matrix 1.16
360        let http_request = custom_field_request
361            .try_into_http_request::<Vec<u8>>(
362                "http://localhost/",
363                SendAccessToken::None,
364                Cow::Owned(SupportedVersions::from_parts(
365                    &["v1.16".to_owned()],
366                    &Default::default(),
367                )),
368            )
369            .unwrap();
370        assert_eq!(
371            http_request.uri().path(),
372            "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
373        );
374    }
375
376    #[test]
377    fn deserialize_response() {
378        use ruma_common::api::IncomingResponseExt as _;
379
380        let body = json!({
381            "custom_field": "value",
382        })
383        .to_string();
384
385        let response =
386            Response::try_from_http_response(http::Response::new(body.as_bytes())).unwrap();
387        let value = response.value.unwrap();
388        assert_eq!(value.field_name().as_str(), "custom_field");
389        assert_eq!(value.value().as_str().unwrap(), "value");
390
391        let response =
392            Response::try_from_http_response(http::Response::new(b"{}".as_slice())).unwrap();
393        assert!(response.value.is_none());
394    }
395
396    /// Mock a response from the homeserver to a request of type `R` and return the given `value` as
397    /// a typed response.
398    fn get_static_response<R: ruma_common::api::OutgoingRequest>(
399        value: Option<ProfileFieldValue>,
400    ) -> Result<R::IncomingResponse, ruma_common::api::error::FromHttpResponseError<R::EndpointError>>
401    {
402        use ruma_common::api::IncomingResponseExt as _;
403
404        let body =
405            value.map(|value| to_json_vec(&value).unwrap()).unwrap_or_else(|| b"{}".to_vec());
406        R::IncomingResponse::try_from_http_response(http::Response::new(body.as_slice()))
407    }
408
409    #[test]
410    fn static_request_and_valid_response() {
411        use crate::profile::AvatarUrl;
412
413        let response = get_static_response::<RequestStatic<AvatarUrl>>(Some(
414            ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
415        ))
416        .unwrap();
417        assert_eq!(response.value.unwrap(), "mxc://localhost/abcdef");
418
419        let response = get_static_response::<RequestStatic<AvatarUrl>>(None).unwrap();
420        assert!(response.value.is_none());
421    }
422
423    #[test]
424    fn static_request_and_invalid_response() {
425        use crate::profile::AvatarUrl;
426
427        get_static_response::<RequestStatic<AvatarUrl>>(Some(ProfileFieldValue::DisplayName(
428            "Alice".to_owned(),
429        )))
430        .unwrap_err();
431    }
432}
433
434#[cfg(all(test, feature = "server"))]
435mod tests_server {
436    use ruma_common::{
437        owned_mxc_uri,
438        profile::{ProfileFieldName, ProfileFieldValue},
439    };
440    use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
441
442    use super::v3::{Request, Response};
443
444    #[test]
445    fn deserialize_request() {
446        use ruma_common::api::IncomingRequest;
447
448        let request = Request::try_from_http_request(
449            http::Request::get(
450                "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
451            )
452            .body(Vec::<u8>::new())
453            .unwrap(),
454            &["@alice:localhost", "displayname"],
455        )
456        .unwrap();
457
458        assert_eq!(request.user_id, "@alice:localhost");
459        assert_eq!(request.field, ProfileFieldName::DisplayName);
460    }
461
462    #[test]
463    fn serialize_response() {
464        use ruma_common::api::OutgoingResponseExt;
465
466        let response =
467            Response::new(ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")));
468
469        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
470
471        assert_eq!(
472            from_json_slice::<JsonValue>(http_response.body().as_ref()).unwrap(),
473            json!({
474                "avatar_url": "mxc://localhost/abcdef",
475            })
476        );
477    }
478}