1pub mod v3 {
6 use std::marker::PhantomData;
17
18 use ruma_common::{
19 api::{auth_scheme::NoAuthentication, path_builder::VersionHistory, Metadata},
20 metadata, OwnedUserId,
21 };
22
23 use crate::profile::{
24 profile_field_serde::StaticProfileFieldVisitor, ProfileFieldName, ProfileFieldValue,
25 StaticProfileField,
26 };
27
28 metadata! {
29 method: GET,
30 rate_limited: false,
31 authentication: NoAuthentication,
32 history: {
34 unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}",
35 1.0 => "/_matrix/client/r0/profile/{user_id}/{field}",
36 1.1 => "/_matrix/client/v3/profile/{user_id}/{field}",
37 }
38 }
39
40 #[derive(Clone, Debug)]
42 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
43 pub struct Request {
44 pub user_id: OwnedUserId,
46
47 pub field: ProfileFieldName,
49 }
50
51 impl Request {
52 pub fn new(user_id: OwnedUserId, field: ProfileFieldName) -> Self {
54 Self { user_id, field }
55 }
56
57 pub fn new_static<F: StaticProfileField>(user_id: OwnedUserId) -> RequestStatic<F> {
59 RequestStatic::new(user_id)
60 }
61 }
62
63 #[cfg(feature = "client")]
64 impl ruma_common::api::OutgoingRequest for Request {
65 type EndpointError = crate::Error;
66 type IncomingResponse = Response;
67
68 fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
69 self,
70 base_url: &str,
71 access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
72 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
73 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
74 use ruma_common::api::{auth_scheme::AuthScheme, path_builder::PathBuilder};
75
76 let url = if self.field.existed_before_extended_profiles() {
77 Self::make_endpoint_url(considering, base_url, &[&self.user_id, &self.field], "")?
78 } else {
79 crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
80 considering,
81 base_url,
82 &[&self.user_id, &self.field],
83 "",
84 )?
85 };
86
87 let mut http_request =
88 http::Request::builder().method(Self::METHOD).uri(url).body(T::default())?;
89
90 Self::Authentication::add_authentication(&mut http_request, access_token)?;
91
92 Ok(http_request)
93 }
94 }
95
96 #[cfg(feature = "server")]
97 impl ruma_common::api::IncomingRequest for Request {
98 type EndpointError = crate::Error;
99 type OutgoingResponse = Response;
100
101 fn try_from_http_request<B, S>(
102 request: http::Request<B>,
103 path_args: &[S],
104 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
105 where
106 B: AsRef<[u8]>,
107 S: AsRef<str>,
108 {
109 Self::check_request_method(request.method())?;
110
111 let (user_id, field) =
112 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
113 _,
114 serde::de::value::Error,
115 >::new(
116 path_args.iter().map(::std::convert::AsRef::as_ref),
117 ))?;
118
119 Ok(Self { user_id, field })
120 }
121 }
122
123 #[derive(Debug)]
125 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
126 pub struct RequestStatic<F: StaticProfileField> {
127 pub user_id: OwnedUserId,
129
130 field: PhantomData<F>,
132 }
133
134 impl<F: StaticProfileField> RequestStatic<F> {
135 pub fn new(user_id: OwnedUserId) -> Self {
137 Self { user_id, field: PhantomData }
138 }
139 }
140
141 impl<F: StaticProfileField> Clone for RequestStatic<F> {
142 fn clone(&self) -> Self {
143 Self { user_id: self.user_id.clone(), field: self.field }
144 }
145 }
146
147 impl<F: StaticProfileField> Metadata for RequestStatic<F> {
148 const METHOD: http::Method = Request::METHOD;
149 const RATE_LIMITED: bool = Request::RATE_LIMITED;
150 type Authentication = <Request as Metadata>::Authentication;
151 type PathBuilder = <Request as Metadata>::PathBuilder;
152 const PATH_BUILDER: VersionHistory = Request::PATH_BUILDER;
153 }
154
155 #[cfg(feature = "client")]
156 impl<F: StaticProfileField> ruma_common::api::OutgoingRequest for RequestStatic<F> {
157 type EndpointError = crate::Error;
158 type IncomingResponse = ResponseStatic<F>;
159
160 fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
161 self,
162 base_url: &str,
163 access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
164 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
165 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
166 Request::new(self.user_id, F::NAME.into()).try_into_http_request(
167 base_url,
168 access_token,
169 considering,
170 )
171 }
172 }
173
174 #[derive(Debug, Clone, Default)]
176 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
177 pub struct Response {
178 pub value: Option<ProfileFieldValue>,
180 }
181
182 impl Response {
183 pub fn new(value: ProfileFieldValue) -> Self {
185 Self { value: Some(value) }
186 }
187 }
188
189 #[cfg(feature = "client")]
190 impl ruma_common::api::IncomingResponse for Response {
191 type EndpointError = crate::Error;
192
193 fn try_from_http_response<T: AsRef<[u8]>>(
194 response: http::Response<T>,
195 ) -> Result<Self, ruma_common::api::error::FromHttpResponseError<Self::EndpointError>>
196 {
197 use ruma_common::api::EndpointError;
198
199 use crate::profile::profile_field_serde::deserialize_profile_field_value_option;
200
201 if response.status().as_u16() >= 400 {
202 return Err(ruma_common::api::error::FromHttpResponseError::Server(
203 Self::EndpointError::from_http_response(response),
204 ));
205 }
206
207 let mut de = serde_json::Deserializer::from_slice(response.body().as_ref());
208 let value = deserialize_profile_field_value_option(&mut de)?;
209 de.end()?;
210
211 Ok(Self { value })
212 }
213 }
214
215 #[cfg(feature = "server")]
216 impl ruma_common::api::OutgoingResponse for Response {
217 fn try_into_http_response<T: Default + bytes::BufMut>(
218 self,
219 ) -> Result<http::Response<T>, ruma_common::api::error::IntoHttpError> {
220 use ruma_common::serde::JsonObject;
221
222 let body = self
223 .value
224 .as_ref()
225 .map(|value| ruma_common::serde::json_to_buf(value))
226 .unwrap_or_else(||
227 ruma_common::serde::json_to_buf(&JsonObject::new()))?;
229
230 Ok(http::Response::builder()
231 .status(http::StatusCode::OK)
232 .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
233 .body(body)?)
234 }
235 }
236
237 #[derive(Debug)]
239 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
240 pub struct ResponseStatic<F: StaticProfileField> {
241 pub value: Option<F::Value>,
243 }
244
245 impl<F: StaticProfileField> Clone for ResponseStatic<F>
246 where
247 F::Value: Clone,
248 {
249 fn clone(&self) -> Self {
250 Self { value: self.value.clone() }
251 }
252 }
253
254 #[cfg(feature = "client")]
255 impl<F: StaticProfileField> ruma_common::api::IncomingResponse for ResponseStatic<F> {
256 type EndpointError = crate::Error;
257
258 fn try_from_http_response<T: AsRef<[u8]>>(
259 response: http::Response<T>,
260 ) -> Result<Self, ruma_common::api::error::FromHttpResponseError<Self::EndpointError>>
261 {
262 use ruma_common::api::EndpointError;
263 use serde::de::Deserializer;
264
265 if response.status().as_u16() >= 400 {
266 return Err(ruma_common::api::error::FromHttpResponseError::Server(
267 Self::EndpointError::from_http_response(response),
268 ));
269 }
270
271 let value = serde_json::Deserializer::from_slice(response.into_body().as_ref())
272 .deserialize_map(StaticProfileFieldVisitor(PhantomData::<F>))?;
273
274 Ok(Self { value })
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use ruma_common::{owned_mxc_uri, owned_user_id};
282 use serde_json::{
283 from_slice as from_json_slice, json, to_vec as to_json_vec, Value as JsonValue,
284 };
285
286 use super::v3::{Request, RequestStatic, Response};
287 use crate::profile::{ProfileFieldName, ProfileFieldValue};
288
289 #[test]
290 #[cfg(feature = "client")]
291 fn serialize_request() {
292 use std::borrow::Cow;
293
294 use ruma_common::api::{auth_scheme::SendAccessToken, OutgoingRequest, SupportedVersions};
295
296 let avatar_url_request =
298 Request::new(owned_user_id!("@alice:localhost"), ProfileFieldName::AvatarUrl);
299
300 let http_request = avatar_url_request
302 .clone()
303 .try_into_http_request::<Vec<u8>>(
304 "http://localhost/",
305 SendAccessToken::None,
306 Cow::Owned(SupportedVersions::from_parts(
307 &["v1.11".to_owned()],
308 &Default::default(),
309 )),
310 )
311 .unwrap();
312 assert_eq!(
313 http_request.uri().path(),
314 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
315 );
316
317 let http_request = avatar_url_request
319 .try_into_http_request::<Vec<u8>>(
320 "http://localhost/",
321 SendAccessToken::None,
322 Cow::Owned(SupportedVersions::from_parts(
323 &["v1.16".to_owned()],
324 &Default::default(),
325 )),
326 )
327 .unwrap();
328 assert_eq!(
329 http_request.uri().path(),
330 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
331 );
332
333 let custom_field_request =
335 Request::new(owned_user_id!("@alice:localhost"), "dev.ruma.custom_field".into());
336
337 let http_request = custom_field_request
339 .clone()
340 .try_into_http_request::<Vec<u8>>(
341 "http://localhost/",
342 SendAccessToken::None,
343 Cow::Owned(SupportedVersions::from_parts(
344 &["v1.11".to_owned()],
345 &Default::default(),
346 )),
347 )
348 .unwrap();
349 assert_eq!(
350 http_request.uri().path(),
351 "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
352 );
353
354 let http_request = custom_field_request
356 .try_into_http_request::<Vec<u8>>(
357 "http://localhost/",
358 SendAccessToken::None,
359 Cow::Owned(SupportedVersions::from_parts(
360 &["v1.16".to_owned()],
361 &Default::default(),
362 )),
363 )
364 .unwrap();
365 assert_eq!(
366 http_request.uri().path(),
367 "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
368 );
369 }
370
371 #[test]
372 #[cfg(feature = "server")]
373 fn deserialize_request() {
374 use ruma_common::api::IncomingRequest;
375
376 let request = Request::try_from_http_request(
377 http::Request::get(
378 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
379 )
380 .body(Vec::<u8>::new())
381 .unwrap(),
382 &["@alice:localhost", "displayname"],
383 )
384 .unwrap();
385
386 assert_eq!(request.user_id, "@alice:localhost");
387 assert_eq!(request.field, ProfileFieldName::DisplayName);
388 }
389
390 #[test]
391 #[cfg(feature = "server")]
392 fn serialize_response() {
393 use ruma_common::api::OutgoingResponse;
394
395 let response =
396 Response::new(ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")));
397
398 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
399
400 assert_eq!(
401 from_json_slice::<JsonValue>(http_response.body().as_ref()).unwrap(),
402 json!({
403 "avatar_url": "mxc://localhost/abcdef",
404 })
405 );
406 }
407
408 #[test]
409 #[cfg(feature = "client")]
410 fn deserialize_response() {
411 use ruma_common::api::IncomingResponse;
412
413 let body = to_json_vec(&json!({
414 "custom_field": "value",
415 }))
416 .unwrap();
417
418 let response = Response::try_from_http_response(http::Response::new(body)).unwrap();
419 let value = response.value.unwrap();
420 assert_eq!(value.field_name().as_str(), "custom_field");
421 assert_eq!(value.value().as_str().unwrap(), "value");
422
423 let empty_body = to_json_vec(&json!({})).unwrap();
424
425 let response = Response::try_from_http_response(http::Response::new(empty_body)).unwrap();
426 assert!(response.value.is_none());
427 }
428
429 #[cfg(feature = "client")]
432 fn get_static_response<R: ruma_common::api::OutgoingRequest>(
433 value: Option<ProfileFieldValue>,
434 ) -> Result<R::IncomingResponse, ruma_common::api::error::FromHttpResponseError<R::EndpointError>>
435 {
436 use ruma_common::api::IncomingResponse;
437
438 let body =
439 value.map(|value| to_json_vec(&value).unwrap()).unwrap_or_else(|| b"{}".to_vec());
440 R::IncomingResponse::try_from_http_response(http::Response::new(body))
441 }
442
443 #[test]
444 #[cfg(feature = "client")]
445 fn static_request_and_valid_response() {
446 use crate::profile::AvatarUrl;
447
448 let response = get_static_response::<RequestStatic<AvatarUrl>>(Some(
449 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
450 ))
451 .unwrap();
452 assert_eq!(response.value.unwrap(), "mxc://localhost/abcdef");
453
454 let response = get_static_response::<RequestStatic<AvatarUrl>>(None).unwrap();
455 assert!(response.value.is_none());
456 }
457
458 #[test]
459 #[cfg(feature = "client")]
460 fn static_request_and_invalid_response() {
461 use crate::profile::AvatarUrl;
462
463 get_static_response::<RequestStatic<AvatarUrl>>(Some(ProfileFieldValue::DisplayName(
464 "Alice".to_owned(),
465 )))
466 .unwrap_err();
467 }
468}