1pub mod v3 {
6 use ruma_common::{
17 api::{auth_scheme::AccessToken, response, Metadata},
18 metadata, OwnedUserId,
19 };
20
21 use crate::profile::{profile_field_serde::ProfileFieldValueVisitor, ProfileFieldValue};
22
23 metadata! {
24 method: PUT,
25 rate_limited: true,
26 authentication: AccessToken,
27 history: {
29 unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}",
30 1.0 => "/_matrix/client/r0/profile/{user_id}/{field}",
31 1.1 => "/_matrix/client/v3/profile/{user_id}/{field}",
32 }
33 }
34
35 #[derive(Debug, Clone)]
37 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
38 pub struct Request {
39 pub user_id: OwnedUserId,
41
42 pub value: ProfileFieldValue,
44 }
45
46 impl Request {
47 pub fn new(user_id: OwnedUserId, value: ProfileFieldValue) -> Self {
49 Self { user_id, value }
50 }
51 }
52
53 #[cfg(feature = "client")]
54 impl ruma_common::api::OutgoingRequest for Request {
55 type EndpointError = crate::Error;
56 type IncomingResponse = Response;
57
58 fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
59 self,
60 base_url: &str,
61 access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
62 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
63 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
64 use ruma_common::api::{auth_scheme::AuthScheme, path_builder::PathBuilder};
65
66 let field = self.value.field_name();
67
68 let url = if field.existed_before_extended_profiles() {
69 Self::make_endpoint_url(considering, base_url, &[&self.user_id, &field], "")?
70 } else {
71 crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
72 considering,
73 base_url,
74 &[&self.user_id, &field],
75 "",
76 )?
77 };
78
79 let mut http_request = http::Request::builder()
80 .method(Self::METHOD)
81 .uri(url)
82 .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
83 .body(ruma_common::serde::json_to_buf(&self.value)?)?;
84
85 Self::Authentication::add_authentication(&mut http_request, access_token).map_err(
86 |error| ruma_common::api::error::IntoHttpError::Authentication(error.into()),
87 )?;
88
89 Ok(http_request)
90 }
91 }
92
93 #[cfg(feature = "server")]
94 impl ruma_common::api::IncomingRequest for Request {
95 type EndpointError = crate::Error;
96 type OutgoingResponse = Response;
97
98 fn try_from_http_request<B, S>(
99 request: http::Request<B>,
100 path_args: &[S],
101 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
102 where
103 B: AsRef<[u8]>,
104 S: AsRef<str>,
105 {
106 use serde::de::{Deserializer, Error as _};
107
108 use crate::profile::ProfileFieldName;
109
110 Self::check_request_method(request.method())?;
111
112 let (user_id, field): (OwnedUserId, ProfileFieldName) =
113 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
114 _,
115 serde::de::value::Error,
116 >::new(
117 path_args.iter().map(::std::convert::AsRef::as_ref),
118 ))?;
119
120 let value = serde_json::Deserializer::from_slice(request.body().as_ref())
121 .deserialize_map(ProfileFieldValueVisitor(Some(field.clone())))?
122 .ok_or_else(|| serde_json::Error::custom(format!("missing field `{field}`")))?;
123
124 Ok(Request { user_id, value })
125 }
126 }
127
128 #[response(error = crate::Error)]
130 #[derive(Default)]
131 pub struct Response {}
132
133 impl Response {
134 pub fn new() -> Self {
136 Self {}
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use assert_matches2::assert_matches;
144 use ruma_common::{owned_mxc_uri, owned_user_id};
145 use serde_json::{
146 from_slice as from_json_slice, json, to_vec as to_json_vec, Value as JsonValue,
147 };
148
149 use super::v3::Request;
150 use crate::profile::ProfileFieldValue;
151
152 #[test]
153 #[cfg(feature = "client")]
154 fn serialize_request() {
155 use std::borrow::Cow;
156
157 use http::header;
158 use ruma_common::api::{auth_scheme::SendAccessToken, OutgoingRequest, SupportedVersions};
159
160 let avatar_url_request = Request::new(
162 owned_user_id!("@alice:localhost"),
163 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
164 );
165
166 let http_request = avatar_url_request
168 .clone()
169 .try_into_http_request::<Vec<u8>>(
170 "http://localhost/",
171 SendAccessToken::Always("access_token"),
172 Cow::Owned(SupportedVersions::from_parts(
173 &["v1.11".to_owned()],
174 &Default::default(),
175 )),
176 )
177 .unwrap();
178 assert_eq!(
179 http_request.uri().path(),
180 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
181 );
182 assert_eq!(
183 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
184 json!({
185 "avatar_url": "mxc://localhost/abcdef",
186 })
187 );
188 assert_eq!(
189 http_request.headers().get(header::AUTHORIZATION).unwrap(),
190 "Bearer access_token"
191 );
192
193 let http_request = avatar_url_request
195 .try_into_http_request::<Vec<u8>>(
196 "http://localhost/",
197 SendAccessToken::Always("access_token"),
198 Cow::Owned(SupportedVersions::from_parts(
199 &["v1.16".to_owned()],
200 &Default::default(),
201 )),
202 )
203 .unwrap();
204 assert_eq!(
205 http_request.uri().path(),
206 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
207 );
208 assert_eq!(
209 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
210 json!({
211 "avatar_url": "mxc://localhost/abcdef",
212 })
213 );
214 assert_eq!(
215 http_request.headers().get(header::AUTHORIZATION).unwrap(),
216 "Bearer access_token"
217 );
218
219 let custom_field_request = Request::new(
221 owned_user_id!("@alice:localhost"),
222 ProfileFieldValue::new("dev.ruma.custom_field", json!(true)).unwrap(),
223 );
224
225 let http_request = custom_field_request
227 .clone()
228 .try_into_http_request::<Vec<u8>>(
229 "http://localhost/",
230 SendAccessToken::Always("access_token"),
231 Cow::Owned(SupportedVersions::from_parts(
232 &["v1.11".to_owned()],
233 &Default::default(),
234 )),
235 )
236 .unwrap();
237 assert_eq!(
238 http_request.uri().path(),
239 "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
240 );
241 assert_eq!(
242 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
243 json!({
244 "dev.ruma.custom_field": true,
245 })
246 );
247 assert_eq!(
248 http_request.headers().get(header::AUTHORIZATION).unwrap(),
249 "Bearer access_token"
250 );
251
252 let http_request = custom_field_request
254 .try_into_http_request::<Vec<u8>>(
255 "http://localhost/",
256 SendAccessToken::Always("access_token"),
257 Cow::Owned(SupportedVersions::from_parts(
258 &["v1.16".to_owned()],
259 &Default::default(),
260 )),
261 )
262 .unwrap();
263 assert_eq!(
264 http_request.uri().path(),
265 "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
266 );
267 assert_eq!(
268 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
269 json!({
270 "dev.ruma.custom_field": true,
271 })
272 );
273 assert_eq!(
274 http_request.headers().get(header::AUTHORIZATION).unwrap(),
275 "Bearer access_token"
276 );
277 }
278
279 #[test]
280 #[cfg(feature = "server")]
281 fn deserialize_request_valid_field() {
282 use ruma_common::api::IncomingRequest;
283
284 let body = to_json_vec(&json!({
285 "displayname": "Alice",
286 }))
287 .unwrap();
288
289 let request = Request::try_from_http_request(
290 http::Request::put(
291 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
292 )
293 .body(body)
294 .unwrap(),
295 &["@alice:localhost", "displayname"],
296 )
297 .unwrap();
298
299 assert_eq!(request.user_id, "@alice:localhost");
300 assert_matches!(request.value, ProfileFieldValue::DisplayName(display_name));
301 assert_eq!(display_name, "Alice");
302 }
303
304 #[test]
305 #[cfg(feature = "server")]
306 fn deserialize_request_invalid_field() {
307 use ruma_common::api::IncomingRequest;
308
309 let body = to_json_vec(&json!({
310 "custom_field": "value",
311 }))
312 .unwrap();
313
314 Request::try_from_http_request(
315 http::Request::put(
316 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
317 )
318 .body(body)
319 .unwrap(),
320 &["@alice:localhost", "displayname"],
321 )
322 .unwrap_err();
323 }
324}