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::{auth_scheme::AccessToken, 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 + AsRef<[u8]>>(
59            self,
60            base_url: &str,
61            access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
62            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
63        ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
64            use ruma_common::api::{auth_scheme::AuthScheme, path_builder::PathBuilder};
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 mut http_request = http::Request::builder()
80                .method(Self::METHOD)
81                .uri(url)
82                .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
83                .body(ruma_common::serde::json_to_buf(&self.value)?)?;
84
85            Self::Authentication::add_authentication(&mut http_request, access_token).map_err(
86                |error| ruma_common::api::error::IntoHttpError::Authentication(error.into()),
87            )?;
88
89            Ok(http_request)
90        }
91    }
92
93    #[cfg(feature = "server")]
94    impl ruma_common::api::IncomingRequest for Request {
95        type EndpointError = crate::Error;
96        type OutgoingResponse = Response;
97
98        fn try_from_http_request<B, S>(
99            request: http::Request<B>,
100            path_args: &[S],
101        ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
102        where
103            B: AsRef<[u8]>,
104            S: AsRef<str>,
105        {
106            use serde::de::{Deserializer, Error as _};
107
108            use crate::profile::ProfileFieldName;
109
110            Self::check_request_method(request.method())?;
111
112            let (user_id, field): (OwnedUserId, ProfileFieldName) =
113                serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
114                    _,
115                    serde::de::value::Error,
116                >::new(
117                    path_args.iter().map(::std::convert::AsRef::as_ref),
118                ))?;
119
120            let value = serde_json::Deserializer::from_slice(request.body().as_ref())
121                .deserialize_map(ProfileFieldValueVisitor(Some(field.clone())))?
122                .ok_or_else(|| serde_json::Error::custom(format!("missing field `{field}`")))?;
123
124            Ok(Request { user_id, value })
125        }
126    }
127
128    /// Response type for the `set_profile_field` endpoint.
129    #[response(error = crate::Error)]
130    #[derive(Default)]
131    pub struct Response {}
132
133    impl Response {
134        /// Creates an empty `Response`.
135        pub fn new() -> Self {
136            Self {}
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use assert_matches2::assert_matches;
144    use ruma_common::{owned_mxc_uri, owned_user_id};
145    use serde_json::{
146        from_slice as from_json_slice, json, to_vec as to_json_vec, Value as JsonValue,
147    };
148
149    use super::v3::Request;
150    use crate::profile::ProfileFieldValue;
151
152    #[test]
153    #[cfg(feature = "client")]
154    fn serialize_request() {
155        use std::borrow::Cow;
156
157        use http::header;
158        use ruma_common::api::{auth_scheme::SendAccessToken, OutgoingRequest, SupportedVersions};
159
160        // Profile field that existed in Matrix 1.0.
161        let avatar_url_request = Request::new(
162            owned_user_id!("@alice:localhost"),
163            ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
164        );
165
166        // Matrix 1.11.
167        let http_request = avatar_url_request
168            .clone()
169            .try_into_http_request::<Vec<u8>>(
170                "http://localhost/",
171                SendAccessToken::Always("access_token"),
172                Cow::Owned(SupportedVersions::from_parts(
173                    &["v1.11".to_owned()],
174                    &Default::default(),
175                )),
176            )
177            .unwrap();
178        assert_eq!(
179            http_request.uri().path(),
180            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
181        );
182        assert_eq!(
183            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
184            json!({
185                "avatar_url": "mxc://localhost/abcdef",
186            })
187        );
188        assert_eq!(
189            http_request.headers().get(header::AUTHORIZATION).unwrap(),
190            "Bearer access_token"
191        );
192
193        // Matrix 1.16.
194        let http_request = avatar_url_request
195            .try_into_http_request::<Vec<u8>>(
196                "http://localhost/",
197                SendAccessToken::Always("access_token"),
198                Cow::Owned(SupportedVersions::from_parts(
199                    &["v1.16".to_owned()],
200                    &Default::default(),
201                )),
202            )
203            .unwrap();
204        assert_eq!(
205            http_request.uri().path(),
206            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
207        );
208        assert_eq!(
209            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
210            json!({
211                "avatar_url": "mxc://localhost/abcdef",
212            })
213        );
214        assert_eq!(
215            http_request.headers().get(header::AUTHORIZATION).unwrap(),
216            "Bearer access_token"
217        );
218
219        // Profile field that didn't exist in Matrix 1.0.
220        let custom_field_request = Request::new(
221            owned_user_id!("@alice:localhost"),
222            ProfileFieldValue::new("dev.ruma.custom_field", json!(true)).unwrap(),
223        );
224
225        // Matrix 1.11.
226        let http_request = custom_field_request
227            .clone()
228            .try_into_http_request::<Vec<u8>>(
229                "http://localhost/",
230                SendAccessToken::Always("access_token"),
231                Cow::Owned(SupportedVersions::from_parts(
232                    &["v1.11".to_owned()],
233                    &Default::default(),
234                )),
235            )
236            .unwrap();
237        assert_eq!(
238            http_request.uri().path(),
239            "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
240        );
241        assert_eq!(
242            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
243            json!({
244                "dev.ruma.custom_field": true,
245            })
246        );
247        assert_eq!(
248            http_request.headers().get(header::AUTHORIZATION).unwrap(),
249            "Bearer access_token"
250        );
251
252        // Matrix 1.16.
253        let http_request = custom_field_request
254            .try_into_http_request::<Vec<u8>>(
255                "http://localhost/",
256                SendAccessToken::Always("access_token"),
257                Cow::Owned(SupportedVersions::from_parts(
258                    &["v1.16".to_owned()],
259                    &Default::default(),
260                )),
261            )
262            .unwrap();
263        assert_eq!(
264            http_request.uri().path(),
265            "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
266        );
267        assert_eq!(
268            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
269            json!({
270                "dev.ruma.custom_field": true,
271            })
272        );
273        assert_eq!(
274            http_request.headers().get(header::AUTHORIZATION).unwrap(),
275            "Bearer access_token"
276        );
277    }
278
279    #[test]
280    #[cfg(feature = "server")]
281    fn deserialize_request_valid_field() {
282        use ruma_common::api::IncomingRequest;
283
284        let body = to_json_vec(&json!({
285            "displayname": "Alice",
286        }))
287        .unwrap();
288
289        let request = Request::try_from_http_request(
290            http::Request::put(
291                "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
292            )
293            .body(body)
294            .unwrap(),
295            &["@alice:localhost", "displayname"],
296        )
297        .unwrap();
298
299        assert_eq!(request.user_id, "@alice:localhost");
300        assert_matches!(request.value, ProfileFieldValue::DisplayName(display_name));
301        assert_eq!(display_name, "Alice");
302    }
303
304    #[test]
305    #[cfg(feature = "server")]
306    fn deserialize_request_invalid_field() {
307        use ruma_common::api::IncomingRequest;
308
309        let body = to_json_vec(&json!({
310            "custom_field": "value",
311        }))
312        .unwrap();
313
314        Request::try_from_http_request(
315            http::Request::put(
316                "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
317            )
318            .body(body)
319            .unwrap(),
320            &["@alice:localhost", "displayname"],
321        )
322        .unwrap_err();
323    }
324}