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 #[derive(ruma_common::serde::_FakeDeriveSerde)]
68 #[cfg_attr(feature = "client", derive(serde::Serialize, ruma_common::api::OutgoingBodyJson))]
69 #[cfg_attr(feature = "server", derive(serde::Deserialize))]
70 #[serde(transparent)]
71 #[cfg_attr(not(feature = "client"), expect(dead_code))]
73 pub struct RequestBody(ProfileFieldValue);
74
75 #[cfg(feature = "client")]
76 impl ruma_common::api::OutgoingRequest for Request {
77 type Body = RequestBody;
78 type EndpointError = Error;
79 type IncomingResponse = Response;
80
81 fn try_into_http_request_inner(
82 self,
83 base_url: &str,
84 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
85 ) -> Result<http::Request<RequestBody>, ruma_common::api::error::IntoHttpError> {
86 use ruma_common::api::{Metadata, path_builder::PathBuilder};
87
88 use crate::profile::field_existed_before_extended_profiles;
89
90 let field = self.value.field_name();
91
92 let query_string = serde_html_form::to_string(RequestQuery {
93 #[cfg(feature = "unstable-msc4466")]
94 propagate_to: self.propagate_to,
95 })?;
96
97 let url = if field_existed_before_extended_profiles(&field) {
98 Self::make_endpoint_url(
99 considering,
100 base_url,
101 &[&self.user_id, &field],
102 &query_string,
103 )?
104 } else {
105 crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
106 considering,
107 base_url,
108 &[&self.user_id, &field],
109 &query_string,
110 )?
111 };
112
113 let http_request = http::Request::builder()
114 .method(Self::METHOD)
115 .uri(url)
116 .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
117 .body(RequestBody(self.value))?;
118
119 Ok(http_request)
120 }
121 }
122
123 #[cfg(feature = "server")]
124 impl ruma_common::api::IncomingRequest for Request {
125 type EndpointError = Error;
126 type OutgoingResponse = Response;
127
128 fn try_from_http_request<B, S>(
129 request: http::Request<B>,
130 path_args: &[S],
131 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
132 where
133 B: AsRef<[u8]>,
134 S: AsRef<str>,
135 {
136 use ruma_common::profile::{ProfileFieldName, ProfileFieldValueVisitor};
137 use serde::de::{Deserializer, Error as _};
138
139 Self::check_request_method(request.method())?;
140
141 let (user_id, field): (OwnedUserId, ProfileFieldName) =
142 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
143 _,
144 serde::de::value::Error,
145 >::new(
146 path_args.iter().map(::std::convert::AsRef::as_ref),
147 ))?;
148
149 let value = serde_json::Deserializer::from_slice(request.body().as_ref())
150 .deserialize_map(ProfileFieldValueVisitor::new(Some(field.clone())))?
151 .ok_or_else(|| serde_json::Error::custom(format!("missing field `{field}`")))?;
152
153 let RequestQuery {
154 #[cfg(feature = "unstable-msc4466")]
155 propagate_to,
156 } = serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
157
158 Ok(Request {
159 user_id,
160 value,
161 #[cfg(feature = "unstable-msc4466")]
162 propagate_to,
163 })
164 }
165 }
166
167 #[response]
169 #[derive(Default)]
170 pub struct Response {}
171
172 impl Response {
173 pub fn new() -> Self {
175 Self {}
176 }
177 }
178
179 #[derive(Debug)]
180 #[cfg_attr(feature = "client", derive(serde::Serialize))]
181 #[cfg_attr(feature = "server", derive(serde::Deserialize))]
182 struct RequestQuery {
183 #[cfg(feature = "unstable-msc4466")]
184 #[serde(rename = "computer.gingershaped.msc4466.propagate_to")]
185 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
186 propagate_to: PropagateTo,
187 }
188}
189
190#[cfg(all(test, feature = "client"))]
191mod tests_client {
192 use std::borrow::Cow;
193
194 use http::header;
195 use ruma_common::{
196 api::{OutgoingRequestExt as _, SupportedVersions, auth_scheme::SendAccessToken},
197 owned_mxc_uri, owned_user_id,
198 profile::ProfileFieldValue,
199 };
200 use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
201
202 use super::v3::Request;
203
204 #[test]
205 fn serialize_request() {
206 let avatar_url_request = Request::new(
208 owned_user_id!("@alice:localhost"),
209 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
210 );
211
212 let http_request = avatar_url_request
214 .clone()
215 .try_into_http_request::<Vec<u8>>(
216 "http://localhost/",
217 SendAccessToken::Always("access_token"),
218 Cow::Owned(SupportedVersions::from_parts(
219 &["v1.11".to_owned()],
220 &Default::default(),
221 )),
222 )
223 .unwrap();
224 assert_eq!(
225 http_request.uri().path(),
226 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
227 );
228 assert_eq!(
229 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
230 json!({
231 "avatar_url": "mxc://localhost/abcdef",
232 })
233 );
234 assert_eq!(
235 http_request.headers().get(header::AUTHORIZATION).unwrap(),
236 "Bearer access_token"
237 );
238
239 let http_request = avatar_url_request
241 .try_into_http_request::<Vec<u8>>(
242 "http://localhost/",
243 SendAccessToken::Always("access_token"),
244 Cow::Owned(SupportedVersions::from_parts(
245 &["v1.16".to_owned()],
246 &Default::default(),
247 )),
248 )
249 .unwrap();
250 assert_eq!(
251 http_request.uri().path(),
252 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
253 );
254 assert_eq!(
255 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
256 json!({
257 "avatar_url": "mxc://localhost/abcdef",
258 })
259 );
260 assert_eq!(
261 http_request.headers().get(header::AUTHORIZATION).unwrap(),
262 "Bearer access_token"
263 );
264
265 let custom_field_request = Request::new(
267 owned_user_id!("@alice:localhost"),
268 ProfileFieldValue::new("dev.ruma.custom_field", json!(true)).unwrap(),
269 );
270
271 let http_request = custom_field_request
273 .clone()
274 .try_into_http_request::<Vec<u8>>(
275 "http://localhost/",
276 SendAccessToken::Always("access_token"),
277 Cow::Owned(SupportedVersions::from_parts(
278 &["v1.11".to_owned()],
279 &Default::default(),
280 )),
281 )
282 .unwrap();
283 assert_eq!(
284 http_request.uri().path(),
285 "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
286 );
287 assert_eq!(
288 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
289 json!({
290 "dev.ruma.custom_field": true,
291 })
292 );
293 assert_eq!(
294 http_request.headers().get(header::AUTHORIZATION).unwrap(),
295 "Bearer access_token"
296 );
297
298 let http_request = custom_field_request
300 .try_into_http_request::<Vec<u8>>(
301 "http://localhost/",
302 SendAccessToken::Always("access_token"),
303 Cow::Owned(SupportedVersions::from_parts(
304 &["v1.16".to_owned()],
305 &Default::default(),
306 )),
307 )
308 .unwrap();
309 assert_eq!(
310 http_request.uri().path(),
311 "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
312 );
313 assert_eq!(
314 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
315 json!({
316 "dev.ruma.custom_field": true,
317 })
318 );
319 assert_eq!(
320 http_request.headers().get(header::AUTHORIZATION).unwrap(),
321 "Bearer access_token"
322 );
323 }
324}
325
326#[cfg(all(test, feature = "server"))]
327mod tests_server {
328 use assert_matches2::assert_let;
329 use ruma_common::{api::IncomingRequest, profile::ProfileFieldValue};
330 use serde_json::{json, to_vec as to_json_vec};
331
332 use super::v3::Request;
333
334 #[test]
335 fn deserialize_request_valid_field() {
336 let body = to_json_vec(&json!({
337 "displayname": "Alice",
338 }))
339 .unwrap();
340
341 let request = Request::try_from_http_request(
342 http::Request::put(
343 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
344 )
345 .body(body)
346 .unwrap(),
347 &["@alice:localhost", "displayname"],
348 )
349 .unwrap();
350
351 assert_eq!(request.user_id, "@alice:localhost");
352 assert_let!(ProfileFieldValue::DisplayName(display_name) = request.value);
353 assert_eq!(display_name, "Alice");
354 }
355
356 #[test]
357 fn deserialize_request_invalid_field() {
358 let body = to_json_vec(&json!({
359 "custom_field": "value",
360 }))
361 .unwrap();
362
363 Request::try_from_http_request(
364 http::Request::put(
365 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
366 )
367 .body(body)
368 .unwrap(),
369 &["@alice:localhost", "displayname"],
370 )
371 .unwrap_err();
372 }
373}