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    #[cfg(feature = "client")]
68    #[derive(serde::Serialize, ruma_common::api::OutgoingBodyJson)]
69    #[serde(transparent)]
70    pub struct RequestBody(ProfileFieldValue);
71
72    #[cfg(feature = "client")]
73    impl ruma_common::api::OutgoingRequest for Request {
74        type Body = RequestBody;
75        type EndpointError = Error;
76        type IncomingResponse = Response;
77
78        fn try_into_http_request_inner(
79            self,
80            base_url: &str,
81            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
82        ) -> Result<http::Request<RequestBody>, ruma_common::api::error::IntoHttpError> {
83            use ruma_common::api::{Metadata, path_builder::PathBuilder};
84
85            use crate::profile::field_existed_before_extended_profiles;
86
87            let field = self.value.field_name();
88
89            let query_string = serde_html_form::to_string(RequestQuery {
90                #[cfg(feature = "unstable-msc4466")]
91                propagate_to: self.propagate_to,
92            })?;
93
94            let url = if field_existed_before_extended_profiles(&field) {
95                Self::make_endpoint_url(
96                    considering,
97                    base_url,
98                    &[&self.user_id, &field],
99                    &query_string,
100                )?
101            } else {
102                crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
103                    considering,
104                    base_url,
105                    &[&self.user_id, &field],
106                    &query_string,
107                )?
108            };
109
110            let http_request = http::Request::builder()
111                .method(Self::METHOD)
112                .uri(url)
113                .body(RequestBody(self.value))?;
114
115            Ok(http_request)
116        }
117    }
118
119    #[cfg(feature = "server")]
120    impl ruma_common::api::IncomingRequest for Request {
121        type EndpointError = Error;
122        type OutgoingResponse = Response;
123
124        fn try_from_http_request_inner(
125            request: http::Request<&[u8]>,
126            path_args: &[&str],
127        ) -> Result<Self, ruma_common::api::error::DeserializationError> {
128            use ruma_common::profile::{ProfileFieldName, ProfileFieldValueVisitor};
129            use serde::de::{Deserializer, Error as _};
130
131            let (user_id, field): (OwnedUserId, ProfileFieldName) =
132                serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
133                    _,
134                    serde::de::value::Error,
135                >::new(path_args.iter().copied()))?;
136
137            let value = serde_json::Deserializer::from_slice(request.body())
138                .deserialize_map(ProfileFieldValueVisitor::new(Some(field.clone())))?
139                .ok_or_else(|| serde_json::Error::custom(format!("missing field `{field}`")))?;
140
141            let RequestQuery {
142                #[cfg(feature = "unstable-msc4466")]
143                propagate_to,
144            } = serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
145
146            Ok(Request {
147                user_id,
148                value,
149                #[cfg(feature = "unstable-msc4466")]
150                propagate_to,
151            })
152        }
153    }
154
155    /// Response type for the `set_profile_field` endpoint.
156    #[response]
157    #[derive(Default)]
158    pub struct Response {}
159
160    impl Response {
161        /// Creates an empty `Response`.
162        pub fn new() -> Self {
163            Self {}
164        }
165    }
166
167    #[derive(Debug)]
168    #[cfg_attr(feature = "client", derive(serde::Serialize))]
169    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
170    struct RequestQuery {
171        #[cfg(feature = "unstable-msc4466")]
172        #[serde(rename = "computer.gingershaped.msc4466.propagate_to")]
173        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
174        propagate_to: PropagateTo,
175    }
176}
177
178#[cfg(all(test, feature = "client"))]
179mod tests_client {
180    use std::borrow::Cow;
181
182    use http::header;
183    use ruma_common::{
184        api::{OutgoingRequestExt as _, SupportedVersions, auth_scheme::SendAccessToken},
185        owned_mxc_uri, owned_user_id,
186        profile::ProfileFieldValue,
187    };
188    use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
189
190    use super::v3::Request;
191
192    #[test]
193    fn serialize_request() {
194        // Profile field that existed in Matrix 1.0.
195        let avatar_url_request = Request::new(
196            owned_user_id!("@alice:localhost"),
197            ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
198        );
199
200        // Matrix 1.11.
201        let http_request = avatar_url_request
202            .clone()
203            .try_into_http_request::<Vec<u8>>(
204                "http://localhost/",
205                SendAccessToken::Always("access_token"),
206                Cow::Owned(SupportedVersions::from_parts(
207                    &["v1.11".to_owned()],
208                    &Default::default(),
209                )),
210            )
211            .unwrap();
212        assert_eq!(
213            http_request.uri().path(),
214            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
215        );
216        assert_eq!(
217            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
218            json!({
219                "avatar_url": "mxc://localhost/abcdef",
220            })
221        );
222        assert_eq!(
223            http_request.headers().get(header::AUTHORIZATION).unwrap(),
224            "Bearer access_token"
225        );
226
227        // Matrix 1.16.
228        let http_request = avatar_url_request
229            .try_into_http_request::<Vec<u8>>(
230                "http://localhost/",
231                SendAccessToken::Always("access_token"),
232                Cow::Owned(SupportedVersions::from_parts(
233                    &["v1.16".to_owned()],
234                    &Default::default(),
235                )),
236            )
237            .unwrap();
238        assert_eq!(
239            http_request.uri().path(),
240            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
241        );
242        assert_eq!(
243            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
244            json!({
245                "avatar_url": "mxc://localhost/abcdef",
246            })
247        );
248        assert_eq!(
249            http_request.headers().get(header::AUTHORIZATION).unwrap(),
250            "Bearer access_token"
251        );
252
253        // Profile field that didn't exist in Matrix 1.0.
254        let custom_field_request = Request::new(
255            owned_user_id!("@alice:localhost"),
256            ProfileFieldValue::new("dev.ruma.custom_field", json!(true)).unwrap(),
257        );
258
259        // Matrix 1.11.
260        let http_request = custom_field_request
261            .clone()
262            .try_into_http_request::<Vec<u8>>(
263                "http://localhost/",
264                SendAccessToken::Always("access_token"),
265                Cow::Owned(SupportedVersions::from_parts(
266                    &["v1.11".to_owned()],
267                    &Default::default(),
268                )),
269            )
270            .unwrap();
271        assert_eq!(
272            http_request.uri().path(),
273            "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
274        );
275        assert_eq!(
276            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
277            json!({
278                "dev.ruma.custom_field": true,
279            })
280        );
281        assert_eq!(
282            http_request.headers().get(header::AUTHORIZATION).unwrap(),
283            "Bearer access_token"
284        );
285
286        // Matrix 1.16.
287        let http_request = custom_field_request
288            .try_into_http_request::<Vec<u8>>(
289                "http://localhost/",
290                SendAccessToken::Always("access_token"),
291                Cow::Owned(SupportedVersions::from_parts(
292                    &["v1.16".to_owned()],
293                    &Default::default(),
294                )),
295            )
296            .unwrap();
297        assert_eq!(
298            http_request.uri().path(),
299            "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
300        );
301        assert_eq!(
302            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
303            json!({
304                "dev.ruma.custom_field": true,
305            })
306        );
307        assert_eq!(
308            http_request.headers().get(header::AUTHORIZATION).unwrap(),
309            "Bearer access_token"
310        );
311    }
312}
313
314#[cfg(all(test, feature = "server"))]
315mod tests_server {
316    use assert_matches2::assert_let;
317    use ruma_common::{api::IncomingRequestExt as _, profile::ProfileFieldValue};
318    use serde_json::{json, to_vec as to_json_vec};
319
320    use super::v3::Request;
321
322    #[test]
323    fn deserialize_request_valid_field() {
324        let body = to_json_vec(&json!({
325            "displayname": "Alice",
326        }))
327        .unwrap();
328
329        let request = Request::try_from_http_request(
330            http::Request::put(
331                "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
332            )
333            .body(body.as_slice())
334            .unwrap(),
335            &["@alice:localhost", "displayname"],
336        )
337        .unwrap();
338
339        assert_eq!(request.user_id, "@alice:localhost");
340        assert_let!(ProfileFieldValue::DisplayName(display_name) = request.value);
341        assert_eq!(display_name, "Alice");
342    }
343
344    #[test]
345    fn deserialize_request_invalid_field() {
346        let body = to_json_vec(&json!({
347            "custom_field": "value",
348        }))
349        .unwrap();
350
351        Request::try_from_http_request(
352            http::Request::put(
353                "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
354            )
355            .body(body.as_slice())
356            .unwrap(),
357            &["@alice:localhost", "displayname"],
358        )
359        .unwrap_err();
360    }
361}