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