1pub mod v3 {
6 use std::marker::PhantomData;
17
18 use ruma_common::{
19 api::{Metadata, VersionHistory},
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>(
69 self,
70 base_url: &str,
71 access_token: ruma_common::api::SendAccessToken<'_>,
72 considering: &'_ ruma_common::api::SupportedVersions,
73 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
74 use http::header::{self, HeaderValue};
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_builder = http::Request::builder()
88 .method(Self::METHOD)
89 .uri(url)
90 .header(header::CONTENT_TYPE, "application/json");
91
92 if let Some(access_token) = access_token.get_not_required_for_endpoint() {
93 http_request_builder = http_request_builder.header(
94 header::AUTHORIZATION,
95 HeaderValue::from_str(&format!("Bearer {access_token}"))?,
96 );
97 }
98
99 Ok(http_request_builder.body(T::default())?)
100 }
101 }
102
103 #[cfg(feature = "server")]
104 impl ruma_common::api::IncomingRequest for Request {
105 type EndpointError = crate::Error;
106 type OutgoingResponse = Response;
107
108 fn try_from_http_request<B, S>(
109 request: http::Request<B>,
110 path_args: &[S],
111 ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
112 where
113 B: AsRef<[u8]>,
114 S: AsRef<str>,
115 {
116 if request.method() != Self::METHOD {
117 return Err(ruma_common::api::error::FromHttpRequestError::MethodMismatch {
118 expected: Self::METHOD,
119 received: request.method().clone(),
120 });
121 }
122
123 let (user_id, field) =
124 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
125 _,
126 serde::de::value::Error,
127 >::new(
128 path_args.iter().map(::std::convert::AsRef::as_ref),
129 ))?;
130
131 Ok(Self { user_id, field })
132 }
133 }
134
135 #[derive(Debug)]
137 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
138 pub struct RequestStatic<F: StaticProfileField> {
139 pub user_id: OwnedUserId,
141
142 field: PhantomData<F>,
144 }
145
146 impl<F: StaticProfileField> RequestStatic<F> {
147 pub fn new(user_id: OwnedUserId) -> Self {
149 Self { user_id, field: PhantomData }
150 }
151 }
152
153 impl<F: StaticProfileField> Clone for RequestStatic<F> {
154 fn clone(&self) -> Self {
155 Self { user_id: self.user_id.clone(), field: self.field }
156 }
157 }
158
159 impl<F: StaticProfileField> Metadata for RequestStatic<F> {
160 const METHOD: http::Method = Request::METHOD;
161 const RATE_LIMITED: bool = Request::RATE_LIMITED;
162 type Authentication = <Request as Metadata>::Authentication;
163 const HISTORY: VersionHistory = Request::HISTORY;
164 }
165
166 #[cfg(feature = "client")]
167 impl<F: StaticProfileField> ruma_common::api::OutgoingRequest for RequestStatic<F> {
168 type EndpointError = crate::Error;
169 type IncomingResponse = ResponseStatic<F>;
170
171 fn try_into_http_request<T: Default + bytes::BufMut>(
172 self,
173 base_url: &str,
174 access_token: ruma_common::api::SendAccessToken<'_>,
175 considering: &'_ ruma_common::api::SupportedVersions,
176 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
177 Request::new(self.user_id, F::NAME.into()).try_into_http_request(
178 base_url,
179 access_token,
180 considering,
181 )
182 }
183 }
184
185 #[derive(Debug, Clone, Default)]
187 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
188 pub struct Response {
189 pub value: Option<ProfileFieldValue>,
191 }
192
193 impl Response {
194 pub fn new(value: ProfileFieldValue) -> Self {
196 Self { value: Some(value) }
197 }
198 }
199
200 #[cfg(feature = "client")]
201 impl ruma_common::api::IncomingResponse for Response {
202 type EndpointError = crate::Error;
203
204 fn try_from_http_response<T: AsRef<[u8]>>(
205 response: http::Response<T>,
206 ) -> Result<Self, ruma_common::api::error::FromHttpResponseError<Self::EndpointError>>
207 {
208 use ruma_common::api::EndpointError;
209
210 use crate::profile::profile_field_serde::deserialize_profile_field_value_option;
211
212 if response.status().as_u16() >= 400 {
213 return Err(ruma_common::api::error::FromHttpResponseError::Server(
214 Self::EndpointError::from_http_response(response),
215 ));
216 }
217
218 let mut de = serde_json::Deserializer::from_slice(response.body().as_ref());
219 let value = deserialize_profile_field_value_option(&mut de)?;
220 de.end()?;
221
222 Ok(Self { value })
223 }
224 }
225
226 #[cfg(feature = "server")]
227 impl ruma_common::api::OutgoingResponse for Response {
228 fn try_into_http_response<T: Default + bytes::BufMut>(
229 self,
230 ) -> Result<http::Response<T>, ruma_common::api::error::IntoHttpError> {
231 use ruma_common::serde::JsonObject;
232
233 let body = self
234 .value
235 .as_ref()
236 .map(|value| ruma_common::serde::json_to_buf(value))
237 .unwrap_or_else(||
238 ruma_common::serde::json_to_buf(&JsonObject::new()))?;
240
241 Ok(http::Response::builder()
242 .status(http::StatusCode::OK)
243 .header(http::header::CONTENT_TYPE, "application/json")
244 .body(body)?)
245 }
246 }
247
248 #[derive(Debug)]
250 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
251 pub struct ResponseStatic<F: StaticProfileField> {
252 pub value: Option<F::Value>,
254 }
255
256 impl<F: StaticProfileField> Clone for ResponseStatic<F>
257 where
258 F::Value: Clone,
259 {
260 fn clone(&self) -> Self {
261 Self { value: self.value.clone() }
262 }
263 }
264
265 #[cfg(feature = "client")]
266 impl<F: StaticProfileField> ruma_common::api::IncomingResponse for ResponseStatic<F> {
267 type EndpointError = crate::Error;
268
269 fn try_from_http_response<T: AsRef<[u8]>>(
270 response: http::Response<T>,
271 ) -> Result<Self, ruma_common::api::error::FromHttpResponseError<Self::EndpointError>>
272 {
273 use ruma_common::api::EndpointError;
274 use serde::de::Deserializer;
275
276 if response.status().as_u16() >= 400 {
277 return Err(ruma_common::api::error::FromHttpResponseError::Server(
278 Self::EndpointError::from_http_response(response),
279 ));
280 }
281
282 let value = serde_json::Deserializer::from_slice(response.into_body().as_ref())
283 .deserialize_map(StaticProfileFieldVisitor(PhantomData::<F>))?;
284
285 Ok(Self { value })
286 }
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use ruma_common::{owned_mxc_uri, owned_user_id};
293 use serde_json::{
294 from_slice as from_json_slice, json, to_vec as to_json_vec, Value as JsonValue,
295 };
296
297 use super::v3::{Request, RequestStatic, Response};
298 use crate::profile::{ProfileFieldName, ProfileFieldValue};
299
300 #[test]
301 #[cfg(feature = "client")]
302 fn serialize_request() {
303 use ruma_common::api::{OutgoingRequest, SendAccessToken, SupportedVersions};
304
305 let avatar_url_request =
307 Request::new(owned_user_id!("@alice:localhost"), ProfileFieldName::AvatarUrl);
308
309 let http_request = avatar_url_request
311 .clone()
312 .try_into_http_request::<Vec<u8>>(
313 "http://localhost/",
314 SendAccessToken::None,
315 &SupportedVersions::from_parts(&["v1.11".to_owned()], &Default::default()),
316 )
317 .unwrap();
318 assert_eq!(
319 http_request.uri().path(),
320 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
321 );
322
323 let http_request = avatar_url_request
325 .try_into_http_request::<Vec<u8>>(
326 "http://localhost/",
327 SendAccessToken::None,
328 &SupportedVersions::from_parts(&["v1.16".to_owned()], &Default::default()),
329 )
330 .unwrap();
331 assert_eq!(
332 http_request.uri().path(),
333 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
334 );
335
336 let custom_field_request =
338 Request::new(owned_user_id!("@alice:localhost"), "dev.ruma.custom_field".into());
339
340 let http_request = custom_field_request
342 .clone()
343 .try_into_http_request::<Vec<u8>>(
344 "http://localhost/",
345 SendAccessToken::None,
346 &SupportedVersions::from_parts(&["v1.11".to_owned()], &Default::default()),
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 &SupportedVersions::from_parts(&["v1.16".to_owned()], &Default::default()),
360 )
361 .unwrap();
362 assert_eq!(
363 http_request.uri().path(),
364 "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
365 );
366 }
367
368 #[test]
369 #[cfg(feature = "server")]
370 fn deserialize_request() {
371 use ruma_common::api::IncomingRequest;
372
373 let request = Request::try_from_http_request(
374 http::Request::get(
375 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
376 )
377 .body(Vec::<u8>::new())
378 .unwrap(),
379 &["@alice:localhost", "displayname"],
380 )
381 .unwrap();
382
383 assert_eq!(request.user_id, "@alice:localhost");
384 assert_eq!(request.field, ProfileFieldName::DisplayName);
385 }
386
387 #[test]
388 #[cfg(feature = "server")]
389 fn serialize_response() {
390 use ruma_common::api::OutgoingResponse;
391
392 let response =
393 Response::new(ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")));
394
395 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
396
397 assert_eq!(
398 from_json_slice::<JsonValue>(http_response.body().as_ref()).unwrap(),
399 json!({
400 "avatar_url": "mxc://localhost/abcdef",
401 })
402 );
403 }
404
405 #[test]
406 #[cfg(feature = "client")]
407 fn deserialize_response() {
408 use ruma_common::api::IncomingResponse;
409
410 let body = to_json_vec(&json!({
411 "custom_field": "value",
412 }))
413 .unwrap();
414
415 let response = Response::try_from_http_response(http::Response::new(body)).unwrap();
416 let value = response.value.unwrap();
417 assert_eq!(value.field_name().as_str(), "custom_field");
418 assert_eq!(value.value().as_str().unwrap(), "value");
419
420 let empty_body = to_json_vec(&json!({})).unwrap();
421
422 let response = Response::try_from_http_response(http::Response::new(empty_body)).unwrap();
423 assert!(response.value.is_none());
424 }
425
426 #[cfg(feature = "client")]
429 fn get_static_response<R: ruma_common::api::OutgoingRequest>(
430 value: Option<ProfileFieldValue>,
431 ) -> Result<R::IncomingResponse, ruma_common::api::error::FromHttpResponseError<R::EndpointError>>
432 {
433 use ruma_common::api::IncomingResponse;
434
435 let body =
436 value.map(|value| to_json_vec(&value).unwrap()).unwrap_or_else(|| b"{}".to_vec());
437 R::IncomingResponse::try_from_http_response(http::Response::new(body))
438 }
439
440 #[test]
441 #[cfg(feature = "client")]
442 fn static_request_and_valid_response() {
443 use crate::profile::AvatarUrl;
444
445 let response = get_static_response::<RequestStatic<AvatarUrl>>(Some(
446 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
447 ))
448 .unwrap();
449 assert_eq!(response.value.unwrap(), "mxc://localhost/abcdef");
450
451 let response = get_static_response::<RequestStatic<AvatarUrl>>(None).unwrap();
452 assert!(response.value.is_none());
453 }
454
455 #[test]
456 #[cfg(feature = "client")]
457 fn static_request_and_invalid_response() {
458 use crate::profile::AvatarUrl;
459
460 get_static_response::<RequestStatic<AvatarUrl>>(Some(ProfileFieldValue::DisplayName(
461 "Alice".to_owned(),
462 )))
463 .unwrap_err();
464 }
465}