Skip to content

Commit c9d3088

Browse files
zecakehpoljar
authored andcommitted
feat(sdk): Add support for fetching custom profile fields
Signed-off-by: Kévin Commaille <[email protected]>
1 parent 68b902e commit c9d3088

File tree

3 files changed

+167
-14
lines changed

3 files changed

+167
-14
lines changed

crates/matrix-sdk/src/account.rs

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use js_int::uint;
2323
#[cfg(feature = "experimental-element-recent-emojis")]
2424
use matrix_sdk_base::recent_emojis::RecentEmojisContent;
2525
use matrix_sdk_base::{
26-
StateStoreDataKey, StateStoreDataValue,
26+
SendOutsideWasm, StateStoreDataKey, StateStoreDataValue, SyncOutsideWasm,
2727
media::{MediaFormat, MediaRequestParameters},
2828
store::StateStoreExt,
2929
};
@@ -40,7 +40,8 @@ use ruma::{
4040
config::{get_global_account_data, set_global_account_data},
4141
error::ErrorKind,
4242
profile::{
43-
get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
43+
AvatarUrl, DisplayName, ProfileFieldName, ProfileFieldValue, StaticProfileField,
44+
get_profile, get_profile_field, set_avatar_url, set_display_name,
4445
},
4546
uiaa::AuthData,
4647
},
@@ -107,11 +108,7 @@ impl Account {
107108
/// ```
108109
pub async fn get_display_name(&self) -> Result<Option<String>> {
109110
let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
110-
#[allow(deprecated)]
111-
let request = get_display_name::v3::Request::new(user_id.to_owned());
112-
let request_config = self.client.request_config().force_auth();
113-
let response = self.client.send(request).with_request_config(request_config).await?;
114-
Ok(response.displayname)
111+
self.fetch_profile_field_of_static::<DisplayName>(user_id.to_owned()).await
115112
}
116113

117114
/// Set the display name of the account.
@@ -163,13 +160,10 @@ impl Account {
163160
/// ```
164161
pub async fn get_avatar_url(&self) -> Result<Option<OwnedMxcUri>> {
165162
let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
166-
#[allow(deprecated)]
167-
let request = get_avatar_url::v3::Request::new(user_id.to_owned());
168-
169-
let config = Some(RequestConfig::new().force_auth());
163+
let avatar_url =
164+
self.fetch_profile_field_of_static::<AvatarUrl>(user_id.to_owned()).await?;
170165

171-
let response = self.client.send(request).with_request_config(config).await?;
172-
if let Some(url) = response.avatar_url.clone() {
166+
if let Some(url) = avatar_url.clone() {
173167
// If an avatar is found cache it.
174168
let _ = self
175169
.client
@@ -187,7 +181,7 @@ impl Account {
187181
.remove_kv_data(StateStoreDataKey::UserAvatarUrl(user_id))
188182
.await;
189183
}
190-
Ok(response.avatar_url)
184+
Ok(avatar_url)
191185
}
192186

193187
/// Get the URL of the account's avatar, if is stored in cache.
@@ -328,6 +322,80 @@ impl Account {
328322
.await?)
329323
}
330324

325+
/// Get the given field from the given user's profile.
326+
///
327+
/// # Arguments
328+
///
329+
/// * `user_id` - The ID of the user to get the profile field of.
330+
///
331+
/// * `field` - The name of the profile field to get.
332+
///
333+
/// # Returns
334+
///
335+
/// Returns an error if the request fails or if deserialization of the
336+
/// response fails.
337+
///
338+
/// If the field is not set, the server should respond with an error with an
339+
/// [`ErrorCode::NotFound`], but it might also respond with an empty
340+
/// response, which would result in `Ok(None)`. Note that this error code
341+
/// might also mean that the given user ID doesn't exist.
342+
///
343+
/// [`ErrorCode::NotFound`]: ruma::api::client::error::ErrorCode::NotFound
344+
pub async fn fetch_profile_field_of(
345+
&self,
346+
user_id: OwnedUserId,
347+
field: ProfileFieldName,
348+
) -> Result<Option<ProfileFieldValue>> {
349+
let request = get_profile_field::v3::Request::new(user_id, field);
350+
let response = self
351+
.client
352+
.send(request)
353+
.with_request_config(RequestConfig::short_retry().force_auth())
354+
.await?;
355+
356+
Ok(response.value)
357+
}
358+
359+
/// Get the given statically-known field from the given user's profile.
360+
///
361+
/// # Arguments
362+
///
363+
/// * `user_id` - The ID of the user to get the profile field of.
364+
///
365+
/// # Returns
366+
///
367+
/// Returns an error if the request fails or if deserialization of the
368+
/// response fails.
369+
///
370+
/// If the field is not set, the server should respond with an error with an
371+
/// [`ErrorCode::NotFound`], but it might also respond with an empty
372+
/// response, which would result in `Ok(None)`. Note that this error code
373+
/// might also mean that the given user ID doesn't exist.
374+
///
375+
/// [`ErrorCode::NotFound`]: ruma::api::client::error::ErrorCode::NotFound
376+
pub async fn fetch_profile_field_of_static<F>(
377+
&self,
378+
user_id: OwnedUserId,
379+
) -> Result<Option<F::Value>>
380+
where
381+
F: StaticProfileField
382+
+ std::fmt::Debug
383+
+ Clone
384+
+ SendOutsideWasm
385+
+ SyncOutsideWasm
386+
+ 'static,
387+
F::Value: SendOutsideWasm + SyncOutsideWasm,
388+
{
389+
let request = get_profile_field::v3::Request::new_static::<F>(user_id);
390+
let response = self
391+
.client
392+
.send(request)
393+
.with_request_config(RequestConfig::short_retry().force_auth())
394+
.await?;
395+
396+
Ok(response.value)
397+
}
398+
331399
/// Change the password of the account.
332400
///
333401
/// # Arguments

crates/matrix-sdk/src/test_utils/mocks/mod.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use ruma::{
3535
DeviceId, EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedDeviceId, OwnedEventId,
3636
OwnedOneTimeKeyId, OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId,
3737
api::client::{
38+
profile::ProfileFieldName,
3839
receipt::create_receipt::v3::ReceiptType,
3940
room::Visibility,
4041
sync::sync_events::v5,
@@ -1622,6 +1623,17 @@ impl MatrixMockServer {
16221623
Mock::given(method("GET")).and(path_regex(r"^/_matrix/client/v1/rooms/.*/hierarchy"));
16231624
self.mock_endpoint(mock, GetHierarchyEndpoint).expect_default_access_token()
16241625
}
1626+
1627+
/// Create a prebuilt mock for the endpoint used to get a profile field.
1628+
pub fn mock_get_profile_field(
1629+
&self,
1630+
user_id: &UserId,
1631+
field: ProfileFieldName,
1632+
) -> MockEndpoint<'_, GetProfileFieldEndpoint> {
1633+
let mock = Mock::given(method("GET"))
1634+
.and(path(format!("/_matrix/client/v3/profile/{user_id}/{field}")));
1635+
self.mock_endpoint(mock, GetProfileFieldEndpoint { field })
1636+
}
16251637
}
16261638

16271639
/// A specification for a push rule ID.
@@ -4658,3 +4670,22 @@ impl<'a> MockEndpoint<'a, SlidingSyncEndpoint> {
46584670
let _summary = sliding_sync.sync_once().await.unwrap();
46594671
}
46604672
}
4673+
4674+
/// A prebuilt mock for `GET /_matrix/client/*/profile/{user_id}/{key_name}`.
4675+
pub struct GetProfileFieldEndpoint {
4676+
field: ProfileFieldName,
4677+
}
4678+
4679+
impl<'a> MockEndpoint<'a, GetProfileFieldEndpoint> {
4680+
/// Returns a successful response containing the given value, if any.
4681+
pub fn ok_with_value(self, value: Option<Value>) -> MatrixMock<'a> {
4682+
if let Some(value) = value {
4683+
let field = self.endpoint.field.to_string();
4684+
self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
4685+
field: value,
4686+
})))
4687+
} else {
4688+
self.ok_empty_json()
4689+
}
4690+
}
4691+
}

crates/matrix-sdk/tests/integration/account.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
use assert_matches2::assert_matches;
2+
use matrix_sdk::test_utils::mocks::MatrixMockServer;
13
use matrix_sdk_test::async_test;
4+
use ruma::api::{
5+
MatrixVersion,
6+
client::profile::{ProfileFieldName, ProfileFieldValue, TimeZone},
7+
};
28
use serde_json::json;
39
use wiremock::{
410
Mock, Request, ResponseTemplate,
@@ -57,3 +63,51 @@ async fn test_account_deactivation() {
5763
assert!(client.account().deactivate(None, None, true).await.is_ok());
5864
}
5965
}
66+
67+
#[async_test]
68+
async fn test_fetch_profile_field() {
69+
let tz = "Africa/Bujumbura";
70+
let display_name = "Alice";
71+
72+
let server = MatrixMockServer::new().await;
73+
let client = server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
74+
let user_id = client.user_id().unwrap();
75+
76+
server
77+
.mock_get_profile_field(user_id, ProfileFieldName::TimeZone)
78+
.ok_with_value(Some(tz.into()))
79+
.expect(2)
80+
.named("get m.tz profile field")
81+
.mount()
82+
.await;
83+
server
84+
.mock_get_profile_field(user_id, ProfileFieldName::DisplayName)
85+
.ok_with_value(Some(display_name.into()))
86+
.mock_once()
87+
.named("get displayname profile field")
88+
.mount()
89+
.await;
90+
server
91+
.mock_get_profile_field(user_id, ProfileFieldName::AvatarUrl)
92+
.ok_with_value(None)
93+
.mock_once()
94+
.named("get avatar_url profile field")
95+
.mount()
96+
.await;
97+
98+
let account = client.account();
99+
100+
let res_avatar_url = account.get_avatar_url().await.unwrap();
101+
assert_eq!(res_avatar_url, None);
102+
let res_display_name = account.get_display_name().await.unwrap();
103+
assert_eq!(res_display_name.as_deref(), Some(display_name));
104+
let res_value = account
105+
.fetch_profile_field_of(user_id.to_owned(), ProfileFieldName::TimeZone)
106+
.await
107+
.unwrap();
108+
assert_matches!(res_value, Some(ProfileFieldValue::TimeZone(res_tz)));
109+
assert_eq!(res_tz, tz);
110+
let res_tz =
111+
account.fetch_profile_field_of_static::<TimeZone>(user_id.to_owned()).await.unwrap();
112+
assert_eq!(res_tz.as_deref(), Some(tz));
113+
}

0 commit comments

Comments
 (0)