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.18/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    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 = 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::{Metadata, auth_scheme::AuthScheme, path_builder::PathBuilder};
65
66            use crate::profile::field_existed_before_extended_profiles;
67
68            let field = self.value.field_name();
69
70            let url = if field_existed_before_extended_profiles(&field) {
71                Self::make_endpoint_url(considering, base_url, &[&self.user_id, &field], "")?
72            } else {
73                crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
74                    considering,
75                    base_url,
76                    &[&self.user_id, &field],
77                    "",
78                )?
79            };
80
81            let mut http_request = http::Request::builder()
82                .method(Self::METHOD)
83                .uri(url)
84                .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
85                .body(ruma_common::serde::json_to_buf(&self.value)?)?;
86
87            Self::Authentication::add_authentication(&mut http_request, access_token).map_err(
88                |error| ruma_common::api::error::IntoHttpError::Authentication(error.into()),
89            )?;
90
91            Ok(http_request)
92        }
93    }
94
95    #[cfg(feature = "server")]
96    impl ruma_common::api::IncomingRequest for Request {
97        type EndpointError = Error;
98        type OutgoingResponse = Response;
99
100        fn try_from_http_request<B, S>(
101            request: http::Request<B>,
102            path_args: &[S],
103        ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
104        where
105            B: AsRef<[u8]>,
106            S: AsRef<str>,
107        {
108            use ruma_common::profile::{ProfileFieldName, ProfileFieldValueVisitor};
109            use serde::de::{Deserializer, Error as _};
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::new(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]
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(all(test, feature = "client"))]
143mod tests_client {
144    use std::borrow::Cow;
145
146    use http::header;
147    use ruma_common::{
148        api::{OutgoingRequest, SupportedVersions, auth_scheme::SendAccessToken},
149        owned_mxc_uri, owned_user_id,
150        profile::ProfileFieldValue,
151    };
152    use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
153
154    use super::v3::Request;
155
156    #[test]
157    fn serialize_request() {
158        // Profile field that existed in Matrix 1.0.
159        let avatar_url_request = Request::new(
160            owned_user_id!("@alice:localhost"),
161            ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
162        );
163
164        // Matrix 1.11.
165        let http_request = avatar_url_request
166            .clone()
167            .try_into_http_request::<Vec<u8>>(
168                "http://localhost/",
169                SendAccessToken::Always("access_token"),
170                Cow::Owned(SupportedVersions::from_parts(
171                    &["v1.11".to_owned()],
172                    &Default::default(),
173                )),
174            )
175            .unwrap();
176        assert_eq!(
177            http_request.uri().path(),
178            "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
179        );
180        assert_eq!(
181            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
182            json!({
183                "avatar_url": "mxc://localhost/abcdef",
184            })
185        );
186        assert_eq!(
187            http_request.headers().get(header::AUTHORIZATION).unwrap(),
188            "Bearer access_token"
189        );
190
191        // Matrix 1.16.
192        let http_request = avatar_url_request
193            .try_into_http_request::<Vec<u8>>(
194                "http://localhost/",
195                SendAccessToken::Always("access_token"),
196                Cow::Owned(SupportedVersions::from_parts(
197                    &["v1.16".to_owned()],
198                    &Default::default(),
199                )),
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                Cow::Owned(SupportedVersions::from_parts(
230                    &["v1.11".to_owned()],
231                    &Default::default(),
232                )),
233            )
234            .unwrap();
235        assert_eq!(
236            http_request.uri().path(),
237            "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
238        );
239        assert_eq!(
240            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
241            json!({
242                "dev.ruma.custom_field": true,
243            })
244        );
245        assert_eq!(
246            http_request.headers().get(header::AUTHORIZATION).unwrap(),
247            "Bearer access_token"
248        );
249
250        // Matrix 1.16.
251        let http_request = custom_field_request
252            .try_into_http_request::<Vec<u8>>(
253                "http://localhost/",
254                SendAccessToken::Always("access_token"),
255                Cow::Owned(SupportedVersions::from_parts(
256                    &["v1.16".to_owned()],
257                    &Default::default(),
258                )),
259            )
260            .unwrap();
261        assert_eq!(
262            http_request.uri().path(),
263            "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
264        );
265        assert_eq!(
266            from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
267            json!({
268                "dev.ruma.custom_field": true,
269            })
270        );
271        assert_eq!(
272            http_request.headers().get(header::AUTHORIZATION).unwrap(),
273            "Bearer access_token"
274        );
275    }
276}
277
278#[cfg(all(test, feature = "server"))]
279mod tests_server {
280    use assert_matches2::assert_let;
281    use ruma_common::{api::IncomingRequest, profile::ProfileFieldValue};
282    use serde_json::{json, to_vec as to_json_vec};
283
284    use super::v3::Request;
285
286    #[test]
287    fn deserialize_request_valid_field() {
288        let body = to_json_vec(&json!({
289            "displayname": "Alice",
290        }))
291        .unwrap();
292
293        let request = Request::try_from_http_request(
294            http::Request::put(
295                "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
296            )
297            .body(body)
298            .unwrap(),
299            &["@alice:localhost", "displayname"],
300        )
301        .unwrap();
302
303        assert_eq!(request.user_id, "@alice:localhost");
304        assert_let!(ProfileFieldValue::DisplayName(display_name) = request.value);
305        assert_eq!(display_name, "Alice");
306    }
307
308    #[test]
309    fn deserialize_request_invalid_field() {
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}