1pub mod v3 {
6 use ruma_common::{
17 api::{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>(
59 self,
60 base_url: &str,
61 access_token: ruma_common::api::SendAccessToken<'_>,
62 considering: &'_ ruma_common::api::SupportedVersions,
63 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
64 use http::{header, HeaderValue};
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 http_request = http::Request::builder()
80 .method(Self::METHOD)
81 .uri(url)
82 .header(header::CONTENT_TYPE, "application/json")
83 .header(
84 header::AUTHORIZATION,
85 HeaderValue::from_str(&format!(
86 "Bearer {}",
87 access_token
88 .get_required_for_endpoint()
89 .ok_or(ruma_common::api::error::IntoHttpError::NeedsAuthentication)?
90 ))?,
91 )
92 .body(ruma_common::serde::json_to_buf(&self.value)?)
93 .unwrap();
96
97 Ok(http_request)
98 }
99 }
100
101 #[cfg(feature = "server")]
102 impl ruma_common::api::IncomingRequest for Request {
103 type EndpointError = crate::Error;
104 type OutgoingResponse = Response;
105
106 fn try_from_http_request<B, S>(
107 request: http::Request<B>,
108 path_args: &[S],
109 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
110 where
111 B: AsRef<[u8]>,
112 S: AsRef<str>,
113 {
114 use serde::de::{Deserializer, Error as _};
115
116 use crate::profile::ProfileFieldName;
117
118 let (user_id, field): (OwnedUserId, ProfileFieldName) =
119 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
120 _,
121 serde::de::value::Error,
122 >::new(
123 path_args.iter().map(::std::convert::AsRef::as_ref),
124 ))?;
125
126 let value = serde_json::Deserializer::from_slice(request.body().as_ref())
127 .deserialize_map(ProfileFieldValueVisitor(Some(field.clone())))?
128 .ok_or_else(|| serde_json::Error::custom(format!("missing field `{field}`")))?;
129
130 Ok(Request { user_id, value })
131 }
132 }
133
134 #[response(error = crate::Error)]
136 #[derive(Default)]
137 pub struct Response {}
138
139 impl Response {
140 pub fn new() -> Self {
142 Self {}
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use assert_matches2::assert_matches;
150 use ruma_common::{owned_mxc_uri, owned_user_id};
151 use serde_json::{
152 from_slice as from_json_slice, json, to_vec as to_json_vec, Value as JsonValue,
153 };
154
155 use super::v3::Request;
156 use crate::profile::ProfileFieldValue;
157
158 #[test]
159 #[cfg(feature = "client")]
160 fn serialize_request() {
161 use http::header;
162 use ruma_common::api::{OutgoingRequest, SendAccessToken, SupportedVersions};
163
164 let avatar_url_request = Request::new(
166 owned_user_id!("@alice:localhost"),
167 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
168 );
169
170 let http_request = avatar_url_request
172 .clone()
173 .try_into_http_request::<Vec<u8>>(
174 "http://localhost/",
175 SendAccessToken::Always("access_token"),
176 &SupportedVersions::from_parts(&["v1.11".to_owned()], &Default::default()),
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 &SupportedVersions::from_parts(&["v1.16".to_owned()], &Default::default()),
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 &SupportedVersions::from_parts(&["v1.11".to_owned()], &Default::default()),
230 )
231 .unwrap();
232 assert_eq!(
233 http_request.uri().path(),
234 "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
235 );
236 assert_eq!(
237 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
238 json!({
239 "dev.ruma.custom_field": true,
240 })
241 );
242 assert_eq!(
243 http_request.headers().get(header::AUTHORIZATION).unwrap(),
244 "Bearer access_token"
245 );
246
247 let http_request = custom_field_request
249 .try_into_http_request::<Vec<u8>>(
250 "http://localhost/",
251 SendAccessToken::Always("access_token"),
252 &SupportedVersions::from_parts(&["v1.16".to_owned()], &Default::default()),
253 )
254 .unwrap();
255 assert_eq!(
256 http_request.uri().path(),
257 "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
258 );
259 assert_eq!(
260 from_json_slice::<JsonValue>(http_request.body().as_ref()).unwrap(),
261 json!({
262 "dev.ruma.custom_field": true,
263 })
264 );
265 assert_eq!(
266 http_request.headers().get(header::AUTHORIZATION).unwrap(),
267 "Bearer access_token"
268 );
269 }
270
271 #[test]
272 #[cfg(feature = "server")]
273 fn deserialize_request_valid_field() {
274 use ruma_common::api::IncomingRequest;
275
276 let body = to_json_vec(&json!({
277 "displayname": "Alice",
278 }))
279 .unwrap();
280
281 let request = Request::try_from_http_request(
282 http::Request::put(
283 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
284 )
285 .body(body)
286 .unwrap(),
287 &["@alice:localhost", "displayname"],
288 )
289 .unwrap();
290
291 assert_eq!(request.user_id, "@alice:localhost");
292 assert_matches!(request.value, ProfileFieldValue::DisplayName(display_name));
293 assert_eq!(display_name, "Alice");
294 }
295
296 #[test]
297 #[cfg(feature = "server")]
298 fn deserialize_request_invalid_field() {
299 use ruma_common::api::IncomingRequest;
300
301 let body = to_json_vec(&json!({
302 "custom_field": "value",
303 }))
304 .unwrap();
305
306 Request::try_from_http_request(
307 http::Request::put(
308 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
309 )
310 .body(body)
311 .unwrap(),
312 &["@alice:localhost", "displayname"],
313 )
314 .unwrap_err();
315 }
316}