1use super::group_permissions::GroupMutablePermissionsError;
2use super::mls_sync::GroupMessageProcessingError;
3use super::summary::SyncSummary;
4use super::{intents::IntentError, validated_commit::CommitValidationError};
5use crate::identity::IdentityError;
6use crate::mls_store::MlsStoreError;
7use crate::worker::device_sync::DeviceSyncError;
8use crate::{
9 client::ClientError, identity_updates::InstallationDiffError, intents::ProcessIntentError,
10 subscriptions::LocalEventError,
11};
12use openmls::{
13 error::LibraryError,
14 group::{
15 CommitToPendingProposalsError, CreateGroupContextExtProposalError, ProposalError,
16 ProposeAddMemberError, ProposeRemoveMemberError,
17 },
18 prelude::{BasicCredentialError, Error as TlsCodecError},
19};
20use thiserror::Error;
21use xmtp_common::ErrorCode;
22use xmtp_common::retry::RetryableError;
23use xmtp_content_types::CodecError;
24use xmtp_cryptography::signature::IdentifierValidationError;
25use xmtp_db::NotFound;
26use xmtp_db::sql_key_store;
27use xmtp_mls_common::group_metadata::GroupMetadataError;
28use xmtp_mls_common::group_mutable_metadata::GroupMutableMetadataError;
29use xmtp_mls_common::mls_ext::payload_encryption::{UnwrapPayloadError, WrapPayloadError};
30use xmtp_proto::types::GroupId;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct FailedInstallationIds(pub Vec<Vec<u8>>);
35
36impl std::fmt::Display for FailedInstallationIds {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 let encoded: Vec<String> = self.0.iter().map(hex::encode).collect();
39 write!(f, "{}", encoded.join(","))
40 }
41}
42
43#[derive(Debug, Error, ErrorCode)]
44pub enum GroupError {
45 #[error(transparent)]
46 #[error_code(inherit)]
47 OutgoingPreparation(#[from] super::mls_sync::publish::OutgoingPreparationError),
48 #[error(transparent)]
49 #[error_code(inherit)]
50 NotFound(#[from] NotFound),
51 #[error("Max user limit exceeded.")]
55 UserLimitExceeded,
56 #[error("SequenceId not found in local db")]
61 MissingSequenceId,
62 #[error("Addresses not found {0:?}")]
66 AddressNotFound(Vec<String>),
67 #[error("api error: {0}")]
71 WrappedApi(#[from] xmtp_api::ApiError),
72 #[error("invalid group membership")]
76 InvalidGroupMembership,
77 #[error(transparent)]
81 LeaveCantProcessed(#[from] GroupLeaveValidationError),
82 #[error("storage error: {0}")]
86 Storage(#[from] xmtp_db::StorageError),
87 #[error("intent error: {0}")]
91 Intent(#[from] IntentError),
92 #[error("create message: {0}")]
96 CreateMessage(#[from] openmls::prelude::CreateMessageError),
97 #[error("TLS Codec error: {0}")]
101 TlsError(#[from] TlsCodecError),
102 #[error("add members: {0}")]
106 UpdateGroupMembership(
107 #[from] openmls::prelude::UpdateGroupMembershipError<sql_key_store::SqlKeyStoreError>,
108 ),
109 #[error("group create: {0}")]
113 GroupCreate(#[from] openmls::group::NewGroupError<sql_key_store::SqlKeyStoreError>),
114 #[error("self update: {0}")]
118 SelfUpdate(#[from] openmls::group::SelfUpdateError<sql_key_store::SqlKeyStoreError>),
119 #[error("welcome error: {0}")]
123 WelcomeError(#[from] openmls::prelude::WelcomeError<sql_key_store::SqlKeyStoreError>),
124 #[error("Invalid extension {0}")]
128 InvalidExtension(#[from] openmls::prelude::InvalidExtensionError),
129 #[error("Invalid signature: {0}")]
133 Signature(#[from] openmls::prelude::SignatureError),
134 #[error("client: {0}")]
138 Client(#[from] ClientError),
139 #[error("receive error: {0}")]
143 ReceiveError(#[from] GroupMessageProcessingError),
144 #[error(transparent)]
148 AddressValidation(#[from] IdentifierValidationError),
149 #[error(transparent)]
153 LocalEvent(#[from] LocalEventError),
154 #[error("Public Keys {0:?} are not valid ed25519 public keys")]
158 InvalidPublicKeys(Vec<Vec<u8>>),
159 #[error("Commit validation error {0}")]
163 CommitValidation(#[from] CommitValidationError),
164 #[error("identity error: {0}")]
168 Identity(#[from] IdentityError),
169 #[error("conversion error: {0}")]
173 ConversionError(#[from] xmtp_proto::ConversionError),
174 #[error("crypto error: {0}")]
178 CryptoError(#[from] openmls::prelude::CryptoError),
179 #[error("create group context proposal error: {0}")]
183 CreateGroupContextExtProposalError(
184 #[from] CreateGroupContextExtProposalError<sql_key_store::SqlKeyStoreError>,
185 ),
186 #[error("propose add member error: {0}")]
190 ProposeAddMember(#[from] ProposeAddMemberError<sql_key_store::SqlKeyStoreError>),
191 #[error("propose remove member error: {0}")]
195 ProposeRemoveMember(#[from] ProposeRemoveMemberError<sql_key_store::SqlKeyStoreError>),
196 #[error("proposal error: {0}")]
200 Proposal(#[from] ProposalError<sql_key_store::SqlKeyStoreError>),
201 #[error("commit to pending proposals error: {0}")]
205 CommitToPendingProposals(
206 #[from] CommitToPendingProposalsError<sql_key_store::SqlKeyStoreError>,
207 ),
208 #[error("merge pending commit error: {0}")]
212 MergePendingCommit(
213 #[from] openmls::group::MergePendingCommitError<sql_key_store::SqlKeyStoreError>,
214 ),
215 #[error("Proposals not supported: {0}")]
219 ProposalsNotSupported(String),
220 #[error("min_version {requested} exceeds own pkg_version {own}")]
225 MinVersionExceedsOwnVersion { requested: String, own: String },
226 #[error("min_version {requested} would downgrade existing floor {current}")]
231 MinVersionDowngrade { requested: String, current: String },
232 #[error("invalid min_version {value:?}: {reason}")]
238 InvalidMinVersion { value: String, reason: String },
239 #[error("component source error: {0}")]
244 ComponentSource(#[from] super::app_data::component_source::ComponentSourceError),
245 #[error("app data commit error: {0}")]
252 AppDataCommit(#[from] super::app_data::GroupAppDataError<sql_key_store::SqlKeyStoreError>),
253 #[error("bootstrap synthesis error: {0}")]
262 BootstrapSynthesis(#[from] super::app_data::migration::BootstrapSynthesisError),
263 #[error("bootstrap commit error: {0}")]
269 BootstrapCommit(
270 #[from] super::app_data::migration::BootstrapCommitError<sql_key_store::SqlKeyStoreError>,
271 ),
272 #[error("Credential error")]
276 CredentialError(#[from] BasicCredentialError),
277 #[error("LeafNode error")]
281 LeafNodeError(#[from] LibraryError),
282 #[error("Installation diff error: {0}")]
286 InstallationDiff(#[from] InstallationDiffError),
287 #[error("PSKs are not support")]
291 NoPSKSupport,
292 #[error("sql key store error: {0}")]
296 SqlKeyStore(#[from] sql_key_store::SqlKeyStoreError),
297 #[error("Sync failed to wait for intent: {}", _0)]
301 SyncFailedToWait(Box<SyncSummary>),
302 #[error(transparent)]
304 #[error_code(inherit)]
305 StreamBarrier(#[from] crate::subscriptions::barrier::BarrierError),
306 #[error("Intent {intent_id} was published but processing is not confirmed")]
308 PublishedButUnconfirmed {
309 intent_id: i32,
310 #[source]
311 cause: Option<Box<crate::subscriptions::barrier::BarrierError>>,
312 },
313 #[error("Missing pending commit")]
317 MissingPendingCommit,
318 #[error(transparent)]
322 ProcessIntent(#[from] ProcessIntentError),
323 #[error("Failed to load lock")]
327 LockUnavailable,
328 #[error("Exceeded max characters for this field. Must be under: {length}")]
332 TooManyCharacters { length: usize },
333 #[error(
340 "app data update was superseded; expected {expected:?} but the committed value is {actual:?}"
341 )]
342 AppDataSuperseded { expected: String, actual: String },
343 #[error("Group is paused until version {0} is available")]
347 GroupPausedUntilUpdate(String),
348 #[error("Group is inactive")]
352 GroupInactive,
353 #[error("{}", _0.to_string())]
357 Sync(#[from] Box<SyncSummary>),
358 #[error(transparent)]
362 Db(#[from] xmtp_db::ConnectionError),
363 #[error(transparent)]
367 MlsStore(#[from] MlsStoreError),
368 #[error(transparent)]
372 MetadataPermissionsError(#[from] MetadataPermissionsError),
373 #[error("Failed to verify all installations: failedInstallations={0}")]
377 FailedToVerifyInstallations(FailedInstallationIds),
378 #[error("no welcomes to send")]
382 NoWelcomesToSend,
383 #[error("Codec error: {0}")]
387 CodecError(#[from] CodecError),
388 #[error(transparent)]
392 WrapWelcome(#[from] WrapPayloadError),
393 #[error(transparent)]
397 UnwrapWelcome(#[from] UnwrapPayloadError),
398 #[error("Failed to retrieve welcome data from topic {0}")]
402 WelcomeDataNotFound(String),
403 #[error("Welcome requires group {group_id} to process through {anchor}")]
405 WelcomeGroupPrefixPending { group_id: GroupId, anchor: u64 },
406 #[error("Welcome join metadata is invalid")]
408 InvalidWelcomeMetadata,
409 #[error("Welcome requires client version {0}")]
411 UnsupportedWelcomeVersion(String),
412 #[error("Result was not initialized")]
416 UninitializedResult,
417 #[error(transparent)]
421 Diesel(#[from] xmtp_db::diesel::result::Error),
422 #[error(transparent)]
426 UninitializedField(#[from] derive_builder::UninitializedFieldError),
427 #[error(transparent)]
431 DeleteMessage(#[from] DeleteMessageError),
432 #[error(transparent)]
436 DeviceSync(#[from] Box<DeviceSyncError>),
437}
438
439#[derive(Error, Debug)]
440pub enum DeleteMessageError {
441 #[error("Message not found: {0}")]
442 MessageNotFound(String),
443 #[error("Not authorized to delete this message")]
444 NotAuthorized,
445 #[error("Cannot delete this message type")]
446 NonDeletableMessage,
447 #[error("Message already deleted")]
448 MessageAlreadyDeleted,
449}
450
451impl RetryableError for DeleteMessageError {
452 fn is_retryable(&self) -> bool {
453 false
454 }
455}
456
457impl From<prost::EncodeError> for GroupError {
458 fn from(value: prost::EncodeError) -> Self {
459 GroupError::ConversionError(value.into())
460 }
461}
462
463impl From<prost::DecodeError> for GroupError {
464 fn from(value: prost::DecodeError) -> Self {
465 GroupError::ConversionError(value.into())
466 }
467}
468
469impl From<SyncSummary> for GroupError {
470 fn from(value: SyncSummary) -> Self {
471 GroupError::Sync(Box::new(value))
472 }
473}
474
475#[derive(Error, Debug)]
476pub enum MetadataPermissionsError {
477 #[error(transparent)]
478 Permissions(#[from] GroupMutablePermissionsError),
479 #[error(transparent)]
480 Mutable(#[from] GroupMutableMetadataError),
481 #[error("Metadata error {0}")]
482 GroupMetadata(#[from] GroupMetadataError),
483 #[error("Metadata update must specify a metadata field")]
484 InvalidPermissionUpdate,
485 #[error("cannot change metadata of DM")]
486 DmGroupMetadataForbidden,
487 #[error(transparent)]
488 DmValidation(#[from] DmValidationError),
489 #[error("Invalid extension: {0}")]
490 InvalidExtension(#[from] openmls::prelude::InvalidExtensionError),
491 #[error(transparent)]
497 ComponentSource(#[from] crate::groups::app_data::component_source::ComponentSourceError),
498}
499
500impl RetryableError for MetadataPermissionsError {
501 fn is_retryable(&self) -> bool {
502 false
503 }
504}
505
506#[derive(Error, Debug)]
507pub enum GroupLeaveValidationError {
508 #[error("cannot leave a DM conversation")]
509 DmLeaveForbidden,
510 #[error("cannot leave a group that has only one member")]
511 SingleMemberLeaveRejected,
512 #[error("super-admin cannot leave a group; must be demoted first")]
513 SuperAdminLeaveForbidden,
514 #[error("inbox ID already exists in the pending leave list")]
515 InboxAlreadyInPendingList,
516 #[error("inbox ID does not exist in the pending leave list")]
517 InboxNotInPendingList,
518 #[error("only a member of the group can send a leave request or retract a leave request")]
519 NotAGroupMember,
520}
521
522impl RetryableError for GroupLeaveValidationError {
523 fn is_retryable(&self) -> bool {
524 false
525 }
526}
527
528#[derive(Error, Debug)]
529pub enum DmValidationError {
530 #[error("DM group must have DmMembers set")]
531 OurInboxMustBeMember,
532 #[error("DM group must have our inbox as one of the dm members")]
533 MustHaveMembersSet,
534 #[error("Invalid conversation type for DM group")]
535 InvalidConversationType,
536 #[error("DM members do not match expected inboxes")]
537 ExpectedInboxesDoNotMatch,
538 #[error("DM group must have empty admin and super admin lists")]
539 MustHaveEmptyAdminAndSuperAdmin,
540 #[error("Invalid permissions for DM group")]
541 InvalidPermissions,
542}
543
544impl RetryableError for DmValidationError {
545 fn is_retryable(&self) -> bool {
546 match self {
547 Self::OurInboxMustBeMember
548 | Self::MustHaveMembersSet
549 | Self::InvalidConversationType
550 | Self::ExpectedInboxesDoNotMatch
551 | Self::MustHaveEmptyAdminAndSuperAdmin
552 | Self::InvalidPermissions => false,
553 }
554 }
555}
556
557impl RetryableError for GroupError {
558 fn is_retryable(&self) -> bool {
559 match self {
560 Self::OutgoingPreparation(error) => error.is_retryable(),
561 Self::Client(client_error) => client_error.is_retryable(),
562 Self::Storage(storage) => storage.is_retryable(),
563 Self::ReceiveError(msg) => msg.is_retryable(),
564 Self::Identity(identity) => identity.is_retryable(),
565 Self::UpdateGroupMembership(update) => update.is_retryable(),
566 Self::GroupCreate(group) => group.is_retryable(),
567 Self::SelfUpdate(update) => update.is_retryable(),
568 Self::WelcomeError(
569 openmls::prelude::WelcomeError::UnsupportedMlsVersion
570 | openmls::prelude::WelcomeError::UnsupportedExtensions,
571 ) => true,
572 Self::WelcomeError(welcome) => welcome.is_retryable(),
573 Self::SqlKeyStore(sql) => sql.is_retryable(),
574 Self::InstallationDiff(diff) => diff.is_retryable(),
575 Self::CreateGroupContextExtProposalError(create) => create.is_retryable(),
576 Self::ProposeAddMember(e) => e.is_retryable(),
577 Self::ProposeRemoveMember(e) => e.is_retryable(),
578 Self::Proposal(e) => e.is_retryable(),
579 Self::CommitToPendingProposals(e) => e.is_retryable(),
580 Self::ProposalsNotSupported(_) => false,
581 Self::MinVersionExceedsOwnVersion { .. } => false,
582 Self::MinVersionDowngrade { .. } => false,
583 Self::InvalidMinVersion { .. } => false,
584 Self::ComponentSource(_) => false,
585 Self::AppDataCommit(e) => e.is_retryable(),
586 Self::BootstrapSynthesis(e) => e.is_retryable(),
591 Self::BootstrapCommit(_) => false,
592 Self::CommitValidation(err) => err.is_retryable(),
593 Self::WrappedApi(err) => err.is_retryable(),
594 Self::ProcessIntent(err) => err.is_retryable(),
595 Self::LocalEvent(err) => err.is_retryable(),
596 Self::LockUnavailable => true,
597 Self::WelcomeGroupPrefixPending { .. } | Self::UnsupportedWelcomeVersion(_) => true,
598 Self::SyncFailedToWait(_) => true,
599 Self::StreamBarrier(error) => error.is_retryable(),
600 Self::PublishedButUnconfirmed { .. } => true,
601 Self::CodecError(_) => true,
602 Self::Sync(s) => s.is_retryable(),
603 Self::Db(e) => e.is_retryable(),
604 Self::MlsStore(e) => e.is_retryable(),
605 Self::MetadataPermissionsError(e) => e.is_retryable(),
606 Self::WrapWelcome(e) => e.is_retryable(),
607 Self::UnwrapWelcome(e) => e.is_retryable(),
608 Self::Diesel(e) => e.is_retryable(),
609 Self::LeaveCantProcessed(e) => e.is_retryable(),
610 Self::DeleteMessage(e) => e.is_retryable(),
611 Self::DeviceSync(e) => e.is_retryable(),
612 Self::MergePendingCommit(e) => e.is_retryable(),
613 Self::MissingSequenceId => true,
620 Self::NotFound(_)
621 | Self::InvalidWelcomeMetadata
622 | Self::UserLimitExceeded
623 | Self::InvalidGroupMembership
624 | Self::Intent(_)
625 | Self::CreateMessage(_)
626 | Self::TlsError(_)
627 | Self::AddressNotFound(_)
628 | Self::InvalidExtension(_)
629 | Self::Signature(_)
630 | Self::LeafNodeError(_)
631 | Self::NoPSKSupport
632 | Self::MissingPendingCommit
633 | Self::AddressValidation(_)
634 | Self::InvalidPublicKeys(_)
635 | Self::CredentialError(_)
636 | Self::ConversionError(_)
637 | Self::CryptoError(_)
638 | Self::TooManyCharacters { .. }
639 | Self::AppDataSuperseded { .. }
640 | Self::GroupPausedUntilUpdate(_)
641 | Self::GroupInactive
642 | Self::FailedToVerifyInstallations(_)
643 | Self::NoWelcomesToSend
644 | Self::WelcomeDataNotFound(_)
645 | Self::UninitializedField(_)
646 | Self::UninitializedResult => false,
647 }
648 }
649}
650
651impl crate::worker::NeedsDbReconnect for GroupError {
652 fn needs_db_reconnect(&self) -> bool {
655 match self {
656 Self::Storage(s) => s.db_needs_connection(),
657 Self::Client(c) => c.db_needs_connection(),
658 Self::Db(c) => c.db_needs_connection(),
659 Self::MlsStore(s) => s.needs_db_reconnect(),
660 Self::Identity(i) => i.needs_db_reconnect(),
661 Self::DeviceSync(d) => d.needs_db_reconnect(),
662 Self::StreamBarrier(error) => error.needs_db_reconnect(),
663 Self::PublishedButUnconfirmed { cause, .. } => cause
664 .as_ref()
665 .is_some_and(|error| error.needs_db_reconnect()),
666 Self::ReceiveError(error) => error.needs_db_reconnect(),
667 Self::Sync(summary) | Self::SyncFailedToWait(summary) => summary.needs_db_reconnect(),
668 _ => false,
669 }
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676
677 #[xmtp_common::test]
678 fn missing_sequence_id_is_retryable() {
679 assert!(GroupError::MissingSequenceId.is_retryable());
684 }
685
686 #[xmtp_common::test]
687 fn failed_to_verify_installations_includes_hex_ids() {
688 let err = GroupError::FailedToVerifyInstallations(FailedInstallationIds(vec![
689 vec![0xAB; 32],
690 vec![0xCD; 32],
691 ]));
692 let message = err.to_string();
693 assert!(message.contains("failedInstallations="));
694 assert!(message.contains(&hex::encode([0xAB; 32])));
695 assert!(message.contains(&hex::encode([0xCD; 32])));
696 assert!(!err.is_retryable());
697 }
698}