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