matrix_sdk_ffi/
error.rs

1use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
2
3use matrix_sdk::{
4    authentication::oidc::OidcError, encryption::CryptoStoreError, event_cache::EventCacheError,
5    reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
6    NotificationSettingsError as SdkNotificationSettingsError,
7    QueueWedgeError as SdkQueueWedgeError, StoreError,
8};
9use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
10use ruma::api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter};
11use uniffi::UnexpectedUniFFICallbackError;
12
13use crate::{room_list::RoomListError, timeline::FocusEventError};
14
15#[derive(Debug, thiserror::Error, uniffi::Error)]
16pub enum ClientError {
17    #[error("client error: {msg}")]
18    Generic { msg: String },
19    #[error("api error {code}: {msg}")]
20    MatrixApi { kind: ErrorKind, code: String, msg: String },
21}
22
23impl ClientError {
24    pub(crate) fn new<E: Display>(error: E) -> Self {
25        Self::Generic { msg: error.to_string() }
26    }
27}
28
29impl From<anyhow::Error> for ClientError {
30    fn from(e: anyhow::Error) -> ClientError {
31        ClientError::Generic { msg: format!("{e:#}") }
32    }
33}
34
35impl From<reqwest::Error> for ClientError {
36    fn from(e: reqwest::Error) -> Self {
37        Self::new(e)
38    }
39}
40
41impl From<UnexpectedUniFFICallbackError> for ClientError {
42    fn from(e: UnexpectedUniFFICallbackError) -> Self {
43        Self::new(e)
44    }
45}
46
47impl From<matrix_sdk::Error> for ClientError {
48    fn from(e: matrix_sdk::Error) -> Self {
49        match e {
50            matrix_sdk::Error::Http(http_error) => {
51                if let Some(api_error) = http_error.as_client_api_error() {
52                    if let ErrorBody::Standard { kind, message } = &api_error.body {
53                        let code = kind.errcode().to_string();
54                        let Ok(kind) = kind.to_owned().try_into() else {
55                            // We couldn't parse the API error, so we return a generic one instead
56                            return Self::Generic { msg: message.to_string() };
57                        };
58                        return Self::MatrixApi { kind, code, msg: message.to_owned() };
59                    }
60                }
61                Self::Generic { msg: http_error.to_string() }
62            }
63            _ => Self::Generic { msg: e.to_string() },
64        }
65    }
66}
67
68impl From<StoreError> for ClientError {
69    fn from(e: StoreError) -> Self {
70        Self::new(e)
71    }
72}
73
74impl From<CryptoStoreError> for ClientError {
75    fn from(e: CryptoStoreError) -> Self {
76        Self::new(e)
77    }
78}
79
80impl From<HttpError> for ClientError {
81    fn from(e: HttpError) -> Self {
82        Self::new(e)
83    }
84}
85
86impl From<IdParseError> for ClientError {
87    fn from(e: IdParseError) -> Self {
88        Self::new(e)
89    }
90}
91
92impl From<serde_json::Error> for ClientError {
93    fn from(e: serde_json::Error) -> Self {
94        Self::new(e)
95    }
96}
97
98impl From<url::ParseError> for ClientError {
99    fn from(e: url::ParseError) -> Self {
100        Self::new(e)
101    }
102}
103
104impl From<mime::FromStrError> for ClientError {
105    fn from(e: mime::FromStrError) -> Self {
106        Self::new(e)
107    }
108}
109
110impl From<encryption_sync_service::Error> for ClientError {
111    fn from(e: encryption_sync_service::Error) -> Self {
112        Self::new(e)
113    }
114}
115
116impl From<timeline::Error> for ClientError {
117    fn from(e: timeline::Error) -> Self {
118        Self::new(e)
119    }
120}
121
122impl From<timeline::UnsupportedEditItem> for ClientError {
123    fn from(e: timeline::UnsupportedEditItem) -> Self {
124        Self::new(e)
125    }
126}
127
128impl From<notification_client::Error> for ClientError {
129    fn from(e: notification_client::Error) -> Self {
130        Self::new(e)
131    }
132}
133
134impl From<sync_service::Error> for ClientError {
135    fn from(e: sync_service::Error) -> Self {
136        Self::new(e)
137    }
138}
139
140impl From<OidcError> for ClientError {
141    fn from(e: OidcError) -> Self {
142        Self::new(e)
143    }
144}
145
146impl From<RoomError> for ClientError {
147    fn from(e: RoomError) -> Self {
148        Self::new(e)
149    }
150}
151
152impl From<RoomListError> for ClientError {
153    fn from(e: RoomListError) -> Self {
154        Self::new(e)
155    }
156}
157
158impl From<EventCacheError> for ClientError {
159    fn from(e: EventCacheError) -> Self {
160        Self::new(e)
161    }
162}
163
164impl From<EditError> for ClientError {
165    fn from(e: EditError) -> Self {
166        Self::new(e)
167    }
168}
169
170impl From<RoomSendQueueError> for ClientError {
171    fn from(e: RoomSendQueueError) -> Self {
172        Self::new(e)
173    }
174}
175
176impl From<NotYetImplemented> for ClientError {
177    fn from(_: NotYetImplemented) -> Self {
178        Self::new("This functionality is not implemented yet.")
179    }
180}
181
182impl From<FocusEventError> for ClientError {
183    fn from(e: FocusEventError) -> Self {
184        Self::new(e)
185    }
186}
187
188/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
189/// String.
190///
191/// Represent a failed to send unrecoverable error of an event sent via the
192/// send_queue. It is a serializable representation of a client error, see
193/// `From` implementation for more details. These errors can not be
194/// automatically retried, but yet some manual action can be taken before retry
195/// sending. If not the only solution is to delete the local event.
196#[derive(Debug, Clone, uniffi::Enum)]
197pub enum QueueWedgeError {
198    /// This error occurs when there are some insecure devices in the room, and
199    /// the current encryption setting prohibit sharing with them.
200    InsecureDevices {
201        /// The insecure devices as a Map of userID to deviceID.
202        user_device_map: HashMap<String, Vec<String>>,
203    },
204
205    /// This error occurs when a previously verified user is not anymore, and
206    /// the current encryption setting prohibit sharing when it happens.
207    IdentityViolations {
208        /// The users that are expected to be verified but are not.
209        users: Vec<String>,
210    },
211
212    /// It is required to set up cross-signing and properly erify the current
213    /// session before sending.
214    CrossVerificationRequired,
215
216    /// Some media content to be sent has disappeared from the cache.
217    MissingMediaContent,
218
219    /// Some mime type couldn't be parsed.
220    InvalidMimeType { mime_type: String },
221
222    /// Other errors.
223    GenericApiError { msg: String },
224}
225
226/// Simple display implementation that strips out userIds/DeviceIds to avoid
227/// accidental logging.
228impl Display for QueueWedgeError {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        match self {
231            QueueWedgeError::InsecureDevices { .. } => {
232                f.write_str("There are insecure devices in the room")
233            }
234            QueueWedgeError::IdentityViolations { .. } => {
235                f.write_str("Some users that were previously verified are not anymore")
236            }
237            QueueWedgeError::CrossVerificationRequired => {
238                f.write_str("Own verification is required")
239            }
240            QueueWedgeError::MissingMediaContent => {
241                f.write_str("Media to be sent disappeared from local storage")
242            }
243            QueueWedgeError::InvalidMimeType { mime_type } => {
244                write!(f, "Invalid mime type '{mime_type}' for media upload")
245            }
246            QueueWedgeError::GenericApiError { msg } => f.write_str(msg),
247        }
248    }
249}
250
251impl From<SdkQueueWedgeError> for QueueWedgeError {
252    fn from(value: SdkQueueWedgeError) -> Self {
253        match value {
254            SdkQueueWedgeError::InsecureDevices { user_device_map } => Self::InsecureDevices {
255                user_device_map: user_device_map
256                    .iter()
257                    .map(|(user_id, devices)| {
258                        (
259                            user_id.to_string(),
260                            devices.iter().map(|device_id| device_id.to_string()).collect(),
261                        )
262                    })
263                    .collect(),
264            },
265            SdkQueueWedgeError::IdentityViolations { users } => Self::IdentityViolations {
266                users: users.iter().map(ruma::OwnedUserId::to_string).collect(),
267            },
268            SdkQueueWedgeError::CrossVerificationRequired => Self::CrossVerificationRequired,
269            SdkQueueWedgeError::MissingMediaContent => Self::MissingMediaContent,
270            SdkQueueWedgeError::InvalidMimeType { mime_type } => {
271                Self::InvalidMimeType { mime_type }
272            }
273            SdkQueueWedgeError::GenericApiError { msg } => Self::GenericApiError { msg },
274        }
275    }
276}
277
278#[derive(Debug, thiserror::Error, uniffi::Error)]
279#[uniffi(flat_error)]
280pub enum RoomError {
281    #[error("Invalid attachment data")]
282    InvalidAttachmentData,
283    #[error("Invalid attachment mime type")]
284    InvalidAttachmentMimeType,
285    #[error("Invalid media info")]
286    InvalidMediaInfo,
287    #[error("Timeline unavailable")]
288    TimelineUnavailable,
289    #[error("Invalid thumbnail data")]
290    InvalidThumbnailData,
291    #[error("Failed sending attachment")]
292    FailedSendingAttachment,
293}
294
295#[derive(Debug, thiserror::Error, uniffi::Error)]
296#[uniffi(flat_error)]
297pub enum MediaInfoError {
298    #[error("Required value missing from the media info")]
299    MissingField,
300    #[error("Media info field invalid")]
301    InvalidField,
302}
303
304#[derive(Debug, thiserror::Error, uniffi::Error)]
305pub enum NotificationSettingsError {
306    #[error("client error: {msg}")]
307    Generic { msg: String },
308    /// Invalid parameter.
309    #[error("Invalid parameter: {msg}")]
310    InvalidParameter { msg: String },
311    /// Invalid room id.
312    #[error("Invalid room ID {room_id}")]
313    InvalidRoomId { room_id: String },
314    /// Rule not found
315    #[error("Rule not found: {rule_id}")]
316    RuleNotFound { rule_id: String },
317    /// Unable to add push rule.
318    #[error("Unable to add push rule")]
319    UnableToAddPushRule,
320    /// Unable to remove push rule.
321    #[error("Unable to remove push rule")]
322    UnableToRemovePushRule,
323    /// Unable to save the push rules
324    #[error("Unable to save push rules")]
325    UnableToSavePushRules,
326    /// Unable to update push rule.
327    #[error("Unable to update push rule")]
328    UnableToUpdatePushRule,
329}
330
331impl From<SdkNotificationSettingsError> for NotificationSettingsError {
332    fn from(value: SdkNotificationSettingsError) -> Self {
333        match value {
334            SdkNotificationSettingsError::RuleNotFound(rule_id) => Self::RuleNotFound { rule_id },
335            SdkNotificationSettingsError::UnableToAddPushRule => Self::UnableToAddPushRule,
336            SdkNotificationSettingsError::UnableToRemovePushRule => Self::UnableToRemovePushRule,
337            SdkNotificationSettingsError::UnableToSavePushRules => Self::UnableToSavePushRules,
338            SdkNotificationSettingsError::InvalidParameter(msg) => Self::InvalidParameter { msg },
339            SdkNotificationSettingsError::UnableToUpdatePushRule => Self::UnableToUpdatePushRule,
340        }
341    }
342}
343
344impl From<matrix_sdk::Error> for NotificationSettingsError {
345    fn from(e: matrix_sdk::Error) -> Self {
346        Self::Generic { msg: e.to_string() }
347    }
348}
349
350/// Something has not been implemented yet.
351#[derive(thiserror::Error, Debug)]
352#[error("not implemented yet")]
353pub struct NotYetImplemented;
354
355#[derive(Clone, Debug, PartialEq, Eq, uniffi::Enum)]
356// Please keep the variants sorted alphabetically.
357pub enum ErrorKind {
358    /// `M_BAD_ALIAS`
359    ///
360    /// One or more [room aliases] within the `m.room.canonical_alias` event do
361    /// not point to the room ID for which the state event is to be sent to.
362    ///
363    /// [room aliases]: https://spec.matrix.org/latest/client-server-api/#room-aliases
364    BadAlias,
365
366    /// `M_BAD_JSON`
367    ///
368    /// The request contained valid JSON, but it was malformed in some way, e.g.
369    /// missing required keys, invalid values for keys.
370    BadJson,
371
372    /// `M_BAD_STATE`
373    ///
374    /// The state change requested cannot be performed, such as attempting to
375    /// unban a user who is not banned.
376    BadState,
377
378    /// `M_BAD_STATUS`
379    ///
380    /// The application service returned a bad status.
381    BadStatus {
382        /// The HTTP status code of the response.
383        status: Option<u16>,
384
385        /// The body of the response.
386        body: Option<String>,
387    },
388
389    /// `M_CANNOT_LEAVE_SERVER_NOTICE_ROOM`
390    ///
391    /// The user is unable to reject an invite to join the [server notices]
392    /// room.
393    ///
394    /// [server notices]: https://spec.matrix.org/latest/client-server-api/#server-notices
395    CannotLeaveServerNoticeRoom,
396
397    /// `M_CANNOT_OVERWRITE_MEDIA`
398    ///
399    /// The [`create_content_async`] endpoint was called with a media ID that
400    /// already has content.
401    ///
402    /// [`create_content_async`]: crate::media::create_content_async
403    CannotOverwriteMedia,
404
405    /// `M_CAPTCHA_INVALID`
406    ///
407    /// The Captcha provided did not match what was expected.
408    CaptchaInvalid,
409
410    /// `M_CAPTCHA_NEEDED`
411    ///
412    /// A Captcha is required to complete the request.
413    CaptchaNeeded,
414
415    /// `M_CONNECTION_FAILED`
416    ///
417    /// The connection to the application service failed.
418    ConnectionFailed,
419
420    /// `M_CONNECTION_TIMEOUT`
421    ///
422    /// The connection to the application service timed out.
423    ConnectionTimeout,
424
425    /// `M_DUPLICATE_ANNOTATION`
426    ///
427    /// The request is an attempt to send a [duplicate annotation].
428    ///
429    /// [duplicate annotation]: https://spec.matrix.org/latest/client-server-api/#avoiding-duplicate-annotations
430    DuplicateAnnotation,
431
432    /// `M_EXCLUSIVE`
433    ///
434    /// The resource being requested is reserved by an application service, or
435    /// the application service making the request has not created the
436    /// resource.
437    Exclusive,
438
439    /// `M_FORBIDDEN`
440    ///
441    /// Forbidden access, e.g. joining a room without permission, failed login.
442    Forbidden,
443
444    /// `M_GUEST_ACCESS_FORBIDDEN`
445    ///
446    /// The room or resource does not permit [guests] to access it.
447    ///
448    /// [guests]: https://spec.matrix.org/latest/client-server-api/#guest-access
449    GuestAccessForbidden,
450
451    /// `M_INCOMPATIBLE_ROOM_VERSION`
452    ///
453    /// The client attempted to join a room that has a version the server does
454    /// not support.
455    IncompatibleRoomVersion {
456        /// The room's version.
457        room_version: String,
458    },
459
460    /// `M_INVALID_PARAM`
461    ///
462    /// A parameter that was specified has the wrong value. For example, the
463    /// server expected an integer and instead received a string.
464    InvalidParam,
465
466    /// `M_INVALID_ROOM_STATE`
467    ///
468    /// The initial state implied by the parameters to the [`create_room`]
469    /// request is invalid, e.g. the user's `power_level` is set below that
470    /// necessary to set the room name.
471    ///
472    /// [`create_room`]: crate::room::create_room
473    InvalidRoomState,
474
475    /// `M_INVALID_USERNAME`
476    ///
477    /// The desired user name is not valid.
478    InvalidUsername,
479
480    /// `M_LIMIT_EXCEEDED`
481    ///
482    /// The request has been refused due to [rate limiting]: too many requests
483    /// have been sent in a short period of time.
484    ///
485    /// [rate limiting]: https://spec.matrix.org/latest/client-server-api/#rate-limiting
486    LimitExceeded {
487        /// How long a client should wait before they can try again.
488        retry_after_ms: Option<u64>,
489    },
490
491    /// `M_MISSING_PARAM`
492    ///
493    /// A required parameter was missing from the request.
494    MissingParam,
495
496    /// `M_MISSING_TOKEN`
497    ///
498    /// No [access token] was specified for the request, but one is required.
499    ///
500    /// [access token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
501    MissingToken,
502
503    /// `M_NOT_FOUND`
504    ///
505    /// No resource was found for this request.
506    NotFound,
507
508    /// `M_NOT_JSON`
509    ///
510    /// The request did not contain valid JSON.
511    NotJson,
512
513    /// `M_NOT_YET_UPLOADED`
514    ///
515    /// An `mxc:` URI generated with the [`create_mxc_uri`] endpoint was used
516    /// and the content is not yet available.
517    ///
518    /// [`create_mxc_uri`]: crate::media::create_mxc_uri
519    NotYetUploaded,
520
521    /// `M_RESOURCE_LIMIT_EXCEEDED`
522    ///
523    /// The request cannot be completed because the homeserver has reached a
524    /// resource limit imposed on it. For example, a homeserver held in a
525    /// shared hosting environment may reach a resource limit if it starts
526    /// using too much memory or disk space.
527    ResourceLimitExceeded {
528        /// A URI giving a contact method for the server administrator.
529        admin_contact: String,
530    },
531
532    /// `M_ROOM_IN_USE`
533    ///
534    /// The [room alias] specified in the [`create_room`] request is already
535    /// taken.
536    ///
537    /// [`create_room`]: crate::room::create_room
538    /// [room alias]: https://spec.matrix.org/latest/client-server-api/#room-aliases
539    RoomInUse,
540
541    /// `M_SERVER_NOT_TRUSTED`
542    ///
543    /// The client's request used a third-party server, e.g. identity server,
544    /// that this server does not trust.
545    ServerNotTrusted,
546
547    /// `M_THREEPID_AUTH_FAILED`
548    ///
549    /// Authentication could not be performed on the [third-party identifier].
550    ///
551    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
552    ThreepidAuthFailed,
553
554    /// `M_THREEPID_DENIED`
555    ///
556    /// The server does not permit this [third-party identifier]. This may
557    /// happen if the server only permits, for example, email addresses from
558    /// a particular domain.
559    ///
560    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
561    ThreepidDenied,
562
563    /// `M_THREEPID_IN_USE`
564    ///
565    /// The [third-party identifier] is already in use by another user.
566    ///
567    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
568    ThreepidInUse,
569
570    /// `M_THREEPID_MEDIUM_NOT_SUPPORTED`
571    ///
572    /// The homeserver does not support adding a [third-party identifier] of the
573    /// given medium.
574    ///
575    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
576    ThreepidMediumNotSupported,
577
578    /// `M_THREEPID_NOT_FOUND`
579    ///
580    /// No account matching the given [third-party identifier] could be found.
581    ///
582    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
583    ThreepidNotFound,
584
585    /// `M_TOO_LARGE`
586    ///
587    /// The request or entity was too large.
588    TooLarge,
589
590    /// `M_UNABLE_TO_AUTHORISE_JOIN`
591    ///
592    /// The room is [restricted] and none of the conditions can be validated by
593    /// the homeserver. This can happen if the homeserver does not know
594    /// about any of the rooms listed as conditions, for example.
595    ///
596    /// [restricted]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
597    UnableToAuthorizeJoin,
598
599    /// `M_UNABLE_TO_GRANT_JOIN`
600    ///
601    /// A different server should be attempted for the join. This is typically
602    /// because the resident server can see that the joining user satisfies
603    /// one or more conditions, such as in the case of [restricted rooms],
604    /// but the resident server would be unable to meet the authorization
605    /// rules.
606    ///
607    /// [restricted rooms]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
608    UnableToGrantJoin,
609
610    /// `M_UNAUTHORIZED`
611    ///
612    /// The request was not correctly authorized. Usually due to login failures.
613    Unauthorized,
614
615    /// `M_UNKNOWN`
616    ///
617    /// An unknown error has occurred.
618    Unknown,
619
620    /// `M_UNKNOWN_TOKEN`
621    ///
622    /// The [access or refresh token] specified was not recognized.
623    ///
624    /// [access or refresh token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
625    UnknownToken {
626        /// If this is `true`, the client is in a "[soft logout]" state, i.e.
627        /// the server requires re-authentication but the session is not
628        /// invalidated. The client can acquire a new access token by
629        /// specifying the device ID it is already using to the login API.
630        ///
631        /// [soft logout]: https://spec.matrix.org/latest/client-server-api/#soft-logout
632        soft_logout: bool,
633    },
634
635    /// `M_UNRECOGNIZED`
636    ///
637    /// The server did not understand the request.
638    ///
639    /// This is expected to be returned with a 404 HTTP status code if the
640    /// endpoint is not implemented or a 405 HTTP status code if the
641    /// endpoint is implemented, but the incorrect HTTP method is used.
642    Unrecognized,
643
644    /// `M_UNSUPPORTED_ROOM_VERSION`
645    ///
646    /// The request to [`create_room`] used a room version that the server does
647    /// not support.
648    ///
649    /// [`create_room`]: crate::room::create_room
650    UnsupportedRoomVersion,
651
652    /// `M_URL_NOT_SET`
653    ///
654    /// The application service doesn't have a URL configured.
655    UrlNotSet,
656
657    /// `M_USER_DEACTIVATED`
658    ///
659    /// The user ID associated with the request has been deactivated.
660    UserDeactivated,
661
662    /// `M_USER_IN_USE`
663    ///
664    /// The desired user ID is already taken.
665    UserInUse,
666
667    /// `M_USER_LOCKED`
668    ///
669    /// The account has been [locked] and cannot be used at this time.
670    ///
671    /// [locked]: https://spec.matrix.org/latest/client-server-api/#account-locking
672    UserLocked,
673
674    /// `M_USER_SUSPENDED`
675    ///
676    /// The account has been [suspended] and can only be used for limited
677    /// actions at this time.
678    ///
679    /// [suspended]: https://spec.matrix.org/latest/client-server-api/#account-suspension
680    UserSuspended,
681
682    /// `M_WEAK_PASSWORD`
683    ///
684    /// The password was [rejected] by the server for being too weak.
685    ///
686    /// [rejected]: https://spec.matrix.org/latest/client-server-api/#notes-on-password-management
687    WeakPassword,
688
689    /// `M_WRONG_ROOM_KEYS_VERSION`
690    ///
691    /// The version of the [room keys backup] provided in the request does not
692    /// match the current backup version.
693    ///
694    /// [room keys backup]: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
695    WrongRoomKeysVersion {
696        /// The currently active backup version.
697        current_version: Option<String>,
698    },
699
700    /// A custom API error.
701    Custom { errcode: String },
702}
703
704impl TryFrom<RumaApiErrorKind> for ErrorKind {
705    type Error = NotYetImplemented;
706    fn try_from(value: RumaApiErrorKind) -> Result<Self, Self::Error> {
707        match &value {
708            RumaApiErrorKind::BadAlias => Ok(ErrorKind::BadAlias),
709            RumaApiErrorKind::BadJson => Ok(ErrorKind::BadJson),
710            RumaApiErrorKind::BadState => Ok(ErrorKind::BadState),
711            RumaApiErrorKind::BadStatus { status, body } => Ok(ErrorKind::BadStatus {
712                status: status.map(|code| code.clone().as_u16()),
713                body: body.clone(),
714            }),
715            RumaApiErrorKind::CannotLeaveServerNoticeRoom => {
716                Ok(ErrorKind::CannotLeaveServerNoticeRoom)
717            }
718            RumaApiErrorKind::CannotOverwriteMedia => Ok(ErrorKind::CannotOverwriteMedia),
719            RumaApiErrorKind::CaptchaInvalid => Ok(ErrorKind::CaptchaInvalid),
720            RumaApiErrorKind::CaptchaNeeded => Ok(ErrorKind::CaptchaNeeded),
721            RumaApiErrorKind::ConnectionFailed => Ok(ErrorKind::ConnectionFailed),
722            RumaApiErrorKind::ConnectionTimeout => Ok(ErrorKind::ConnectionTimeout),
723            RumaApiErrorKind::DuplicateAnnotation => Ok(ErrorKind::DuplicateAnnotation),
724            RumaApiErrorKind::Exclusive => Ok(ErrorKind::Exclusive),
725            RumaApiErrorKind::Forbidden { .. } => Ok(ErrorKind::Forbidden),
726            RumaApiErrorKind::GuestAccessForbidden => Ok(ErrorKind::GuestAccessForbidden),
727            RumaApiErrorKind::IncompatibleRoomVersion { room_version } => {
728                Ok(ErrorKind::IncompatibleRoomVersion { room_version: room_version.to_string() })
729            }
730            RumaApiErrorKind::InvalidParam => Ok(ErrorKind::InvalidParam),
731            RumaApiErrorKind::InvalidRoomState => Ok(ErrorKind::InvalidRoomState),
732            RumaApiErrorKind::InvalidUsername => Ok(ErrorKind::InvalidUsername),
733            RumaApiErrorKind::LimitExceeded { retry_after } => {
734                let retry_after_ms = match retry_after {
735                    Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
736                    Some(RetryAfter::DateTime(system_time)) => {
737                        let duration = system_time.duration_since(SystemTime::now()).ok();
738                        duration.map(|duration| duration.as_millis() as u64)
739                    }
740                    None => None,
741                };
742                Ok(ErrorKind::LimitExceeded { retry_after_ms })
743            }
744            RumaApiErrorKind::MissingParam => Ok(ErrorKind::MissingParam),
745            RumaApiErrorKind::MissingToken => Ok(ErrorKind::MissingToken),
746            RumaApiErrorKind::NotFound => Ok(ErrorKind::NotFound),
747            RumaApiErrorKind::NotJson => Ok(ErrorKind::NotJson),
748            RumaApiErrorKind::NotYetUploaded => Ok(ErrorKind::NotYetUploaded),
749            RumaApiErrorKind::ResourceLimitExceeded { admin_contact } => {
750                Ok(ErrorKind::ResourceLimitExceeded { admin_contact: admin_contact.to_owned() })
751            }
752            RumaApiErrorKind::RoomInUse => Ok(ErrorKind::RoomInUse),
753            RumaApiErrorKind::ServerNotTrusted => Ok(ErrorKind::ServerNotTrusted),
754            RumaApiErrorKind::ThreepidAuthFailed => Ok(ErrorKind::ThreepidAuthFailed),
755            RumaApiErrorKind::ThreepidDenied => Ok(ErrorKind::ThreepidDenied),
756            RumaApiErrorKind::ThreepidInUse => Ok(ErrorKind::ThreepidInUse),
757            RumaApiErrorKind::ThreepidMediumNotSupported => {
758                Ok(ErrorKind::ThreepidMediumNotSupported)
759            }
760            RumaApiErrorKind::ThreepidNotFound => Ok(ErrorKind::ThreepidNotFound),
761            RumaApiErrorKind::TooLarge => Ok(ErrorKind::TooLarge),
762            RumaApiErrorKind::UnableToAuthorizeJoin => Ok(ErrorKind::UnableToAuthorizeJoin),
763            RumaApiErrorKind::UnableToGrantJoin => Ok(ErrorKind::UnableToGrantJoin),
764            RumaApiErrorKind::Unauthorized => Ok(ErrorKind::Unauthorized),
765            RumaApiErrorKind::Unknown => Ok(ErrorKind::Unknown),
766            RumaApiErrorKind::UnknownToken { soft_logout } => {
767                Ok(ErrorKind::UnknownToken { soft_logout: soft_logout.to_owned() })
768            }
769            RumaApiErrorKind::Unrecognized => Ok(ErrorKind::Unrecognized),
770            RumaApiErrorKind::UnsupportedRoomVersion => Ok(ErrorKind::UnsupportedRoomVersion),
771            RumaApiErrorKind::UrlNotSet => Ok(ErrorKind::UrlNotSet),
772            RumaApiErrorKind::UserDeactivated => Ok(ErrorKind::UserDeactivated),
773            RumaApiErrorKind::UserInUse => Ok(ErrorKind::UserInUse),
774            RumaApiErrorKind::UserLocked => Ok(ErrorKind::UserLocked),
775            RumaApiErrorKind::UserSuspended => Ok(ErrorKind::UserSuspended),
776            RumaApiErrorKind::WeakPassword => Ok(ErrorKind::WeakPassword),
777            RumaApiErrorKind::WrongRoomKeysVersion { current_version } => {
778                Ok(ErrorKind::WrongRoomKeysVersion { current_version: current_version.to_owned() })
779            }
780            RumaApiErrorKind::_Custom { .. } => {
781                // There is no way to map the extra values since they're private, so we omit
782                // them
783                Ok(ErrorKind::Custom { errcode: value.errcode().to_string() })
784            }
785            // In any other case, return it as the mapping not being yet implemented
786            _ => Err(NotYetImplemented),
787        }
788    }
789}