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