1pub mod v3 {
6 use std::marker::PhantomData;
17
18 #[cfg(feature = "client")]
19 use ruma_common::api::EmptyBody;
20 use ruma_common::{
21 OwnedUserId,
22 api::{Metadata, auth_scheme::NoAccessToken, error::Error, path_builder::VersionHistory},
23 metadata,
24 profile::{ProfileFieldName, ProfileFieldValue, StaticProfileField},
25 };
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 Body = EmptyBody;
65 type EndpointError = Error;
66 type IncomingResponse = Response;
67
68 fn try_into_http_request_inner(
69 self,
70 base_url: &str,
71 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
72 ) -> Result<http::Request<EmptyBody>, ruma_common::api::error::IntoHttpError> {
73 use ruma_common::api::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 http_request =
89 http::Request::builder().method(Self::METHOD).uri(url).body(EmptyBody)?;
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 Self::check_request_method(request.method())?;
109
110 let (user_id, field) =
111 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
112 _,
113 serde::de::value::Error,
114 >::new(
115 path_args.iter().map(::std::convert::AsRef::as_ref),
116 ))?;
117
118 Ok(Self { user_id, field })
119 }
120 }
121
122 #[derive(Debug)]
124 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
125 pub struct RequestStatic<F: StaticProfileField> {
126 pub user_id: OwnedUserId,
128
129 field: PhantomData<F>,
131 }
132
133 impl<F: StaticProfileField> RequestStatic<F> {
134 pub fn new(user_id: OwnedUserId) -> Self {
136 Self { user_id, field: PhantomData }
137 }
138 }
139
140 impl<F: StaticProfileField> Clone for RequestStatic<F> {
141 fn clone(&self) -> Self {
142 Self { user_id: self.user_id.clone(), field: self.field }
143 }
144 }
145
146 impl<F: StaticProfileField> Metadata for RequestStatic<F> {
147 const METHOD: http::Method = Request::METHOD;
148 const RATE_LIMITED: bool = Request::RATE_LIMITED;
149 type Authentication = <Request as Metadata>::Authentication;
150 type PathBuilder = <Request as Metadata>::PathBuilder;
151 const PATH_BUILDER: VersionHistory = Request::PATH_BUILDER;
152 }
153
154 #[cfg(feature = "client")]
155 impl<F: StaticProfileField> ruma_common::api::OutgoingRequest for RequestStatic<F> {
156 type Body = EmptyBody;
157 type EndpointError = Error;
158 type IncomingResponse = ResponseStatic<F>;
159
160 fn try_into_http_request_inner(
161 self,
162 base_url: &str,
163 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
164 ) -> Result<http::Request<EmptyBody>, ruma_common::api::error::IntoHttpError> {
165 Request::new(self.user_id, F::NAME.into())
166 .try_into_http_request_inner(base_url, considering)
167 }
168 }
169
170 #[derive(Debug, Clone, Default)]
172 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
173 pub struct Response {
174 pub value: Option<ProfileFieldValue>,
176 }
177
178 impl Response {
179 pub fn new(value: ProfileFieldValue) -> Self {
181 Self { value: Some(value) }
182 }
183 }
184
185 #[doc(hidden)]
186 #[derive(ruma_common::serde::_FakeDeriveSerde)]
187 #[cfg_attr(feature = "server", derive(ruma_common::api::OutgoingBodyJson))]
188 #[serde(transparent)]
189 pub struct ResponseBody(Option<ProfileFieldValue>);
190
191 #[cfg(feature = "server")]
192 impl serde::Serialize for ResponseBody {
193 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
194 where
195 S: serde::Serializer,
196 {
197 use ruma_common::serde::JsonObject;
198
199 if let Some(value) = &self.0 {
200 value.serialize(serializer)
201 } else {
202 JsonObject::new().serialize(serializer)
203 }
204 }
205 }
206
207 #[cfg(feature = "client")]
208 impl<'de> serde::Deserialize<'de> for ResponseBody {
209 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
210 where
211 D: serde::Deserializer<'de>,
212 {
213 use ruma_common::profile::ProfileFieldValueVisitor;
214
215 let value = deserializer.deserialize_map(ProfileFieldValueVisitor::new(None))?;
216
217 Ok(Self(value))
218 }
219 }
220
221 #[cfg(feature = "client")]
222 impl ruma_common::api::IncomingResponse for Response {
223 type EndpointError = Error;
224
225 fn try_from_http_response_inner(
226 response: http::Response<&[u8]>,
227 ) -> Result<Self, ruma_common::api::error::DeserializationError> {
228 let ResponseBody(value) = serde_json::from_slice(response.body())?;
229 Ok(Self { value })
230 }
231 }
232
233 #[cfg(feature = "server")]
234 impl ruma_common::api::OutgoingResponse for Response {
235 type Body = ResponseBody;
236
237 fn try_into_http_response_inner(
238 self,
239 ) -> Result<http::Response<Self::Body>, ruma_common::api::error::IntoHttpError> {
240 Ok(http::Response::builder()
241 .status(http::StatusCode::OK)
242 .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
243 .body(ResponseBody(self.value))?)
244 }
245 }
246
247 #[derive(Debug)]
249 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
250 pub struct ResponseStatic<F: StaticProfileField> {
251 pub value: Option<F::Value>,
253 }
254
255 impl<F: StaticProfileField> Clone for ResponseStatic<F>
256 where
257 F::Value: Clone,
258 {
259 fn clone(&self) -> Self {
260 Self { value: self.value.clone() }
261 }
262 }
263
264 #[cfg(feature = "client")]
265 impl<F: StaticProfileField> ruma_common::api::IncomingResponse for ResponseStatic<F> {
266 type EndpointError = Error;
267
268 fn try_from_http_response_inner(
269 response: http::Response<&[u8]>,
270 ) -> Result<Self, ruma_common::api::error::DeserializationError> {
271 use serde::de::Deserializer;
272
273 use crate::profile::profile_field_serde::StaticProfileFieldVisitor;
274
275 let value = serde_json::Deserializer::from_slice(response.into_body())
276 .deserialize_map(StaticProfileFieldVisitor(PhantomData::<F>))?;
277
278 Ok(Self { value })
279 }
280 }
281}
282
283#[cfg(all(test, feature = "client"))]
284mod tests_client {
285 use ruma_common::{
286 owned_mxc_uri, owned_user_id,
287 profile::{ProfileFieldName, ProfileFieldValue},
288 };
289 use serde_json::{json, to_vec as to_json_vec};
290
291 use super::v3::{Request, RequestStatic, Response};
292
293 #[test]
294 fn serialize_request() {
295 use std::borrow::Cow;
296
297 use ruma_common::api::{
298 OutgoingRequestExt as _, SupportedVersions, auth_scheme::SendAccessToken,
299 };
300
301 let avatar_url_request =
303 Request::new(owned_user_id!("@alice:localhost"), ProfileFieldName::AvatarUrl);
304
305 let http_request = avatar_url_request
307 .clone()
308 .try_into_http_request::<Vec<u8>>(
309 "http://localhost/",
310 SendAccessToken::None,
311 Cow::Owned(SupportedVersions::from_parts(
312 &["v1.11".to_owned()],
313 &Default::default(),
314 )),
315 )
316 .unwrap();
317 assert_eq!(
318 http_request.uri().path(),
319 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
320 );
321
322 let http_request = avatar_url_request
324 .try_into_http_request::<Vec<u8>>(
325 "http://localhost/",
326 SendAccessToken::None,
327 Cow::Owned(SupportedVersions::from_parts(
328 &["v1.16".to_owned()],
329 &Default::default(),
330 )),
331 )
332 .unwrap();
333 assert_eq!(
334 http_request.uri().path(),
335 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
336 );
337
338 let custom_field_request =
340 Request::new(owned_user_id!("@alice:localhost"), "dev.ruma.custom_field".into());
341
342 let http_request = custom_field_request
344 .clone()
345 .try_into_http_request::<Vec<u8>>(
346 "http://localhost/",
347 SendAccessToken::None,
348 Cow::Owned(SupportedVersions::from_parts(
349 &["v1.11".to_owned()],
350 &Default::default(),
351 )),
352 )
353 .unwrap();
354 assert_eq!(
355 http_request.uri().path(),
356 "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
357 );
358
359 let http_request = custom_field_request
361 .try_into_http_request::<Vec<u8>>(
362 "http://localhost/",
363 SendAccessToken::None,
364 Cow::Owned(SupportedVersions::from_parts(
365 &["v1.16".to_owned()],
366 &Default::default(),
367 )),
368 )
369 .unwrap();
370 assert_eq!(
371 http_request.uri().path(),
372 "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
373 );
374 }
375
376 #[test]
377 fn deserialize_response() {
378 use ruma_common::api::IncomingResponseExt as _;
379
380 let body = json!({
381 "custom_field": "value",
382 })
383 .to_string();
384
385 let response =
386 Response::try_from_http_response(http::Response::new(body.as_bytes())).unwrap();
387 let value = response.value.unwrap();
388 assert_eq!(value.field_name().as_str(), "custom_field");
389 assert_eq!(value.value().as_str().unwrap(), "value");
390
391 let response =
392 Response::try_from_http_response(http::Response::new(b"{}".as_slice())).unwrap();
393 assert!(response.value.is_none());
394 }
395
396 fn get_static_response<R: ruma_common::api::OutgoingRequest>(
399 value: Option<ProfileFieldValue>,
400 ) -> Result<R::IncomingResponse, ruma_common::api::error::FromHttpResponseError<R::EndpointError>>
401 {
402 use ruma_common::api::IncomingResponseExt as _;
403
404 let body =
405 value.map(|value| to_json_vec(&value).unwrap()).unwrap_or_else(|| b"{}".to_vec());
406 R::IncomingResponse::try_from_http_response(http::Response::new(body.as_slice()))
407 }
408
409 #[test]
410 fn static_request_and_valid_response() {
411 use crate::profile::AvatarUrl;
412
413 let response = get_static_response::<RequestStatic<AvatarUrl>>(Some(
414 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
415 ))
416 .unwrap();
417 assert_eq!(response.value.unwrap(), "mxc://localhost/abcdef");
418
419 let response = get_static_response::<RequestStatic<AvatarUrl>>(None).unwrap();
420 assert!(response.value.is_none());
421 }
422
423 #[test]
424 fn static_request_and_invalid_response() {
425 use crate::profile::AvatarUrl;
426
427 get_static_response::<RequestStatic<AvatarUrl>>(Some(ProfileFieldValue::DisplayName(
428 "Alice".to_owned(),
429 )))
430 .unwrap_err();
431 }
432}
433
434#[cfg(all(test, feature = "server"))]
435mod tests_server {
436 use ruma_common::{
437 owned_mxc_uri,
438 profile::{ProfileFieldName, ProfileFieldValue},
439 };
440 use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
441
442 use super::v3::{Request, Response};
443
444 #[test]
445 fn deserialize_request() {
446 use ruma_common::api::IncomingRequest;
447
448 let request = Request::try_from_http_request(
449 http::Request::get(
450 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
451 )
452 .body(Vec::<u8>::new())
453 .unwrap(),
454 &["@alice:localhost", "displayname"],
455 )
456 .unwrap();
457
458 assert_eq!(request.user_id, "@alice:localhost");
459 assert_eq!(request.field, ProfileFieldName::DisplayName);
460 }
461
462 #[test]
463 fn serialize_response() {
464 use ruma_common::api::OutgoingResponseExt;
465
466 let response =
467 Response::new(ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")));
468
469 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
470
471 assert_eq!(
472 from_json_slice::<JsonValue>(http_response.body().as_ref()).unwrap(),
473 json!({
474 "avatar_url": "mxc://localhost/abcdef",
475 })
476 );
477 }
478}