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 will
127    /// always fail the first time with a 401 status code, the response body
128    /// 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
144            // disconnected from the internet, or that the remote is, so retry a
145            // few times.
146            HttpError::Reqwest(_) => RetryKind::NetworkFailure,
147
148            HttpError::Api(error) => match error.as_ref() {
149                FromHttpResponseError::Server(api_error) => RetryKind::from_api_error(api_error),
150                _ => RetryKind::Permanent,
151            },
152            _ => RetryKind::Permanent,
153        }
154    }
155
156    /// If this is a server error, returns whether it matches the expected
157    /// format for an endpoint that is not implemented.
158    pub fn is_endpoint_not_implemented(&self) -> bool {
159        self.as_client_api_error().is_some_and(|error| error.is_endpoint_not_implemented())
160    }
161}
162
163impl From<FromHttpResponseError<RumaApiError>> for HttpError {
164    fn from(value: FromHttpResponseError<RumaApiError>) -> Self {
165        Self::Api(Box::new(value))
166    }
167}
168
169/// How should we behave with respect to retry behavior after an [`HttpError`]
170/// happened?
171pub(crate) enum RetryKind {
172    /// The request failed because of an error at the network layer.
173    NetworkFailure,
174
175    /// The request failed with a "transient" error, meaning it could be retried
176    /// either soon, or after a given amount of time expressed in `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. if
228    /// we received an error from a reverse proxy while the Matrix homeserver is
229    /// down.
230    fn from_status_code(status_code: StatusCode) -> Self {
231        // If the status code is 429, this is requesting a retry in HTTP,
232        // without the custom `errcode`. Treat that as a retriable request with
233        // no specified retry_after delay.
234        //
235        // All 5xx errors are considered transient, including non-standard ones
236        // like 520 ("web server returned an unknown error", from Cloudflare or
237        // another reverse proxy): they reflect the state of the server at the
238        // time of the request, and the request may well succeed when retried
239        // later.
240        if status_code == StatusCode::TOO_MANY_REQUESTS || status_code.is_server_error() {
241            RetryKind::Transient { retry_after: None }
242        } else {
243            RetryKind::Permanent
244        }
245    }
246}
247
248/// Internal representation of errors.
249#[derive(Error, Debug)]
250#[non_exhaustive]
251pub enum Error {
252    /// Error doing an HTTP request.
253    #[error(transparent)]
254    Http(Box<HttpError>),
255
256    /// Queried endpoint requires authentication but was called on an anonymous
257    /// client.
258    #[error("the queried endpoint requires authentication but was called before logging in")]
259    AuthenticationRequired,
260
261    /// This request failed because the local data wasn't sufficient.
262    #[error("Local cache doesn't contain all necessary data to perform the action.")]
263    InsufficientData,
264
265    /// Attempting to restore a session after the olm-machine has already been
266    /// set up fails
267    #[cfg(feature = "e2e-encryption")]
268    #[error("The olm machine has already been initialized")]
269    BadCryptoStoreState,
270
271    /// Attempting to access the olm-machine but it is not yet available.
272    #[cfg(feature = "e2e-encryption")]
273    #[error("The olm machine isn't yet available")]
274    NoOlmMachine,
275
276    /// An error de/serializing type for the `StateStore`
277    #[error(transparent)]
278    SerdeJson(#[from] JsonError),
279
280    /// An IO error happened.
281    #[error(transparent)]
282    Io(#[from] IoError),
283
284    /// An error occurred in the crypto store.
285    #[cfg(feature = "e2e-encryption")]
286    #[error(transparent)]
287    CryptoStoreError(Box<CryptoStoreError>),
288
289    /// An error occurred with a cross-process store lock.
290    #[error(transparent)]
291    CrossProcessLockError(Box<CrossProcessLockError>),
292
293    /// An error occurred during a E2EE operation.
294    #[cfg(feature = "e2e-encryption")]
295    #[error(transparent)]
296    OlmError(Box<OlmError>),
297
298    /// An error occurred during a E2EE group operation.
299    #[cfg(feature = "e2e-encryption")]
300    #[error(transparent)]
301    MegolmError(Box<MegolmError>),
302
303    /// An error occurred during decryption.
304    #[cfg(feature = "e2e-encryption")]
305    #[error(transparent)]
306    DecryptorError(#[from] DecryptorError),
307
308    /// An error occurred during signing or verifying.
309    #[cfg(feature = "e2e-encryption")]
310    #[error(transparent)]
311    SignatureError(#[from] SignatureError),
312
313    /// An error occurred in the state store.
314    #[error(transparent)]
315    StateStore(Box<StoreError>),
316
317    /// An error occurred in the event cache store.
318    #[error(transparent)]
319    EventCacheStore(Box<EventCacheStoreError>),
320
321    /// An error occurred in the media store.
322    #[error(transparent)]
323    MediaStore(Box<MediaStoreError>),
324
325    /// An error encountered when trying to parse an identifier.
326    #[error(transparent)]
327    Identifier(#[from] IdParseError),
328
329    /// An error encountered when trying to parse a url.
330    #[error(transparent)]
331    Url(#[from] UrlParseError),
332
333    /// An error while scanning a QR code.
334    #[cfg(feature = "qrcode")]
335    #[error(transparent)]
336    QrCodeScanError(Box<ScanError>),
337
338    /// An error encountered when trying to parse a user tag name.
339    #[error(transparent)]
340    UserTagName(#[from] InvalidUserTagName),
341
342    /// An error occurred within sliding-sync
343    #[error(transparent)]
344    SlidingSync(Box<SlidingSyncError>),
345
346    /// Attempted to call a method on a room that requires the user to have a
347    /// specific membership state in the room, but the membership state is
348    /// different.
349    #[error("wrong room state: {0}")]
350    WrongRoomState(Box<WrongRoomState>),
351
352    /// Session callbacks have been set multiple times.
353    #[error("session callbacks have been set multiple times")]
354    MultipleSessionCallbacks,
355
356    /// An error occurred interacting with the OAuth 2.0 API.
357    #[error(transparent)]
358    OAuth(Box<OAuthError>),
359
360    /// A concurrent request to a deduplicated request has failed.
361    #[error("a concurrent request failed; see logs for details")]
362    ConcurrentRequestFailed,
363
364    /// An other error was raised.
365    ///
366    /// This might happen because encryption was enabled on the base-crate but
367    /// not here and that raised.
368    #[cfg(not(target_family = "wasm"))]
369    #[error("unknown error: {0}")]
370    UnknownError(Box<dyn std::error::Error + Send + Sync>),
371
372    /// An other error was raised.
373    #[cfg(target_family = "wasm")]
374    #[error("unknown error: {0}")]
375    UnknownError(Box<dyn std::error::Error>),
376
377    /// An error coming from the event cache subsystem.
378    #[error(transparent)]
379    EventCache(Box<EventCacheError>),
380
381    /// An item has been wedged in the send queue.
382    #[error(transparent)]
383    SendQueueWedgeError(Box<QueueWedgeError>),
384
385    /// Backups are not enabled
386    #[error("backups are not enabled")]
387    BackupNotEnabled,
388
389    /// It's forbidden to ignore your own user.
390    #[error("can't ignore the logged-in user")]
391    CantIgnoreLoggedInUser,
392
393    /// An error happened during handling of a media subrequest.
394    #[error(transparent)]
395    Media(#[from] MediaError),
396
397    /// An error happened while attempting to reply to an event.
398    #[error(transparent)]
399    ReplyError(#[from] ReplyError),
400
401    /// An error happened while attempting to change power levels.
402    #[error("power levels error: {0}")]
403    PowerLevels(#[from] PowerLevelsError),
404
405    /// We timed out attempting to complete an operation.
406    #[error("timed out")]
407    Timeout,
408}
409
410#[rustfmt::skip] // stop rustfmt breaking the `<code>` in docs across multiple lines
411impl Error {
412    /// If `self` is
413    /// <code>[Http](Self::Http)([Api](HttpError::Api)([Server](FromHttpResponseError::Server)(e)))</code>,
414    /// returns `Some(e)`.
415    ///
416    /// Otherwise, returns `None`.
417    pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> {
418        as_variant!(self, Self::Http).and_then(|e| e.as_ruma_api_error())
419    }
420
421    /// If `self` is a server error in the `errcode` + `error` format expected
422    /// for client API endpoints, returns it.
423    pub fn as_client_api_error(&self) -> Option<&ruma::api::error::Error> {
424        self.as_ruma_api_error().and_then(as_variant!(UiaaResponse::MatrixError))
425    }
426
427    /// If `self` is a server error in the `errcode` + `error` format expected
428    /// for client API endpoints, returns the error kind (`errcode`).
429    pub fn client_api_error_kind(&self) -> Option<&ErrorKind> {
430        self.as_client_api_error().and_then(ruma::api::error::Error::error_kind)
431    }
432
433    /// Try to destructure the error into an universal interactive auth info.
434    ///
435    /// Some requests require universal interactive auth, doing such a request
436    /// will always fail the first time with a 401 status code, the response
437    /// body will contain info how the client can authenticate.
438    ///
439    /// The request will need to be retried, this time containing additional
440    /// authentication data.
441    ///
442    /// This method is an convenience method to get to the info the server
443    /// returned on the first, failed request.
444    pub fn as_uiaa_response(&self) -> Option<&UiaaInfo> {
445        self.as_ruma_api_error().and_then(as_variant!(UiaaResponse::AuthResponse))
446    }
447}
448
449impl From<HttpError> for Error {
450    fn from(error: HttpError) -> Self {
451        Error::Http(Box::new(error))
452    }
453}
454
455#[cfg(feature = "e2e-encryption")]
456impl From<CryptoStoreError> for Error {
457    fn from(error: CryptoStoreError) -> Self {
458        Error::CryptoStoreError(Box::new(error))
459    }
460}
461
462impl From<CrossProcessLockError> for Error {
463    fn from(error: CrossProcessLockError) -> Self {
464        Error::CrossProcessLockError(Box::new(error))
465    }
466}
467
468impl From<CrossProcessLockUnobtained> for Error {
469    fn from(error: CrossProcessLockUnobtained) -> Self {
470        CrossProcessLockError::from(error).into()
471    }
472}
473
474#[cfg(feature = "e2e-encryption")]
475impl From<OlmError> for Error {
476    fn from(error: OlmError) -> Self {
477        Error::OlmError(Box::new(error))
478    }
479}
480
481#[cfg(feature = "e2e-encryption")]
482impl From<MegolmError> for Error {
483    fn from(error: MegolmError) -> Self {
484        Error::MegolmError(Box::new(error))
485    }
486}
487
488impl From<StoreError> for Error {
489    fn from(error: StoreError) -> Self {
490        Error::StateStore(Box::new(error))
491    }
492}
493
494impl From<EventCacheStoreError> for Error {
495    fn from(error: EventCacheStoreError) -> Self {
496        Error::EventCacheStore(Box::new(error))
497    }
498}
499
500impl From<MediaStoreError> for Error {
501    fn from(error: MediaStoreError) -> Self {
502        Error::MediaStore(Box::new(error))
503    }
504}
505
506#[cfg(feature = "qrcode")]
507impl From<ScanError> for Error {
508    fn from(error: ScanError) -> Self {
509        Error::QrCodeScanError(Box::new(error))
510    }
511}
512
513impl From<SlidingSyncError> for Error {
514    fn from(error: SlidingSyncError) -> Self {
515        Error::SlidingSync(Box::new(error))
516    }
517}
518
519impl From<OAuthError> for Error {
520    fn from(error: OAuthError) -> Self {
521        Error::OAuth(Box::new(error))
522    }
523}
524
525impl From<EventCacheError> for Error {
526    fn from(error: EventCacheError) -> Self {
527        Error::EventCache(Box::new(error))
528    }
529}
530
531impl From<QueueWedgeError> for Error {
532    fn from(error: QueueWedgeError) -> Self {
533        Error::SendQueueWedgeError(Box::new(error))
534    }
535}
536
537#[cfg(feature = "e2e-encryption")]
538impl From<BootstrapCrossSigningError> for Error {
539    fn from(error: BootstrapCrossSigningError) -> Self {
540        match error {
541            BootstrapCrossSigningError::CryptoStore(e) => e.into(),
542            BootstrapCrossSigningError::Signature(e) => e.into(),
543        }
544    }
545}
546
547/// Error for the room key importing functionality.
548#[cfg(feature = "e2e-encryption")]
549#[derive(Error, Debug)]
550// This is allowed because key importing isn't enabled under wasm.
551#[allow(dead_code)]
552pub enum RoomKeyImportError {
553    /// An error de/serializing type for the `StateStore`
554    #[error(transparent)]
555    SerdeJson(#[from] JsonError),
556
557    /// The crypto store isn't yet open. Logging in is required to open the
558    /// crypto store.
559    #[error("The crypto store hasn't been yet opened, can't import yet.")]
560    StoreClosed,
561
562    /// An IO error happened.
563    #[error(transparent)]
564    Io(#[from] IoError),
565
566    /// An error occurred in the crypto store.
567    #[error(transparent)]
568    CryptoStore(#[from] CryptoStoreError),
569
570    /// An error occurred while importing the key export.
571    #[error(transparent)]
572    Export(#[from] KeyExportError),
573}
574
575impl From<FromHttpResponseError<ruma::api::error::Error>> for HttpError {
576    fn from(err: FromHttpResponseError<ruma::api::error::Error>) -> Self {
577        Self::Api(Box::new(err.map(Into::into)))
578    }
579}
580
581impl From<SdkBaseError> for Error {
582    fn from(e: SdkBaseError) -> Self {
583        match e {
584            SdkBaseError::StateStore(e) => Self::StateStore(Box::new(e)),
585            #[cfg(feature = "e2e-encryption")]
586            SdkBaseError::CryptoStore(e) => Self::CryptoStoreError(Box::new(e)),
587            #[cfg(feature = "e2e-encryption")]
588            SdkBaseError::BadCryptoStoreState => Self::BadCryptoStoreState,
589            #[cfg(feature = "e2e-encryption")]
590            SdkBaseError::OlmError(e) => Self::OlmError(Box::new(e)),
591            #[cfg(feature = "eyre")]
592            _ => Self::UnknownError(eyre::eyre!(e).into()),
593            #[cfg(all(not(feature = "eyre"), feature = "anyhow", not(target_family = "wasm")))]
594            _ => Self::UnknownError(anyhow::anyhow!(e).into()),
595            #[cfg(all(not(feature = "eyre"), feature = "anyhow", target_family = "wasm"))]
596            _ => Self::UnknownError(e.into()),
597            #[cfg(all(
598                not(feature = "eyre"),
599                not(feature = "anyhow"),
600                not(target_family = "wasm")
601            ))]
602            _ => {
603                let e: Box<dyn std::error::Error + Send + Sync> = format!("{e:?}").into();
604                Self::UnknownError(e)
605            }
606            #[cfg(all(not(feature = "eyre"), not(feature = "anyhow"), target_family = "wasm"))]
607            _ => {
608                let e: Box<dyn std::error::Error> = format!("{e:?}").into();
609                Self::UnknownError(e)
610            }
611        }
612    }
613}
614
615impl From<ReqwestError> for Error {
616    fn from(e: ReqwestError) -> Self {
617        Error::Http(Box::new(HttpError::Reqwest(e)))
618    }
619}
620
621/// Errors that can happen when interacting with the beacon API.
622#[derive(Debug, Error)]
623pub enum BeaconError {
624    /// A network error occurred.
625    #[error("Network error: {0}")]
626    Network(#[from] HttpError),
627
628    /// The beacon information is not found.
629    #[error("Existing beacon information not found.")]
630    NotFound,
631
632    /// The redacted event is not an error, but it's not useful for the client.
633    #[error("Beacon event is redacted and cannot be processed.")]
634    Redacted,
635
636    /// The client must join the room to access the beacon information.
637    #[error("Must join the room to access beacon information.")]
638    Stripped,
639
640    /// The beacon event could not be deserialized.
641    #[error("Deserialization error: {0}")]
642    Deserialization(#[from] serde_json::Error),
643
644    /// The beacon event is expired.
645    #[error("The beacon event has expired.")]
646    NotLive,
647
648    /// Allow for other errors to be wrapped.
649    #[error("Other error: {0}")]
650    Other(Box<Error>),
651}
652
653impl From<Error> for BeaconError {
654    fn from(err: Error) -> Self {
655        BeaconError::Other(Box::new(err))
656    }
657}
658
659/// Errors that can happen when refreshing an access token.
660///
661/// This is usually only returned by [`Client::refresh_access_token()`], unless
662/// [handling refresh tokens] is activated for the `Client`.
663///
664/// [`Client::refresh_access_token()`]: crate::Client::refresh_access_token()
665/// [handling refresh tokens]: crate::ClientBuilder::handle_refresh_tokens()
666#[derive(Debug, Error, Clone)]
667pub enum RefreshTokenError {
668    /// Tried to send a refresh token request without a refresh token.
669    #[error("missing refresh token")]
670    RefreshTokenRequired,
671
672    /// An error occurred interacting with the native Matrix authentication API.
673    #[error(transparent)]
674    MatrixAuth(Arc<HttpError>),
675
676    /// An error occurred interacting with the OAuth 2.0 API.
677    #[error(transparent)]
678    OAuth(#[from] Arc<OAuthError>),
679}
680
681/// Errors that can occur when manipulating push notification settings.
682#[derive(Debug, Error, Clone, PartialEq)]
683pub enum NotificationSettingsError {
684    /// Invalid parameter.
685    #[error("Invalid parameter `{0}`")]
686    InvalidParameter(String),
687    /// Unable to add push rule.
688    #[error("Unable to add push rule")]
689    UnableToAddPushRule,
690    /// Unable to remove push rule.
691    #[error("Unable to remove push rule")]
692    UnableToRemovePushRule,
693    /// Unable to update push rule.
694    #[error("Unable to update push rule")]
695    UnableToUpdatePushRule,
696    /// Rule not found
697    #[error("Rule `{0}` not found")]
698    RuleNotFound(String),
699    /// Unable to save the push rules
700    #[error("Unable to save push rules")]
701    UnableToSavePushRules,
702}
703
704impl NotificationSettingsError {
705    /// Whether this error is the [`RuleNotFound`](Self::RuleNotFound) variant.
706    pub fn is_rule_not_found(&self) -> bool {
707        matches!(self, Self::RuleNotFound(_))
708    }
709}
710
711impl From<InsertPushRuleError> for NotificationSettingsError {
712    fn from(_: InsertPushRuleError) -> Self {
713        Self::UnableToAddPushRule
714    }
715}
716
717impl From<RemovePushRuleError> for NotificationSettingsError {
718    fn from(_: RemovePushRuleError) -> Self {
719        Self::UnableToRemovePushRule
720    }
721}
722
723#[derive(Debug, Error)]
724#[error("expected: {expected}, got: {got:?}")]
725pub struct WrongRoomState {
726    expected: &'static str,
727    got: RoomState,
728}
729
730impl WrongRoomState {
731    pub(crate) fn new(expected: &'static str, got: RoomState) -> Self {
732        Self { expected, got }
733    }
734}