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