1pub mod v3 {
6 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: {
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 #[derive(Debug, Clone)]
40 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
41 pub struct Request {
42 pub user_id: OwnedUserId,
44
45 pub value: ProfileFieldValue,
47
48 #[cfg(feature = "unstable-msc4466")]
51 pub propagate_to: PropagateTo,
52 }
53
54 impl Request {
55 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]
157 #[derive(Default)]
158 pub struct Response {}
159
160 impl Response {
161 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 let avatar_url_request = Request::new(
196 owned_user_id!("@alice:localhost"),
197 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
198 );
199
200 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 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 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 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 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}