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