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