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 `set_profile_field`, but
152 // we still need to use it in case it is `None` and the server doesn't support
153 // 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 `set_profile_field`, but
254 // we still need to use it in case it is `None` and the server doesn't support
255 // 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()`],
310 /// followed 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 ///
385 /// * `field` - The name of the profile field to get.
386 ///
387 /// # Returns
388 ///
389 /// Returns an error if the request fails or if deserialization of the
390 /// response fails.
391 ///
392 /// If the field is not set, the server should respond with an error with an
393 /// [`ErrorCode::NotFound`], but it might also respond with an empty
394 /// response, which would result in `Ok(None)`. Note that this error code
395 /// might also mean that the given user ID doesn't exist.
396 ///
397 /// [`ErrorCode::NotFound`]: ruma::api::error::ErrorCode::NotFound
398 pub async fn fetch_profile_field_of(
399 &self,
400 user_id: OwnedUserId,
401 field: ProfileFieldName,
402 ) -> Result<Option<ProfileFieldValue>> {
403 let request = get_profile_field::v3::Request::new(user_id, field);
404 let response = self
405 .client
406 .send(request)
407 .with_request_config(RequestConfig::short_retry().force_auth())
408 .await?;
409
410 Ok(response.value)
411 }
412
413 /// Get the given statically-known field from the given user's profile.
414 ///
415 /// # Arguments
416 ///
417 /// * `user_id` - The ID of the user to get the profile field of.
418 ///
419 /// # Returns
420 ///
421 /// Returns an error if the request fails or if deserialization of the
422 /// response fails.
423 ///
424 /// If the field is not set, the server should respond with an error with an
425 /// [`ErrorCode::NotFound`], but it might also respond with an empty
426 /// response, which would result in `Ok(None)`. Note that this error code
427 /// might also mean that the given user ID doesn't exist.
428 ///
429 /// [`ErrorCode::NotFound`]: ruma::api::error::ErrorCode::NotFound
430 pub async fn fetch_profile_field_of_static<F>(
431 &self,
432 user_id: OwnedUserId,
433 ) -> Result<Option<F::Value>>
434 where
435 F: StaticProfileField
436 + std::fmt::Debug
437 + Clone
438 + SendOutsideWasm
439 + SyncOutsideWasm
440 + 'static,
441 F::Value: SendOutsideWasm + SyncOutsideWasm,
442 {
443 let request = get_profile_field::v3::Request::new_static::<F>(user_id);
444 let response = self
445 .client
446 .send(request)
447 .with_request_config(RequestConfig::short_retry().force_auth())
448 .await?;
449
450 Ok(response.value)
451 }
452
453 /// Set the user's status (MSC4426 `m.status` profile field).
454 ///
455 /// Replaces any existing status. Use [`Self::clear_status`] to remove it.
456 ///
457 /// # Arguments
458 ///
459 /// * `emoji` - the status emoji. The MSC limits this to 32 bytes; not
460 /// enforced client-side.
461 /// * `text` - the status text. The MSC limits this to 256 bytes; not
462 /// enforced client-side.
463 #[cfg(feature = "unstable-msc4426")]
464 pub async fn set_status(&self, emoji: String, text: String) -> Result<()> {
465 let value = StatusProfileField::new(text, emoji);
466 self.set_profile_field(ProfileFieldValue::Status(value)).await
467 }
468
469 /// Clear the user's status (deletes the MSC4426 `m.status` profile field).
470 #[cfg(feature = "unstable-msc4426")]
471 pub async fn clear_status(&self) -> Result<()> {
472 self.delete_profile_field(ProfileFieldName::Status).await
473 }
474
475 /// Set the user's call indicator (MSC4426 `m.call` profile field).
476 ///
477 /// # Arguments
478 ///
479 /// * `call_joined_ts` - when the user joined the current call, in seconds
480 /// since the Unix epoch. `None` if the joined time isn't known.
481 #[cfg(feature = "unstable-msc4426")]
482 pub async fn set_call(&self, call_joined_ts: Option<SecondsSinceUnixEpoch>) -> Result<()> {
483 let mut value = CallProfileField::new();
484 value.call_joined_ts = call_joined_ts;
485 self.set_profile_field(ProfileFieldValue::Call(value)).await
486 }
487
488 /// Clear the user's call indicator (deletes the MSC4426 `m.call` profile
489 /// field).
490 #[cfg(feature = "unstable-msc4426")]
491 pub async fn clear_call(&self) -> Result<()> {
492 self.delete_profile_field(ProfileFieldName::Call).await
493 }
494
495 /// Set the given field of our own user's profile.
496 ///
497 /// [`Client::homeserver_capabilities()`] should be called first to check it
498 /// the field can be set on the homeserver.
499 ///
500 /// # Arguments
501 ///
502 /// * `value` - The value of the profile field to set.
503 ///
504 /// # Returns
505 ///
506 /// Returns an error if the request fails.
507 pub async fn set_profile_field(&self, value: ProfileFieldValue) -> Result<()> {
508 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
509 let request = set_profile_field::v3::Request::new(user_id.to_owned(), value.clone());
510 self.client.send(request).await?;
511
512 let mut changes = UserProfileChanges::new();
513 changes.insert_updated_value(value);
514 self.own_profile_updated(changes).await;
515
516 Ok(())
517 }
518
519 /// Delete the given field of our own user's profile.
520 ///
521 /// [`Client::homeserver_capabilities()`] should be called first to check it
522 /// the field can be modified on the homeserver.
523 ///
524 /// # Arguments
525 ///
526 /// * `field` - The profile field to delete.
527 ///
528 /// # Returns
529 ///
530 /// Returns an error if the server doesn't support extended profile fields
531 /// of if the request fails in some other way.
532 pub async fn delete_profile_field(&self, field: ProfileFieldName) -> Result<()> {
533 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
534 let request = delete_profile_field::v3::Request::new(user_id.to_owned(), field.clone());
535 self.client.send(request).await?;
536
537 let mut changes = UserProfileChanges::new();
538 changes.removed.push(field);
539 self.own_profile_updated(changes).await;
540
541 Ok(())
542 }
543
544 /// Apply the given changes to the locally stored copy of our own profile,
545 /// so they are observable before the next sync reflects them.
546 async fn own_profile_updated(&self, changes: UserProfileChanges) {
547 match self.client.is_global_profile_sync_enabled().await {
548 Ok(true) => {}
549
550 Ok(false) => {
551 debug!("Server doesn't support global profile sync: skip the local echo.");
552 return;
553 }
554
555 Err(error) => {
556 warn!(?error, "Unknown support for global profile sync: skipping the local echo.");
557 return;
558 }
559 }
560
561 if let Err(error) =
562 self.client.base_client().own_profile_updated(UserProfileUpdate::Updated(changes)).await
563 {
564 // The homeserver has already accepted the changes at this point, so we
565 // only need to log the failure.
566 warn!(?error, "Failed to update the locally stored copy of our own profile");
567 }
568 }
569
570 /// Change the password of the account.
571 ///
572 /// # Arguments
573 ///
574 /// * `new_password` - The new password to set.
575 ///
576 /// * `auth_data` - This request uses the [User-Interactive Authentication
577 /// API][uiaa]. The first request needs to set this to `None` and will
578 /// always fail with an [`UiaaResponse`]. The response will contain
579 /// information for the interactive auth and the same request needs to be
580 /// made but this time with some `auth_data` provided.
581 ///
582 /// # Returns
583 ///
584 /// This method might return an [`ErrorKind::WeakPassword`] error if the new
585 /// password is considered insecure by the homeserver, with details about
586 /// the strength requirements in the error's message.
587 ///
588 /// # Examples
589 ///
590 /// ```no_run
591 /// # use matrix_sdk::Client;
592 /// # use matrix_sdk::ruma::{
593 /// # api::client::{
594 /// # account::change_password::v3::{Request as ChangePasswordRequest},
595 /// # uiaa::{AuthData, Dummy},
596 /// # },
597 /// # assign,
598 /// # };
599 /// # use url::Url;
600 /// # async {
601 /// # let homeserver = Url::parse("http://localhost:8080")?;
602 /// # let client = Client::new(homeserver).await?;
603 /// client.account().change_password(
604 /// "myverysecretpassword",
605 /// Some(AuthData::Dummy(Dummy::new())),
606 /// ).await?;
607 /// # anyhow::Ok(()) };
608 /// ```
609 /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
610 /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
611 /// [`ErrorKind::WeakPassword`]: ruma::api::error::ErrorKind::WeakPassword
612 pub async fn change_password(
613 &self,
614 new_password: &str,
615 auth_data: Option<AuthData>,
616 ) -> Result<change_password::v3::Response> {
617 let request = assign!(change_password::v3::Request::new(new_password.to_owned()), {
618 auth: auth_data,
619 });
620 Ok(self.client.send(request).await?)
621 }
622
623 /// Deactivate this account definitively.
624 ///
625 /// # Arguments
626 ///
627 /// * `id_server` - The identity server from which to unbind the user’s
628 /// [Third Party Identifiers][3pid].
629 ///
630 /// * `auth_data` - This request uses the [User-Interactive Authentication
631 /// API][uiaa]. The first request needs to set this to `None` and will
632 /// always fail with an [`UiaaResponse`]. The response will contain
633 /// information for the interactive auth and the same request needs to be
634 /// made but this time with some `auth_data` provided.
635 ///
636 /// * `erase` - Whether the user would like their content to be erased as
637 /// much as possible from the server.
638 ///
639 /// # Examples
640 ///
641 /// ```no_run
642 /// # use matrix_sdk::Client;
643 /// # use matrix_sdk::ruma::{
644 /// # api::client::{
645 /// # account::change_password::v3::{Request as ChangePasswordRequest},
646 /// # uiaa::{AuthData, Dummy},
647 /// # },
648 /// # assign,
649 /// # };
650 /// # use url::Url;
651 /// # async {
652 /// # let homeserver = Url::parse("http://localhost:8080")?;
653 /// # let client = Client::new(homeserver).await?;
654 /// # let account = client.account();
655 /// let response = account.deactivate(None, None, false).await;
656 ///
657 /// // Proceed with UIAA.
658 /// # anyhow::Ok(()) };
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 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
702 pub async fn get_3pids(&self) -> Result<get_3pids::v3::Response> {
703 let request = get_3pids::v3::Request::new();
704 Ok(self.client.send(request).await?)
705 }
706
707 /// Request a token to validate an email address as a [Third Party
708 /// Identifier][3pid].
709 ///
710 /// This is the first step in registering an email address as 3PID. Next,
711 /// call [`Account::add_3pid()`] with the same `client_secret` and the
712 /// returned `sid`.
713 ///
714 /// # Arguments
715 ///
716 /// * `client_secret` - A client-generated secret string used to protect
717 /// this session.
718 ///
719 /// * `email` - The email address to validate.
720 ///
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 ///
729 /// * `submit_url` - If present, the user will submit the token to the
730 /// client, that must send it to this URL. If not, the client will not be
731 /// involved in the token submission.
732 ///
733 /// This method might return an [`ErrorKind::ThreepidInUse`] error if the
734 /// email address is already registered for this account or another, or an
735 /// [`ErrorKind::ThreepidDenied`] error if it is denied.
736 ///
737 /// # Examples
738 ///
739 /// ```no_run
740 /// # use matrix_sdk::Client;
741 /// # use matrix_sdk::ruma::{ClientSecret, uint};
742 /// # use url::Url;
743 /// # async {
744 /// # let homeserver = Url::parse("http://localhost:8080")?;
745 /// # let client = Client::new(homeserver).await?;
746 /// # let account = client.account();
747 /// # let secret = ClientSecret::parse("secret")?;
748 /// let token_response = account
749 /// .request_3pid_email_token(&secret, "john@matrix.org", uint!(0))
750 /// .await?;
751 ///
752 /// // Wait for the user to confirm that the token was submitted or prompt
753 /// // the user for the token and send it to submit_url.
754 ///
755 /// let uiaa_response =
756 /// account.add_3pid(&secret, &token_response.sid, None).await;
757 ///
758 /// // Proceed with UIAA.
759 /// # anyhow::Ok(()) };
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 [Third Party
779 /// Identifier][3pid].
780 ///
781 /// This is the first step in registering a phone number as 3PID. Next,
782 /// call [`Account::add_3pid()`] with the same `client_secret` and the
783 /// returned `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 ///
796 /// * `send_attempt` - The attempt number. This number needs to be
797 /// incremented if you want to request another token for the same
798 /// validation.
799 ///
800 /// # Returns
801 ///
802 /// * `sid` - The session ID to be used in following requests for this 3PID.
803 ///
804 /// * `submit_url` - If present, the user will submit the token to the
805 /// client, that must send it to this URL. If not, the client will not be
806 /// involved in the token submission.
807 ///
808 /// This method might return an [`ErrorKind::ThreepidInUse`] error if the
809 /// phone number is already registered for this account or another, or an
810 /// [`ErrorKind::ThreepidDenied`] error if it is denied.
811 ///
812 /// # Examples
813 ///
814 /// ```no_run
815 /// # use matrix_sdk::Client;
816 /// # use matrix_sdk::ruma::{ClientSecret, uint};
817 /// # use url::Url;
818 /// # async {
819 /// # let homeserver = Url::parse("http://localhost:8080")?;
820 /// # let client = Client::new(homeserver).await?;
821 /// # let account = client.account();
822 /// # let secret = ClientSecret::parse("secret")?;
823 /// let token_response = account
824 /// .request_3pid_msisdn_token(&secret, "FR", "0123456789", uint!(0))
825 /// .await?;
826 ///
827 /// // Wait for the user to confirm that the token was submitted or prompt
828 /// // the user for the token and send it to submit_url.
829 ///
830 /// let uiaa_response =
831 /// account.add_3pid(&secret, &token_response.sid, None).await;
832 ///
833 /// // Proceed with UIAA.
834 /// # anyhow::Ok(()) };
835 /// ```
836 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
837 /// [`ErrorKind::ThreepidInUse`]: ruma::api::error::ErrorKind::ThreepidInUse
838 /// [`ErrorKind::ThreepidDenied`]: ruma::api::error::ErrorKind::ThreepidDenied
839 pub async fn request_3pid_msisdn_token(
840 &self,
841 client_secret: &ClientSecret,
842 country: &str,
843 phone_number: &str,
844 send_attempt: UInt,
845 ) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
846 let request = request_3pid_management_token_via_msisdn::v3::Request::new(
847 client_secret.to_owned(),
848 country.to_owned(),
849 phone_number.to_owned(),
850 send_attempt,
851 );
852 Ok(self.client.send(request).await?)
853 }
854
855 /// Add a [Third Party Identifier][3pid] on the homeserver for this
856 /// account.
857 ///
858 /// This 3PID may be used by the homeserver to authenticate the user
859 /// during sensitive operations.
860 ///
861 /// This method should be called after
862 /// [`Account::request_3pid_email_token()`] or
863 /// [`Account::request_3pid_msisdn_token()`] to complete the 3PID
864 ///
865 /// # Arguments
866 ///
867 /// * `client_secret` - The same client secret used in
868 /// [`Account::request_3pid_email_token()`] or
869 /// [`Account::request_3pid_msisdn_token()`].
870 ///
871 /// * `sid` - The session ID returned in
872 /// [`Account::request_3pid_email_token()`] or
873 /// [`Account::request_3pid_msisdn_token()`].
874 ///
875 /// * `auth_data` - This request uses the [User-Interactive Authentication
876 /// API][uiaa]. The first request needs to set this to `None` and will
877 /// always fail with an [`UiaaResponse`]. The response will contain
878 /// information for the interactive auth and the same request needs to be
879 /// made but this time with some `auth_data` provided.
880 ///
881 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
882 /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
883 /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
884 pub async fn add_3pid(
885 &self,
886 client_secret: &ClientSecret,
887 sid: &SessionId,
888 auth_data: Option<AuthData>,
889 ) -> Result<add_3pid::v3::Response> {
890 #[rustfmt::skip] // rustfmt wants to merge the next two lines
891 let request =
892 assign!(add_3pid::v3::Request::new(client_secret.to_owned(), sid.to_owned()), {
893 auth: auth_data
894 });
895 Ok(self.client.send(request).await?)
896 }
897
898 /// Delete a [Third Party Identifier][3pid] from the homeserver for this
899 /// account.
900 ///
901 /// # Arguments
902 ///
903 /// * `address` - The 3PID being removed.
904 ///
905 /// * `medium` - The type of the 3PID.
906 ///
907 /// * `id_server` - The identity server to unbind from. If not provided, the
908 /// homeserver should unbind the 3PID from the identity server it was
909 /// bound to previously.
910 ///
911 /// # Returns
912 ///
913 /// * [`ThirdPartyIdRemovalStatus::Success`] if the 3PID was also unbound
914 /// from the identity server.
915 ///
916 /// * [`ThirdPartyIdRemovalStatus::NoSupport`] if the 3PID was not unbound
917 /// from the identity server. This can also mean that the 3PID was not
918 /// bound to an identity server in the first place.
919 ///
920 /// # Examples
921 ///
922 /// ```no_run
923 /// # use matrix_sdk::Client;
924 /// # use matrix_sdk::ruma::thirdparty::Medium;
925 /// # use matrix_sdk::ruma::api::client::account::ThirdPartyIdRemovalStatus;
926 /// # use url::Url;
927 /// # async {
928 /// # let homeserver = Url::parse("http://localhost:8080")?;
929 /// # let client = Client::new(homeserver).await?;
930 /// # let account = client.account();
931 /// match account
932 /// .delete_3pid("paul@matrix.org", Medium::Email, None)
933 /// .await?
934 /// .id_server_unbind_result
935 /// {
936 /// ThirdPartyIdRemovalStatus::Success => {
937 /// println!("3PID unbound from the Identity Server");
938 /// }
939 /// _ => println!("Could not unbind 3PID from the Identity Server"),
940 /// }
941 /// # anyhow::Ok(()) };
942 /// ```
943 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
944 /// [`ThirdPartyIdRemovalStatus::Success`]: ruma::api::client::account::ThirdPartyIdRemovalStatus::Success
945 /// [`ThirdPartyIdRemovalStatus::NoSupport`]: ruma::api::client::account::ThirdPartyIdRemovalStatus::NoSupport
946 pub async fn delete_3pid(
947 &self,
948 address: &str,
949 medium: Medium,
950 id_server: Option<&str>,
951 ) -> Result<delete_3pid::v3::Response> {
952 let request = assign!(delete_3pid::v3::Request::new(medium, address.to_owned()), {
953 id_server: id_server.map(ToOwned::to_owned),
954 });
955 Ok(self.client.send(request).await?)
956 }
957
958 /// Get the content of an account data event of statically-known type, from
959 /// storage.
960 ///
961 /// # Examples
962 ///
963 /// ```no_run
964 /// # use matrix_sdk::Client;
965 /// # async {
966 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
967 /// # let account = client.account();
968 /// use matrix_sdk::ruma::events::ignored_user_list::IgnoredUserListEventContent;
969 ///
970 /// let maybe_content = account.account_data::<IgnoredUserListEventContent>().await?;
971 /// if let Some(raw_content) = maybe_content {
972 /// let content = raw_content.deserialize()?;
973 /// println!("Ignored users:");
974 /// for user_id in content.ignored_users.keys() {
975 /// println!("- {user_id}");
976 /// }
977 /// }
978 /// # anyhow::Ok(()) };
979 /// ```
980 pub async fn account_data<C>(&self) -> Result<Option<Raw<C>>>
981 where
982 C: GlobalAccountDataEventContent + StaticEventContent<IsPrefix = ruma::events::False>,
983 {
984 get_raw_content(self.client.state_store().get_account_data_event_static::<C>().await?)
985 }
986
987 /// Get the content of an account data event of a given type, from storage.
988 pub async fn account_data_raw(
989 &self,
990 event_type: GlobalAccountDataEventType,
991 ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>> {
992 get_raw_content(self.client.state_store().get_account_data_event(event_type).await?)
993 }
994
995 /// Fetch a global account data event from the server.
996 ///
997 /// The content from the response will not be persisted in the store.
998 ///
999 /// Examples
1000 ///
1001 /// ```no_run
1002 /// # use matrix_sdk::Client;
1003 /// # async {
1004 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
1005 /// # let account = client.account();
1006 /// use matrix_sdk::ruma::events::{ignored_user_list::IgnoredUserListEventContent, GlobalAccountDataEventType};
1007 ///
1008 /// if let Some(raw_content) = account.fetch_account_data(GlobalAccountDataEventType::IgnoredUserList).await? {
1009 /// let content = raw_content.deserialize_as_unchecked::<IgnoredUserListEventContent>()?;
1010 ///
1011 /// println!("Ignored users:");
1012 ///
1013 /// for user_id in content.ignored_users.keys() {
1014 /// println!("- {user_id}");
1015 /// }
1016 /// }
1017 /// # anyhow::Ok(()) };
1018 pub async fn fetch_account_data(
1019 &self,
1020 event_type: GlobalAccountDataEventType,
1021 ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>> {
1022 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1023
1024 let request = get_global_account_data::v3::Request::new(own_user.to_owned(), event_type);
1025
1026 match self.client.send(request).await {
1027 Ok(r) => Ok(Some(r.account_data)),
1028 Err(e) => {
1029 if let Some(kind) = e.client_api_error_kind() {
1030 if kind == &ErrorKind::NotFound { Ok(None) } else { Err(e.into()) }
1031 } else {
1032 Err(e.into())
1033 }
1034 }
1035 }
1036 }
1037
1038 /// Fetch an account data event of statically-known type from the server.
1039 pub async fn fetch_account_data_static<C>(&self) -> Result<Option<Raw<C>>>
1040 where
1041 C: GlobalAccountDataEventContent + StaticEventContent<IsPrefix = ruma::events::False>,
1042 {
1043 Ok(self.fetch_account_data(C::TYPE.into()).await?.map(Raw::cast_unchecked))
1044 }
1045
1046 /// Set the given account data event.
1047 ///
1048 /// # Examples
1049 ///
1050 /// ```no_run
1051 /// # use matrix_sdk::Client;
1052 /// # async {
1053 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
1054 /// # let account = client.account();
1055 /// use matrix_sdk::ruma::{
1056 /// events::ignored_user_list::{IgnoredUser, IgnoredUserListEventContent},
1057 /// user_id,
1058 /// };
1059 ///
1060 /// let mut content = account
1061 /// .account_data::<IgnoredUserListEventContent>()
1062 /// .await?
1063 /// .map(|c| c.deserialize())
1064 /// .transpose()?
1065 /// .unwrap_or_default();
1066 /// content
1067 /// .ignored_users
1068 /// .insert(user_id!("@foo:bar.com").to_owned(), IgnoredUser::new());
1069 /// account.set_account_data(content).await?;
1070 /// # anyhow::Ok(()) };
1071 /// ```
1072 pub async fn set_account_data<T>(
1073 &self,
1074 content: T,
1075 ) -> Result<set_global_account_data::v3::Response>
1076 where
1077 T: GlobalAccountDataEventContent,
1078 {
1079 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1080
1081 let request = set_global_account_data::v3::Request::new(own_user.to_owned(), &content)?;
1082
1083 Ok(self.client.send(request).await?)
1084 }
1085
1086 /// Set the given raw account data event.
1087 pub async fn set_account_data_raw(
1088 &self,
1089 event_type: GlobalAccountDataEventType,
1090 content: Raw<AnyGlobalAccountDataEventContent>,
1091 ) -> Result<set_global_account_data::v3::Response> {
1092 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1093
1094 let request =
1095 set_global_account_data::v3::Request::new_raw(own_user.to_owned(), event_type, content);
1096
1097 Ok(self.client.send(request).await?)
1098 }
1099
1100 /// Marks the room identified by `room_id` as a "direct chat" with each
1101 /// user in `user_ids`.
1102 ///
1103 /// # Arguments
1104 ///
1105 /// * `room_id` - The room ID of the direct message room.
1106 /// * `user_ids` - The user IDs to be associated with this direct message
1107 /// room.
1108 pub async fn mark_as_dm(&self, room_id: &RoomId, user_ids: &[OwnedUserId]) -> Result<()> {
1109 use ruma::events::direct::DirectEventContent;
1110
1111 // This function does a read/update/store of an account data event stored on the
1112 // homeserver. We first fetch the existing account data event, the event
1113 // contains a map which gets updated by this method, finally we upload the
1114 // modified event.
1115 //
1116 // To prevent multiple calls to this method trying to update the map of DMs same
1117 // time, and thus trampling on each other we introduce a lock which acts
1118 // as a semaphore.
1119 let _guard = self.client.locks().mark_as_dm_lock.lock().await;
1120
1121 // Now we need to mark the room as a DM for ourselves, we fetch the
1122 // existing `m.direct` event and append the room to the list of DMs we
1123 // have with this user.
1124
1125 // We are fetching the content from the server because we currently can't rely
1126 // on `/sync` giving us the correct data in a timely manner.
1127 let raw_content = self.fetch_account_data_static::<DirectEventContent>().await?;
1128
1129 let mut content = if let Some(raw_content) = raw_content {
1130 // Log the error and pass it upwards if we fail to deserialize the m.direct
1131 // event.
1132 raw_content.deserialize().map_err(|err| {
1133 error!("unable to deserialize m.direct event content; aborting request to mark {room_id} as dm: {err}");
1134 err
1135 })?
1136 } else {
1137 // If there was no m.direct event server-side, create a default one.
1138 Default::default()
1139 };
1140
1141 for user_id in user_ids {
1142 content.entry(user_id.into()).or_default().push(room_id.to_owned());
1143 }
1144
1145 // TODO: We should probably save the fact that we need to send this out
1146 // because otherwise we might end up in a state where we have a DM that
1147 // isn't marked as one.
1148 self.set_account_data(content).await?;
1149
1150 Ok(())
1151 }
1152
1153 /// Adds the given user ID to the account's ignore list.
1154 pub async fn ignore_user(&self, user_id: &UserId) -> Result<()> {
1155 let own_user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1156 if user_id == own_user_id {
1157 return Err(Error::CantIgnoreLoggedInUser);
1158 }
1159
1160 let mut ignored_user_list = self.get_ignored_user_list_event_content().await?;
1161 ignored_user_list.ignored_users.insert(user_id.to_owned(), IgnoredUser::new());
1162
1163 self.set_account_data(ignored_user_list).await?;
1164
1165 // In theory, we should also clear some caches here, because they may include
1166 // events sent by the ignored user. In practice, we expect callers to
1167 // take care of this, or subsystems to listen to user list changes and
1168 // clear caches accordingly.
1169
1170 Ok(())
1171 }
1172
1173 /// Removes the given user ID from the account's ignore list.
1174 pub async fn unignore_user(&self, user_id: &UserId) -> Result<()> {
1175 let mut ignored_user_list = self.get_ignored_user_list_event_content().await?;
1176
1177 // Only update account data if the user was ignored in the first place.
1178 if ignored_user_list.ignored_users.remove(user_id).is_some() {
1179 self.set_account_data(ignored_user_list).await?;
1180 }
1181
1182 // See comment in `ignore_user`.
1183 Ok(())
1184 }
1185
1186 async fn get_ignored_user_list_event_content(&self) -> Result<IgnoredUserListEventContent> {
1187 let ignored_user_list = self
1188 .account_data::<IgnoredUserListEventContent>()
1189 .await?
1190 .map(|c| c.deserialize())
1191 .transpose()?
1192 .unwrap_or_default();
1193 Ok(ignored_user_list)
1194 }
1195
1196 /// Get the current push rules from storage.
1197 ///
1198 /// If no push rules event was found, or it fails to deserialize, a ruleset
1199 /// with the server-default push rules is returned.
1200 ///
1201 /// Panics if called when the client is not logged in.
1202 pub async fn push_rules(&self) -> Result<Ruleset> {
1203 Ok(self
1204 .account_data::<PushRulesEventContent>()
1205 .await?
1206 .and_then(|r| match r.deserialize() {
1207 Ok(r) => Some(r.global),
1208 Err(e) => {
1209 error!("Push rules event failed to deserialize: {e}");
1210 None
1211 }
1212 })
1213 .unwrap_or_else(|| {
1214 Ruleset::server_default(
1215 self.client.user_id().expect("The client should be logged in"),
1216 )
1217 }))
1218 }
1219
1220 /// Retrieves the user's recently visited room list
1221 pub async fn get_recently_visited_rooms(&self) -> Result<Vec<OwnedRoomId>> {
1222 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1223 let data = self
1224 .client
1225 .state_store()
1226 .get_kv_data(StateStoreDataKey::RecentlyVisitedRooms(user_id))
1227 .await?;
1228
1229 Ok(data
1230 .map(|v| {
1231 v.into_recently_visited_rooms()
1232 .expect("Session data is not a list of recently visited rooms")
1233 })
1234 .unwrap_or_default())
1235 }
1236
1237 /// Moves/inserts the given room to the front of the recently visited list
1238 pub async fn track_recently_visited_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
1239 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1240
1241 // Get the previously stored recently visited rooms
1242 let mut recently_visited_rooms = self.get_recently_visited_rooms().await?;
1243
1244 // Remove all other occurrences of the new room_id
1245 recently_visited_rooms.retain(|r| r != &room_id);
1246
1247 // And insert it as the most recent
1248 recently_visited_rooms.insert(0, room_id);
1249
1250 // Cap the whole list to the VISITED_ROOMS_LIMIT
1251 recently_visited_rooms.truncate(Self::VISITED_ROOMS_LIMIT);
1252
1253 let data = StateStoreDataValue::RecentlyVisitedRooms(recently_visited_rooms);
1254 self.client
1255 .state_store()
1256 .set_kv_data(StateStoreDataKey::RecentlyVisitedRooms(user_id), data)
1257 .await?;
1258 Ok(())
1259 }
1260
1261 /// Observes the media preview configuration.
1262 ///
1263 /// 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.
1264 ///
1265 /// This will return the initial value of the configuration and a stream
1266 /// that will yield new values as they are received.
1267 ///
1268 /// The initial value is the one that was stored in the account data
1269 /// when the client was started.
1270 /// and the following code is using a temporary solution until we know which
1271 /// Matrix version will support the stable type.
1272 ///
1273 /// # Examples
1274 ///
1275 /// ```no_run
1276 /// # use futures_util::{pin_mut, StreamExt};
1277 /// # use matrix_sdk::Client;
1278 /// # use matrix_sdk::ruma::events::media_preview_config::MediaPreviews;
1279 /// # use url::Url;
1280 /// # async {
1281 /// # let homeserver = Url::parse("http://localhost:8080")?;
1282 /// # let client = Client::new(homeserver).await?;
1283 /// let account = client.account();
1284 ///
1285 /// let (initial_config, config_stream) =
1286 /// account.observe_media_preview_config().await?;
1287 ///
1288 /// println!("Initial media preview config: {:?}", initial_config);
1289 ///
1290 /// pin_mut!(config_stream);
1291 /// while let Some(new_config) = config_stream.next().await {
1292 /// println!("Updated media preview config: {:?}", new_config);
1293 /// }
1294 /// # anyhow::Ok(()) };
1295 /// ```
1296 pub async fn observe_media_preview_config(
1297 &self,
1298 ) -> Result<
1299 (
1300 Option<MediaPreviewConfigEventContent>,
1301 impl Stream<Item = MediaPreviewConfigEventContent> + use<>,
1302 ),
1303 Error,
1304 > {
1305 // We need to create two observers, one for the stable event and one for the
1306 // unstable and combine them into a single stream.
1307 let first_observer = self
1308 .client
1309 .observe_events::<GlobalAccountDataEvent<MediaPreviewConfigEventContent>, ()>();
1310
1311 let stream = first_observer.subscribe().map(|event| event.0.content);
1312
1313 let second_observer = self
1314 .client
1315 .observe_events::<GlobalAccountDataEvent<UnstableMediaPreviewConfigEventContent>, ()>();
1316
1317 let second_stream = second_observer.subscribe().map(|event| event.0.content.0);
1318
1319 let mut combined_stream = stream::select(stream, second_stream);
1320
1321 let result_stream = async_stream::stream! {
1322 // The observers need to be alive for the individual streams to be alive, so let's now
1323 // create a stream that takes ownership of them.
1324 let _first_observer = first_observer;
1325 let _second_observer = second_observer;
1326
1327 while let Some(item) = combined_stream.next().await {
1328 yield item
1329 }
1330 };
1331
1332 // We need to get the initial value of the media preview config event
1333 // we do this after creating the observers to make sure that we don't
1334 // create a race condition
1335 let initial_value = self.get_media_preview_config_event_content().await?;
1336
1337 Ok((initial_value, result_stream))
1338 }
1339
1340 /// Fetch the media preview configuration event content from the server.
1341 ///
1342 /// Will check first for the stable event and then for the unstable one.
1343 pub async fn fetch_media_preview_config_event_content(
1344 &self,
1345 ) -> Result<Option<MediaPreviewConfigEventContent>> {
1346 // First we check if there is a value in the stable event
1347 let media_preview_config =
1348 self.fetch_account_data_static::<MediaPreviewConfigEventContent>().await?;
1349
1350 let media_preview_config = if let Some(media_preview_config) = media_preview_config {
1351 Some(media_preview_config)
1352 } else {
1353 // If there is no value in the stable event, we check the unstable
1354 self.fetch_account_data_static::<UnstableMediaPreviewConfigEventContent>()
1355 .await?
1356 .map(Raw::cast)
1357 };
1358
1359 // We deserialize the content of the event, if is not found we return the
1360 // default
1361 let media_preview_config = media_preview_config.and_then(|value| value.deserialize().ok());
1362
1363 Ok(media_preview_config)
1364 }
1365
1366 /// Get the media preview configuration event content stored in the cache.
1367 ///
1368 /// Will check first for the stable event and then for the unstable one.
1369 pub async fn get_media_preview_config_event_content(
1370 &self,
1371 ) -> Result<Option<MediaPreviewConfigEventContent>> {
1372 let media_preview_config = self
1373 .account_data::<MediaPreviewConfigEventContent>()
1374 .await?
1375 .and_then(|r| r.deserialize().ok());
1376
1377 if let Some(media_preview_config) = media_preview_config {
1378 Ok(Some(media_preview_config))
1379 } else {
1380 Ok(self
1381 .account_data::<UnstableMediaPreviewConfigEventContent>()
1382 .await?
1383 .and_then(|r| r.deserialize().ok())
1384 .map(Into::into))
1385 }
1386 }
1387
1388 /// Set the media previews display policy in the timeline.
1389 ///
1390 /// This will always use the unstable event until we know which Matrix
1391 /// version will support it.
1392 pub async fn set_media_previews_display_policy(&self, policy: MediaPreviews) -> Result<()> {
1393 let mut media_preview_config =
1394 self.fetch_media_preview_config_event_content().await?.unwrap_or_default();
1395 media_preview_config.media_previews = Some(policy);
1396
1397 // Updating the unstable account data
1398 let unstable_media_preview_config =
1399 UnstableMediaPreviewConfigEventContent::from(media_preview_config);
1400 self.set_account_data(unstable_media_preview_config).await?;
1401 Ok(())
1402 }
1403
1404 /// Set the display policy for avatars in invite requests.
1405 ///
1406 /// This will always use the unstable event until we know which matrix
1407 /// version will support it.
1408 pub async fn set_invite_avatars_display_policy(&self, policy: InviteAvatars) -> Result<()> {
1409 let mut media_preview_config =
1410 self.fetch_media_preview_config_event_content().await?.unwrap_or_default();
1411 media_preview_config.invite_avatars = Some(policy);
1412
1413 // Updating the unstable account data
1414 let unstable_media_preview_config =
1415 UnstableMediaPreviewConfigEventContent::from(media_preview_config);
1416 self.set_account_data(unstable_media_preview_config).await?;
1417 Ok(())
1418 }
1419
1420 /// Adds a recently used emoji to the list and uploads the updated
1421 /// `io.element.recent_emoji` content to the global account data.
1422 ///
1423 /// Before updating the data, it'll fetch it from the homeserver, to make
1424 /// sure the updated values are always used. However, note this could still
1425 /// result in a race condition if it's used concurrently.
1426 #[cfg(feature = "experimental-element-recent-emojis")]
1427 pub async fn add_recent_emoji(&self, emoji: &str) -> Result<()> {
1428 let Some(user_id) = self.client.user_id() else {
1429 return Err(Error::AuthenticationRequired);
1430 };
1431 let mut recent_emojis = self.get_recent_emojis(true).await?;
1432
1433 let index = recent_emojis.iter().position(|(unicode, _)| unicode == emoji);
1434
1435 // Truncate to the max allowed size, which will remove any emojis that
1436 // haven't been used in a very long time. This will also ease the pressure on
1437 // `remove` and `insert` shifting lots of elements in the list
1438 recent_emojis.truncate(MAX_RECENT_EMOJI_COUNT);
1439
1440 // Remove the emoji from the list if it was present and get it's `count` value
1441 let count = if let Some(index) = index { recent_emojis.remove(index).1 } else { uint!(0) };
1442
1443 // Insert the emoji with the updated count at the start of the list, so it's
1444 // considered the most recently used emoji
1445 recent_emojis.insert(0, (emoji.to_owned(), count + uint!(1)));
1446
1447 // If the item was a new one, the list will now be `MAX_RECENT_EMOJI_COUNT` + 1,
1448 // so truncate it again (this is a no-op if it already has the right size)
1449 recent_emojis.truncate(MAX_RECENT_EMOJI_COUNT);
1450
1451 let request = UpdateGlobalAccountDataRequest::new(
1452 user_id.to_owned(),
1453 &RecentEmojisContent::new(recent_emojis),
1454 )?;
1455 let _ = self.client.send(request).await?;
1456
1457 Ok(())
1458 }
1459
1460 /// Gets the list of recently used emojis from the `io.element.recent_emoji`
1461 /// global account data.
1462 ///
1463 /// If the `refresh` param is `true`, the data will be fetched from the
1464 /// homeserver instead of the local storage.
1465 #[cfg(feature = "experimental-element-recent-emojis")]
1466 pub async fn get_recent_emojis(&self, refresh: bool) -> Result<Vec<(String, UInt)>> {
1467 let content = if refresh {
1468 let Some(user_id) = self.client.user_id() else {
1469 return Err(Error::AuthenticationRequired);
1470 };
1471 let event_type = RecentEmojisContent::default().event_type();
1472 let response = self
1473 .client
1474 .send(get_global_account_data::v3::Request::new(
1475 user_id.to_owned(),
1476 event_type.clone(),
1477 ))
1478 .await?;
1479 let content = response.account_data.cast_unchecked().deserialize()?;
1480 Some(content)
1481 } else {
1482 self.client
1483 .state_store()
1484 .get_account_data_event_static::<RecentEmojisContent>()
1485 .await?
1486 .map(|raw| raw.deserialize().map(|event| event.content))
1487 .transpose()?
1488 };
1489
1490 if let Some(content) = content {
1491 // Sort by count, descending. For items with the same count, since they were
1492 // previously ordered by recency in the list, more recent emojis will be
1493 // returned first.
1494 let sorted_emojis = content
1495 .recent_emoji
1496 .into_iter()
1497 // Items with higher counts should be first
1498 .sorted_by(|(_, count_a), (_, count_b)| count_b.cmp(count_a))
1499 // Make sure we take only up to MAX_RECENT_EMOJI_COUNT
1500 .take(MAX_RECENT_EMOJI_COUNT)
1501 .collect();
1502 Ok(sorted_emojis)
1503 } else {
1504 Ok(Vec::new())
1505 }
1506 }
1507}
1508
1509fn get_raw_content<Ev, C>(raw: Option<Raw<Ev>>) -> Result<Option<Raw<C>>> {
1510 #[derive(Deserialize)]
1511 #[serde(bound = "C: Sized")] // Replace default Deserialize bound
1512 struct GetRawContent<C> {
1513 content: Raw<C>,
1514 }
1515
1516 Ok(raw
1517 .map(|event| event.deserialize_as_unchecked::<GetRawContent<C>>())
1518 .transpose()?
1519 .map(|get_raw| get_raw.content))
1520}
1521
1522#[cfg(test)]
1523mod tests {
1524 use assert_matches::assert_matches;
1525 use matrix_sdk_test::async_test;
1526
1527 use crate::{Error, test_utils::client::MockClientBuilder};
1528
1529 #[async_test]
1530 async fn test_dont_ignore_oneself() {
1531 let client = MockClientBuilder::new(None).build().await;
1532
1533 // It's forbidden to ignore the logged-in user.
1534 assert_matches!(
1535 client.account().ignore_user(client.user_id().unwrap()).await,
1536 Err(Error::CantIgnoreLoggedInUser)
1537 );
1538 }
1539}
1540
1541#[cfg(test)]
1542#[cfg(feature = "experimental-element-recent-emojis")]
1543mod test_recent_emojis {
1544 use js_int::{UInt, uint};
1545 use matrix_sdk_base::recent_emojis::RecentEmojisContent;
1546 use matrix_sdk_test::{async_test, event_factory::EventFactory};
1547
1548 use crate::{
1549 account::MAX_RECENT_EMOJI_COUNT, config::SyncSettings, test_utils::mocks::MatrixMockServer,
1550 };
1551
1552 #[async_test]
1553 async fn test_recent_emojis() {
1554 let server = MatrixMockServer::new().await;
1555 let client = server.client_builder().build().await;
1556 let user_id = client.user_id().expect("session_id");
1557
1558 server
1559 .mock_add_recent_emojis()
1560 .ok(user_id)
1561 .named("Update recent emojis global account data")
1562 .mock_once()
1563 .mount()
1564 .await;
1565
1566 let recent_emojis = client.account().get_recent_emojis(false).await.expect("recent emojis");
1567 assert!(recent_emojis.is_empty());
1568
1569 let emoji_list = vec![
1570 (":/".to_owned(), uint!(1)),
1571 (":)".to_owned(), uint!(12)),
1572 (":D".to_owned(), uint!(12)),
1573 ];
1574
1575 server
1576 .mock_get_recent_emojis()
1577 .ok(user_id, emoji_list.clone())
1578 .named("Fetch recent emojis")
1579 .mock_once()
1580 .mount()
1581 .await;
1582
1583 client.account().add_recent_emoji(":)").await.expect("adding emoji");
1584
1585 server
1586 .mock_sync()
1587 .ok(|builder| {
1588 let content = RecentEmojisContent::new(emoji_list);
1589 let event_builder = EventFactory::new().global_account_data(content);
1590 builder.add_global_account_data(event_builder);
1591 })
1592 .named("Sync")
1593 .mount()
1594 .await;
1595
1596 client.sync_once(SyncSettings::default()).await.expect("sync failed");
1597
1598 let recent_emojis = client.account().get_recent_emojis(false).await.expect("recent emojis");
1599
1600 // Assert size
1601 assert_eq!(recent_emojis.len(), 3);
1602
1603 // Assert ordering: first by times used, then by recency
1604 assert_eq!(recent_emojis[0].0, ":)");
1605 assert_eq!(recent_emojis[1].0, ":D");
1606 assert_eq!(recent_emojis[2].0, ":/");
1607 }
1608
1609 #[async_test]
1610 async fn test_max_recent_emoji_count() {
1611 let server = MatrixMockServer::new().await;
1612 let client = server.client_builder().build().await;
1613 let user_id = client.user_id().expect("session_id");
1614
1615 // This list is > the MAX_RECENT_EMOJI_COUNT
1616 let long_emoji_list = (0..MAX_RECENT_EMOJI_COUNT * 2)
1617 .map(|i| (i.to_string(), uint!(1)))
1618 .collect::<Vec<(String, UInt)>>();
1619
1620 // Initially we locally don't have any emojis
1621 let recent_emojis = client.account().get_recent_emojis(false).await.expect("recent emojis");
1622 assert!(recent_emojis.is_empty());
1623
1624 server
1625 .mock_get_recent_emojis()
1626 .ok(user_id, long_emoji_list.clone())
1627 .named("Fetch recent emojis")
1628 .expect(3)
1629 .mount()
1630 .await;
1631
1632 // Now with a list of emojis longer than the max count, we fetch the emoji list
1633 let recent_emojis = client.account().get_recent_emojis(true).await.expect("recent emojis");
1634
1635 // It should only return until the max count
1636 assert_eq!(recent_emojis.len(), MAX_RECENT_EMOJI_COUNT);
1637 assert_eq!(recent_emojis, long_emoji_list[..MAX_RECENT_EMOJI_COUNT]);
1638
1639 // Simulate the logic we expect when adding a new emoji:
1640 // 1. Remove the existing emoji if present
1641 // 2. Increase its count value and insert it at the front.
1642 // 3. Truncate at MAX_RECENT_EMOJI_COUNT
1643 let expected_updated_emoji_list = {
1644 let mut list = long_emoji_list.clone();
1645 let item = list.remove(50);
1646 list.insert(0, (item.0, item.1 + uint!(1)));
1647 list.truncate(MAX_RECENT_EMOJI_COUNT);
1648 list
1649 };
1650
1651 // Now if we add a new emoji that was not in the list, the last one in the list
1652 // should be gone
1653 server
1654 .mock_add_recent_emojis()
1655 .match_emojis_in_request_body(expected_updated_emoji_list)
1656 .ok(user_id)
1657 .named("Update recent emojis global account data with existing emoji")
1658 .mock_once()
1659 .mount()
1660 .await;
1661
1662 client.account().add_recent_emoji("50").await.expect("adding emoji");
1663
1664 // Do the same, but now with a new emoji that wasn't previously in the list
1665 let expected_updated_emoji_list = {
1666 let mut list = long_emoji_list.clone();
1667 let item = (":D".to_owned(), uint!(1));
1668 list.insert(0, item);
1669 list.truncate(MAX_RECENT_EMOJI_COUNT);
1670 list
1671 };
1672
1673 // We should still have `MAX_RECENT_EMOJI_COUNT` items
1674 server
1675 .mock_add_recent_emojis()
1676 .match_emojis_in_request_body(expected_updated_emoji_list)
1677 .ok(user_id)
1678 .named("Update recent emojis global account data with new emoji")
1679 .mock_once()
1680 .mount()
1681 .await;
1682
1683 client.account().add_recent_emoji(":D").await.expect("adding emoji");
1684 }
1685}