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