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