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