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, StaticProfileField},
23 };
24
25 metadata! {
26 method: GET,
27 rate_limited: false,
28 authentication: NoAccessToken,
29 history: {
31 unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}",
32 1.0 => "/_matrix/client/r0/profile/{user_id}/{field}",
33 1.1 => "/_matrix/client/v3/profile/{user_id}/{field}",
34 }
35 }
36
37 #[derive(Clone, Debug)]
39 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
40 pub struct Request {
41 pub user_id: OwnedUserId,
43
44 pub field: ProfileFieldName,
46 }
47
48 impl Request {
49 pub fn new(user_id: OwnedUserId, field: ProfileFieldName) -> Self {
51 Self { user_id, field }
52 }
53
54 pub fn new_static<F: StaticProfileField>(user_id: OwnedUserId) -> RequestStatic<F> {
56 RequestStatic::new(user_id)
57 }
58 }
59
60 #[cfg(feature = "client")]
61 impl ruma_common::api::OutgoingRequest for Request {
62 type EndpointError = Error;
63 type IncomingResponse = Response;
64
65 fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
66 self,
67 base_url: &str,
68 access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
69 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
70 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
71 use ruma_common::api::{auth_scheme::AuthScheme, path_builder::PathBuilder};
72
73 use crate::profile::field_existed_before_extended_profiles;
74
75 let url = if field_existed_before_extended_profiles(&self.field) {
76 Self::make_endpoint_url(considering, base_url, &[&self.user_id, &self.field], "")?
77 } else {
78 crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
79 considering,
80 base_url,
81 &[&self.user_id, &self.field],
82 "",
83 )?
84 };
85
86 let mut http_request =
87 http::Request::builder().method(Self::METHOD).uri(url).body(T::default())?;
88
89 Self::Authentication::add_authentication(&mut http_request, access_token)?;
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 EndpointError = Error;
157 type IncomingResponse = ResponseStatic<F>;
158
159 fn try_into_http_request<T: Default + bytes::BufMut + AsRef<[u8]>>(
160 self,
161 base_url: &str,
162 access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
163 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
164 ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
165 Request::new(self.user_id, F::NAME.into()).try_into_http_request(
166 base_url,
167 access_token,
168 considering,
169 )
170 }
171 }
172
173 #[derive(Debug, Clone, Default)]
175 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
176 pub struct Response {
177 pub value: Option<ProfileFieldValue>,
179 }
180
181 impl Response {
182 pub fn new(value: ProfileFieldValue) -> Self {
184 Self { value: Some(value) }
185 }
186 }
187
188 #[cfg(feature = "client")]
189 impl ruma_common::api::IncomingResponse for Response {
190 type EndpointError = Error;
191
192 fn try_from_http_response<T: AsRef<[u8]>>(
193 response: http::Response<T>,
194 ) -> Result<Self, ruma_common::api::error::FromHttpResponseError<Self::EndpointError>>
195 {
196 use ruma_common::{api::EndpointError, profile::ProfileFieldValueVisitor};
197 use serde::Deserializer;
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 = de.deserialize_map(ProfileFieldValueVisitor::new(None))?;
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 = 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::{
282 owned_mxc_uri, owned_user_id,
283 profile::{ProfileFieldName, ProfileFieldValue},
284 };
285 use serde_json::{json, to_vec as to_json_vec};
286
287 use super::v3::{Request, RequestStatic, Response};
288
289 #[test]
290 fn serialize_request() {
291 use std::borrow::Cow;
292
293 use ruma_common::api::{OutgoingRequest, SupportedVersions, auth_scheme::SendAccessToken};
294
295 let avatar_url_request =
297 Request::new(owned_user_id!("@alice:localhost"), ProfileFieldName::AvatarUrl);
298
299 let http_request = avatar_url_request
301 .clone()
302 .try_into_http_request::<Vec<u8>>(
303 "http://localhost/",
304 SendAccessToken::None,
305 Cow::Owned(SupportedVersions::from_parts(
306 &["v1.11".to_owned()],
307 &Default::default(),
308 )),
309 )
310 .unwrap();
311 assert_eq!(
312 http_request.uri().path(),
313 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
314 );
315
316 let http_request = avatar_url_request
318 .try_into_http_request::<Vec<u8>>(
319 "http://localhost/",
320 SendAccessToken::None,
321 Cow::Owned(SupportedVersions::from_parts(
322 &["v1.16".to_owned()],
323 &Default::default(),
324 )),
325 )
326 .unwrap();
327 assert_eq!(
328 http_request.uri().path(),
329 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
330 );
331
332 let custom_field_request =
334 Request::new(owned_user_id!("@alice:localhost"), "dev.ruma.custom_field".into());
335
336 let http_request = custom_field_request
338 .clone()
339 .try_into_http_request::<Vec<u8>>(
340 "http://localhost/",
341 SendAccessToken::None,
342 Cow::Owned(SupportedVersions::from_parts(
343 &["v1.11".to_owned()],
344 &Default::default(),
345 )),
346 )
347 .unwrap();
348 assert_eq!(
349 http_request.uri().path(),
350 "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
351 );
352
353 let http_request = custom_field_request
355 .try_into_http_request::<Vec<u8>>(
356 "http://localhost/",
357 SendAccessToken::None,
358 Cow::Owned(SupportedVersions::from_parts(
359 &["v1.16".to_owned()],
360 &Default::default(),
361 )),
362 )
363 .unwrap();
364 assert_eq!(
365 http_request.uri().path(),
366 "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
367 );
368 }
369
370 #[test]
371 fn deserialize_response() {
372 use ruma_common::api::IncomingResponse;
373
374 let body = to_json_vec(&json!({
375 "custom_field": "value",
376 }))
377 .unwrap();
378
379 let response = Response::try_from_http_response(http::Response::new(body)).unwrap();
380 let value = response.value.unwrap();
381 assert_eq!(value.field_name().as_str(), "custom_field");
382 assert_eq!(value.value().as_str().unwrap(), "value");
383
384 let empty_body = to_json_vec(&json!({})).unwrap();
385
386 let response = Response::try_from_http_response(http::Response::new(empty_body)).unwrap();
387 assert!(response.value.is_none());
388 }
389
390 fn get_static_response<R: ruma_common::api::OutgoingRequest>(
393 value: Option<ProfileFieldValue>,
394 ) -> Result<R::IncomingResponse, ruma_common::api::error::FromHttpResponseError<R::EndpointError>>
395 {
396 use ruma_common::api::IncomingResponse;
397
398 let body =
399 value.map(|value| to_json_vec(&value).unwrap()).unwrap_or_else(|| b"{}".to_vec());
400 R::IncomingResponse::try_from_http_response(http::Response::new(body))
401 }
402
403 #[test]
404 fn static_request_and_valid_response() {
405 use crate::profile::AvatarUrl;
406
407 let response = get_static_response::<RequestStatic<AvatarUrl>>(Some(
408 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
409 ))
410 .unwrap();
411 assert_eq!(response.value.unwrap(), "mxc://localhost/abcdef");
412
413 let response = get_static_response::<RequestStatic<AvatarUrl>>(None).unwrap();
414 assert!(response.value.is_none());
415 }
416
417 #[test]
418 fn static_request_and_invalid_response() {
419 use crate::profile::AvatarUrl;
420
421 get_static_response::<RequestStatic<AvatarUrl>>(Some(ProfileFieldValue::DisplayName(
422 "Alice".to_owned(),
423 )))
424 .unwrap_err();
425 }
426}
427
428#[cfg(all(test, feature = "server"))]
429mod tests_server {
430 use ruma_common::{
431 owned_mxc_uri,
432 profile::{ProfileFieldName, ProfileFieldValue},
433 };
434 use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
435
436 use super::v3::{Request, Response};
437
438 #[test]
439 fn deserialize_request() {
440 use ruma_common::api::IncomingRequest;
441
442 let request = Request::try_from_http_request(
443 http::Request::get(
444 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
445 )
446 .body(Vec::<u8>::new())
447 .unwrap(),
448 &["@alice:localhost", "displayname"],
449 )
450 .unwrap();
451
452 assert_eq!(request.user_id, "@alice:localhost");
453 assert_eq!(request.field, ProfileFieldName::DisplayName);
454 }
455
456 #[test]
457 fn serialize_response() {
458 use ruma_common::api::OutgoingResponse;
459
460 let response =
461 Response::new(ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")));
462
463 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
464
465 assert_eq!(
466 from_json_slice::<JsonValue>(http_response.body().as_ref()).unwrap(),
467 json!({
468 "avatar_url": "mxc://localhost/abcdef",
469 })
470 );
471 }
472}