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