Skip to main content

matrix_sdk/
error.rs

1// Copyright 2020 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Error conditions.
16
17use std::{io::Error as IoError, sync::Arc, time::Duration};
18
19use as_variant::as_variant;
20use http::StatusCode;
21#[cfg(feature = "qrcode")]
22use matrix_sdk_base::crypto::ScanError;
23#[cfg(feature = "e2e-encryption")]
24use matrix_sdk_base::crypto::{
25    CryptoStoreError, DecryptorError, KeyExportError, MegolmError, OlmError,
26};
27use matrix_sdk_base::{
28    Error as SdkBaseError, QueueWedgeError, RoomState, StoreError,
29    cross_process_lock::CrossProcessLockUnobtained, event_cache::store::EventCacheStoreError,
30    media::store::MediaStoreError,
31};
32use reqwest::Error as ReqwestError;
33use ruma::{
34    IdParseError,
35    api::{
36        client::uiaa::{UiaaInfo, UiaaResponse},
37        error::{ErrorKind, FromHttpResponseError, IntoHttpError, RetryAfter},
38    },
39    events::{room::power_levels::PowerLevelsError, tag::InvalidUserTagName},
40    push::{InsertPushRuleError, RemovePushRuleError},
41};
42#[cfg(target_os = "android")]
43use rustls::client::VerifierBuilderError;
44use serde_json::Error as JsonError;
45use thiserror::Error;
46use url::ParseError as UrlParseError;
47
48use crate::{
49    authentication::oauth::OAuthError, cross_process_lock::CrossProcessLockError,
50    event_cache::EventCacheError, media::MediaError, room::reply::ReplyError,
51    sliding_sync::Error as SlidingSyncError,
52};
53
54/// Result type of the matrix-sdk.
55pub type Result<T, E = Error> = std::result::Result<T, E>;
56
57/// Result type of a pure HTTP request.
58pub type HttpResult<T> = std::result::Result<T, HttpError>;
59
60/// An error response from a Matrix API call, using a client API specific
61/// representation if the endpoint is from that.
62pub type RumaApiError = UiaaResponse;
63
64/// An HTTP error, representing either a connection error or an error while
65/// converting the raw HTTP response into a Matrix response.
66#[derive(Error, Debug)]
67pub enum HttpError {
68    /// Error at the HTTP layer.
69    #[error(transparent)]
70    Reqwest(#[from] ReqwestError),
71
72    /// API response error (deserialization, or a Matrix-specific error).
73    // `Box` its inner value to reduce the enum size.
74    #[error(transparent)]
75    Api(#[from] Box<FromHttpResponseError<RumaApiError>>),
76
77    /// Error when creating an API request (e.g. serialization of
78    /// body/headers/query parameters).
79    #[error(transparent)]
80    IntoHttp(IntoHttpError),
81
82    /// Error while refreshing the access token.
83    #[error(transparent)]
84    RefreshToken(RefreshTokenError),
85
86    /// Error while fetching data that is cached.
87    ///
88    /// This variant is present for convenience because cached data wraps
89    /// [`HttpError`] into an [`Arc`] to be able to clone it.
90    #[error(transparent)]
91    Cached(Arc<HttpError>),
92
93    /// Error creating the TLS verifier.
94    #[cfg(target_os = "android")]
95    #[error(transparent)]
96    VerifierBuilder(#[from] VerifierBuilderError),
97}
98
99#[rustfmt::skip] // stop rustfmt breaking the `<code>` in docs across multiple lines
100impl HttpError {
101    /// If `self` is
102    /// <code>[Api](Self::Api)([Server](FromHttpResponseError::Server)(e))</code>,
103    /// returns `Some(e)`.
104    ///
105    /// Otherwise, returns `None`.
106    pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> {
107        match self {
108            Self::Api(error) => {
109                as_variant!(error.as_ref(), FromHttpResponseError::Server)
110            },
111            _ => None
112        }
113    }
114}
115
116// Another impl block that's formatted with rustfmt.
117impl HttpError {
118    /// If `self` is a server error in the `errcode` + `error` format expected
119    /// for client API endpoints, returns it.
120    pub fn as_client_api_error(&self) -> Option<&ruma::api::error::Error> {
121        self.as_ruma_api_error().and_then(as_variant!(UiaaResponse::MatrixError))
122    }
123
124    /// If `self` is a server error in the `errcode` + `error` format expected
125    /// for client API endpoints, returns the error kind (`errcode`).
126    pub fn client_api_error_kind(&self) -> Option<&ErrorKind> {
127        self.as_client_api_error().and_then(ruma::api::error::Error::error_kind)
128    }
129
130    /// Try to destructure the error into a user-interactive auth info.
131    ///
132    /// Some requests require user-interactive auth, doing such a request
133    /// will always fail the first time with a 401 status code, the response
134    /// body will contain info on how the client can authenticate.
135    ///
136    /// The request will need to be retried, this time containing additional
137    /// authentication data.
138    ///
139    /// This method is a convenience method to get to the info the server
140    /// returned on the first, failed request.
141    pub fn as_uiaa_response(&self) -> Option<&UiaaInfo> {
142        self.as_ruma_api_error().and_then(as_variant!(UiaaResponse::AuthResponse))
143    }
144
145    /// Returns whether an HTTP error response should be qualified as transient
146    /// or permanent.
147    pub(crate) fn retry_kind(&self) -> RetryKind {
148        match self {
149            // If it was a plain network error, it's either that we're disconnected from the
150            // internet, or that the remote is, so retry a few times.
151            HttpError::Reqwest(_) => RetryKind::NetworkFailure,
152
153            HttpError::Api(error) => match error.as_ref() {
154                FromHttpResponseError::Server(api_error) => RetryKind::from_api_error(api_error),
155                _ => RetryKind::Permanent,
156            },
157            _ => RetryKind::Permanent,
158        }
159    }
160
161    /// If this is a server error, returns whether it matches the expected
162    /// format for an endpoint that is not implemented.
163    pub fn is_endpoint_not_implemented(&self) -> bool {
164        self.as_client_api_error().is_some_and(|error| error.is_endpoint_not_implemented())
165    }
166}
167
168impl From<FromHttpResponseError<RumaApiError>> for HttpError {
169    fn from(value: FromHttpResponseError<RumaApiError>) -> Self {
170        Self::Api(Box::new(value))
171    }
172}
173
174/// How should we behave with respect to retry behavior after an [`HttpError`]
175/// happened?
176pub(crate) enum RetryKind {
177    /// The request failed because of an error at the network layer.
178    NetworkFailure,
179
180    /// The request failed with a "transient" error, meaning it could be retried
181    /// either soon, or after a given amount of time expressed in
182    /// `retry_after`.
183    Transient {
184        // This is used only for attempts to retry, so on non-wasm32 code (in the `native` module).
185        #[cfg_attr(target_family = "wasm", allow(dead_code))]
186        retry_after: Option<Duration>,
187    },
188
189    /// The request failed with a non-transient error, and retrying it would
190    /// likely cause the same error again, so it's not worth retrying.
191    Permanent,
192}
193
194impl RetryKind {
195    /// Construct a [`RetryKind`] from a Ruma API error.
196    ///
197    /// The Ruma API error is for errors which have the standard error response
198    /// format defined in the [spec].
199    ///
200    /// [spec]: https://spec.matrix.org/v1.11/client-server-api/#standard-error-response
201    fn from_api_error(api_error: &UiaaResponse) -> Self {
202        match api_error {
203            UiaaResponse::MatrixError(client_error) => match client_error.error_kind() {
204                Some(ErrorKind::LimitExceeded(limit_exceeded)) => {
205                    RetryKind::from_retry_after(limit_exceeded.retry_after.as_ref())
206                }
207                Some(ErrorKind::Unrecognized) => RetryKind::Permanent,
208                _ => RetryKind::from_status_code(client_error.status_code),
209            },
210            UiaaResponse::AuthResponse(_) => RetryKind::Permanent,
211        }
212    }
213
214    /// Create a [`RetryKind`] if we have found a [`RetryAfter`] defined in an
215    /// error.
216    ///
217    /// This method should be used for errors where the server explicitly tells
218    /// us how long we must wait before we retry the request again.
219    fn from_retry_after(retry_after: Option<&RetryAfter>) -> Self {
220        let retry_after = retry_after
221            .and_then(|retry_after| match retry_after {
222                RetryAfter::Delay(d) => Some(d),
223                RetryAfter::DateTime(_) => None,
224            })
225            .copied();
226
227        Self::Transient { retry_after }
228    }
229
230    /// Construct a [`RetryKind`] from a HTTP [`StatusCode`].
231    ///
232    /// This should be used if we don't have a more specific Matrix style error
233    /// which gives us more information about the nature of the error, i.e.
234    /// if we received an error from a reverse proxy while the Matrix
235    /// homeserver is down.
236    fn from_status_code(status_code: StatusCode) -> Self {
237        if status_code.as_u16() == 520 {
238            // Cloudflare or some other proxy server sent this, meaning the actual
239            // homeserver sent some unknown error back
240            RetryKind::Permanent
241        } else if status_code == StatusCode::TOO_MANY_REQUESTS || status_code.is_server_error() {
242            // If the status code is 429, this is requesting a retry in HTTP, without the
243            // custom `errcode`. Treat that as a retriable request with no specified
244            // retry_after delay.
245            RetryKind::Transient { retry_after: None }
246        } else {
247            RetryKind::Permanent
248        }
249    }
250}
251
252/// Internal representation of errors.
253#[derive(Error, Debug)]
254#[non_exhaustive]
255pub enum Error {
256    /// Error doing an HTTP request.
257    #[error(transparent)]
258    Http(Box<HttpError>),
259
260    /// Queried endpoint requires authentication but was called on an anonymous
261    /// client.
262    #[error("the queried endpoint requires authentication but was called before logging in")]
263    AuthenticationRequired,
264
265    /// This request failed because the local data wasn't sufficient.
266    #[error("Local cache doesn't contain all necessary data to perform the action.")]
267    InsufficientData,
268
269    /// Attempting to restore a session after the olm-machine has already been
270    /// set up fails
271    #[cfg(feature = "e2e-encryption")]
272    #[error("The olm machine has already been initialized")]
273    BadCryptoStoreState,
274
275    /// Attempting to access the olm-machine but it is not yet available.
276    #[cfg(feature = "e2e-encryption")]
277    #[error("The olm machine isn't yet available")]
278    NoOlmMachine,
279
280    /// An error de/serializing type for the `StateStore`
281    #[error(transparent)]
282    SerdeJson(#[from] JsonError),
283
284    /// An IO error happened.
285    #[error(transparent)]
286    Io(#[from] IoError),
287
288    /// An error occurred in the crypto store.
289    #[cfg(feature = "e2e-encryption")]
290    #[error(transparent)]
291    CryptoStoreError(Box<CryptoStoreError>),
292
293    /// An error occurred with a cross-process store lock.
294    #[error(transparent)]
295    CrossProcessLockError(Box<CrossProcessLockError>),
296
297    /// An error occurred during a E2EE operation.
298    #[cfg(feature = "e2e-encryption")]
299    #[error(transparent)]
300    OlmError(Box<OlmError>),
301
302    /// An error occurred during a E2EE group operation.
303    #[cfg(feature = "e2e-encryption")]
304    #[error(transparent)]
305    MegolmError(Box<MegolmError>),
306
307    /// An error occurred during decryption.
308    #[cfg(feature = "e2e-encryption")]
309    #[error(transparent)]
310    DecryptorError(#[from] DecryptorError),
311
312    /// An error occurred in the state store.
313    #[error(transparent)]
314    StateStore(Box<StoreError>),
315
316    /// An error occurred in the event cache store.
317    #[error(transparent)]
318    EventCacheStore(Box<EventCacheStoreError>),
319
320    /// An error occurred in the media store.
321    #[error(transparent)]
322    MediaStore(Box<MediaStoreError>),
323
324    /// An error encountered when trying to parse an identifier.
325    #[error(transparent)]
326    Identifier(#[from] IdParseError),
327
328    /// An error encountered when trying to parse a url.
329    #[error(transparent)]
330    Url(#[from] UrlParseError),
331
332    /// An error while scanning a QR code.
333    #[cfg(feature = "qrcode")]
334    #[error(transparent)]
335    QrCodeScanError(Box<ScanError>),
336
337    /// An error encountered when trying to parse a user tag name.
338    #[error(transparent)]
339    UserTagName(#[from] InvalidUserTagName),
340
341    /// An error occurred within sliding-sync
342    #[error(transparent)]
343    SlidingSync(Box<SlidingSyncError>),
344
345    /// Attempted to call a method on a room that requires the user to have a
346    /// specific membership state in the room, but the membership state is
347    /// different.
348    #[error("wrong room state: {0}")]
349    WrongRoomState(Box<WrongRoomState>),
350
351    /// Session callbacks have been set multiple times.
352    #[error("session callbacks have been set multiple times")]
353    MultipleSessionCallbacks,
354
355    /// An error occurred interacting with the OAuth 2.0 API.
356    #[error(transparent)]
357    OAuth(Box<OAuthError>),
358
359    /// A concurrent request to a deduplicated request has failed.
360    #[error("a concurrent request failed; see logs for details")]
361    ConcurrentRequestFailed,
362
363    /// An other error was raised.
364    ///
365    /// This might happen because encryption was enabled on the base-crate
366    /// but not here and that raised.
367    #[cfg(not(target_family = "wasm"))]
368    #[error("unknown error: {0}")]
369    UnknownError(Box<dyn std::error::Error + Send + Sync>),
370
371    /// An other error was raised.
372    #[cfg(target_family = "wasm")]
373    #[error("unknown error: {0}")]
374    UnknownError(Box<dyn std::error::Error>),
375
376    /// An error coming from the event cache subsystem.
377    #[error(transparent)]
378    EventCache(Box<EventCacheError>),
379
380    /// An item has been wedged in the send queue.
381    #[error(transparent)]
382    SendQueueWedgeError(Box<QueueWedgeError>),
383
384    /// Backups are not enabled
385    #[error("backups are not enabled")]
386    BackupNotEnabled,
387
388    /// It's forbidden to ignore your own user.
389    #[error("can't ignore the logged-in user")]
390    CantIgnoreLoggedInUser,
391
392    /// An error happened during handling of a media subrequest.
393    #[error(transparent)]
394    Media(#[from] MediaError),
395
396    /// An error happened while attempting to reply to an event.
397    #[error(transparent)]
398    ReplyError(#[from] ReplyError),
399
400    /// An error happened while attempting to change power levels.
401    #[error("power levels error: {0}")]
402    PowerLevels(#[from] PowerLevelsError),
403
404    /// We timed out attempting to complete an operation.
405    #[error("timed out")]
406    Timeout,
407}
408
409#[rustfmt::skip] // stop rustfmt breaking the `<code>` in docs across multiple lines
410impl Error {
411    /// If `self` is
412    /// <code>[Http](Self::Http)([Api](HttpError::Api)([Server](FromHttpResponseError::Server)(e)))</code>,
413    /// returns `Some(e)`.
414    ///
415    /// Otherwise, returns `None`.
416    pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> {
417        as_variant!(self, Self::Http).and_then(|e| e.as_ruma_api_error())
418    }
419
420    /// If `self` is a server error in the `errcode` + `error` format expected
421    /// for client API endpoints, returns it.
422    pub fn as_client_api_error(&self) -> Option<&ruma::api::error::Error> {
423        self.as_ruma_api_error().and_then(as_variant!(UiaaResponse::MatrixError))
424    }
425
426    /// If `self` is a server error in the `errcode` + `error` format expected
427    /// for client API endpoints, returns the error kind (`errcode`).
428    pub fn client_api_error_kind(&self) -> Option<&ErrorKind> {
429        self.as_client_api_error().and_then(ruma::api::error::Error::error_kind)
430    }
431
432    /// Try to destructure the error into an universal interactive auth info.
433    ///
434    /// Some requests require universal interactive auth, doing such a request
435    /// will always fail the first time with a 401 status code, the response
436    /// body will contain info how the client can authenticate.
437    ///
438    /// The request will need to be retried, this time containing additional
439    /// authentication data.
440    ///
441    /// This method is an convenience method to get to the info the server
442    /// returned on the first, failed request.
443    pub fn as_uiaa_response(&self) -> Option<&UiaaInfo> {
444        self.as_ruma_api_error().and_then(as_variant!(UiaaResponse::AuthResponse))
445    }
446}
447
448impl From<HttpError> for Error {
449    fn from(error: HttpError) -> Self {
450        Error::Http(Box::new(error))
451    }
452}
453
454#[cfg(feature = "e2e-encryption")]
455impl From<CryptoStoreError> for Error {
456    fn from(error: CryptoStoreError) -> Self {
457        Error::CryptoStoreError(Box::new(error))
458    }
459}
460
461impl From<CrossProcessLockError> for Error {
462    fn from(error: CrossProcessLockError) -> Self {
463        Error::CrossProcessLockError(Box::new(error))
464    }
465}
466
467impl From<CrossProcessLockUnobtained> for Error {
468    fn from(error: CrossProcessLockUnobtained) -> Self {
469        CrossProcessLockError::from(error).into()
470    }
471}
472
473#[cfg(feature = "e2e-encryption")]
474impl From<OlmError> for Error {
475    fn from(error: OlmError) -> Self {
476        Error::OlmError(Box::new(error))
477    }
478}
479
480#[cfg(feature = "e2e-encryption")]
481impl From<MegolmError> for Error {
482    fn from(error: MegolmError) -> Self {
483        Error::MegolmError(Box::new(error))
484    }
485}
486
487impl From<StoreError> for Error {
488    fn from(error: StoreError) -> Self {
489        Error::StateStore(Box::new(error))
490    }
491}
492
493impl From<EventCacheStoreError> for Error {
494    fn from(error: EventCacheStoreError) -> Self {
495        Error::EventCacheStore(Box::new(error))
496    }
497}
498
499impl From<MediaStoreError> for Error {
500    fn from(error: MediaStoreError) -> Self {
501        Error::MediaStore(Box::new(error))
502    }
503}
504
505#[cfg(feature = "qrcode")]
506impl From<ScanError> for Error {
507    fn from(error: ScanError) -> Self {
508        Error::QrCodeScanError(Box::new(error))
509    }
510}
511
512impl From<SlidingSyncError> for Error {
513    fn from(error: SlidingSyncError) -> Self {
514        Error::SlidingSync(Box::new(error))
515    }
516}
517
518impl From<OAuthError> for Error {
519    fn from(error: OAuthError) -> Self {
520        Error::OAuth(Box::new(error))
521    }
522}
523
524impl From<EventCacheError> for Error {
525    fn from(error: EventCacheError) -> Self {
526        Error::EventCache(Box::new(error))
527    }
528}
529
530impl From<QueueWedgeError> for Error {
531    fn from(error: QueueWedgeError) -> Self {
532        Error::SendQueueWedgeError(Box::new(error))
533    }
534}
535
536/// Error for the room key importing functionality.
537#[cfg(feature = "e2e-encryption")]
538#[derive(Error, Debug)]
539// This is allowed because key importing isn't enabled under wasm.
540#[allow(dead_code)]
541pub enum RoomKeyImportError {
542    /// An error de/serializing type for the `StateStore`
543    #[error(transparent)]
544    SerdeJson(#[from] JsonError),
545
546    /// The crypto store isn't yet open. Logging in is required to open the
547    /// crypto store.
548    #[error("The crypto store hasn't been yet opened, can't import yet.")]
549    StoreClosed,
550
551    /// An IO error happened.
552    #[error(transparent)]
553    Io(#[from] IoError),
554
555    /// An error occurred in the crypto store.
556    #[error(transparent)]
557    CryptoStore(#[from] CryptoStoreError),
558
559    /// An error occurred while importing the key export.
560    #[error(transparent)]
561    Export(#[from] KeyExportError),
562}
563
564impl From<FromHttpResponseError<ruma::api::error::Error>> for HttpError {
565    fn from(err: FromHttpResponseError<ruma::api::error::Error>) -> Self {
566        Self::Api(Box::new(err.map(Into::into)))
567    }
568}
569
570impl From<SdkBaseError> for Error {
571    fn from(e: SdkBaseError) -> Self {
572        match e {
573            SdkBaseError::StateStore(e) => Self::StateStore(Box::new(e)),
574            #[cfg(feature = "e2e-encryption")]
575            SdkBaseError::CryptoStore(e) => Self::CryptoStoreError(Box::new(e)),
576            #[cfg(feature = "e2e-encryption")]
577            SdkBaseError::BadCryptoStoreState => Self::BadCryptoStoreState,
578            #[cfg(feature = "e2e-encryption")]
579            SdkBaseError::OlmError(e) => Self::OlmError(Box::new(e)),
580            #[cfg(feature = "eyre")]
581            _ => Self::UnknownError(eyre::eyre!(e).into()),
582            #[cfg(all(not(feature = "eyre"), feature = "anyhow", not(target_family = "wasm")))]
583            _ => Self::UnknownError(anyhow::anyhow!(e).into()),
584            #[cfg(all(not(feature = "eyre"), feature = "anyhow", target_family = "wasm"))]
585            _ => Self::UnknownError(e.into()),
586            #[cfg(all(
587                not(feature = "eyre"),
588                not(feature = "anyhow"),
589                not(target_family = "wasm")
590            ))]
591            _ => {
592                let e: Box<dyn std::error::Error + Send + Sync> = format!("{e:?}").into();
593                Self::UnknownError(e)
594            }
595            #[cfg(all(not(feature = "eyre"), not(feature = "anyhow"), target_family = "wasm"))]
596            _ => {
597                let e: Box<dyn std::error::Error> = format!("{e:?}").into();
598                Self::UnknownError(e)
599            }
600        }
601    }
602}
603
604impl From<ReqwestError> for Error {
605    fn from(e: ReqwestError) -> Self {
606        Error::Http(Box::new(HttpError::Reqwest(e)))
607    }
608}
609
610/// Errors that can happen when interacting with the beacon API.
611#[derive(Debug, Error)]
612pub enum BeaconError {
613    /// A network error occurred.
614    #[error("Network error: {0}")]
615    Network(#[from] HttpError),
616
617    /// The beacon information is not found.
618    #[error("Existing beacon information not found.")]
619    NotFound,
620
621    /// The redacted event is not an error, but it's not useful for the client.
622    #[error("Beacon event is redacted and cannot be processed.")]
623    Redacted,
624
625    /// The client must join the room to access the beacon information.
626    #[error("Must join the room to access beacon information.")]
627    Stripped,
628
629    /// The beacon event could not be deserialized.
630    #[error("Deserialization error: {0}")]
631    Deserialization(#[from] serde_json::Error),
632
633    /// The beacon event is expired.
634    #[error("The beacon event has expired.")]
635    NotLive,
636
637    /// Allow for other errors to be wrapped.
638    #[error("Other error: {0}")]
639    Other(Box<Error>),
640}
641
642impl From<Error> for BeaconError {
643    fn from(err: Error) -> Self {
644        BeaconError::Other(Box::new(err))
645    }
646}
647
648/// Errors that can happen when refreshing an access token.
649///
650/// This is usually only returned by [`Client::refresh_access_token()`], unless
651/// [handling refresh tokens] is activated for the `Client`.
652///
653/// [`Client::refresh_access_token()`]: crate::Client::refresh_access_token()
654/// [handling refresh tokens]: crate::ClientBuilder::handle_refresh_tokens()
655#[derive(Debug, Error, Clone)]
656pub enum RefreshTokenError {
657    /// Tried to send a refresh token request without a refresh token.
658    #[error("missing refresh token")]
659    RefreshTokenRequired,
660
661    /// An error occurred interacting with the native Matrix authentication API.
662    #[error(transparent)]
663    MatrixAuth(Arc<HttpError>),
664
665    /// An error occurred interacting with the OAuth 2.0 API.
666    #[error(transparent)]
667    OAuth(#[from] Arc<OAuthError>),
668}
669
670/// Errors that can occur when manipulating push notification settings.
671#[derive(Debug, Error, Clone, PartialEq)]
672pub enum NotificationSettingsError {
673    /// Invalid parameter.
674    #[error("Invalid parameter `{0}`")]
675    InvalidParameter(String),
676    /// Unable to add push rule.
677    #[error("Unable to add push rule")]
678    UnableToAddPushRule,
679    /// Unable to remove push rule.
680    #[error("Unable to remove push rule")]
681    UnableToRemovePushRule,
682    /// Unable to update push rule.
683    #[error("Unable to update push rule")]
684    UnableToUpdatePushRule,
685    /// Rule not found
686    #[error("Rule `{0}` not found")]
687    RuleNotFound(String),
688    /// Unable to save the push rules
689    #[error("Unable to save push rules")]
690    UnableToSavePushRules,
691}
692
693impl NotificationSettingsError {
694    /// Whether this error is the [`RuleNotFound`](Self::RuleNotFound) variant.
695    pub fn is_rule_not_found(&self) -> bool {
696        matches!(self, Self::RuleNotFound(_))
697    }
698}
699
700impl From<InsertPushRuleError> for NotificationSettingsError {
701    fn from(_: InsertPushRuleError) -> Self {
702        Self::UnableToAddPushRule
703    }
704}
705
706impl From<RemovePushRuleError> for NotificationSettingsError {
707    fn from(_: RemovePushRuleError) -> Self {
708        Self::UnableToRemovePushRule
709    }
710}
711
712#[derive(Debug, Error)]
713#[error("expected: {expected}, got: {got:?}")]
714pub struct WrongRoomState {
715    expected: &'static str,
716    got: RoomState,
717}
718
719impl WrongRoomState {
720    pub(crate) fn new(expected: &'static str, got: RoomState) -> Self {
721        Self { expected, got }
722    }
723}