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/latest/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        api::{response, Metadata},
18        metadata, OwnedUserId,
19    };
20
21    use crate::profile::{profile_field_serde::ProfileFieldValueVisitor, ProfileFieldValue};
22
23    metadata! {
24        method: PUT,
25        rate_limited: true,
26        authentication: AccessToken,
27        // History valid for fields that existed in Matrix 1.0, i.e. `displayname` and `avatar_url`.
28        history: {
29            unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}",
30            1.0 => "/_matrix/client/r0/profile/{user_id}/{field}",
31            1.1 => "/_matrix/client/v3/profile/{user_id}/{field}",
32        }
33    }
34
35    /// Request type for the `set_profile_field` endpoint.
36    #[derive(Debug, Clone)]
37    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
38    pub struct Request {
39        /// The user whose profile will be updated.
40        pub user_id: OwnedUserId,
41
42        /// The value of the profile field to set.
43        pub value: ProfileFieldValue,
44    }
45
46    impl Request {
47        /// Creates a new `Request` with the given user ID, field and value.
48        pub fn new(user_id: OwnedUserId, value: ProfileFieldValue) -> Self {
49            Self { user_id, value }
50        }
51    }
52
53    #[cfg(feature = "client")]
54    impl ruma_common::api::OutgoingRequest for Request {
55        type EndpointError = crate::Error;
56        type IncomingResponse = Response;
57
58        fn try_into_http_request<T: Default + bytes::BufMut>(
59            self,
60            base_url: &str,
61            access_token: ruma_common::api::SendAccessToken<'_>,
62            considering: &'_ ruma_common::api::SupportedVersions,
63        ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
64            use http::{header, HeaderValue};
65
66            let field = self.value.field_name();
67
68            let url = if field.existed_before_extended_profiles() {
69                Self::make_endpoint_url(considering, base_url, &[&self.user_id, &field], "")?
70            } else {
71                crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
72                    considering,
73                    base_url,
74                    &[&self.user_id, &field],
75                    "",
76                )?
77            };
78
79            let http_request = http::Request::builder()
80                .method(Self::METHOD)
81                .uri(url)
82                .header(header::CONTENT_TYPE, "application/json")
83                .header(
84                    header::AUTHORIZATION,
85                    HeaderValue::from_str(&format!(
86                        "Bearer {}",
87                        access_token
88                            .get_required_for_endpoint()
89                            .ok_or(ruma_common::api::error::IntoHttpError::NeedsAuthentication)?
90                    ))?,
91                )
92                .body(ruma_common::serde::json_to_buf(&self.value)?)
93                // this cannot fail because we don't give user-supplied data to any of the
94                // builder methods
95                .unwrap();
96
97            Ok(http_request)
98        }
99    }
100
101    #[cfg(feature = "server")]
102    impl ruma_common::api::IncomingRequest for Request {
103        type EndpointError = crate::Error;
104        type OutgoingResponse = Response;
105
106        fn try_from_http_request<B, S>(
107            request: http::Request<B>,
108            path_args: &[S],
109        ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
110        where
111            B: AsRef<[u8]>,
112            S: AsRef<str>,
113        {
114            use serde::de::{Deserializer, Error as _};
115
116            use crate::profile::ProfileFieldName;
117
118            let (user_id, field): (OwnedUserId, ProfileFieldName) =
119                serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
120                    _,
121                    serde::de::value::Error,
122                >::new(
123                    path_args.iter().map(::std::convert::AsRef::as_ref),
124                ))?;
125
126            let value = serde_json::Deserializer::from_slice(request.body().as_ref())
127                .deserialize_map(ProfileFieldValueVisitor(Some(field.clone())))?
128                .ok_or_else(|| serde_json::Error::custom(format!("missing field `{field}`")))?;
129
130            Ok(Request { user_id, value })
131        }
132    }
133
134    /// Response type for the `set_profile_field` endpoint.
135    #[response(error = crate::Error)]
136    #[derive(Default)]
137    pub struct Response {}
138
139    impl Response {
140        /// Creates an empty `Response`.
141        pub fn new() -> Self {
142            Self {}
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use assert_matches2::assert_matches;
150    use ruma_common::{owned_mxc_uri, owned_user_id};
151    use serde_json::{
152        from_slice as from_json_slice, json, to_vec as to_json_vec, Value as JsonValue,
153    };
154
155    use super::v3::Request;
156    use crate::profile::ProfileFieldValue;
157
158    #[test]
159    #[cfg(feature = "client")]
160    fn serialize_request() {
161        use http::header;
162        use ruma_common::api::{OutgoingRequest, SendAccessToken, SupportedVersions};
163
164        // Profile field that existed in Matrix 1.0.
165        let avatar_url_request = Request::new(
166            owned_user_id!("@alice:localhost"),
167            ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
168        );
169
170        // Matrix 1.11.
171        let http_request = avatar_url_request
172            .clone()
173            .try_into_http_request::<Vec<u8>>(
174                "http://localhost/",
175                SendAccessToken::Always("access_token"),
176                &SupportedVersions::from_parts(&["v1.11".to_owned()], &Default::default()),
177            )
178            .unwrap();
179        assert_eq!(
180            http_request.uri().path(),
181            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
182        );
183        assert_eq!(
184            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
185            json!({
186                "avatar_url": "mxc://localhost/abcdef",
187            })
188        );
189        assert_eq!(
190            http_request.headers().get(header::AUTHORIZATION).unwrap(),
191            "Bearer access_token"
192        );
193
194        // Matrix 1.16.
195        let http_request = avatar_url_request
196            .try_into_http_request::<Vec<u8>>(
197                "http://localhost/",
198                SendAccessToken::Always("access_token"),
199                &SupportedVersions::from_parts(&["v1.16".to_owned()], &Default::default()),
200            )
201            .unwrap();
202        assert_eq!(
203            http_request.uri().path(),
204            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
205        );
206        assert_eq!(
207            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
208            json!({
209                "avatar_url": "mxc://localhost/abcdef",
210            })
211        );
212        assert_eq!(
213            http_request.headers().get(header::AUTHORIZATION).unwrap(),
214            "Bearer access_token"
215        );
216
217        // Profile field that didn't exist in Matrix 1.0.
218        let custom_field_request = Request::new(
219            owned_user_id!("@alice:localhost"),
220            ProfileFieldValue::new("dev.ruma.custom_field", json!(true)).unwrap(),
221        );
222
223        // Matrix 1.11.
224        let http_request = custom_field_request
225            .clone()
226            .try_into_http_request::<Vec<u8>>(
227                "http://localhost/",
228                SendAccessToken::Always("access_token"),
229                &SupportedVersions::from_parts(&["v1.11".to_owned()], &Default::default()),
230            )
231            .unwrap();
232        assert_eq!(
233            http_request.uri().path(),
234            "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
235        );
236        assert_eq!(
237            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
238            json!({
239                "dev.ruma.custom_field": true,
240            })
241        );
242        assert_eq!(
243            http_request.headers().get(header::AUTHORIZATION).unwrap(),
244            "Bearer access_token"
245        );
246
247        // Matrix 1.16.
248        let http_request = custom_field_request
249            .try_into_http_request::<Vec<u8>>(
250                "http://localhost/",
251                SendAccessToken::Always("access_token"),
252                &SupportedVersions::from_parts(&["v1.16".to_owned()], &Default::default()),
253            )
254            .unwrap();
255        assert_eq!(
256            http_request.uri().path(),
257            "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
258        );
259        assert_eq!(
260            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
261            json!({
262                "dev.ruma.custom_field": true,
263            })
264        );
265        assert_eq!(
266            http_request.headers().get(header::AUTHORIZATION).unwrap(),
267            "Bearer access_token"
268        );
269    }
270
271    #[test]
272    #[cfg(feature = "server")]
273    fn deserialize_request_valid_field() {
274        use ruma_common::api::IncomingRequest;
275
276        let body = to_json_vec(&json!({
277            "displayname": "Alice",
278        }))
279        .unwrap();
280
281        let request = Request::try_from_http_request(
282            http::Request::put(
283                "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
284            )
285            .body(body)
286            .unwrap(),
287            &["@alice:localhost", "displayname"],
288        )
289        .unwrap();
290
291        assert_eq!(request.user_id, "@alice:localhost");
292        assert_matches!(request.value, ProfileFieldValue::DisplayName(display_name));
293        assert_eq!(display_name, "Alice");
294    }
295
296    #[test]
297    #[cfg(feature = "server")]
298    fn deserialize_request_invalid_field() {
299        use ruma_common::api::IncomingRequest;
300
301        let body = to_json_vec(&json!({
302            "custom_field": "value",
303        }))
304        .unwrap();
305
306        Request::try_from_http_request(
307            http::Request::put(
308                "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
309            )
310            .body(body)
311            .unwrap(),
312            &["@alice:localhost", "displayname"],
313        )
314        .unwrap_err();
315    }
316}