matrix_sdk/account.rs
1// Copyright 2020 Damir Jelić
2// Copyright 2020 The Matrix.org Foundation C.I.C.
3// Copyright 2022 Kévin Commaille
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17use futures_core::Stream;
18use futures_util::{StreamExt, stream};
19#[cfg(feature = "experimental-element-recent-emojis")]
20use itertools::Itertools;
21#[cfg(feature = "experimental-element-recent-emojis")]
22use js_int::uint;
23#[cfg(feature = "experimental-element-recent-emojis")]
24use matrix_sdk_base::recent_emojis::RecentEmojisContent;
25use matrix_sdk_base::{
26 StateStoreDataKey, StateStoreDataValue,
27 media::{MediaFormat, MediaRequestParameters},
28 store::StateStoreExt,
29};
30use mime::Mime;
31#[cfg(feature = "experimental-element-recent-emojis")]
32use ruma::api::client::config::set_global_account_data::v3::Request as UpdateGlobalAccountDataRequest;
33use ruma::{
34 ClientSecret, MxcUri, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, SessionId, UInt, UserId,
35 api::client::{
36 account::{
37 add_3pid, change_password, deactivate, delete_3pid, get_3pids,
38 request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
39 },
40 config::{get_global_account_data, set_global_account_data},
41 error::ErrorKind,
42 profile::{
43 get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
44 },
45 uiaa::AuthData,
46 },
47 assign,
48 events::{
49 AnyGlobalAccountDataEventContent, GlobalAccountDataEvent, GlobalAccountDataEventContent,
50 GlobalAccountDataEventType, StaticEventContent,
51 ignored_user_list::{IgnoredUser, IgnoredUserListEventContent},
52 media_preview_config::{
53 InviteAvatars, MediaPreviewConfigEventContent, MediaPreviews,
54 UnstableMediaPreviewConfigEventContent,
55 },
56 push_rules::PushRulesEventContent,
57 room::MediaSource,
58 },
59 push::Ruleset,
60 serde::Raw,
61 thirdparty::Medium,
62};
63use serde::Deserialize;
64use tracing::error;
65
66use crate::{Client, Error, Result, config::RequestConfig};
67
68/// The maximum number of recent emojis that should be stored and loaded.
69#[cfg(feature = "experimental-element-recent-emojis")]
70const MAX_RECENT_EMOJI_COUNT: usize = 100;
71
72/// A high-level API to manage the client owner's account.
73///
74/// All the methods on this struct send a request to the homeserver.
75#[derive(Debug, Clone)]
76pub struct Account {
77 /// The underlying HTTP client.
78 client: Client,
79}
80
81impl Account {
82 /// The maximum number of visited room identifiers to keep in the state
83 /// store.
84 const VISITED_ROOMS_LIMIT: usize = 20;
85
86 pub(crate) fn new(client: Client) -> Self {
87 Self { client }
88 }
89
90 /// Get the display name of the account.
91 ///
92 /// # Examples
93 ///
94 /// ```no_run
95 /// # use matrix_sdk::Client;
96 /// # use url::Url;
97 /// # async {
98 /// # let homeserver = Url::parse("http://example.com")?;
99 /// let user = "example";
100 /// let client = Client::new(homeserver).await?;
101 /// client.matrix_auth().login_username(user, "password").send().await?;
102 ///
103 /// if let Some(name) = client.account().get_display_name().await? {
104 /// println!("Logged in as user '{user}' with display name '{name}'");
105 /// }
106 /// # anyhow::Ok(()) };
107 /// ```
108 pub async fn get_display_name(&self) -> Result<Option<String>> {
109 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)
115 }
116
117 /// Set the display name of the account.
118 ///
119 /// # Examples
120 ///
121 /// ```no_run
122 /// # use matrix_sdk::Client;
123 /// # use url::Url;
124 /// # async {
125 /// # let homeserver = Url::parse("http://example.com")?;
126 /// let user = "example";
127 /// let client = Client::new(homeserver).await?;
128 /// client.matrix_auth().login_username(user, "password").send().await?;
129 ///
130 /// client.account().set_display_name(Some("Alice")).await?;
131 /// # anyhow::Ok(()) };
132 /// ```
133 pub async fn set_display_name(&self, name: Option<&str>) -> Result<()> {
134 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
135 #[allow(deprecated)]
136 let request =
137 set_display_name::v3::Request::new(user_id.to_owned(), name.map(ToOwned::to_owned));
138 self.client.send(request).await?;
139 Ok(())
140 }
141
142 /// Get the MXC URI of the account's avatar, if set.
143 ///
144 /// This always sends a request to the server to retrieve this information.
145 /// If successful, this fills the cache, and makes it so that
146 /// [`Self::get_cached_avatar_url`] will always return something.
147 ///
148 /// # Examples
149 ///
150 /// ```no_run
151 /// # use matrix_sdk::Client;
152 /// # use url::Url;
153 /// # async {
154 /// # let homeserver = Url::parse("http://example.com")?;
155 /// # let user = "example";
156 /// let client = Client::new(homeserver).await?;
157 /// client.matrix_auth().login_username(user, "password").send().await?;
158 ///
159 /// if let Some(url) = client.account().get_avatar_url().await? {
160 /// println!("Your avatar's mxc url is {url}");
161 /// }
162 /// # anyhow::Ok(()) };
163 /// ```
164 pub async fn get_avatar_url(&self) -> Result<Option<OwnedMxcUri>> {
165 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());
170
171 let response = self.client.send(request).with_request_config(config).await?;
172 if let Some(url) = response.avatar_url.clone() {
173 // If an avatar is found cache it.
174 let _ = self
175 .client
176 .state_store()
177 .set_kv_data(
178 StateStoreDataKey::UserAvatarUrl(user_id),
179 StateStoreDataValue::UserAvatarUrl(url),
180 )
181 .await;
182 } else {
183 // If there is no avatar the user has removed it and we uncache it.
184 let _ = self
185 .client
186 .state_store()
187 .remove_kv_data(StateStoreDataKey::UserAvatarUrl(user_id))
188 .await;
189 }
190 Ok(response.avatar_url)
191 }
192
193 /// Get the URL of the account's avatar, if is stored in cache.
194 pub async fn get_cached_avatar_url(&self) -> Result<Option<OwnedMxcUri>> {
195 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
196 let data = self
197 .client
198 .state_store()
199 .get_kv_data(StateStoreDataKey::UserAvatarUrl(user_id))
200 .await?;
201 Ok(data.map(|v| v.into_user_avatar_url().expect("Session data is not a user avatar url")))
202 }
203
204 /// Set the MXC URI of the account's avatar.
205 ///
206 /// The avatar is unset if `url` is `None`.
207 pub async fn set_avatar_url(&self, url: Option<&MxcUri>) -> Result<()> {
208 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
209 #[allow(deprecated)]
210 let request =
211 set_avatar_url::v3::Request::new(user_id.to_owned(), url.map(ToOwned::to_owned));
212 self.client.send(request).await?;
213 Ok(())
214 }
215
216 /// Get the account's avatar, if set.
217 ///
218 /// Returns the avatar.
219 ///
220 /// If a thumbnail is requested no guarantee on the size of the image is
221 /// given.
222 ///
223 /// # Arguments
224 ///
225 /// * `format` - The desired format of the avatar.
226 ///
227 /// # Examples
228 ///
229 /// ```no_run
230 /// # use matrix_sdk::Client;
231 /// # use matrix_sdk::ruma::room_id;
232 /// # use matrix_sdk::media::MediaFormat;
233 /// # use url::Url;
234 /// # async {
235 /// # let homeserver = Url::parse("http://example.com")?;
236 /// # let user = "example";
237 /// let client = Client::new(homeserver).await?;
238 /// client.matrix_auth().login_username(user, "password").send().await?;
239 ///
240 /// if let Some(avatar) = client.account().get_avatar(MediaFormat::File).await?
241 /// {
242 /// std::fs::write("avatar.png", avatar);
243 /// }
244 /// # anyhow::Ok(()) };
245 /// ```
246 pub async fn get_avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
247 if let Some(url) = self.get_avatar_url().await? {
248 let request = MediaRequestParameters { source: MediaSource::Plain(url), format };
249 Ok(Some(self.client.media().get_media_content(&request, true).await?))
250 } else {
251 Ok(None)
252 }
253 }
254
255 /// Upload and set the account's avatar.
256 ///
257 /// This will upload the data produced by the reader to the homeserver's
258 /// content repository, and set the user's avatar to the MXC URI for the
259 /// uploaded file.
260 ///
261 /// This is a convenience method for calling [`Media::upload()`],
262 /// followed by [`Account::set_avatar_url()`].
263 ///
264 /// Returns the MXC URI of the uploaded avatar.
265 ///
266 /// # Examples
267 ///
268 /// ```no_run
269 /// # use std::fs;
270 /// # use matrix_sdk::Client;
271 /// # use url::Url;
272 /// # async {
273 /// # let homeserver = Url::parse("http://localhost:8080")?;
274 /// # let client = Client::new(homeserver).await?;
275 /// let image = fs::read("/home/example/selfie.jpg")?;
276 ///
277 /// client.account().upload_avatar(&mime::IMAGE_JPEG, image).await?;
278 /// # anyhow::Ok(()) };
279 /// ```
280 ///
281 /// [`Media::upload()`]: crate::Media::upload
282 pub async fn upload_avatar(&self, content_type: &Mime, data: Vec<u8>) -> Result<OwnedMxcUri> {
283 let upload_response = self.client.media().upload(content_type, data, None).await?;
284 self.set_avatar_url(Some(&upload_response.content_uri)).await?;
285 Ok(upload_response.content_uri)
286 }
287
288 /// Get the profile of this account.
289 ///
290 /// Allows to get all the profile data in a single call.
291 ///
292 /// # Examples
293 ///
294 /// ```no_run
295 /// # use matrix_sdk::Client;
296 /// use ruma::api::client::profile::{AvatarUrl, DisplayName};
297 /// # use url::Url;
298 /// # async {
299 /// # let homeserver = Url::parse("http://localhost:8080")?;
300 /// # let client = Client::new(homeserver).await?;
301 ///
302 /// let profile = client.account().fetch_user_profile().await?;
303 /// let display_name = profile.get_static::<DisplayName>()?;
304 /// let avatar_url = profile.get_static::<AvatarUrl>()?;
305 ///
306 /// println!("You are '{display_name:?}' with avatar '{avatar_url:?}'");
307 /// # anyhow::Ok(()) };
308 /// ```
309 pub async fn fetch_user_profile(&self) -> Result<get_profile::v3::Response> {
310 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
311 self.fetch_user_profile_of(user_id).await
312 }
313
314 /// Get the profile for a given user id
315 ///
316 /// # Arguments
317 ///
318 /// * `user_id` the matrix id this function downloads the profile for
319 pub async fn fetch_user_profile_of(
320 &self,
321 user_id: &UserId,
322 ) -> Result<get_profile::v3::Response> {
323 let request = get_profile::v3::Request::new(user_id.to_owned());
324 Ok(self
325 .client
326 .send(request)
327 .with_request_config(RequestConfig::short_retry().force_auth())
328 .await?)
329 }
330
331 /// Change the password of the account.
332 ///
333 /// # Arguments
334 ///
335 /// * `new_password` - The new password to set.
336 ///
337 /// * `auth_data` - This request uses the [User-Interactive Authentication
338 /// API][uiaa]. The first request needs to set this to `None` and will
339 /// always fail with an [`UiaaResponse`]. The response will contain
340 /// information for the interactive auth and the same request needs to be
341 /// made but this time with some `auth_data` provided.
342 ///
343 /// # Returns
344 ///
345 /// This method might return an [`ErrorKind::WeakPassword`] error if the new
346 /// password is considered insecure by the homeserver, with details about
347 /// the strength requirements in the error's message.
348 ///
349 /// # Examples
350 ///
351 /// ```no_run
352 /// # use matrix_sdk::Client;
353 /// # use matrix_sdk::ruma::{
354 /// # api::client::{
355 /// # account::change_password::v3::{Request as ChangePasswordRequest},
356 /// # uiaa::{AuthData, Dummy},
357 /// # },
358 /// # assign,
359 /// # };
360 /// # use url::Url;
361 /// # async {
362 /// # let homeserver = Url::parse("http://localhost:8080")?;
363 /// # let client = Client::new(homeserver).await?;
364 /// client.account().change_password(
365 /// "myverysecretpassword",
366 /// Some(AuthData::Dummy(Dummy::new())),
367 /// ).await?;
368 /// # anyhow::Ok(()) };
369 /// ```
370 /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
371 /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
372 /// [`ErrorKind::WeakPassword`]: ruma::api::client::error::ErrorKind::WeakPassword
373 pub async fn change_password(
374 &self,
375 new_password: &str,
376 auth_data: Option<AuthData>,
377 ) -> Result<change_password::v3::Response> {
378 let request = assign!(change_password::v3::Request::new(new_password.to_owned()), {
379 auth: auth_data,
380 });
381 Ok(self.client.send(request).await?)
382 }
383
384 /// Deactivate this account definitively.
385 ///
386 /// # Arguments
387 ///
388 /// * `id_server` - The identity server from which to unbind the user’s
389 /// [Third Party Identifiers][3pid].
390 ///
391 /// * `auth_data` - This request uses the [User-Interactive Authentication
392 /// API][uiaa]. The first request needs to set this to `None` and will
393 /// always fail with an [`UiaaResponse`]. The response will contain
394 /// information for the interactive auth and the same request needs to be
395 /// made but this time with some `auth_data` provided.
396 ///
397 /// * `erase` - Whether the user would like their content to be erased as
398 /// much as possible from the server.
399 ///
400 /// # Examples
401 ///
402 /// ```no_run
403 /// # use matrix_sdk::Client;
404 /// # use matrix_sdk::ruma::{
405 /// # api::client::{
406 /// # account::change_password::v3::{Request as ChangePasswordRequest},
407 /// # uiaa::{AuthData, Dummy},
408 /// # },
409 /// # assign,
410 /// # };
411 /// # use url::Url;
412 /// # async {
413 /// # let homeserver = Url::parse("http://localhost:8080")?;
414 /// # let client = Client::new(homeserver).await?;
415 /// # let account = client.account();
416 /// let response = account.deactivate(None, None, false).await;
417 ///
418 /// // Proceed with UIAA.
419 /// # anyhow::Ok(()) };
420 /// ```
421 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
422 /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
423 /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
424 pub async fn deactivate(
425 &self,
426 id_server: Option<&str>,
427 auth_data: Option<AuthData>,
428 erase_data: bool,
429 ) -> Result<deactivate::v3::Response> {
430 let request = assign!(deactivate::v3::Request::new(), {
431 id_server: id_server.map(ToOwned::to_owned),
432 auth: auth_data,
433 erase: erase_data,
434 });
435 Ok(self.client.send(request).await?)
436 }
437
438 /// Get the registered [Third Party Identifiers][3pid] on the homeserver of
439 /// the account.
440 ///
441 /// These 3PIDs may be used by the homeserver to authenticate the user
442 /// during sensitive operations.
443 ///
444 /// # Examples
445 ///
446 /// ```no_run
447 /// # use matrix_sdk::Client;
448 /// # use url::Url;
449 /// # async {
450 /// # let homeserver = Url::parse("http://localhost:8080")?;
451 /// # let client = Client::new(homeserver).await?;
452 /// let threepids = client.account().get_3pids().await?.threepids;
453 ///
454 /// for threepid in threepids {
455 /// println!(
456 /// "Found 3PID '{}' of type '{}'",
457 /// threepid.address, threepid.medium
458 /// );
459 /// }
460 /// # anyhow::Ok(()) };
461 /// ```
462 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
463 pub async fn get_3pids(&self) -> Result<get_3pids::v3::Response> {
464 let request = get_3pids::v3::Request::new();
465 Ok(self.client.send(request).await?)
466 }
467
468 /// Request a token to validate an email address as a [Third Party
469 /// Identifier][3pid].
470 ///
471 /// This is the first step in registering an email address as 3PID. Next,
472 /// call [`Account::add_3pid()`] with the same `client_secret` and the
473 /// returned `sid`.
474 ///
475 /// # Arguments
476 ///
477 /// * `client_secret` - A client-generated secret string used to protect
478 /// this session.
479 ///
480 /// * `email` - The email address to validate.
481 ///
482 /// * `send_attempt` - The attempt number. This number needs to be
483 /// incremented if you want to request another token for the same
484 /// validation.
485 ///
486 /// # Returns
487 ///
488 /// * `sid` - The session ID to be used in following requests for this 3PID.
489 ///
490 /// * `submit_url` - If present, the user will submit the token to the
491 /// client, that must send it to this URL. If not, the client will not be
492 /// involved in the token submission.
493 ///
494 /// This method might return an [`ErrorKind::ThreepidInUse`] error if the
495 /// email address is already registered for this account or another, or an
496 /// [`ErrorKind::ThreepidDenied`] error if it is denied.
497 ///
498 /// # Examples
499 ///
500 /// ```no_run
501 /// # use matrix_sdk::Client;
502 /// # use matrix_sdk::ruma::{ClientSecret, uint};
503 /// # use url::Url;
504 /// # async {
505 /// # let homeserver = Url::parse("http://localhost:8080")?;
506 /// # let client = Client::new(homeserver).await?;
507 /// # let account = client.account();
508 /// # let secret = ClientSecret::parse("secret")?;
509 /// let token_response = account
510 /// .request_3pid_email_token(&secret, "john@matrix.org", uint!(0))
511 /// .await?;
512 ///
513 /// // Wait for the user to confirm that the token was submitted or prompt
514 /// // the user for the token and send it to submit_url.
515 ///
516 /// let uiaa_response =
517 /// account.add_3pid(&secret, &token_response.sid, None).await;
518 ///
519 /// // Proceed with UIAA.
520 /// # anyhow::Ok(()) };
521 /// ```
522 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
523 /// [`ErrorKind::ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse
524 /// [`ErrorKind::ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied
525 pub async fn request_3pid_email_token(
526 &self,
527 client_secret: &ClientSecret,
528 email: &str,
529 send_attempt: UInt,
530 ) -> Result<request_3pid_management_token_via_email::v3::Response> {
531 let request = request_3pid_management_token_via_email::v3::Request::new(
532 client_secret.to_owned(),
533 email.to_owned(),
534 send_attempt,
535 );
536 Ok(self.client.send(request).await?)
537 }
538
539 /// Request a token to validate a phone number as a [Third Party
540 /// Identifier][3pid].
541 ///
542 /// This is the first step in registering a phone number as 3PID. Next,
543 /// call [`Account::add_3pid()`] with the same `client_secret` and the
544 /// returned `sid`.
545 ///
546 /// # Arguments
547 ///
548 /// * `client_secret` - A client-generated secret string used to protect
549 /// this session.
550 ///
551 /// * `country` - The two-letter uppercase ISO-3166-1 alpha-2 country code
552 /// that the number in phone_number should be parsed as if it were dialled
553 /// from.
554 ///
555 /// * `phone_number` - The phone number to validate.
556 ///
557 /// * `send_attempt` - The attempt number. This number needs to be
558 /// incremented if you want to request another token for the same
559 /// validation.
560 ///
561 /// # Returns
562 ///
563 /// * `sid` - The session ID to be used in following requests for this 3PID.
564 ///
565 /// * `submit_url` - If present, the user will submit the token to the
566 /// client, that must send it to this URL. If not, the client will not be
567 /// involved in the token submission.
568 ///
569 /// This method might return an [`ErrorKind::ThreepidInUse`] error if the
570 /// phone number is already registered for this account or another, or an
571 /// [`ErrorKind::ThreepidDenied`] error if it is denied.
572 ///
573 /// # Examples
574 ///
575 /// ```no_run
576 /// # use matrix_sdk::Client;
577 /// # use matrix_sdk::ruma::{ClientSecret, uint};
578 /// # use url::Url;
579 /// # async {
580 /// # let homeserver = Url::parse("http://localhost:8080")?;
581 /// # let client = Client::new(homeserver).await?;
582 /// # let account = client.account();
583 /// # let secret = ClientSecret::parse("secret")?;
584 /// let token_response = account
585 /// .request_3pid_msisdn_token(&secret, "FR", "0123456789", uint!(0))
586 /// .await?;
587 ///
588 /// // Wait for the user to confirm that the token was submitted or prompt
589 /// // the user for the token and send it to submit_url.
590 ///
591 /// let uiaa_response =
592 /// account.add_3pid(&secret, &token_response.sid, None).await;
593 ///
594 /// // Proceed with UIAA.
595 /// # anyhow::Ok(()) };
596 /// ```
597 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
598 /// [`ErrorKind::ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse
599 /// [`ErrorKind::ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied
600 pub async fn request_3pid_msisdn_token(
601 &self,
602 client_secret: &ClientSecret,
603 country: &str,
604 phone_number: &str,
605 send_attempt: UInt,
606 ) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
607 let request = request_3pid_management_token_via_msisdn::v3::Request::new(
608 client_secret.to_owned(),
609 country.to_owned(),
610 phone_number.to_owned(),
611 send_attempt,
612 );
613 Ok(self.client.send(request).await?)
614 }
615
616 /// Add a [Third Party Identifier][3pid] on the homeserver for this
617 /// account.
618 ///
619 /// This 3PID may be used by the homeserver to authenticate the user
620 /// during sensitive operations.
621 ///
622 /// This method should be called after
623 /// [`Account::request_3pid_email_token()`] or
624 /// [`Account::request_3pid_msisdn_token()`] to complete the 3PID
625 ///
626 /// # Arguments
627 ///
628 /// * `client_secret` - The same client secret used in
629 /// [`Account::request_3pid_email_token()`] or
630 /// [`Account::request_3pid_msisdn_token()`].
631 ///
632 /// * `sid` - The session ID returned in
633 /// [`Account::request_3pid_email_token()`] or
634 /// [`Account::request_3pid_msisdn_token()`].
635 ///
636 /// * `auth_data` - This request uses the [User-Interactive Authentication
637 /// API][uiaa]. The first request needs to set this to `None` and will
638 /// always fail with an [`UiaaResponse`]. The response will contain
639 /// information for the interactive auth and the same request needs to be
640 /// made but this time with some `auth_data` provided.
641 ///
642 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
643 /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
644 /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
645 pub async fn add_3pid(
646 &self,
647 client_secret: &ClientSecret,
648 sid: &SessionId,
649 auth_data: Option<AuthData>,
650 ) -> Result<add_3pid::v3::Response> {
651 #[rustfmt::skip] // rustfmt wants to merge the next two lines
652 let request =
653 assign!(add_3pid::v3::Request::new(client_secret.to_owned(), sid.to_owned()), {
654 auth: auth_data
655 });
656 Ok(self.client.send(request).await?)
657 }
658
659 /// Delete a [Third Party Identifier][3pid] from the homeserver for this
660 /// account.
661 ///
662 /// # Arguments
663 ///
664 /// * `address` - The 3PID being removed.
665 ///
666 /// * `medium` - The type of the 3PID.
667 ///
668 /// * `id_server` - The identity server to unbind from. If not provided, the
669 /// homeserver should unbind the 3PID from the identity server it was
670 /// bound to previously.
671 ///
672 /// # Returns
673 ///
674 /// * [`ThirdPartyIdRemovalStatus::Success`] if the 3PID was also unbound
675 /// from the identity server.
676 ///
677 /// * [`ThirdPartyIdRemovalStatus::NoSupport`] if the 3PID was not unbound
678 /// from the identity server. This can also mean that the 3PID was not
679 /// bound to an identity server in the first place.
680 ///
681 /// # Examples
682 ///
683 /// ```no_run
684 /// # use matrix_sdk::Client;
685 /// # use matrix_sdk::ruma::thirdparty::Medium;
686 /// # use matrix_sdk::ruma::api::client::account::ThirdPartyIdRemovalStatus;
687 /// # use url::Url;
688 /// # async {
689 /// # let homeserver = Url::parse("http://localhost:8080")?;
690 /// # let client = Client::new(homeserver).await?;
691 /// # let account = client.account();
692 /// match account
693 /// .delete_3pid("paul@matrix.org", Medium::Email, None)
694 /// .await?
695 /// .id_server_unbind_result
696 /// {
697 /// ThirdPartyIdRemovalStatus::Success => {
698 /// println!("3PID unbound from the Identity Server");
699 /// }
700 /// _ => println!("Could not unbind 3PID from the Identity Server"),
701 /// }
702 /// # anyhow::Ok(()) };
703 /// ```
704 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
705 /// [`ThirdPartyIdRemovalStatus::Success`]: ruma::api::client::account::ThirdPartyIdRemovalStatus::Success
706 /// [`ThirdPartyIdRemovalStatus::NoSupport`]: ruma::api::client::account::ThirdPartyIdRemovalStatus::NoSupport
707 pub async fn delete_3pid(
708 &self,
709 address: &str,
710 medium: Medium,
711 id_server: Option<&str>,
712 ) -> Result<delete_3pid::v3::Response> {
713 let request = assign!(delete_3pid::v3::Request::new(medium, address.to_owned()), {
714 id_server: id_server.map(ToOwned::to_owned),
715 });
716 Ok(self.client.send(request).await?)
717 }
718
719 /// Get the content of an account data event of statically-known type, from
720 /// storage.
721 ///
722 /// # Examples
723 ///
724 /// ```no_run
725 /// # use matrix_sdk::Client;
726 /// # async {
727 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
728 /// # let account = client.account();
729 /// use matrix_sdk::ruma::events::ignored_user_list::IgnoredUserListEventContent;
730 ///
731 /// let maybe_content = account.account_data::<IgnoredUserListEventContent>().await?;
732 /// if let Some(raw_content) = maybe_content {
733 /// let content = raw_content.deserialize()?;
734 /// println!("Ignored users:");
735 /// for user_id in content.ignored_users.keys() {
736 /// println!("- {user_id}");
737 /// }
738 /// }
739 /// # anyhow::Ok(()) };
740 /// ```
741 pub async fn account_data<C>(&self) -> Result<Option<Raw<C>>>
742 where
743 C: GlobalAccountDataEventContent + StaticEventContent<IsPrefix = ruma::events::False>,
744 {
745 get_raw_content(self.client.state_store().get_account_data_event_static::<C>().await?)
746 }
747
748 /// Get the content of an account data event of a given type, from storage.
749 pub async fn account_data_raw(
750 &self,
751 event_type: GlobalAccountDataEventType,
752 ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>> {
753 get_raw_content(self.client.state_store().get_account_data_event(event_type).await?)
754 }
755
756 /// Fetch a global account data event from the server.
757 ///
758 /// The content from the response will not be persisted in the store.
759 ///
760 /// Examples
761 ///
762 /// ```no_run
763 /// # use matrix_sdk::Client;
764 /// # async {
765 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
766 /// # let account = client.account();
767 /// use matrix_sdk::ruma::events::{ignored_user_list::IgnoredUserListEventContent, GlobalAccountDataEventType};
768 ///
769 /// if let Some(raw_content) = account.fetch_account_data(GlobalAccountDataEventType::IgnoredUserList).await? {
770 /// let content = raw_content.deserialize_as_unchecked::<IgnoredUserListEventContent>()?;
771 ///
772 /// println!("Ignored users:");
773 ///
774 /// for user_id in content.ignored_users.keys() {
775 /// println!("- {user_id}");
776 /// }
777 /// }
778 /// # anyhow::Ok(()) };
779 pub async fn fetch_account_data(
780 &self,
781 event_type: GlobalAccountDataEventType,
782 ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>> {
783 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
784
785 let request = get_global_account_data::v3::Request::new(own_user.to_owned(), event_type);
786
787 match self.client.send(request).await {
788 Ok(r) => Ok(Some(r.account_data)),
789 Err(e) => {
790 if let Some(kind) = e.client_api_error_kind() {
791 if kind == &ErrorKind::NotFound { Ok(None) } else { Err(e.into()) }
792 } else {
793 Err(e.into())
794 }
795 }
796 }
797 }
798
799 /// Fetch an account data event of statically-known type from the server.
800 pub async fn fetch_account_data_static<C>(&self) -> Result<Option<Raw<C>>>
801 where
802 C: GlobalAccountDataEventContent + StaticEventContent<IsPrefix = ruma::events::False>,
803 {
804 Ok(self.fetch_account_data(C::TYPE.into()).await?.map(Raw::cast_unchecked))
805 }
806
807 /// Set the given account data event.
808 ///
809 /// # Examples
810 ///
811 /// ```no_run
812 /// # use matrix_sdk::Client;
813 /// # async {
814 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
815 /// # let account = client.account();
816 /// use matrix_sdk::ruma::{
817 /// events::ignored_user_list::{IgnoredUser, IgnoredUserListEventContent},
818 /// user_id,
819 /// };
820 ///
821 /// let mut content = account
822 /// .account_data::<IgnoredUserListEventContent>()
823 /// .await?
824 /// .map(|c| c.deserialize())
825 /// .transpose()?
826 /// .unwrap_or_default();
827 /// content
828 /// .ignored_users
829 /// .insert(user_id!("@foo:bar.com").to_owned(), IgnoredUser::new());
830 /// account.set_account_data(content).await?;
831 /// # anyhow::Ok(()) };
832 /// ```
833 pub async fn set_account_data<T>(
834 &self,
835 content: T,
836 ) -> Result<set_global_account_data::v3::Response>
837 where
838 T: GlobalAccountDataEventContent,
839 {
840 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
841
842 let request = set_global_account_data::v3::Request::new(own_user.to_owned(), &content)?;
843
844 Ok(self.client.send(request).await?)
845 }
846
847 /// Set the given raw account data event.
848 pub async fn set_account_data_raw(
849 &self,
850 event_type: GlobalAccountDataEventType,
851 content: Raw<AnyGlobalAccountDataEventContent>,
852 ) -> Result<set_global_account_data::v3::Response> {
853 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
854
855 let request =
856 set_global_account_data::v3::Request::new_raw(own_user.to_owned(), event_type, content);
857
858 Ok(self.client.send(request).await?)
859 }
860
861 /// Marks the room identified by `room_id` as a "direct chat" with each
862 /// user in `user_ids`.
863 ///
864 /// # Arguments
865 ///
866 /// * `room_id` - The room ID of the direct message room.
867 /// * `user_ids` - The user IDs to be associated with this direct message
868 /// room.
869 pub async fn mark_as_dm(&self, room_id: &RoomId, user_ids: &[OwnedUserId]) -> Result<()> {
870 use ruma::events::direct::DirectEventContent;
871
872 // This function does a read/update/store of an account data event stored on the
873 // homeserver. We first fetch the existing account data event, the event
874 // contains a map which gets updated by this method, finally we upload the
875 // modified event.
876 //
877 // To prevent multiple calls to this method trying to update the map of DMs same
878 // time, and thus trampling on each other we introduce a lock which acts
879 // as a semaphore.
880 let _guard = self.client.locks().mark_as_dm_lock.lock().await;
881
882 // Now we need to mark the room as a DM for ourselves, we fetch the
883 // existing `m.direct` event and append the room to the list of DMs we
884 // have with this user.
885
886 // We are fetching the content from the server because we currently can't rely
887 // on `/sync` giving us the correct data in a timely manner.
888 let raw_content = self.fetch_account_data_static::<DirectEventContent>().await?;
889
890 let mut content = if let Some(raw_content) = raw_content {
891 // Log the error and pass it upwards if we fail to deserialize the m.direct
892 // event.
893 raw_content.deserialize().map_err(|err| {
894 error!("unable to deserialize m.direct event content; aborting request to mark {room_id} as dm: {err}");
895 err
896 })?
897 } else {
898 // If there was no m.direct event server-side, create a default one.
899 Default::default()
900 };
901
902 for user_id in user_ids {
903 content.entry(user_id.into()).or_default().push(room_id.to_owned());
904 }
905
906 // TODO: We should probably save the fact that we need to send this out
907 // because otherwise we might end up in a state where we have a DM that
908 // isn't marked as one.
909 self.set_account_data(content).await?;
910
911 Ok(())
912 }
913
914 /// Adds the given user ID to the account's ignore list.
915 pub async fn ignore_user(&self, user_id: &UserId) -> Result<()> {
916 let own_user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
917 if user_id == own_user_id {
918 return Err(Error::CantIgnoreLoggedInUser);
919 }
920
921 let mut ignored_user_list = self.get_ignored_user_list_event_content().await?;
922 ignored_user_list.ignored_users.insert(user_id.to_owned(), IgnoredUser::new());
923
924 self.set_account_data(ignored_user_list).await?;
925
926 // In theory, we should also clear some caches here, because they may include
927 // events sent by the ignored user. In practice, we expect callers to
928 // take care of this, or subsystems to listen to user list changes and
929 // clear caches accordingly.
930
931 Ok(())
932 }
933
934 /// Removes the given user ID from the account's ignore list.
935 pub async fn unignore_user(&self, user_id: &UserId) -> Result<()> {
936 let mut ignored_user_list = self.get_ignored_user_list_event_content().await?;
937
938 // Only update account data if the user was ignored in the first place.
939 if ignored_user_list.ignored_users.remove(user_id).is_some() {
940 self.set_account_data(ignored_user_list).await?;
941 }
942
943 // See comment in `ignore_user`.
944 Ok(())
945 }
946
947 async fn get_ignored_user_list_event_content(&self) -> Result<IgnoredUserListEventContent> {
948 let ignored_user_list = self
949 .account_data::<IgnoredUserListEventContent>()
950 .await?
951 .map(|c| c.deserialize())
952 .transpose()?
953 .unwrap_or_default();
954 Ok(ignored_user_list)
955 }
956
957 /// Get the current push rules from storage.
958 ///
959 /// If no push rules event was found, or it fails to deserialize, a ruleset
960 /// with the server-default push rules is returned.
961 ///
962 /// Panics if called when the client is not logged in.
963 pub async fn push_rules(&self) -> Result<Ruleset> {
964 Ok(self
965 .account_data::<PushRulesEventContent>()
966 .await?
967 .and_then(|r| match r.deserialize() {
968 Ok(r) => Some(r.global),
969 Err(e) => {
970 error!("Push rules event failed to deserialize: {e}");
971 None
972 }
973 })
974 .unwrap_or_else(|| {
975 Ruleset::server_default(
976 self.client.user_id().expect("The client should be logged in"),
977 )
978 }))
979 }
980
981 /// Retrieves the user's recently visited room list
982 pub async fn get_recently_visited_rooms(&self) -> Result<Vec<OwnedRoomId>> {
983 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
984 let data = self
985 .client
986 .state_store()
987 .get_kv_data(StateStoreDataKey::RecentlyVisitedRooms(user_id))
988 .await?;
989
990 Ok(data
991 .map(|v| {
992 v.into_recently_visited_rooms()
993 .expect("Session data is not a list of recently visited rooms")
994 })
995 .unwrap_or_default())
996 }
997
998 /// Moves/inserts the given room to the front of the recently visited list
999 pub async fn track_recently_visited_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
1000 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1001
1002 // Get the previously stored recently visited rooms
1003 let mut recently_visited_rooms = self.get_recently_visited_rooms().await?;
1004
1005 // Remove all other occurrences of the new room_id
1006 recently_visited_rooms.retain(|r| r != &room_id);
1007
1008 // And insert it as the most recent
1009 recently_visited_rooms.insert(0, room_id);
1010
1011 // Cap the whole list to the VISITED_ROOMS_LIMIT
1012 recently_visited_rooms.truncate(Self::VISITED_ROOMS_LIMIT);
1013
1014 let data = StateStoreDataValue::RecentlyVisitedRooms(recently_visited_rooms);
1015 self.client
1016 .state_store()
1017 .set_kv_data(StateStoreDataKey::RecentlyVisitedRooms(user_id), data)
1018 .await?;
1019 Ok(())
1020 }
1021
1022 /// Observes the media preview configuration.
1023 ///
1024 /// This value is linked to the [MSC 4278](https://github.com/matrix-org/matrix-spec-proposals/pull/4278) which is still in an unstable state.
1025 ///
1026 /// This will return the initial value of the configuration and a stream
1027 /// that will yield new values as they are received.
1028 ///
1029 /// The initial value is the one that was stored in the account data
1030 /// when the client was started.
1031 /// and the following code is using a temporary solution until we know which
1032 /// Matrix version will support the stable type.
1033 ///
1034 /// # Examples
1035 ///
1036 /// ```no_run
1037 /// # use futures_util::{pin_mut, StreamExt};
1038 /// # use matrix_sdk::Client;
1039 /// # use matrix_sdk::ruma::events::media_preview_config::MediaPreviews;
1040 /// # use url::Url;
1041 /// # async {
1042 /// # let homeserver = Url::parse("http://localhost:8080")?;
1043 /// # let client = Client::new(homeserver).await?;
1044 /// let account = client.account();
1045 ///
1046 /// let (initial_config, config_stream) =
1047 /// account.observe_media_preview_config().await?;
1048 ///
1049 /// println!("Initial media preview config: {:?}", initial_config);
1050 ///
1051 /// pin_mut!(config_stream);
1052 /// while let Some(new_config) = config_stream.next().await {
1053 /// println!("Updated media preview config: {:?}", new_config);
1054 /// }
1055 /// # anyhow::Ok(()) };
1056 /// ```
1057 pub async fn observe_media_preview_config(
1058 &self,
1059 ) -> Result<
1060 (
1061 Option<MediaPreviewConfigEventContent>,
1062 impl Stream<Item = MediaPreviewConfigEventContent> + use<>,
1063 ),
1064 Error,
1065 > {
1066 // We need to create two observers, one for the stable event and one for the
1067 // unstable and combine them into a single stream.
1068 let first_observer = self
1069 .client
1070 .observe_events::<GlobalAccountDataEvent<MediaPreviewConfigEventContent>, ()>();
1071
1072 let stream = first_observer.subscribe().map(|event| event.0.content);
1073
1074 let second_observer = self
1075 .client
1076 .observe_events::<GlobalAccountDataEvent<UnstableMediaPreviewConfigEventContent>, ()>();
1077
1078 let second_stream = second_observer.subscribe().map(|event| event.0.content.0);
1079
1080 let mut combined_stream = stream::select(stream, second_stream);
1081
1082 let result_stream = async_stream::stream! {
1083 // The observers need to be alive for the individual streams to be alive, so let's now
1084 // create a stream that takes ownership of them.
1085 let _first_observer = first_observer;
1086 let _second_observer = second_observer;
1087
1088 while let Some(item) = combined_stream.next().await {
1089 yield item
1090 }
1091 };
1092
1093 // We need to get the initial value of the media preview config event
1094 // we do this after creating the observers to make sure that we don't
1095 // create a race condition
1096 let initial_value = self.get_media_preview_config_event_content().await?;
1097
1098 Ok((initial_value, result_stream))
1099 }
1100
1101 /// Fetch the media preview configuration event content from the server.
1102 ///
1103 /// Will check first for the stable event and then for the unstable one.
1104 pub async fn fetch_media_preview_config_event_content(
1105 &self,
1106 ) -> Result<Option<MediaPreviewConfigEventContent>> {
1107 // First we check if there is a value in the stable event
1108 let media_preview_config =
1109 self.fetch_account_data_static::<MediaPreviewConfigEventContent>().await?;
1110
1111 let media_preview_config = if let Some(media_preview_config) = media_preview_config {
1112 Some(media_preview_config)
1113 } else {
1114 // If there is no value in the stable event, we check the unstable
1115 self.fetch_account_data_static::<UnstableMediaPreviewConfigEventContent>()
1116 .await?
1117 .map(Raw::cast)
1118 };
1119
1120 // We deserialize the content of the event, if is not found we return the
1121 // default
1122 let media_preview_config = media_preview_config.and_then(|value| value.deserialize().ok());
1123
1124 Ok(media_preview_config)
1125 }
1126
1127 /// Get the media preview configuration event content stored in the cache.
1128 ///
1129 /// Will check first for the stable event and then for the unstable one.
1130 pub async fn get_media_preview_config_event_content(
1131 &self,
1132 ) -> Result<Option<MediaPreviewConfigEventContent>> {
1133 let media_preview_config = self
1134 .account_data::<MediaPreviewConfigEventContent>()
1135 .await?
1136 .and_then(|r| r.deserialize().ok());
1137
1138 if let Some(media_preview_config) = media_preview_config {
1139 Ok(Some(media_preview_config))
1140 } else {
1141 Ok(self
1142 .account_data::<UnstableMediaPreviewConfigEventContent>()
1143 .await?
1144 .and_then(|r| r.deserialize().ok())
1145 .map(Into::into))
1146 }
1147 }
1148
1149 /// Set the media previews display policy in the timeline.
1150 ///
1151 /// This will always use the unstable event until we know which Matrix
1152 /// version will support it.
1153 pub async fn set_media_previews_display_policy(&self, policy: MediaPreviews) -> Result<()> {
1154 let mut media_preview_config =
1155 self.fetch_media_preview_config_event_content().await?.unwrap_or_default();
1156 media_preview_config.media_previews = Some(policy);
1157
1158 // Updating the unstable account data
1159 let unstable_media_preview_config =
1160 UnstableMediaPreviewConfigEventContent::from(media_preview_config);
1161 self.set_account_data(unstable_media_preview_config).await?;
1162 Ok(())
1163 }
1164
1165 /// Set the display policy for avatars in invite requests.
1166 ///
1167 /// This will always use the unstable event until we know which matrix
1168 /// version will support it.
1169 pub async fn set_invite_avatars_display_policy(&self, policy: InviteAvatars) -> Result<()> {
1170 let mut media_preview_config =
1171 self.fetch_media_preview_config_event_content().await?.unwrap_or_default();
1172 media_preview_config.invite_avatars = Some(policy);
1173
1174 // Updating the unstable account data
1175 let unstable_media_preview_config =
1176 UnstableMediaPreviewConfigEventContent::from(media_preview_config);
1177 self.set_account_data(unstable_media_preview_config).await?;
1178 Ok(())
1179 }
1180
1181 /// Adds a recently used emoji to the list and uploads the updated
1182 /// `io.element.recent_emoji` content to the global account data.
1183 ///
1184 /// Before updating the data, it'll fetch it from the homeserver, to make
1185 /// sure the updated values are always used. However, note this could still
1186 /// result in a race condition if it's used concurrently.
1187 #[cfg(feature = "experimental-element-recent-emojis")]
1188 pub async fn add_recent_emoji(&self, emoji: &str) -> Result<()> {
1189 let Some(user_id) = self.client.user_id() else {
1190 return Err(Error::AuthenticationRequired);
1191 };
1192 let mut recent_emojis = self.get_recent_emojis(true).await?;
1193
1194 let index = recent_emojis.iter().position(|(unicode, _)| unicode == emoji);
1195
1196 // Truncate to the max allowed size, which will remove any emojis that
1197 // haven't been used in a very long time. This will also ease the pressure on
1198 // `remove` and `insert` shifting lots of elements in the list
1199 recent_emojis.truncate(MAX_RECENT_EMOJI_COUNT);
1200
1201 // Remove the emoji from the list if it was present and get it's `count` value
1202 let count = if let Some(index) = index { recent_emojis.remove(index).1 } else { uint!(0) };
1203
1204 // Insert the emoji with the updated count at the start of the list, so it's
1205 // considered the most recently used emoji
1206 recent_emojis.insert(0, (emoji.to_owned(), count + uint!(1)));
1207
1208 // If the item was a new one, the list will now be `MAX_RECENT_EMOJI_COUNT` + 1,
1209 // so truncate it again (this is a no-op if it already has the right size)
1210 recent_emojis.truncate(MAX_RECENT_EMOJI_COUNT);
1211
1212 let request = UpdateGlobalAccountDataRequest::new(
1213 user_id.to_owned(),
1214 &RecentEmojisContent::new(recent_emojis),
1215 )?;
1216 let _ = self.client.send(request).await?;
1217
1218 Ok(())
1219 }
1220
1221 /// Gets the list of recently used emojis from the `io.element.recent_emoji`
1222 /// global account data.
1223 ///
1224 /// If the `refresh` param is `true`, the data will be fetched from the
1225 /// homeserver instead of the local storage.
1226 #[cfg(feature = "experimental-element-recent-emojis")]
1227 pub async fn get_recent_emojis(&self, refresh: bool) -> Result<Vec<(String, UInt)>> {
1228 let content = if refresh {
1229 let Some(user_id) = self.client.user_id() else {
1230 return Err(Error::AuthenticationRequired);
1231 };
1232 let event_type = RecentEmojisContent::default().event_type();
1233 let response = self
1234 .client
1235 .send(get_global_account_data::v3::Request::new(
1236 user_id.to_owned(),
1237 event_type.clone(),
1238 ))
1239 .await?;
1240 let content = response.account_data.cast_unchecked().deserialize()?;
1241 Some(content)
1242 } else {
1243 self.client
1244 .state_store()
1245 .get_account_data_event_static::<RecentEmojisContent>()
1246 .await?
1247 .map(|raw| raw.deserialize().map(|event| event.content))
1248 .transpose()?
1249 };
1250
1251 if let Some(content) = content {
1252 // Sort by count, descending. For items with the same count, since they were
1253 // previously ordered by recency in the list, more recent emojis will be
1254 // returned first.
1255 let sorted_emojis = content
1256 .recent_emoji
1257 .into_iter()
1258 // Items with higher counts should be first
1259 .sorted_by(|(_, count_a), (_, count_b)| count_b.cmp(count_a))
1260 // Make sure we take only up to MAX_RECENT_EMOJI_COUNT
1261 .take(MAX_RECENT_EMOJI_COUNT)
1262 .collect();
1263 Ok(sorted_emojis)
1264 } else {
1265 Ok(Vec::new())
1266 }
1267 }
1268}
1269
1270fn get_raw_content<Ev, C>(raw: Option<Raw<Ev>>) -> Result<Option<Raw<C>>> {
1271 #[derive(Deserialize)]
1272 #[serde(bound = "C: Sized")] // Replace default Deserialize bound
1273 struct GetRawContent<C> {
1274 content: Raw<C>,
1275 }
1276
1277 Ok(raw
1278 .map(|event| event.deserialize_as_unchecked::<GetRawContent<C>>())
1279 .transpose()?
1280 .map(|get_raw| get_raw.content))
1281}
1282
1283#[cfg(test)]
1284mod tests {
1285 use assert_matches::assert_matches;
1286 use matrix_sdk_test::async_test;
1287
1288 use crate::{Error, test_utils::client::MockClientBuilder};
1289
1290 #[async_test]
1291 async fn test_dont_ignore_oneself() {
1292 let client = MockClientBuilder::new(None).build().await;
1293
1294 // It's forbidden to ignore the logged-in user.
1295 assert_matches!(
1296 client.account().ignore_user(client.user_id().unwrap()).await,
1297 Err(Error::CantIgnoreLoggedInUser)
1298 );
1299 }
1300}
1301
1302#[cfg(test)]
1303#[cfg(feature = "experimental-element-recent-emojis")]
1304mod test_recent_emojis {
1305 use js_int::{UInt, uint};
1306 use matrix_sdk_base::recent_emojis::RecentEmojisContent;
1307 use matrix_sdk_test::{async_test, event_factory::EventFactory};
1308
1309 use crate::{
1310 account::MAX_RECENT_EMOJI_COUNT, config::SyncSettings, test_utils::mocks::MatrixMockServer,
1311 };
1312
1313 #[async_test]
1314 async fn test_recent_emojis() {
1315 let server = MatrixMockServer::new().await;
1316 let client = server.client_builder().build().await;
1317 let user_id = client.user_id().expect("session_id");
1318
1319 server
1320 .mock_add_recent_emojis()
1321 .ok(user_id)
1322 .named("Update recent emojis global account data")
1323 .mock_once()
1324 .mount()
1325 .await;
1326
1327 let recent_emojis = client.account().get_recent_emojis(false).await.expect("recent emojis");
1328 assert!(recent_emojis.is_empty());
1329
1330 let emoji_list = vec![
1331 (":/".to_owned(), uint!(1)),
1332 (":)".to_owned(), uint!(12)),
1333 (":D".to_owned(), uint!(12)),
1334 ];
1335
1336 server
1337 .mock_get_recent_emojis()
1338 .ok(user_id, emoji_list.clone())
1339 .named("Fetch recent emojis")
1340 .mock_once()
1341 .mount()
1342 .await;
1343
1344 client.account().add_recent_emoji(":)").await.expect("adding emoji");
1345
1346 server
1347 .mock_sync()
1348 .ok(|builder| {
1349 let content = RecentEmojisContent::new(emoji_list);
1350 let event_builder = EventFactory::new().global_account_data(content);
1351 builder.add_global_account_data(event_builder);
1352 })
1353 .named("Sync")
1354 .mount()
1355 .await;
1356
1357 client.sync_once(SyncSettings::default()).await.expect("sync failed");
1358
1359 let recent_emojis = client.account().get_recent_emojis(false).await.expect("recent emojis");
1360
1361 // Assert size
1362 assert_eq!(recent_emojis.len(), 3);
1363
1364 // Assert ordering: first by times used, then by recency
1365 assert_eq!(recent_emojis[0].0, ":)");
1366 assert_eq!(recent_emojis[1].0, ":D");
1367 assert_eq!(recent_emojis[2].0, ":/");
1368 }
1369
1370 #[async_test]
1371 async fn test_max_recent_emoji_count() {
1372 let server = MatrixMockServer::new().await;
1373 let client = server.client_builder().build().await;
1374 let user_id = client.user_id().expect("session_id");
1375
1376 // This list is > the MAX_RECENT_EMOJI_COUNT
1377 let long_emoji_list = (0..MAX_RECENT_EMOJI_COUNT * 2)
1378 .map(|i| (i.to_string(), uint!(1)))
1379 .collect::<Vec<(String, UInt)>>();
1380
1381 // Initially we locally don't have any emojis
1382 let recent_emojis = client.account().get_recent_emojis(false).await.expect("recent emojis");
1383 assert!(recent_emojis.is_empty());
1384
1385 server
1386 .mock_get_recent_emojis()
1387 .ok(user_id, long_emoji_list.clone())
1388 .named("Fetch recent emojis")
1389 .expect(3)
1390 .mount()
1391 .await;
1392
1393 // Now with a list of emojis longer than the max count, we fetch the emoji list
1394 let recent_emojis = client.account().get_recent_emojis(true).await.expect("recent emojis");
1395
1396 // It should only return until the max count
1397 assert_eq!(recent_emojis.len(), MAX_RECENT_EMOJI_COUNT);
1398 assert_eq!(recent_emojis, long_emoji_list[..MAX_RECENT_EMOJI_COUNT]);
1399
1400 // Simulate the logic we expect when adding a new emoji:
1401 // 1. Remove the existing emoji if present
1402 // 2. Increase its count value and insert it at the front.
1403 // 3. Truncate at MAX_RECENT_EMOJI_COUNT
1404 let expected_updated_emoji_list = {
1405 let mut list = long_emoji_list.clone();
1406 let item = list.remove(50);
1407 list.insert(0, (item.0, item.1 + uint!(1)));
1408 list.truncate(MAX_RECENT_EMOJI_COUNT);
1409 list
1410 };
1411
1412 // Now if we add a new emoji that was not in the list, the last one in the list
1413 // should be gone
1414 server
1415 .mock_add_recent_emojis()
1416 .match_emojis_in_request_body(expected_updated_emoji_list)
1417 .ok(user_id)
1418 .named("Update recent emojis global account data with existing emoji")
1419 .mock_once()
1420 .mount()
1421 .await;
1422
1423 client.account().add_recent_emoji("50").await.expect("adding emoji");
1424
1425 // Do the same, but now with a new emoji that wasn't previously in the list
1426 let expected_updated_emoji_list = {
1427 let mut list = long_emoji_list.clone();
1428 let item = (":D".to_owned(), uint!(1));
1429 list.insert(0, item);
1430 list.truncate(MAX_RECENT_EMOJI_COUNT);
1431 list
1432 };
1433
1434 // We should still have `MAX_RECENT_EMOJI_COUNT` items
1435 server
1436 .mock_add_recent_emojis()
1437 .match_emojis_in_request_body(expected_updated_emoji_list)
1438 .ok(user_id)
1439 .named("Update recent emojis global account data with new emoji")
1440 .mock_once()
1441 .mount()
1442 .await;
1443
1444 client.account().add_recent_emoji(":D").await.expect("adding emoji");
1445 }
1446}