1pub mod v3 {
6 use ruma_common::{
17 OwnedUserId,
18 api::{Metadata, auth_scheme::AccessToken, response},
19 metadata,
20 };
21
22 use crate::profile::{ProfileFieldValue, profile_field_serde::ProfileFieldValueVisitor};
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::{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;
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(test)]
143mod tests {
144 use assert_matches2::assert_matches;
145 use ruma_common::{owned_mxc_uri, owned_user_id};
146 use serde_json::{
147 Value as JsonValue, from_slice as from_json_slice, json, to_vec as to_json_vec,
148 };
149
150 use super::v3::Request;
151 use crate::profile::ProfileFieldValue;
152
153 #[test]
154 #[cfg(feature = "client")]
155 fn serialize_request() {
156 use std::borrow::Cow;
157
158 use http::header;
159 use ruma_common::api::{OutgoingRequest, SupportedVersions, auth_scheme::SendAccessToken};
160
161 let avatar_url_request = Request::new(
163 owned_user_id!("@alice:localhost"),
164 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
165 );
166
167 let http_request = avatar_url_request
169 .clone()
170 .try_into_http_request::<Vec<u8>>(
171 "http://localhost/",
172 SendAccessToken::Always("access_token"),
173 Cow::Owned(SupportedVersions::from_parts(
174 &["v1.11".to_owned()],
175 &Default::default(),
176 )),
177 )
178 .unwrap();
179 assert_eq!(
180 http_request.uri().path(),
181 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
182 );
183 assert_eq!(
184 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
185 json!({
186 "avatar_url": "mxc://localhost/abcdef",
187 })
188 );
189 assert_eq!(
190 http_request.headers().get(header::AUTHORIZATION).unwrap(),
191 "Bearer access_token"
192 );
193
194 let http_request = avatar_url_request
196 .try_into_http_request::<Vec<u8>>(
197 "http://localhost/",
198 SendAccessToken::Always("access_token"),
199 Cow::Owned(SupportedVersions::from_parts(
200 &["v1.16".to_owned()],
201 &Default::default(),
202 )),
203 )
204 .unwrap();
205 assert_eq!(
206 http_request.uri().path(),
207 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
208 );
209 assert_eq!(
210 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
211 json!({
212 "avatar_url": "mxc://localhost/abcdef",
213 })
214 );
215 assert_eq!(
216 http_request.headers().get(header::AUTHORIZATION).unwrap(),
217 "Bearer access_token"
218 );
219
220 let custom_field_request = Request::new(
222 owned_user_id!("@alice:localhost"),
223 ProfileFieldValue::new("dev.ruma.custom_field", json!(true)).unwrap(),
224 );
225
226 let http_request = custom_field_request
228 .clone()
229 .try_into_http_request::<Vec<u8>>(
230 "http://localhost/",
231 SendAccessToken::Always("access_token"),
232 Cow::Owned(SupportedVersions::from_parts(
233 &["v1.11".to_owned()],
234 &Default::default(),
235 )),
236 )
237 .unwrap();
238 assert_eq!(
239 http_request.uri().path(),
240 "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
241 );
242 assert_eq!(
243 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
244 json!({
245 "dev.ruma.custom_field": true,
246 })
247 );
248 assert_eq!(
249 http_request.headers().get(header::AUTHORIZATION).unwrap(),
250 "Bearer access_token"
251 );
252
253 let http_request = custom_field_request
255 .try_into_http_request::<Vec<u8>>(
256 "http://localhost/",
257 SendAccessToken::Always("access_token"),
258 Cow::Owned(SupportedVersions::from_parts(
259 &["v1.16".to_owned()],
260 &Default::default(),
261 )),
262 )
263 .unwrap();
264 assert_eq!(
265 http_request.uri().path(),
266 "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
267 );
268 assert_eq!(
269 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
270 json!({
271 "dev.ruma.custom_field": true,
272 })
273 );
274 assert_eq!(
275 http_request.headers().get(header::AUTHORIZATION).unwrap(),
276 "Bearer access_token"
277 );
278 }
279
280 #[test]
281 #[cfg(feature = "server")]
282 fn deserialize_request_valid_field() {
283 use ruma_common::api::IncomingRequest;
284
285 let body = to_json_vec(&json!({
286 "displayname": "Alice",
287 }))
288 .unwrap();
289
290 let request = Request::try_from_http_request(
291 http::Request::put(
292 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
293 )
294 .body(body)
295 .unwrap(),
296 &["@alice:localhost", "displayname"],
297 )
298 .unwrap();
299
300 assert_eq!(request.user_id, "@alice:localhost");
301 assert_matches!(request.value, ProfileFieldValue::DisplayName(display_name));
302 assert_eq!(display_name, "Alice");
303 }
304
305 #[test]
306 #[cfg(feature = "server")]
307 fn deserialize_request_invalid_field() {
308 use ruma_common::api::IncomingRequest;
309
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}