1pub mod v3 {
6 use std::marker::PhantomData;
17
18 use ruma_common::{
19 OwnedUserId,
20 api::{Metadata, auth_scheme::NoAccessToken, error::Error, path_builder::VersionHistory},
21 metadata,
22 profile::{ProfileFieldName, ProfileFieldValue},
23 };
24
25 use crate::profile::StaticProfileField;
26
27 metadata! {
28 method: GET,
29 rate_limited: false,
30 authentication: NoAccessToken,
31 history: {
33 unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}",
34 1.0 => "/_matrix/client/r0/profile/{user_id}/{field}",
35 1.1 => "/_matrix/client/v3/profile/{user_id}/{field}",
36 }
37 }
38
39 #[derive(Clone, Debug)]
41 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
42 pub struct Request {
43 pub user_id: OwnedUserId,
45
46 pub field: ProfileFieldName,
48 }
49
50 impl Request {
51 pub fn new(user_id: OwnedUserId, field: ProfileFieldName) -> Self {
53 Self { user_id, field }
54 }
55
56 pub fn new_static<F: StaticProfileField>(user_id: OwnedUserId) -> RequestStatic<F> {
58 RequestStatic::new(user_id)
59 }
60 }
61
62 #[cfg(feature = "client")]
63 impl ruma_common::api::OutgoingRequest for Request {
64 type EndpointError = Error;
65 type IncomingResponse = Response;
66
67 fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
68 self,
69 base_url: &str,
70 access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
71 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
72 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
73 use ruma_common::api::{auth_scheme::AuthScheme, path_builder::PathBuilder};
74
75 use crate::profile::field_existed_before_extended_profiles;
76
77 let url = if field_existed_before_extended_profiles(&self.field) {
78 Self::make_endpoint_url(considering, base_url, &[&self.user_id, &self.field], "")?
79 } else {
80 crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
81 considering,
82 base_url,
83 &[&self.user_id, &self.field],
84 "",
85 )?
86 };
87
88 let mut http_request =
89 http::Request::builder().method(Self::METHOD).uri(url).body(T::default())?;
90
91 Self::Authentication::add_authentication(&mut http_request, access_token)?;
92
93 Ok(http_request)
94 }
95 }
96
97 #[cfg(feature = "server")]
98 impl ruma_common::api::IncomingRequest for Request {
99 type EndpointError = Error;
100 type OutgoingResponse = Response;
101
102 fn try_from_http_request<B, S>(
103 request: http::Request<B>,
104 path_args: &[S],
105 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
106 where
107 B: AsRef<[u8]>,
108 S: AsRef<str>,
109 {
110 Self::check_request_method(request.method())?;
111
112 let (user_id, field) =
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 Ok(Self { user_id, field })
121 }
122 }
123
124 #[derive(Debug)]
126 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
127 pub struct RequestStatic<F: StaticProfileField> {
128 pub user_id: OwnedUserId,
130
131 field: PhantomData<F>,
133 }
134
135 impl<F: StaticProfileField> RequestStatic<F> {
136 pub fn new(user_id: OwnedUserId) -> Self {
138 Self { user_id, field: PhantomData }
139 }
140 }
141
142 impl<F: StaticProfileField> Clone for RequestStatic<F> {
143 fn clone(&self) -> Self {
144 Self { user_id: self.user_id.clone(), field: self.field }
145 }
146 }
147
148 impl<F: StaticProfileField> Metadata for RequestStatic<F> {
149 const METHOD: http::Method = Request::METHOD;
150 const RATE_LIMITED: bool = Request::RATE_LIMITED;
151 type Authentication = <Request as Metadata>::Authentication;
152 type PathBuilder = <Request as Metadata>::PathBuilder;
153 const PATH_BUILDER: VersionHistory = Request::PATH_BUILDER;
154 }
155
156 #[cfg(feature = "client")]
157 impl<F: StaticProfileField> ruma_common::api::OutgoingRequest for RequestStatic<F> {
158 type EndpointError = Error;
159 type IncomingResponse = ResponseStatic<F>;
160
161 fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
162 self,
163 base_url: &str,
164 access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
165 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
166 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
167 Request::new(self.user_id, F::NAME.into()).try_into_http_request(
168 base_url,
169 access_token,
170 considering,
171 )
172 }
173 }
174
175 #[derive(Debug, Clone, Default)]
177 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
178 pub struct Response {
179 pub value: Option<ProfileFieldValue>,
181 }
182
183 impl Response {
184 pub fn new(value: ProfileFieldValue) -> Self {
186 Self { value: Some(value) }
187 }
188 }
189
190 #[cfg(feature = "client")]
191 impl ruma_common::api::IncomingResponse for Response {
192 type EndpointError = Error;
193
194 fn try_from_http_response<T: AsRef<[u8]>>(
195 response: http::Response<T>,
196 ) -> Result<Self, ruma_common::api::error::FromHttpResponseError<Self::EndpointError>>
197 {
198 use ruma_common::{api::EndpointError, profile::ProfileFieldValueVisitor};
199 use serde::Deserializer;
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 = de.deserialize_map(ProfileFieldValueVisitor::new(None))?;
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 = 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 use crate::profile::profile_field_serde::StaticProfileFieldVisitor;
266
267 if response.status().as_u16() >= 400 {
268 return Err(ruma_common::api::error::FromHttpResponseError::Server(
269 Self::EndpointError::from_http_response(response),
270 ));
271 }
272
273 let value = serde_json::Deserializer::from_slice(response.into_body().as_ref())
274 .deserialize_map(StaticProfileFieldVisitor(PhantomData::<F>))?;
275
276 Ok(Self { value })
277 }
278 }
279}
280
281#[cfg(all(test, feature = "client"))]
282mod tests_client {
283 use ruma_common::{
284 owned_mxc_uri, owned_user_id,
285 profile::{ProfileFieldName, ProfileFieldValue},
286 };
287 use serde_json::{json, to_vec as to_json_vec};
288
289 use super::v3::{Request, RequestStatic, Response};
290
291 #[test]
292 fn serialize_request() {
293 use std::borrow::Cow;
294
295 use ruma_common::api::{OutgoingRequest, SupportedVersions, auth_scheme::SendAccessToken};
296
297 let avatar_url_request =
299 Request::new(owned_user_id!("@alice:localhost"), ProfileFieldName::AvatarUrl);
300
301 let http_request = avatar_url_request
303 .clone()
304 .try_into_http_request::<Vec<u8>>(
305 "http://localhost/",
306 SendAccessToken::None,
307 Cow::Owned(SupportedVersions::from_parts(
308 &["v1.11".to_owned()],
309 &Default::default(),
310 )),
311 )
312 .unwrap();
313 assert_eq!(
314 http_request.uri().path(),
315 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
316 );
317
318 let http_request = avatar_url_request
320 .try_into_http_request::<Vec<u8>>(
321 "http://localhost/",
322 SendAccessToken::None,
323 Cow::Owned(SupportedVersions::from_parts(
324 &["v1.16".to_owned()],
325 &Default::default(),
326 )),
327 )
328 .unwrap();
329 assert_eq!(
330 http_request.uri().path(),
331 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
332 );
333
334 let custom_field_request =
336 Request::new(owned_user_id!("@alice:localhost"), "dev.ruma.custom_field".into());
337
338 let http_request = custom_field_request
340 .clone()
341 .try_into_http_request::<Vec<u8>>(
342 "http://localhost/",
343 SendAccessToken::None,
344 Cow::Owned(SupportedVersions::from_parts(
345 &["v1.11".to_owned()],
346 &Default::default(),
347 )),
348 )
349 .unwrap();
350 assert_eq!(
351 http_request.uri().path(),
352 "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
353 );
354
355 let http_request = custom_field_request
357 .try_into_http_request::<Vec<u8>>(
358 "http://localhost/",
359 SendAccessToken::None,
360 Cow::Owned(SupportedVersions::from_parts(
361 &["v1.16".to_owned()],
362 &Default::default(),
363 )),
364 )
365 .unwrap();
366 assert_eq!(
367 http_request.uri().path(),
368 "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
369 );
370 }
371
372 #[test]
373 fn deserialize_response() {
374 use ruma_common::api::IncomingResponse;
375
376 let body = to_json_vec(&json!({
377 "custom_field": "value",
378 }))
379 .unwrap();
380
381 let response = Response::try_from_http_response(http::Response::new(body)).unwrap();
382 let value = response.value.unwrap();
383 assert_eq!(value.field_name().as_str(), "custom_field");
384 assert_eq!(value.value().as_str().unwrap(), "value");
385
386 let empty_body = to_json_vec(&json!({})).unwrap();
387
388 let response = Response::try_from_http_response(http::Response::new(empty_body)).unwrap();
389 assert!(response.value.is_none());
390 }
391
392 fn get_static_response<R: ruma_common::api::OutgoingRequest>(
395 value: Option<ProfileFieldValue>,
396 ) -> Result<R::IncomingResponse, ruma_common::api::error::FromHttpResponseError<R::EndpointError>>
397 {
398 use ruma_common::api::IncomingResponse;
399
400 let body =
401 value.map(|value| to_json_vec(&value).unwrap()).unwrap_or_else(|| b"{}".to_vec());
402 R::IncomingResponse::try_from_http_response(http::Response::new(body))
403 }
404
405 #[test]
406 fn static_request_and_valid_response() {
407 use crate::profile::AvatarUrl;
408
409 let response = get_static_response::<RequestStatic<AvatarUrl>>(Some(
410 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
411 ))
412 .unwrap();
413 assert_eq!(response.value.unwrap(), "mxc://localhost/abcdef");
414
415 let response = get_static_response::<RequestStatic<AvatarUrl>>(None).unwrap();
416 assert!(response.value.is_none());
417 }
418
419 #[test]
420 fn static_request_and_invalid_response() {
421 use crate::profile::AvatarUrl;
422
423 get_static_response::<RequestStatic<AvatarUrl>>(Some(ProfileFieldValue::DisplayName(
424 "Alice".to_owned(),
425 )))
426 .unwrap_err();
427 }
428}
429
430#[cfg(all(test, feature = "server"))]
431mod tests_server {
432 use ruma_common::{
433 owned_mxc_uri,
434 profile::{ProfileFieldName, ProfileFieldValue},
435 };
436 use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
437
438 use super::v3::{Request, Response};
439
440 #[test]
441 fn deserialize_request() {
442 use ruma_common::api::IncomingRequest;
443
444 let request = Request::try_from_http_request(
445 http::Request::get(
446 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
447 )
448 .body(Vec::<u8>::new())
449 .unwrap(),
450 &["@alice:localhost", "displayname"],
451 )
452 .unwrap();
453
454 assert_eq!(request.user_id, "@alice:localhost");
455 assert_eq!(request.field, ProfileFieldName::DisplayName);
456 }
457
458 #[test]
459 fn serialize_response() {
460 use ruma_common::api::OutgoingResponse;
461
462 let response =
463 Response::new(ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")));
464
465 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
466
467 assert_eq!(
468 from_json_slice::<JsonValue>(http_response.body().as_ref()).unwrap(),
469 json!({
470 "avatar_url": "mxc://localhost/abcdef",
471 })
472 );
473 }
474}