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::{stream, StreamExt};
19use matrix_sdk_base::{
20 media::{MediaFormat, MediaRequestParameters},
21 store::StateStoreExt,
22 StateStoreDataKey, StateStoreDataValue,
23};
24use mime::Mime;
25use ruma::{
26 api::client::{
27 account::{
28 add_3pid, change_password, deactivate, delete_3pid, get_3pids,
29 request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
30 },
31 config::{get_global_account_data, set_global_account_data},
32 error::ErrorKind,
33 profile::{
34 get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
35 },
36 uiaa::AuthData,
37 },
38 assign,
39 events::{
40 ignored_user_list::{IgnoredUser, IgnoredUserListEventContent},
41 media_preview_config::{
42 InviteAvatars, MediaPreviewConfigEventContent, MediaPreviews,
43 UnstableMediaPreviewConfigEventContent,
44 },
45 push_rules::PushRulesEventContent,
46 room::MediaSource,
47 AnyGlobalAccountDataEventContent, GlobalAccountDataEvent, GlobalAccountDataEventContent,
48 GlobalAccountDataEventType, StaticEventContent,
49 },
50 push::Ruleset,
51 serde::Raw,
52 thirdparty::Medium,
53 ClientSecret, MxcUri, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, SessionId, UInt, UserId,
54};
55use serde::Deserialize;
56use tracing::error;
57
58use crate::{config::RequestConfig, Client, Error, Result};
59
60/// A high-level API to manage the client owner's account.
61///
62/// All the methods on this struct send a request to the homeserver.
63#[derive(Debug, Clone)]
64pub struct Account {
65 /// The underlying HTTP client.
66 client: Client,
67}
68
69impl Account {
70 /// The maximum number of visited room identifiers to keep in the state
71 /// store.
72 const VISITED_ROOMS_LIMIT: usize = 20;
73
74 pub(crate) fn new(client: Client) -> Self {
75 Self { client }
76 }
77
78 /// Get the display name of the account.
79 ///
80 /// # Examples
81 ///
82 /// ```no_run
83 /// # use matrix_sdk::Client;
84 /// # use url::Url;
85 /// # async {
86 /// # let homeserver = Url::parse("http://example.com")?;
87 /// let user = "example";
88 /// let client = Client::new(homeserver).await?;
89 /// client.matrix_auth().login_username(user, "password").send().await?;
90 ///
91 /// if let Some(name) = client.account().get_display_name().await? {
92 /// println!("Logged in as user '{user}' with display name '{name}'");
93 /// }
94 /// # anyhow::Ok(()) };
95 /// ```
96 pub async fn get_display_name(&self) -> Result<Option<String>> {
97 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
98 let request = get_display_name::v3::Request::new(user_id.to_owned());
99 let request_config = self.client.request_config().force_auth();
100 let response = self.client.send(request).with_request_config(request_config).await?;
101 Ok(response.displayname)
102 }
103
104 /// Set the display name of the account.
105 ///
106 /// # Examples
107 ///
108 /// ```no_run
109 /// # use matrix_sdk::Client;
110 /// # use url::Url;
111 /// # async {
112 /// # let homeserver = Url::parse("http://example.com")?;
113 /// let user = "example";
114 /// let client = Client::new(homeserver).await?;
115 /// client.matrix_auth().login_username(user, "password").send().await?;
116 ///
117 /// client.account().set_display_name(Some("Alice")).await?;
118 /// # anyhow::Ok(()) };
119 /// ```
120 pub async fn set_display_name(&self, name: Option<&str>) -> Result<()> {
121 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
122 let request =
123 set_display_name::v3::Request::new(user_id.to_owned(), name.map(ToOwned::to_owned));
124 self.client.send(request).await?;
125 Ok(())
126 }
127
128 /// Get the MXC URI of the account's avatar, if set.
129 ///
130 /// This always sends a request to the server to retrieve this information.
131 /// If successful, this fills the cache, and makes it so that
132 /// [`Self::get_cached_avatar_url`] will always return something.
133 ///
134 /// # Examples
135 ///
136 /// ```no_run
137 /// # use matrix_sdk::Client;
138 /// # use url::Url;
139 /// # async {
140 /// # let homeserver = Url::parse("http://example.com")?;
141 /// # let user = "example";
142 /// let client = Client::new(homeserver).await?;
143 /// client.matrix_auth().login_username(user, "password").send().await?;
144 ///
145 /// if let Some(url) = client.account().get_avatar_url().await? {
146 /// println!("Your avatar's mxc url is {url}");
147 /// }
148 /// # anyhow::Ok(()) };
149 /// ```
150 pub async fn get_avatar_url(&self) -> Result<Option<OwnedMxcUri>> {
151 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
152 let request = get_avatar_url::v3::Request::new(user_id.to_owned());
153
154 let config = Some(RequestConfig::new().force_auth());
155
156 let response = self.client.send(request).with_request_config(config).await?;
157 if let Some(url) = response.avatar_url.clone() {
158 // If an avatar is found cache it.
159 let _ = self
160 .client
161 .state_store()
162 .set_kv_data(
163 StateStoreDataKey::UserAvatarUrl(user_id),
164 StateStoreDataValue::UserAvatarUrl(url),
165 )
166 .await;
167 } else {
168 // If there is no avatar the user has removed it and we uncache it.
169 let _ = self
170 .client
171 .state_store()
172 .remove_kv_data(StateStoreDataKey::UserAvatarUrl(user_id))
173 .await;
174 }
175 Ok(response.avatar_url)
176 }
177
178 /// Get the URL of the account's avatar, if is stored in cache.
179 pub async fn get_cached_avatar_url(&self) -> Result<Option<OwnedMxcUri>> {
180 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
181 let data = self
182 .client
183 .state_store()
184 .get_kv_data(StateStoreDataKey::UserAvatarUrl(user_id))
185 .await?;
186 Ok(data.map(|v| v.into_user_avatar_url().expect("Session data is not a user avatar url")))
187 }
188
189 /// Set the MXC URI of the account's avatar.
190 ///
191 /// The avatar is unset if `url` is `None`.
192 pub async fn set_avatar_url(&self, url: Option<&MxcUri>) -> Result<()> {
193 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
194 let request =
195 set_avatar_url::v3::Request::new(user_id.to_owned(), url.map(ToOwned::to_owned));
196 self.client.send(request).await?;
197 Ok(())
198 }
199
200 /// Get the account's avatar, if set.
201 ///
202 /// Returns the avatar.
203 ///
204 /// If a thumbnail is requested no guarantee on the size of the image is
205 /// given.
206 ///
207 /// # Arguments
208 ///
209 /// * `format` - The desired format of the avatar.
210 ///
211 /// # Examples
212 ///
213 /// ```no_run
214 /// # use matrix_sdk::Client;
215 /// # use matrix_sdk::ruma::room_id;
216 /// # use matrix_sdk::media::MediaFormat;
217 /// # use url::Url;
218 /// # async {
219 /// # let homeserver = Url::parse("http://example.com")?;
220 /// # let user = "example";
221 /// let client = Client::new(homeserver).await?;
222 /// client.matrix_auth().login_username(user, "password").send().await?;
223 ///
224 /// if let Some(avatar) = client.account().get_avatar(MediaFormat::File).await?
225 /// {
226 /// std::fs::write("avatar.png", avatar);
227 /// }
228 /// # anyhow::Ok(()) };
229 /// ```
230 pub async fn get_avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
231 if let Some(url) = self.get_avatar_url().await? {
232 let request = MediaRequestParameters { source: MediaSource::Plain(url), format };
233 Ok(Some(self.client.media().get_media_content(&request, true).await?))
234 } else {
235 Ok(None)
236 }
237 }
238
239 /// Upload and set the account's avatar.
240 ///
241 /// This will upload the data produced by the reader to the homeserver's
242 /// content repository, and set the user's avatar to the MXC URI for the
243 /// uploaded file.
244 ///
245 /// This is a convenience method for calling [`Media::upload()`],
246 /// followed by [`Account::set_avatar_url()`].
247 ///
248 /// Returns the MXC URI of the uploaded avatar.
249 ///
250 /// # Examples
251 ///
252 /// ```no_run
253 /// # use std::fs;
254 /// # use matrix_sdk::Client;
255 /// # use url::Url;
256 /// # async {
257 /// # let homeserver = Url::parse("http://localhost:8080")?;
258 /// # let client = Client::new(homeserver).await?;
259 /// let image = fs::read("/home/example/selfie.jpg")?;
260 ///
261 /// client.account().upload_avatar(&mime::IMAGE_JPEG, image).await?;
262 /// # anyhow::Ok(()) };
263 /// ```
264 ///
265 /// [`Media::upload()`]: crate::Media::upload
266 pub async fn upload_avatar(&self, content_type: &Mime, data: Vec<u8>) -> Result<OwnedMxcUri> {
267 let upload_response = self.client.media().upload(content_type, data, None).await?;
268 self.set_avatar_url(Some(&upload_response.content_uri)).await?;
269 Ok(upload_response.content_uri)
270 }
271
272 /// Get the profile of this account.
273 ///
274 /// Allows to get all the profile data in a single call.
275 ///
276 /// # Examples
277 ///
278 /// ```no_run
279 /// # use matrix_sdk::Client;
280 /// use ruma::api::client::profile::{AvatarUrl, DisplayName};
281 /// # use url::Url;
282 /// # async {
283 /// # let homeserver = Url::parse("http://localhost:8080")?;
284 /// # let client = Client::new(homeserver).await?;
285 ///
286 /// let profile = client.account().fetch_user_profile().await?;
287 /// let display_name = profile.get_static::<DisplayName>()?;
288 /// let avatar_url = profile.get_static::<AvatarUrl>()?;
289 ///
290 /// println!("You are '{display_name:?}' with avatar '{avatar_url:?}'");
291 /// # anyhow::Ok(()) };
292 /// ```
293 pub async fn fetch_user_profile(&self) -> Result<get_profile::v3::Response> {
294 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
295 self.fetch_user_profile_of(user_id).await
296 }
297
298 /// Get the profile for a given user id
299 ///
300 /// # Arguments
301 ///
302 /// * `user_id` the matrix id this function downloads the profile for
303 pub async fn fetch_user_profile_of(
304 &self,
305 user_id: &UserId,
306 ) -> Result<get_profile::v3::Response> {
307 let request = get_profile::v3::Request::new(user_id.to_owned());
308 Ok(self
309 .client
310 .send(request)
311 .with_request_config(RequestConfig::short_retry().force_auth())
312 .await?)
313 }
314
315 /// Change the password of the account.
316 ///
317 /// # Arguments
318 ///
319 /// * `new_password` - The new password to set.
320 ///
321 /// * `auth_data` - This request uses the [User-Interactive Authentication
322 /// API][uiaa]. The first request needs to set this to `None` and will
323 /// always fail with an [`UiaaResponse`]. The response will contain
324 /// information for the interactive auth and the same request needs to be
325 /// made but this time with some `auth_data` provided.
326 ///
327 /// # Returns
328 ///
329 /// This method might return an [`ErrorKind::WeakPassword`] error if the new
330 /// password is considered insecure by the homeserver, with details about
331 /// the strength requirements in the error's message.
332 ///
333 /// # Examples
334 ///
335 /// ```no_run
336 /// # use matrix_sdk::Client;
337 /// # use matrix_sdk::ruma::{
338 /// # api::client::{
339 /// # account::change_password::v3::{Request as ChangePasswordRequest},
340 /// # uiaa::{AuthData, Dummy},
341 /// # },
342 /// # assign,
343 /// # };
344 /// # use url::Url;
345 /// # async {
346 /// # let homeserver = Url::parse("http://localhost:8080")?;
347 /// # let client = Client::new(homeserver).await?;
348 /// client.account().change_password(
349 /// "myverysecretpassword",
350 /// Some(AuthData::Dummy(Dummy::new())),
351 /// ).await?;
352 /// # anyhow::Ok(()) };
353 /// ```
354 /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
355 /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
356 /// [`ErrorKind::WeakPassword`]: ruma::api::client::error::ErrorKind::WeakPassword
357 pub async fn change_password(
358 &self,
359 new_password: &str,
360 auth_data: Option<AuthData>,
361 ) -> Result<change_password::v3::Response> {
362 let request = assign!(change_password::v3::Request::new(new_password.to_owned()), {
363 auth: auth_data,
364 });
365 Ok(self.client.send(request).await?)
366 }
367
368 /// Deactivate this account definitively.
369 ///
370 /// # Arguments
371 ///
372 /// * `id_server` - The identity server from which to unbind the user’s
373 /// [Third Party Identifiers][3pid].
374 ///
375 /// * `auth_data` - This request uses the [User-Interactive Authentication
376 /// API][uiaa]. The first request needs to set this to `None` and will
377 /// always fail with an [`UiaaResponse`]. The response will contain
378 /// information for the interactive auth and the same request needs to be
379 /// made but this time with some `auth_data` provided.
380 ///
381 /// * `erase` - Whether the user would like their content to be erased as
382 /// much as possible from the server.
383 ///
384 /// # Examples
385 ///
386 /// ```no_run
387 /// # use matrix_sdk::Client;
388 /// # use matrix_sdk::ruma::{
389 /// # api::client::{
390 /// # account::change_password::v3::{Request as ChangePasswordRequest},
391 /// # uiaa::{AuthData, Dummy},
392 /// # },
393 /// # assign,
394 /// # };
395 /// # use url::Url;
396 /// # async {
397 /// # let homeserver = Url::parse("http://localhost:8080")?;
398 /// # let client = Client::new(homeserver).await?;
399 /// # let account = client.account();
400 /// let response = account.deactivate(None, None, false).await;
401 ///
402 /// // Proceed with UIAA.
403 /// # anyhow::Ok(()) };
404 /// ```
405 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
406 /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
407 /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
408 pub async fn deactivate(
409 &self,
410 id_server: Option<&str>,
411 auth_data: Option<AuthData>,
412 erase_data: bool,
413 ) -> Result<deactivate::v3::Response> {
414 let request = assign!(deactivate::v3::Request::new(), {
415 id_server: id_server.map(ToOwned::to_owned),
416 auth: auth_data,
417 erase: erase_data,
418 });
419 Ok(self.client.send(request).await?)
420 }
421
422 /// Get the registered [Third Party Identifiers][3pid] on the homeserver of
423 /// the account.
424 ///
425 /// These 3PIDs may be used by the homeserver to authenticate the user
426 /// during sensitive operations.
427 ///
428 /// # Examples
429 ///
430 /// ```no_run
431 /// # use matrix_sdk::Client;
432 /// # use url::Url;
433 /// # async {
434 /// # let homeserver = Url::parse("http://localhost:8080")?;
435 /// # let client = Client::new(homeserver).await?;
436 /// let threepids = client.account().get_3pids().await?.threepids;
437 ///
438 /// for threepid in threepids {
439 /// println!(
440 /// "Found 3PID '{}' of type '{}'",
441 /// threepid.address, threepid.medium
442 /// );
443 /// }
444 /// # anyhow::Ok(()) };
445 /// ```
446 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
447 pub async fn get_3pids(&self) -> Result<get_3pids::v3::Response> {
448 let request = get_3pids::v3::Request::new();
449 Ok(self.client.send(request).await?)
450 }
451
452 /// Request a token to validate an email address as a [Third Party
453 /// Identifier][3pid].
454 ///
455 /// This is the first step in registering an email address as 3PID. Next,
456 /// call [`Account::add_3pid()`] with the same `client_secret` and the
457 /// returned `sid`.
458 ///
459 /// # Arguments
460 ///
461 /// * `client_secret` - A client-generated secret string used to protect
462 /// this session.
463 ///
464 /// * `email` - The email address to validate.
465 ///
466 /// * `send_attempt` - The attempt number. This number needs to be
467 /// incremented if you want to request another token for the same
468 /// validation.
469 ///
470 /// # Returns
471 ///
472 /// * `sid` - The session ID to be used in following requests for this 3PID.
473 ///
474 /// * `submit_url` - If present, the user will submit the token to the
475 /// client, that must send it to this URL. If not, the client will not be
476 /// involved in the token submission.
477 ///
478 /// This method might return an [`ErrorKind::ThreepidInUse`] error if the
479 /// email address is already registered for this account or another, or an
480 /// [`ErrorKind::ThreepidDenied`] error if it is denied.
481 ///
482 /// # Examples
483 ///
484 /// ```no_run
485 /// # use matrix_sdk::Client;
486 /// # use matrix_sdk::ruma::{ClientSecret, uint};
487 /// # use url::Url;
488 /// # async {
489 /// # let homeserver = Url::parse("http://localhost:8080")?;
490 /// # let client = Client::new(homeserver).await?;
491 /// # let account = client.account();
492 /// # let secret = ClientSecret::parse("secret")?;
493 /// let token_response = account
494 /// .request_3pid_email_token(&secret, "john@matrix.org", uint!(0))
495 /// .await?;
496 ///
497 /// // Wait for the user to confirm that the token was submitted or prompt
498 /// // the user for the token and send it to submit_url.
499 ///
500 /// let uiaa_response =
501 /// account.add_3pid(&secret, &token_response.sid, None).await;
502 ///
503 /// // Proceed with UIAA.
504 /// # anyhow::Ok(()) };
505 /// ```
506 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
507 /// [`ErrorKind::ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse
508 /// [`ErrorKind::ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied
509 pub async fn request_3pid_email_token(
510 &self,
511 client_secret: &ClientSecret,
512 email: &str,
513 send_attempt: UInt,
514 ) -> Result<request_3pid_management_token_via_email::v3::Response> {
515 let request = request_3pid_management_token_via_email::v3::Request::new(
516 client_secret.to_owned(),
517 email.to_owned(),
518 send_attempt,
519 );
520 Ok(self.client.send(request).await?)
521 }
522
523 /// Request a token to validate a phone number as a [Third Party
524 /// Identifier][3pid].
525 ///
526 /// This is the first step in registering a phone number as 3PID. Next,
527 /// call [`Account::add_3pid()`] with the same `client_secret` and the
528 /// returned `sid`.
529 ///
530 /// # Arguments
531 ///
532 /// * `client_secret` - A client-generated secret string used to protect
533 /// this session.
534 ///
535 /// * `country` - The two-letter uppercase ISO-3166-1 alpha-2 country code
536 /// that the number in phone_number should be parsed as if it were dialled
537 /// from.
538 ///
539 /// * `phone_number` - The phone number to validate.
540 ///
541 /// * `send_attempt` - The attempt number. This number needs to be
542 /// incremented if you want to request another token for the same
543 /// validation.
544 ///
545 /// # Returns
546 ///
547 /// * `sid` - The session ID to be used in following requests for this 3PID.
548 ///
549 /// * `submit_url` - If present, the user will submit the token to the
550 /// client, that must send it to this URL. If not, the client will not be
551 /// involved in the token submission.
552 ///
553 /// This method might return an [`ErrorKind::ThreepidInUse`] error if the
554 /// phone number is already registered for this account or another, or an
555 /// [`ErrorKind::ThreepidDenied`] error if it is denied.
556 ///
557 /// # Examples
558 ///
559 /// ```no_run
560 /// # use matrix_sdk::Client;
561 /// # use matrix_sdk::ruma::{ClientSecret, uint};
562 /// # use url::Url;
563 /// # async {
564 /// # let homeserver = Url::parse("http://localhost:8080")?;
565 /// # let client = Client::new(homeserver).await?;
566 /// # let account = client.account();
567 /// # let secret = ClientSecret::parse("secret")?;
568 /// let token_response = account
569 /// .request_3pid_msisdn_token(&secret, "FR", "0123456789", uint!(0))
570 /// .await?;
571 ///
572 /// // Wait for the user to confirm that the token was submitted or prompt
573 /// // the user for the token and send it to submit_url.
574 ///
575 /// let uiaa_response =
576 /// account.add_3pid(&secret, &token_response.sid, None).await;
577 ///
578 /// // Proceed with UIAA.
579 /// # anyhow::Ok(()) };
580 /// ```
581 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
582 /// [`ErrorKind::ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse
583 /// [`ErrorKind::ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied
584 pub async fn request_3pid_msisdn_token(
585 &self,
586 client_secret: &ClientSecret,
587 country: &str,
588 phone_number: &str,
589 send_attempt: UInt,
590 ) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
591 let request = request_3pid_management_token_via_msisdn::v3::Request::new(
592 client_secret.to_owned(),
593 country.to_owned(),
594 phone_number.to_owned(),
595 send_attempt,
596 );
597 Ok(self.client.send(request).await?)
598 }
599
600 /// Add a [Third Party Identifier][3pid] on the homeserver for this
601 /// account.
602 ///
603 /// This 3PID may be used by the homeserver to authenticate the user
604 /// during sensitive operations.
605 ///
606 /// This method should be called after
607 /// [`Account::request_3pid_email_token()`] or
608 /// [`Account::request_3pid_msisdn_token()`] to complete the 3PID
609 ///
610 /// # Arguments
611 ///
612 /// * `client_secret` - The same client secret used in
613 /// [`Account::request_3pid_email_token()`] or
614 /// [`Account::request_3pid_msisdn_token()`].
615 ///
616 /// * `sid` - The session ID returned in
617 /// [`Account::request_3pid_email_token()`] or
618 /// [`Account::request_3pid_msisdn_token()`].
619 ///
620 /// * `auth_data` - This request uses the [User-Interactive Authentication
621 /// API][uiaa]. The first request needs to set this to `None` and will
622 /// always fail with an [`UiaaResponse`]. The response will contain
623 /// information for the interactive auth and the same request needs to be
624 /// made but this time with some `auth_data` provided.
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 add_3pid(
630 &self,
631 client_secret: &ClientSecret,
632 sid: &SessionId,
633 auth_data: Option<AuthData>,
634 ) -> Result<add_3pid::v3::Response> {
635 #[rustfmt::skip] // rustfmt wants to merge the next two lines
636 let request =
637 assign!(add_3pid::v3::Request::new(client_secret.to_owned(), sid.to_owned()), {
638 auth: auth_data
639 });
640 Ok(self.client.send(request).await?)
641 }
642
643 /// Delete a [Third Party Identifier][3pid] from the homeserver for this
644 /// account.
645 ///
646 /// # Arguments
647 ///
648 /// * `address` - The 3PID being removed.
649 ///
650 /// * `medium` - The type of the 3PID.
651 ///
652 /// * `id_server` - The identity server to unbind from. If not provided, the
653 /// homeserver should unbind the 3PID from the identity server it was
654 /// bound to previously.
655 ///
656 /// # Returns
657 ///
658 /// * [`ThirdPartyIdRemovalStatus::Success`] if the 3PID was also unbound
659 /// from the identity server.
660 ///
661 /// * [`ThirdPartyIdRemovalStatus::NoSupport`] if the 3PID was not unbound
662 /// from the identity server. This can also mean that the 3PID was not
663 /// bound to an identity server in the first place.
664 ///
665 /// # Examples
666 ///
667 /// ```no_run
668 /// # use matrix_sdk::Client;
669 /// # use matrix_sdk::ruma::thirdparty::Medium;
670 /// # use matrix_sdk::ruma::api::client::account::ThirdPartyIdRemovalStatus;
671 /// # use url::Url;
672 /// # async {
673 /// # let homeserver = Url::parse("http://localhost:8080")?;
674 /// # let client = Client::new(homeserver).await?;
675 /// # let account = client.account();
676 /// match account
677 /// .delete_3pid("paul@matrix.org", Medium::Email, None)
678 /// .await?
679 /// .id_server_unbind_result
680 /// {
681 /// ThirdPartyIdRemovalStatus::Success => {
682 /// println!("3PID unbound from the Identity Server");
683 /// }
684 /// _ => println!("Could not unbind 3PID from the Identity Server"),
685 /// }
686 /// # anyhow::Ok(()) };
687 /// ```
688 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
689 /// [`ThirdPartyIdRemovalStatus::Success`]: ruma::api::client::account::ThirdPartyIdRemovalStatus::Success
690 /// [`ThirdPartyIdRemovalStatus::NoSupport`]: ruma::api::client::account::ThirdPartyIdRemovalStatus::NoSupport
691 pub async fn delete_3pid(
692 &self,
693 address: &str,
694 medium: Medium,
695 id_server: Option<&str>,
696 ) -> Result<delete_3pid::v3::Response> {
697 let request = assign!(delete_3pid::v3::Request::new(medium, address.to_owned()), {
698 id_server: id_server.map(ToOwned::to_owned),
699 });
700 Ok(self.client.send(request).await?)
701 }
702
703 /// Get the content of an account data event of statically-known type, from
704 /// storage.
705 ///
706 /// # Examples
707 ///
708 /// ```no_run
709 /// # use matrix_sdk::Client;
710 /// # async {
711 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
712 /// # let account = client.account();
713 /// use matrix_sdk::ruma::events::ignored_user_list::IgnoredUserListEventContent;
714 ///
715 /// let maybe_content = account.account_data::<IgnoredUserListEventContent>().await?;
716 /// if let Some(raw_content) = maybe_content {
717 /// let content = raw_content.deserialize()?;
718 /// println!("Ignored users:");
719 /// for user_id in content.ignored_users.keys() {
720 /// println!("- {user_id}");
721 /// }
722 /// }
723 /// # anyhow::Ok(()) };
724 /// ```
725 pub async fn account_data<C>(&self) -> Result<Option<Raw<C>>>
726 where
727 C: GlobalAccountDataEventContent + StaticEventContent<IsPrefix = ruma::events::False>,
728 {
729 get_raw_content(self.client.state_store().get_account_data_event_static::<C>().await?)
730 }
731
732 /// Get the content of an account data event of a given type, from storage.
733 pub async fn account_data_raw(
734 &self,
735 event_type: GlobalAccountDataEventType,
736 ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>> {
737 get_raw_content(self.client.state_store().get_account_data_event(event_type).await?)
738 }
739
740 /// Fetch a global account data event from the server.
741 ///
742 /// The content from the response will not be persisted in the store.
743 ///
744 /// Examples
745 ///
746 /// ```no_run
747 /// # use matrix_sdk::Client;
748 /// # async {
749 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
750 /// # let account = client.account();
751 /// use matrix_sdk::ruma::events::{ignored_user_list::IgnoredUserListEventContent, GlobalAccountDataEventType};
752 ///
753 /// if let Some(raw_content) = account.fetch_account_data(GlobalAccountDataEventType::IgnoredUserList).await? {
754 /// let content = raw_content.deserialize_as_unchecked::<IgnoredUserListEventContent>()?;
755 ///
756 /// println!("Ignored users:");
757 ///
758 /// for user_id in content.ignored_users.keys() {
759 /// println!("- {user_id}");
760 /// }
761 /// }
762 /// # anyhow::Ok(()) };
763 pub async fn fetch_account_data(
764 &self,
765 event_type: GlobalAccountDataEventType,
766 ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>> {
767 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
768
769 let request = get_global_account_data::v3::Request::new(own_user.to_owned(), event_type);
770
771 match self.client.send(request).await {
772 Ok(r) => Ok(Some(r.account_data)),
773 Err(e) => {
774 if let Some(kind) = e.client_api_error_kind() {
775 if kind == &ErrorKind::NotFound {
776 Ok(None)
777 } else {
778 Err(e.into())
779 }
780 } else {
781 Err(e.into())
782 }
783 }
784 }
785 }
786
787 /// Fetch an account data event of statically-known type from the server.
788 pub async fn fetch_account_data_static<C>(&self) -> Result<Option<Raw<C>>>
789 where
790 C: GlobalAccountDataEventContent + StaticEventContent<IsPrefix = ruma::events::False>,
791 {
792 Ok(self.fetch_account_data(C::TYPE.into()).await?.map(Raw::cast_unchecked))
793 }
794
795 /// Set the given account data event.
796 ///
797 /// # Examples
798 ///
799 /// ```no_run
800 /// # use matrix_sdk::Client;
801 /// # async {
802 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
803 /// # let account = client.account();
804 /// use matrix_sdk::ruma::{
805 /// events::ignored_user_list::{IgnoredUser, IgnoredUserListEventContent},
806 /// user_id,
807 /// };
808 ///
809 /// let mut content = account
810 /// .account_data::<IgnoredUserListEventContent>()
811 /// .await?
812 /// .map(|c| c.deserialize())
813 /// .transpose()?
814 /// .unwrap_or_default();
815 /// content
816 /// .ignored_users
817 /// .insert(user_id!("@foo:bar.com").to_owned(), IgnoredUser::new());
818 /// account.set_account_data(content).await?;
819 /// # anyhow::Ok(()) };
820 /// ```
821 pub async fn set_account_data<T>(
822 &self,
823 content: T,
824 ) -> Result<set_global_account_data::v3::Response>
825 where
826 T: GlobalAccountDataEventContent,
827 {
828 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
829
830 let request = set_global_account_data::v3::Request::new(own_user.to_owned(), &content)?;
831
832 Ok(self.client.send(request).await?)
833 }
834
835 /// Set the given raw account data event.
836 pub async fn set_account_data_raw(
837 &self,
838 event_type: GlobalAccountDataEventType,
839 content: Raw<AnyGlobalAccountDataEventContent>,
840 ) -> Result<set_global_account_data::v3::Response> {
841 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
842
843 let request =
844 set_global_account_data::v3::Request::new_raw(own_user.to_owned(), event_type, content);
845
846 Ok(self.client.send(request).await?)
847 }
848
849 /// Marks the room identified by `room_id` as a "direct chat" with each
850 /// user in `user_ids`.
851 ///
852 /// # Arguments
853 ///
854 /// * `room_id` - The room ID of the direct message room.
855 /// * `user_ids` - The user IDs to be associated with this direct message
856 /// room.
857 pub async fn mark_as_dm(&self, room_id: &RoomId, user_ids: &[OwnedUserId]) -> Result<()> {
858 use ruma::events::direct::DirectEventContent;
859
860 // This function does a read/update/store of an account data event stored on the
861 // homeserver. We first fetch the existing account data event, the event
862 // contains a map which gets updated by this method, finally we upload the
863 // modified event.
864 //
865 // To prevent multiple calls to this method trying to update the map of DMs same
866 // time, and thus trampling on each other we introduce a lock which acts
867 // as a semaphore.
868 let _guard = self.client.locks().mark_as_dm_lock.lock().await;
869
870 // Now we need to mark the room as a DM for ourselves, we fetch the
871 // existing `m.direct` event and append the room to the list of DMs we
872 // have with this user.
873
874 // We are fetching the content from the server because we currently can't rely
875 // on `/sync` giving us the correct data in a timely manner.
876 let raw_content = self.fetch_account_data_static::<DirectEventContent>().await?;
877
878 let mut content = if let Some(raw_content) = raw_content {
879 // Log the error and pass it upwards if we fail to deserialize the m.direct
880 // event.
881 raw_content.deserialize().map_err(|err| {
882 error!("unable to deserialize m.direct event content; aborting request to mark {room_id} as dm: {err}");
883 err
884 })?
885 } else {
886 // If there was no m.direct event server-side, create a default one.
887 Default::default()
888 };
889
890 for user_id in user_ids {
891 content.entry(user_id.into()).or_default().push(room_id.to_owned());
892 }
893
894 // TODO: We should probably save the fact that we need to send this out
895 // because otherwise we might end up in a state where we have a DM that
896 // isn't marked as one.
897 self.set_account_data(content).await?;
898
899 Ok(())
900 }
901
902 /// Adds the given user ID to the account's ignore list.
903 pub async fn ignore_user(&self, user_id: &UserId) -> Result<()> {
904 let own_user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
905 if user_id == own_user_id {
906 return Err(Error::CantIgnoreLoggedInUser);
907 }
908
909 let mut ignored_user_list = self.get_ignored_user_list_event_content().await?;
910 ignored_user_list.ignored_users.insert(user_id.to_owned(), IgnoredUser::new());
911
912 self.set_account_data(ignored_user_list).await?;
913
914 // In theory, we should also clear some caches here, because they may include
915 // events sent by the ignored user. In practice, we expect callers to
916 // take care of this, or subsystems to listen to user list changes and
917 // clear caches accordingly.
918
919 Ok(())
920 }
921
922 /// Removes the given user ID from the account's ignore list.
923 pub async fn unignore_user(&self, user_id: &UserId) -> Result<()> {
924 let mut ignored_user_list = self.get_ignored_user_list_event_content().await?;
925
926 // Only update account data if the user was ignored in the first place.
927 if ignored_user_list.ignored_users.remove(user_id).is_some() {
928 self.set_account_data(ignored_user_list).await?;
929 }
930
931 // See comment in `ignore_user`.
932 Ok(())
933 }
934
935 async fn get_ignored_user_list_event_content(&self) -> Result<IgnoredUserListEventContent> {
936 let ignored_user_list = self
937 .account_data::<IgnoredUserListEventContent>()
938 .await?
939 .map(|c| c.deserialize())
940 .transpose()?
941 .unwrap_or_default();
942 Ok(ignored_user_list)
943 }
944
945 /// Get the current push rules from storage.
946 ///
947 /// If no push rules event was found, or it fails to deserialize, a ruleset
948 /// with the server-default push rules is returned.
949 ///
950 /// Panics if called when the client is not logged in.
951 pub async fn push_rules(&self) -> Result<Ruleset> {
952 Ok(self
953 .account_data::<PushRulesEventContent>()
954 .await?
955 .and_then(|r| match r.deserialize() {
956 Ok(r) => Some(r.global),
957 Err(e) => {
958 error!("Push rules event failed to deserialize: {e}");
959 None
960 }
961 })
962 .unwrap_or_else(|| {
963 Ruleset::server_default(
964 self.client.user_id().expect("The client should be logged in"),
965 )
966 }))
967 }
968
969 /// Retrieves the user's recently visited room list
970 pub async fn get_recently_visited_rooms(&self) -> Result<Vec<OwnedRoomId>> {
971 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
972 let data = self
973 .client
974 .state_store()
975 .get_kv_data(StateStoreDataKey::RecentlyVisitedRooms(user_id))
976 .await?;
977
978 Ok(data
979 .map(|v| {
980 v.into_recently_visited_rooms()
981 .expect("Session data is not a list of recently visited rooms")
982 })
983 .unwrap_or_default())
984 }
985
986 /// Moves/inserts the given room to the front of the recently visited list
987 pub async fn track_recently_visited_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
988 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
989
990 // Get the previously stored recently visited rooms
991 let mut recently_visited_rooms = self.get_recently_visited_rooms().await?;
992
993 // Remove all other occurrences of the new room_id
994 recently_visited_rooms.retain(|r| r != &room_id);
995
996 // And insert it as the most recent
997 recently_visited_rooms.insert(0, room_id);
998
999 // Cap the whole list to the VISITED_ROOMS_LIMIT
1000 recently_visited_rooms.truncate(Self::VISITED_ROOMS_LIMIT);
1001
1002 let data = StateStoreDataValue::RecentlyVisitedRooms(recently_visited_rooms);
1003 self.client
1004 .state_store()
1005 .set_kv_data(StateStoreDataKey::RecentlyVisitedRooms(user_id), data)
1006 .await?;
1007 Ok(())
1008 }
1009
1010 /// Observes the media preview configuration.
1011 ///
1012 /// 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.
1013 ///
1014 /// This will return the initial value of the configuration and a stream
1015 /// that will yield new values as they are received.
1016 ///
1017 /// The initial value is the one that was stored in the account data
1018 /// when the client was started.
1019 /// and the following code is using a temporary solution until we know which
1020 /// Matrix version will support the stable type.
1021 ///
1022 /// # Examples
1023 ///
1024 /// ```no_run
1025 /// # use futures_util::{pin_mut, StreamExt};
1026 /// # use matrix_sdk::Client;
1027 /// # use matrix_sdk::ruma::events::media_preview_config::MediaPreviews;
1028 /// # use url::Url;
1029 /// # async {
1030 /// # let homeserver = Url::parse("http://localhost:8080")?;
1031 /// # let client = Client::new(homeserver).await?;
1032 /// let account = client.account();
1033 ///
1034 /// let (initial_config, config_stream) =
1035 /// account.observe_media_preview_config().await?;
1036 ///
1037 /// println!("Initial media preview config: {:?}", initial_config);
1038 ///
1039 /// pin_mut!(config_stream);
1040 /// while let Some(new_config) = config_stream.next().await {
1041 /// println!("Updated media preview config: {:?}", new_config);
1042 /// }
1043 /// # anyhow::Ok(()) };
1044 /// ```
1045 pub async fn observe_media_preview_config(
1046 &self,
1047 ) -> Result<
1048 (
1049 Option<MediaPreviewConfigEventContent>,
1050 impl Stream<Item = MediaPreviewConfigEventContent>,
1051 ),
1052 Error,
1053 > {
1054 // We need to create two observers, one for the stable event and one for the
1055 // unstable and combine them into a single stream.
1056 let first_observer = self
1057 .client
1058 .observe_events::<GlobalAccountDataEvent<MediaPreviewConfigEventContent>, ()>();
1059
1060 let stream = first_observer.subscribe().map(|event| event.0.content);
1061
1062 let second_observer = self
1063 .client
1064 .observe_events::<GlobalAccountDataEvent<UnstableMediaPreviewConfigEventContent>, ()>();
1065
1066 let second_stream = second_observer.subscribe().map(|event| event.0.content.0);
1067
1068 let mut combined_stream = stream::select(stream, second_stream);
1069
1070 let result_stream = async_stream::stream! {
1071 // The observers need to be alive for the individual streams to be alive, so let's now
1072 // create a stream that takes ownership of them.
1073 let _first_observer = first_observer;
1074 let _second_observer = second_observer;
1075
1076 while let Some(item) = combined_stream.next().await {
1077 yield item
1078 }
1079 };
1080
1081 // We need to get the initial value of the media preview config event
1082 // we do this after creating the observers to make sure that we don't
1083 // create a race condition
1084 let initial_value = self.get_media_preview_config_event_content().await?;
1085
1086 Ok((initial_value, result_stream))
1087 }
1088
1089 /// Fetch the media preview configuration event content from the server.
1090 ///
1091 /// Will check first for the stable event and then for the unstable one.
1092 pub async fn fetch_media_preview_config_event_content(
1093 &self,
1094 ) -> Result<Option<MediaPreviewConfigEventContent>> {
1095 // First we check if there is a value in the stable event
1096 let media_preview_config =
1097 self.fetch_account_data_static::<MediaPreviewConfigEventContent>().await?;
1098
1099 let media_preview_config = if let Some(media_preview_config) = media_preview_config {
1100 Some(media_preview_config)
1101 } else {
1102 // If there is no value in the stable event, we check the unstable
1103 self.fetch_account_data_static::<UnstableMediaPreviewConfigEventContent>()
1104 .await?
1105 .map(Raw::cast)
1106 };
1107
1108 // We deserialize the content of the event, if is not found we return the
1109 // default
1110 let media_preview_config = media_preview_config.and_then(|value| value.deserialize().ok());
1111
1112 Ok(media_preview_config)
1113 }
1114
1115 /// Get the media preview configuration event content stored in the cache.
1116 ///
1117 /// Will check first for the stable event and then for the unstable one.
1118 pub async fn get_media_preview_config_event_content(
1119 &self,
1120 ) -> Result<Option<MediaPreviewConfigEventContent>> {
1121 let media_preview_config = self
1122 .account_data::<MediaPreviewConfigEventContent>()
1123 .await?
1124 .and_then(|r| r.deserialize().ok());
1125
1126 if let Some(media_preview_config) = media_preview_config {
1127 Ok(Some(media_preview_config))
1128 } else {
1129 Ok(self
1130 .account_data::<UnstableMediaPreviewConfigEventContent>()
1131 .await?
1132 .and_then(|r| r.deserialize().ok())
1133 .map(Into::into))
1134 }
1135 }
1136
1137 /// Set the media previews display policy in the timeline.
1138 ///
1139 /// This will always use the unstable event until we know which Matrix
1140 /// version will support it.
1141 pub async fn set_media_previews_display_policy(&self, policy: MediaPreviews) -> Result<()> {
1142 let mut media_preview_config =
1143 self.fetch_media_preview_config_event_content().await?.unwrap_or_default();
1144 media_preview_config.media_previews = Some(policy);
1145
1146 // Updating the unstable account data
1147 let unstable_media_preview_config =
1148 UnstableMediaPreviewConfigEventContent::from(media_preview_config);
1149 self.set_account_data(unstable_media_preview_config).await?;
1150 Ok(())
1151 }
1152
1153 /// Set the display policy for avatars in invite requests.
1154 ///
1155 /// This will always use the unstable event until we know which matrix
1156 /// version will support it.
1157 pub async fn set_invite_avatars_display_policy(&self, policy: InviteAvatars) -> Result<()> {
1158 let mut media_preview_config =
1159 self.fetch_media_preview_config_event_content().await?.unwrap_or_default();
1160 media_preview_config.invite_avatars = Some(policy);
1161
1162 // Updating the unstable account data
1163 let unstable_media_preview_config =
1164 UnstableMediaPreviewConfigEventContent::from(media_preview_config);
1165 self.set_account_data(unstable_media_preview_config).await?;
1166 Ok(())
1167 }
1168}
1169
1170fn get_raw_content<Ev, C>(raw: Option<Raw<Ev>>) -> Result<Option<Raw<C>>> {
1171 #[derive(Deserialize)]
1172 #[serde(bound = "C: Sized")] // Replace default Deserialize bound
1173 struct GetRawContent<C> {
1174 content: Raw<C>,
1175 }
1176
1177 Ok(raw
1178 .map(|event| event.deserialize_as_unchecked::<GetRawContent<C>>())
1179 .transpose()?
1180 .map(|get_raw| get_raw.content))
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185 use assert_matches::assert_matches;
1186 use matrix_sdk_test::async_test;
1187
1188 use crate::{test_utils::client::MockClientBuilder, Error};
1189
1190 #[async_test]
1191 async fn test_dont_ignore_oneself() {
1192 let client = MockClientBuilder::new(None).build().await;
1193
1194 // It's forbidden to ignore the logged-in user.
1195 assert_matches!(
1196 client.account().ignore_user(client.user_id().unwrap()).await,
1197 Err(Error::CantIgnoreLoggedInUser)
1198 );
1199 }
1200}