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 the status code is 429, this is requesting a retry in HTTP, without the
232        // custom `errcode`. Treat that as a retriable request with no specified
233        // retry_after delay.
234        //
235        // All 5xx errors are considered transient, including non-standard ones like
236        // 520 ("web server returned an unknown error", from Cloudflare or another
237        // reverse proxy): they reflect the state of the server at the time of the
238        // request, and the request may well succeed when retried later.
239        if status_code == StatusCode::TOO_MANY_REQUESTS || status_code.is_server_error() {
240            RetryKind::Transient { retry_after: None }
241        } else {
242            RetryKind::Permanent
243        }
244    }
245}
246
247/// Internal representation of errors.
248#[derive(Error, Debug)]
249#[non_exhaustive]
250pub enum Error {
251    /// Error doing an HTTP request.
252    #[error(transparent)]
253    Http(Box<HttpError>),
254
255    /// Queried endpoint requires authentication but was called on an anonymous
256    /// client.
257    #[error("the queried endpoint requires authentication but was called before logging in")]
258    AuthenticationRequired,
259
260    /// This request failed because the local data wasn't sufficient.
261    #[error("Local cache doesn't contain all necessary data to perform the action.")]
262    InsufficientData,
263
264    /// Attempting to restore a session after the olm-machine has already been
265    /// set up fails
266    #[cfg(feature = "e2e-encryption")]
267    #[error("The olm machine has already been initialized")]
268    BadCryptoStoreState,
269
270    /// Attempting to access the olm-machine but it is not yet available.
271    #[cfg(feature = "e2e-encryption")]
272    #[error("The olm machine isn't yet available")]
273    NoOlmMachine,
274
275    /// An error de/serializing type for the `StateStore`
276    #[error(transparent)]
277    SerdeJson(#[from] JsonError),
278
279    /// An IO error happened.
280    #[error(transparent)]
281    Io(#[from] IoError),
282
283    /// An error occurred in the crypto store.
284    #[cfg(feature = "e2e-encryption")]
285    #[error(transparent)]
286    CryptoStoreError(Box<CryptoStoreError>),
287
288    /// An error occurred with a cross-process store lock.
289    #[error(transparent)]
290    CrossProcessLockError(Box<CrossProcessLockError>),
291
292    /// An error occurred during a E2EE operation.
293    #[cfg(feature = "e2e-encryption")]
294    #[error(transparent)]
295    OlmError(Box<OlmError>),
296
297    /// An error occurred during a E2EE group operation.
298    #[cfg(feature = "e2e-encryption")]
299    #[error(transparent)]
300    MegolmError(Box<MegolmError>),
301
302    /// An error occurred during decryption.
303    #[cfg(feature = "e2e-encryption")]
304    #[error(transparent)]
305    DecryptorError(#[from] DecryptorError),
306
307    /// An error occurred during signing or verifying.
308    #[cfg(feature = "e2e-encryption")]
309    #[error(transparent)]
310    SignatureError(#[from] SignatureError),
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#[cfg(feature = "e2e-encryption")]
537impl From<BootstrapCrossSigningError> for Error {
538    fn from(error: BootstrapCrossSigningError) -> Self {
539        match error {
540            BootstrapCrossSigningError::CryptoStore(e) => e.into(),
541            BootstrapCrossSigningError::Signature(e) => e.into(),
542        }
543    }
544}
545
546/// Error for the room key importing functionality.
547#[cfg(feature = "e2e-encryption")]
548#[derive(Error, Debug)]
549// This is allowed because key importing isn't enabled under wasm.
550#[allow(dead_code)]
551pub enum RoomKeyImportError {
552    /// An error de/serializing type for the `StateStore`
553    #[error(transparent)]
554    SerdeJson(#[from] JsonError),
555
556    /// The crypto store isn't yet open. Logging in is required to open the
557    /// crypto store.
558    #[error("The crypto store hasn't been yet opened, can't import yet.")]
559    StoreClosed,
560
561    /// An IO error happened.
562    #[error(transparent)]
563    Io(#[from] IoError),
564
565    /// An error occurred in the crypto store.
566    #[error(transparent)]
567    CryptoStore(#[from] CryptoStoreError),
568
569    /// An error occurred while importing the key export.
570    #[error(transparent)]
571    Export(#[from] KeyExportError),
572}
573
574impl From<FromHttpResponseError<ruma::api::error::Error>> for HttpError {
575    fn from(err: FromHttpResponseError<ruma::api::error::Error>) -> Self {
576        Self::Api(Box::new(err.map(Into::into)))
577    }
578}
579
580impl From<SdkBaseError> for Error {
581    fn from(e: SdkBaseError) -> Self {
582        match e {
583            SdkBaseError::StateStore(e) => Self::StateStore(Box::new(e)),
584            #[cfg(feature = "e2e-encryption")]
585            SdkBaseError::CryptoStore(e) => Self::CryptoStoreError(Box::new(e)),
586            #[cfg(feature = "e2e-encryption")]
587            SdkBaseError::BadCryptoStoreState => Self::BadCryptoStoreState,
588            #[cfg(feature = "e2e-encryption")]
589            SdkBaseError::OlmError(e) => Self::OlmError(Box::new(e)),
590            #[cfg(feature = "eyre")]
591            _ => Self::UnknownError(eyre::eyre!(e).into()),
592            #[cfg(all(not(feature = "eyre"), feature = "anyhow", not(target_family = "wasm")))]
593            _ => Self::UnknownError(anyhow::anyhow!(e).into()),
594            #[cfg(all(not(feature = "eyre"), feature = "anyhow", target_family = "wasm"))]
595            _ => Self::UnknownError(e.into()),
596            #[cfg(all(
597                not(feature = "eyre"),
598                not(feature = "anyhow"),
599                not(target_family = "wasm")
600            ))]
601            _ => {
602                let e: Box<dyn std::error::Error + Send + Sync> = format!("{e:?}").into();
603                Self::UnknownError(e)
604            }
605            #[cfg(all(not(feature = "eyre"), not(feature = "anyhow"), target_family = "wasm"))]
606            _ => {
607                let e: Box<dyn std::error::Error> = format!("{e:?}").into();
608                Self::UnknownError(e)
609            }
610        }
611    }
612}
613
614impl From<ReqwestError> for Error {
615    fn from(e: ReqwestError) -> Self {
616        Error::Http(Box::new(HttpError::Reqwest(e)))
617    }
618}
619
620/// Errors that can happen when interacting with the beacon API.
621#[derive(Debug, Error)]
622pub enum BeaconError {
623    /// A network error occurred.
624    #[error("Network error: {0}")]
625    Network(#[from] HttpError),
626
627    /// The beacon information is not found.
628    #[error("Existing beacon information not found.")]
629    NotFound,
630
631    /// The redacted event is not an error, but it's not useful for the client.
632    #[error("Beacon event is redacted and cannot be processed.")]
633    Redacted,
634
635    /// The client must join the room to access the beacon information.
636    #[error("Must join the room to access beacon information.")]
637    Stripped,
638
639    /// The beacon event could not be deserialized.
640    #[error("Deserialization error: {0}")]
641    Deserialization(#[from] serde_json::Error),
642
643    /// The beacon event is expired.
644    #[error("The beacon event has expired.")]
645    NotLive,
646
647    /// Allow for other errors to be wrapped.
648    #[error("Other error: {0}")]
649    Other(Box<Error>),
650}
651
652impl From<Error> for BeaconError {
653    fn from(err: Error) -> Self {
654        BeaconError::Other(Box::new(err))
655    }
656}
657
658/// Errors that can happen when refreshing an access token.
659///
660/// This is usually only returned by [`Client::refresh_access_token()`], unless
661/// [handling refresh tokens] is activated for the `Client`.
662///
663/// [`Client::refresh_access_token()`]: crate::Client::refresh_access_token()
664/// [handling refresh tokens]: crate::ClientBuilder::handle_refresh_tokens()
665#[derive(Debug, Error, Clone)]
666pub enum RefreshTokenError {
667    /// Tried to send a refresh token request without a refresh token.
668    #[error("missing refresh token")]
669    RefreshTokenRequired,
670
671    /// An error occurred interacting with the native Matrix authentication API.
672    #[error(transparent)]
673    MatrixAuth(Arc<HttpError>),
674
675    /// An error occurred interacting with the OAuth 2.0 API.
676    #[error(transparent)]
677    OAuth(#[from] Arc<OAuthError>),
678}
679
680/// Errors that can occur when manipulating push notification settings.
681#[derive(Debug, Error, Clone, PartialEq)]
682pub enum NotificationSettingsError {
683    /// Invalid parameter.
684    #[error("Invalid parameter `{0}`")]
685    InvalidParameter(String),
686    /// Unable to add push rule.
687    #[error("Unable to add push rule")]
688    UnableToAddPushRule,
689    /// Unable to remove push rule.
690    #[error("Unable to remove push rule")]
691    UnableToRemovePushRule,
692    /// Unable to update push rule.
693    #[error("Unable to update push rule")]
694    UnableToUpdatePushRule,
695    /// Rule not found
696    #[error("Rule `{0}` not found")]
697    RuleNotFound(String),
698    /// Unable to save the push rules
699    #[error("Unable to save push rules")]
700    UnableToSavePushRules,
701}
702
703impl NotificationSettingsError {
704    /// Whether this error is the [`RuleNotFound`](Self::RuleNotFound) variant.
705    pub fn is_rule_not_found(&self) -> bool {
706        matches!(self, Self::RuleNotFound(_))
707    }
708}
709
710impl From<InsertPushRuleError> for NotificationSettingsError {
711    fn from(_: InsertPushRuleError) -> Self {
712        Self::UnableToAddPushRule
713    }
714}
715
716impl From<RemovePushRuleError> for NotificationSettingsError {
717    fn from(_: RemovePushRuleError) -> Self {
718        Self::UnableToRemovePushRule
719    }
720}
721
722#[derive(Debug, Error)]
723#[error("expected: {expected}, got: {got:?}")]
724pub struct WrongRoomState {
725    expected: &'static str,
726    got: RoomState,
727}
728
729impl WrongRoomState {
730    pub(crate) fn new(expected: &'static str, got: RoomState) -> Self {
731        Self { expected, got }
732    }
733}