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    //! [spec]: https://github.com/matrix-org/matrix-spec-proposals/pull/4133
9
10    use std::marker::PhantomData;
11
12    use ruma_common::{
13        api::{request, Metadata},
14        metadata, OwnedUserId,
15    };
16
17    use crate::profile::{
18        profile_field_serde::StaticProfileFieldVisitor, ProfileFieldName, ProfileFieldValue,
19        StaticProfileField,
20    };
21
22    const METADATA: Metadata = metadata! {
23        method: GET,
24        rate_limited: false,
25        authentication: None,
26        history: {
27            unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}",
28            // 1.15 => "/_matrix/client/v3/profile/{user_id}/{field}",
29        }
30    };
31
32    /// Request type for the `get_profile_field` endpoint.
33    #[request(error = crate::Error)]
34    pub struct Request {
35        /// The user whose profile will be fetched.
36        #[ruma_api(path)]
37        pub user_id: OwnedUserId,
38
39        /// The profile field to get.
40        #[ruma_api(path)]
41        pub field: ProfileFieldName,
42    }
43
44    impl Request {
45        /// Creates a new `Request` with the given user ID and field.
46        pub fn new(user_id: OwnedUserId, field: ProfileFieldName) -> Self {
47            Self { user_id, field }
48        }
49
50        /// Creates a new request with the given user ID and statically-known field.
51        pub fn new_static<F: StaticProfileField>(user_id: OwnedUserId) -> RequestStatic<F> {
52            RequestStatic::new(user_id)
53        }
54    }
55
56    /// Request type for the `get_profile_field` endpoint, using a statically-known field.
57    #[derive(Debug)]
58    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
59    pub struct RequestStatic<F: StaticProfileField> {
60        /// The user whose profile will be fetched.
61        pub user_id: OwnedUserId,
62
63        /// The profile field to get.
64        field: PhantomData<F>,
65    }
66
67    impl<F: StaticProfileField> RequestStatic<F> {
68        /// Creates a new request with the given user ID.
69        pub fn new(user_id: OwnedUserId) -> Self {
70            Self { user_id, field: PhantomData }
71        }
72    }
73
74    impl<F: StaticProfileField> Clone for RequestStatic<F> {
75        fn clone(&self) -> Self {
76            Self { user_id: self.user_id.clone(), field: self.field }
77        }
78    }
79
80    #[cfg(feature = "client")]
81    impl<F: StaticProfileField> ruma_common::api::OutgoingRequest for RequestStatic<F> {
82        type EndpointError = crate::Error;
83        type IncomingResponse = ResponseStatic<F>;
84
85        const METADATA: Metadata = METADATA;
86
87        fn try_into_http_request<T: Default + bytes::BufMut>(
88            self,
89            base_url: &str,
90            access_token: ruma_common::api::SendAccessToken<'_>,
91            considering: &'_ ruma_common::api::SupportedVersions,
92        ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
93            Request::new(self.user_id, F::NAME.into()).try_into_http_request(
94                base_url,
95                access_token,
96                considering,
97            )
98        }
99    }
100
101    /// Response type for the `get_profile_field` endpoint.
102    #[derive(Debug, Clone, Default)]
103    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
104    pub struct Response {
105        /// The value of the profile field.
106        pub value: Option<ProfileFieldValue>,
107    }
108
109    impl Response {
110        /// Creates a `Response` with the given value.
111        pub fn new(value: ProfileFieldValue) -> Self {
112            Self { value: Some(value) }
113        }
114    }
115
116    #[cfg(feature = "client")]
117    impl ruma_common::api::IncomingResponse for Response {
118        type EndpointError = crate::Error;
119
120        fn try_from_http_response<T: AsRef<[u8]>>(
121            response: http::Response<T>,
122        ) -> Result<Self, ruma_common::api::error::FromHttpResponseError<Self::EndpointError>>
123        {
124            use ruma_common::api::EndpointError;
125
126            use crate::profile::profile_field_serde::deserialize_profile_field_value_option;
127
128            if response.status().as_u16() >= 400 {
129                return Err(ruma_common::api::error::FromHttpResponseError::Server(
130                    Self::EndpointError::from_http_response(response),
131                ));
132            }
133
134            let mut de = serde_json::Deserializer::from_slice(response.body().as_ref());
135            let value = deserialize_profile_field_value_option(&mut de)?;
136            de.end()?;
137
138            Ok(Self { value })
139        }
140    }
141
142    #[cfg(feature = "server")]
143    impl ruma_common::api::OutgoingResponse for Response {
144        fn try_into_http_response<T: Default + bytes::BufMut>(
145            self,
146        ) -> Result<http::Response<T>, ruma_common::api::error::IntoHttpError> {
147            use ruma_common::serde::JsonObject;
148
149            let body = self
150                .value
151                .as_ref()
152                .map(|value| ruma_common::serde::json_to_buf(value))
153                .unwrap_or_else(||
154                   // Send an empty object.
155                    ruma_common::serde::json_to_buf(&JsonObject::new()))?;
156
157            Ok(http::Response::builder()
158                .status(http::StatusCode::OK)
159                .header(http::header::CONTENT_TYPE, "application/json")
160                .body(body)?)
161        }
162    }
163
164    /// Response type for the `get_profile_field` endpoint, using a statically-known field.
165    #[derive(Debug)]
166    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
167    pub struct ResponseStatic<F: StaticProfileField> {
168        /// The value of the profile field, if it is set.
169        pub value: Option<F::Value>,
170    }
171
172    impl<F: StaticProfileField> Clone for ResponseStatic<F>
173    where
174        F::Value: Clone,
175    {
176        fn clone(&self) -> Self {
177            Self { value: self.value.clone() }
178        }
179    }
180
181    #[cfg(feature = "client")]
182    impl<F: StaticProfileField> ruma_common::api::IncomingResponse for ResponseStatic<F> {
183        type EndpointError = crate::Error;
184
185        fn try_from_http_response<T: AsRef<[u8]>>(
186            response: http::Response<T>,
187        ) -> Result<Self, ruma_common::api::error::FromHttpResponseError<Self::EndpointError>>
188        {
189            use ruma_common::api::EndpointError;
190            use serde::de::Deserializer;
191
192            if response.status().as_u16() >= 400 {
193                return Err(ruma_common::api::error::FromHttpResponseError::Server(
194                    Self::EndpointError::from_http_response(response),
195                ));
196            }
197
198            let value = serde_json::Deserializer::from_slice(response.into_body().as_ref())
199                .deserialize_map(StaticProfileFieldVisitor(PhantomData::<F>))?;
200
201            Ok(Self { value })
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use ruma_common::{owned_mxc_uri, owned_user_id};
209    use serde_json::{
210        from_slice as from_json_slice, json, to_vec as to_json_vec, Value as JsonValue,
211    };
212
213    use super::v3::{Request, RequestStatic, Response};
214    use crate::profile::{ProfileFieldName, ProfileFieldValue};
215
216    #[test]
217    #[cfg(feature = "client")]
218    fn serialize_request() {
219        use ruma_common::api::{OutgoingRequest, SendAccessToken, SupportedVersions};
220
221        let request = Request::new(owned_user_id!("@alice:localhost"), ProfileFieldName::AvatarUrl);
222
223        let http_request = request
224            .try_into_http_request::<Vec<u8>>(
225                "http://localhost/",
226                SendAccessToken::Always("access_token"),
227                &SupportedVersions::from_parts(&["v11".to_owned()], &Default::default()),
228            )
229            .unwrap();
230
231        assert_eq!(
232            http_request.uri().path(),
233            "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/avatar_url"
234        );
235    }
236
237    #[test]
238    #[cfg(feature = "server")]
239    fn deserialize_request() {
240        use ruma_common::api::IncomingRequest;
241
242        let request = Request::try_from_http_request(
243            http::Request::get("http://localhost/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/displayname").body(Vec::<u8>::new()).unwrap(),
244                &["@alice:localhost", "displayname"],
245            ).unwrap();
246
247        assert_eq!(request.user_id, "@alice:localhost");
248        assert_eq!(request.field, ProfileFieldName::DisplayName);
249    }
250
251    #[test]
252    #[cfg(feature = "server")]
253    fn serialize_response() {
254        use ruma_common::api::OutgoingResponse;
255
256        let response =
257            Response::new(ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")));
258
259        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
260
261        assert_eq!(
262            from_json_slice::<JsonValue>(http_response.body().as_ref()).unwrap(),
263            json!({
264                "avatar_url": "mxc://localhost/abcdef",
265            })
266        );
267    }
268
269    #[test]
270    #[cfg(feature = "client")]
271    fn deserialize_response() {
272        use ruma_common::api::IncomingResponse;
273
274        let body = to_json_vec(&json!({
275            "custom_field": "value",
276        }))
277        .unwrap();
278
279        let response = Response::try_from_http_response(http::Response::new(body)).unwrap();
280        let value = response.value.unwrap();
281        assert_eq!(value.field_name().as_str(), "custom_field");
282        assert_eq!(value.value().as_str().unwrap(), "value");
283
284        let empty_body = to_json_vec(&json!({})).unwrap();
285
286        let response = Response::try_from_http_response(http::Response::new(empty_body)).unwrap();
287        assert!(response.value.is_none());
288    }
289
290    /// Mock a response from the homeserver to a request of type `R` and return the given `value` as
291    /// a typed response.
292    #[cfg(feature = "client")]
293    fn get_static_response<R: ruma_common::api::OutgoingRequest>(
294        value: Option<ProfileFieldValue>,
295    ) -> Result<R::IncomingResponse, ruma_common::api::error::FromHttpResponseError<R::EndpointError>>
296    {
297        use ruma_common::api::IncomingResponse;
298
299        let body =
300            value.map(|value| to_json_vec(&value).unwrap()).unwrap_or_else(|| b"{}".to_vec());
301        R::IncomingResponse::try_from_http_response(http::Response::new(body))
302    }
303
304    #[test]
305    #[cfg(feature = "client")]
306    fn static_request_and_valid_response() {
307        use crate::profile::AvatarUrl;
308
309        let response = get_static_response::<RequestStatic<AvatarUrl>>(Some(
310            ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
311        ))
312        .unwrap();
313        assert_eq!(response.value.unwrap(), "mxc://localhost/abcdef");
314
315        let response = get_static_response::<RequestStatic<AvatarUrl>>(None).unwrap();
316        assert!(response.value.is_none());
317    }
318
319    #[test]
320    #[cfg(feature = "client")]
321    fn static_request_and_invalid_response() {
322        use crate::profile::AvatarUrl;
323
324        get_static_response::<RequestStatic<AvatarUrl>>(Some(ProfileFieldValue::DisplayName(
325            "Alice".to_owned(),
326        )))
327        .unwrap_err();
328    }
329}