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