Skip to main content

ruma_client_api/profile/
set_profile_field.rs

1//! `PUT /_matrix/client/*/profile/{userId}/{key_name}`
2//!
3//! Set a field on the profile of the user.
4
5pub mod v3 {
6    //! `/v3/` ([spec])
7    //!
8    //! Although this endpoint has a similar format to [`set_avatar_url`] and [`set_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/#put_matrixclientv3profileuseridkeyname
13    //! [`set_avatar_url`]: crate::profile::set_avatar_url
14    //! [`set_display_name`]: crate::profile::set_display_name
15
16    use ruma_common::{
17        OwnedUserId,
18        api::{auth_scheme::AccessToken, error::Error, response},
19        metadata,
20        profile::ProfileFieldValue,
21    };
22
23    #[cfg(feature = "unstable-msc4466")]
24    use crate::profile::PropagateTo;
25
26    metadata! {
27        method: PUT,
28        rate_limited: true,
29        authentication: AccessToken,
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 `set_profile_field` endpoint.
39    #[derive(Debug, Clone)]
40    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
41    pub struct Request {
42        /// The user whose profile will be updated.
43        pub user_id: OwnedUserId,
44
45        /// The value of the profile field to set.
46        pub value: ProfileFieldValue,
47
48        /// The propagation mode to use for this profile update. Only applicable if the field
49        /// being set is `displayname` or `avatar_url`.
50        #[cfg(feature = "unstable-msc4466")]
51        pub propagate_to: PropagateTo,
52    }
53
54    impl Request {
55        /// Creates a new `Request` with the given user ID, field and value.
56        pub fn new(user_id: OwnedUserId, value: ProfileFieldValue) -> Self {
57            Self {
58                user_id,
59                value,
60                #[cfg(feature = "unstable-msc4466")]
61                propagate_to: PropagateTo::default(),
62            }
63        }
64    }
65
66    #[doc(hidden)]
67    #[derive(ruma_common::serde::_FakeDeriveSerde)]
68    #[cfg_attr(feature = "client", derive(serde::Serialize, ruma_common::api::OutgoingBodyJson))]
69    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
70    #[serde(transparent)]
71    // attribute will go away when we update IncomingRequest to also use RequestBody
72    #[cfg_attr(not(feature = "client"), expect(dead_code))]
73    pub struct RequestBody(ProfileFieldValue);
74
75    #[cfg(feature = "client")]
76    impl ruma_common::api::OutgoingRequest for Request {
77        type Body = RequestBody;
78        type EndpointError = Error;
79        type IncomingResponse = Response;
80
81        fn try_into_http_request_inner(
82            self,
83            base_url: &str,
84            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
85        ) -> Result<http::Request<RequestBody>, ruma_common::api::error::IntoHttpError> {
86            use ruma_common::api::{Metadata, path_builder::PathBuilder};
87
88            use crate::profile::field_existed_before_extended_profiles;
89
90            let field = self.value.field_name();
91
92            let query_string = serde_html_form::to_string(RequestQuery {
93                #[cfg(feature = "unstable-msc4466")]
94                propagate_to: self.propagate_to,
95            })?;
96
97            let url = if field_existed_before_extended_profiles(&field) {
98                Self::make_endpoint_url(
99                    considering,
100                    base_url,
101                    &[&self.user_id, &field],
102                    &query_string,
103                )?
104            } else {
105                crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
106                    considering,
107                    base_url,
108                    &[&self.user_id, &field],
109                    &query_string,
110                )?
111            };
112
113            let http_request = http::Request::builder()
114                .method(Self::METHOD)
115                .uri(url)
116                .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
117                .body(RequestBody(self.value))?;
118
119            Ok(http_request)
120        }
121    }
122
123    #[cfg(feature = "server")]
124    impl ruma_common::api::IncomingRequest for Request {
125        type EndpointError = Error;
126        type OutgoingResponse = Response;
127
128        fn try_from_http_request<B, S>(
129            request: http::Request<B>,
130            path_args: &[S],
131        ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
132        where
133            B: AsRef<[u8]>,
134            S: AsRef<str>,
135        {
136            use ruma_common::profile::{ProfileFieldName, ProfileFieldValueVisitor};
137            use serde::de::{Deserializer, Error as _};
138
139            Self::check_request_method(request.method())?;
140
141            let (user_id, field): (OwnedUserId, ProfileFieldName) =
142                serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
143                    _,
144                    serde::de::value::Error,
145                >::new(
146                    path_args.iter().map(::std::convert::AsRef::as_ref),
147                ))?;
148
149            let value = serde_json::Deserializer::from_slice(request.body().as_ref())
150                .deserialize_map(ProfileFieldValueVisitor::new(Some(field.clone())))?
151                .ok_or_else(|| serde_json::Error::custom(format!("missing field `{field}`")))?;
152
153            let RequestQuery {
154                #[cfg(feature = "unstable-msc4466")]
155                propagate_to,
156            } = serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
157
158            Ok(Request {
159                user_id,
160                value,
161                #[cfg(feature = "unstable-msc4466")]
162                propagate_to,
163            })
164        }
165    }
166
167    /// Response type for the `set_profile_field` endpoint.
168    #[response]
169    #[derive(Default)]
170    pub struct Response {}
171
172    impl Response {
173        /// Creates an empty `Response`.
174        pub fn new() -> Self {
175            Self {}
176        }
177    }
178
179    #[derive(Debug)]
180    #[cfg_attr(feature = "client", derive(serde::Serialize))]
181    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
182    struct RequestQuery {
183        #[cfg(feature = "unstable-msc4466")]
184        #[serde(rename = "computer.gingershaped.msc4466.propagate_to")]
185        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
186        propagate_to: PropagateTo,
187    }
188}
189
190#[cfg(all(test, feature = "client"))]
191mod tests_client {
192    use std::borrow::Cow;
193
194    use http::header;
195    use ruma_common::{
196        api::{OutgoingRequestExt as _, SupportedVersions, auth_scheme::SendAccessToken},
197        owned_mxc_uri, owned_user_id,
198        profile::ProfileFieldValue,
199    };
200    use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
201
202    use super::v3::Request;
203
204    #[test]
205    fn serialize_request() {
206        // Profile field that existed in Matrix 1.0.
207        let avatar_url_request = Request::new(
208            owned_user_id!("@alice:localhost"),
209            ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
210        );
211
212        // Matrix 1.11.
213        let http_request = avatar_url_request
214            .clone()
215            .try_into_http_request::<Vec<u8>>(
216                "http://localhost/",
217                SendAccessToken::Always("access_token"),
218                Cow::Owned(SupportedVersions::from_parts(
219                    &["v1.11".to_owned()],
220                    &Default::default(),
221                )),
222            )
223            .unwrap();
224        assert_eq!(
225            http_request.uri().path(),
226            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
227        );
228        assert_eq!(
229            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
230            json!({
231                "avatar_url": "mxc://localhost/abcdef",
232            })
233        );
234        assert_eq!(
235            http_request.headers().get(header::AUTHORIZATION).unwrap(),
236            "Bearer access_token"
237        );
238
239        // Matrix 1.16.
240        let http_request = avatar_url_request
241            .try_into_http_request::<Vec<u8>>(
242                "http://localhost/",
243                SendAccessToken::Always("access_token"),
244                Cow::Owned(SupportedVersions::from_parts(
245                    &["v1.16".to_owned()],
246                    &Default::default(),
247                )),
248            )
249            .unwrap();
250        assert_eq!(
251            http_request.uri().path(),
252            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
253        );
254        assert_eq!(
255            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
256            json!({
257                "avatar_url": "mxc://localhost/abcdef",
258            })
259        );
260        assert_eq!(
261            http_request.headers().get(header::AUTHORIZATION).unwrap(),
262            "Bearer access_token"
263        );
264
265        // Profile field that didn't exist in Matrix 1.0.
266        let custom_field_request = Request::new(
267            owned_user_id!("@alice:localhost"),
268            ProfileFieldValue::new("dev.ruma.custom_field", json!(true)).unwrap(),
269        );
270
271        // Matrix 1.11.
272        let http_request = custom_field_request
273            .clone()
274            .try_into_http_request::<Vec<u8>>(
275                "http://localhost/",
276                SendAccessToken::Always("access_token"),
277                Cow::Owned(SupportedVersions::from_parts(
278                    &["v1.11".to_owned()],
279                    &Default::default(),
280                )),
281            )
282            .unwrap();
283        assert_eq!(
284            http_request.uri().path(),
285            "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
286        );
287        assert_eq!(
288            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
289            json!({
290                "dev.ruma.custom_field": true,
291            })
292        );
293        assert_eq!(
294            http_request.headers().get(header::AUTHORIZATION).unwrap(),
295            "Bearer access_token"
296        );
297
298        // Matrix 1.16.
299        let http_request = custom_field_request
300            .try_into_http_request::<Vec<u8>>(
301                "http://localhost/",
302                SendAccessToken::Always("access_token"),
303                Cow::Owned(SupportedVersions::from_parts(
304                    &["v1.16".to_owned()],
305                    &Default::default(),
306                )),
307            )
308            .unwrap();
309        assert_eq!(
310            http_request.uri().path(),
311            "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
312        );
313        assert_eq!(
314            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
315            json!({
316                "dev.ruma.custom_field": true,
317            })
318        );
319        assert_eq!(
320            http_request.headers().get(header::AUTHORIZATION).unwrap(),
321            "Bearer access_token"
322        );
323    }
324}
325
326#[cfg(all(test, feature = "server"))]
327mod tests_server {
328    use assert_matches2::assert_let;
329    use ruma_common::{api::IncomingRequest, profile::ProfileFieldValue};
330    use serde_json::{json, to_vec as to_json_vec};
331
332    use super::v3::Request;
333
334    #[test]
335    fn deserialize_request_valid_field() {
336        let body = to_json_vec(&json!({
337            "displayname": "Alice",
338        }))
339        .unwrap();
340
341        let request = Request::try_from_http_request(
342            http::Request::put(
343                "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
344            )
345            .body(body)
346            .unwrap(),
347            &["@alice:localhost", "displayname"],
348        )
349        .unwrap();
350
351        assert_eq!(request.user_id, "@alice:localhost");
352        assert_let!(ProfileFieldValue::DisplayName(display_name) = request.value);
353        assert_eq!(display_name, "Alice");
354    }
355
356    #[test]
357    fn deserialize_request_invalid_field() {
358        let body = to_json_vec(&json!({
359            "custom_field": "value",
360        }))
361        .unwrap();
362
363        Request::try_from_http_request(
364            http::Request::put(
365                "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
366            )
367            .body(body)
368            .unwrap(),
369            &["@alice:localhost", "displayname"],
370        )
371        .unwrap_err();
372    }
373}