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 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 = 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::{Metadata, auth_scheme::AuthScheme, path_builder::PathBuilder};
65
66 use crate::profile::field_existed_before_extended_profiles;
67
68 let field = self.value.field_name();
69
70 let url = if field_existed_before_extended_profiles(&field) {
71 Self::make_endpoint_url(considering, base_url, &[&self.user_id, &field], "")?
72 } else {
73 crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
74 considering,
75 base_url,
76 &[&self.user_id, &field],
77 "",
78 )?
79 };
80
81 let mut http_request = http::Request::builder()
82 .method(Self::METHOD)
83 .uri(url)
84 .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
85 .body(ruma_common::serde::json_to_buf(&self.value)?)?;
86
87 Self::Authentication::add_authentication(&mut http_request, access_token).map_err(
88 |error| ruma_common::api::error::IntoHttpError::Authentication(error.into()),
89 )?;
90
91 Ok(http_request)
92 }
93 }
94
95 #[cfg(feature = "server")]
96 impl ruma_common::api::IncomingRequest for Request {
97 type EndpointError = Error;
98 type OutgoingResponse = Response;
99
100 fn try_from_http_request<B, S>(
101 request: http::Request<B>,
102 path_args: &[S],
103 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
104 where
105 B: AsRef<[u8]>,
106 S: AsRef<str>,
107 {
108 use ruma_common::profile::{ProfileFieldName, ProfileFieldValueVisitor};
109 use serde::de::{Deserializer, Error as _};
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::new(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]
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 profile::ProfileFieldValue,
151 };
152 use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
153
154 use super::v3::Request;
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_let;
281 use ruma_common::{api::IncomingRequest, profile::ProfileFieldValue};
282 use serde_json::{json, to_vec as to_json_vec};
283
284 use super::v3::Request;
285
286 #[test]
287 fn deserialize_request_valid_field() {
288 let body = to_json_vec(&json!({
289 "displayname": "Alice",
290 }))
291 .unwrap();
292
293 let request = Request::try_from_http_request(
294 http::Request::put(
295 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
296 )
297 .body(body)
298 .unwrap(),
299 &["@alice:localhost", "displayname"],
300 )
301 .unwrap();
302
303 assert_eq!(request.user_id, "@alice:localhost");
304 assert_let!(ProfileFieldValue::DisplayName(display_name) = request.value);
305 assert_eq!(display_name, "Alice");
306 }
307
308 #[test]
309 fn deserialize_request_invalid_field() {
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}