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