1pub mod v3 {
6 use ruma_common::{
17 OwnedUserId,
18 api::{auth_scheme::AccessToken, response},
19 metadata,
20 };
21
22 use crate::profile::ProfileFieldValue;
23
24 metadata! {
25 method: PUT,
26 rate_limited: true,
27 authentication: AccessToken,
28 history: {
30 unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}",
31 1.0 => "/_matrix/client/r0/profile/{user_id}/{field}",
32 1.1 => "/_matrix/client/v3/profile/{user_id}/{field}",
33 }
34 }
35
36 #[derive(Debug, Clone)]
38 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
39 pub struct Request {
40 pub user_id: OwnedUserId,
42
43 pub value: ProfileFieldValue,
45 }
46
47 impl Request {
48 pub fn new(user_id: OwnedUserId, value: ProfileFieldValue) -> Self {
50 Self { user_id, value }
51 }
52 }
53
54 #[cfg(feature = "client")]
55 impl ruma_common::api::OutgoingRequest for Request {
56 type EndpointError = crate::Error;
57 type IncomingResponse = Response;
58
59 fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
60 self,
61 base_url: &str,
62 access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
63 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
64 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
65 use ruma_common::api::{Metadata, auth_scheme::AuthScheme, path_builder::PathBuilder};
66
67 let field = self.value.field_name();
68
69 let url = if field.existed_before_extended_profiles() {
70 Self::make_endpoint_url(considering, base_url, &[&self.user_id, &field], "")?
71 } else {
72 crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
73 considering,
74 base_url,
75 &[&self.user_id, &field],
76 "",
77 )?
78 };
79
80 let mut http_request = http::Request::builder()
81 .method(Self::METHOD)
82 .uri(url)
83 .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
84 .body(ruma_common::serde::json_to_buf(&self.value)?)?;
85
86 Self::Authentication::add_authentication(&mut http_request, access_token).map_err(
87 |error| ruma_common::api::error::IntoHttpError::Authentication(error.into()),
88 )?;
89
90 Ok(http_request)
91 }
92 }
93
94 #[cfg(feature = "server")]
95 impl ruma_common::api::IncomingRequest for Request {
96 type EndpointError = crate::Error;
97 type OutgoingResponse = Response;
98
99 fn try_from_http_request<B, S>(
100 request: http::Request<B>,
101 path_args: &[S],
102 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
103 where
104 B: AsRef<[u8]>,
105 S: AsRef<str>,
106 {
107 use serde::de::{Deserializer, Error as _};
108
109 use crate::profile::{ProfileFieldName, profile_field_serde::ProfileFieldValueVisitor};
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(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(error = crate::Error)]
131 #[derive(Default)]
132 pub struct Response {}
133
134 impl Response {
135 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 };
151 use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
152
153 use super::v3::Request;
154 use crate::profile::ProfileFieldValue;
155
156 #[test]
157 fn serialize_request() {
158 let avatar_url_request = Request::new(
160 owned_user_id!("@alice:localhost"),
161 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
162 );
163
164 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 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 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 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 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_matches;
281 use ruma_common::api::IncomingRequest;
282 use serde_json::{json, to_vec as to_json_vec};
283
284 use super::v3::Request;
285 use crate::profile::ProfileFieldValue;
286
287 #[test]
288 fn deserialize_request_valid_field() {
289 let body = to_json_vec(&json!({
290 "displayname": "Alice",
291 }))
292 .unwrap();
293
294 let request = Request::try_from_http_request(
295 http::Request::put(
296 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
297 )
298 .body(body)
299 .unwrap(),
300 &["@alice:localhost", "displayname"],
301 )
302 .unwrap();
303
304 assert_eq!(request.user_id, "@alice:localhost");
305 assert_matches!(request.value, ProfileFieldValue::DisplayName(display_name));
306 assert_eq!(display_name, "Alice");
307 }
308
309 #[test]
310 fn deserialize_request_invalid_field() {
311 let body = to_json_vec(&json!({
312 "custom_field": "value",
313 }))
314 .unwrap();
315
316 Request::try_from_http_request(
317 http::Request::put(
318 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
319 )
320 .body(body)
321 .unwrap(),
322 &["@alice:localhost", "displayname"],
323 )
324 .unwrap_err();
325 }
326}