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