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