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("reserved transcript content type")]
47 ReservedTranscriptContentType,
48 #[error(transparent)]
49 #[error_code(inherit)]
50 OutgoingPreparation(#[from] super::mls_sync::publish::OutgoingPreparationError),
51 #[error(transparent)]
52 #[error_code(inherit)]
53 NotFound(#[from] NotFound),
54 #[error("Max user limit exceeded.")]
58 UserLimitExceeded,
59 #[error("SequenceId not found in local db")]
64 MissingSequenceId,
65 #[error("Addresses not found {0:?}")]
69 AddressNotFound(Vec<String>),
70 #[error("api error: {0}")]
74 WrappedApi(#[from] xmtp_api::ApiError),
75 #[error("invalid group membership")]
79 InvalidGroupMembership,
80 #[error(transparent)]
84 LeaveCantProcessed(#[from] GroupLeaveValidationError),
85 #[error("storage error: {0}")]
89 Storage(#[from] xmtp_db::StorageError),
90 #[error("intent error: {0}")]
94 Intent(#[from] IntentError),
95 #[error("create message: {0}")]
99 CreateMessage(#[from] openmls::prelude::CreateMessageError),
100 #[error("TLS Codec error: {0}")]
104 TlsError(#[from] TlsCodecError),
105 #[error("add members: {0}")]
109 UpdateGroupMembership(
110 #[from] openmls::prelude::UpdateGroupMembershipError<sql_key_store::SqlKeyStoreError>,
111 ),
112 #[error("group create: {0}")]
116 GroupCreate(#[from] openmls::group::NewGroupError<sql_key_store::SqlKeyStoreError>),
117 #[error("self update: {0}")]
121 SelfUpdate(#[from] openmls::group::SelfUpdateError<sql_key_store::SqlKeyStoreError>),
122 #[error("welcome error: {0}")]
126 WelcomeError(#[from] openmls::prelude::WelcomeError<sql_key_store::SqlKeyStoreError>),
127 #[error("Invalid extension {0}")]
131 InvalidExtension(#[from] openmls::prelude::InvalidExtensionError),
132 #[error("Invalid signature: {0}")]
136 Signature(#[from] openmls::prelude::SignatureError),
137 #[error("client: {0}")]
141 Client(#[from] ClientError),
142 #[error("receive error: {0}")]
146 ReceiveError(#[from] GroupMessageProcessingError),
147 #[error(transparent)]
151 AddressValidation(#[from] IdentifierValidationError),
152 #[error(transparent)]
156 LocalEvent(#[from] LocalEventError),
157 #[error("Public Keys {0:?} are not valid ed25519 public keys")]
161 InvalidPublicKeys(Vec<Vec<u8>>),
162 #[error("Commit validation error {0}")]
166 CommitValidation(#[from] CommitValidationError),
167 #[error("identity error: {0}")]
171 Identity(#[from] IdentityError),
172 #[error("conversion error: {0}")]
176 ConversionError(#[from] xmtp_proto::ConversionError),
177 #[error("crypto error: {0}")]
181 CryptoError(#[from] openmls::prelude::CryptoError),
182 #[error("create group context proposal error: {0}")]
186 CreateGroupContextExtProposalError(
187 #[from] CreateGroupContextExtProposalError<sql_key_store::SqlKeyStoreError>,
188 ),
189 #[error("propose add member error: {0}")]
193 ProposeAddMember(#[from] ProposeAddMemberError<sql_key_store::SqlKeyStoreError>),
194 #[error("propose remove member error: {0}")]
198 ProposeRemoveMember(#[from] ProposeRemoveMemberError<sql_key_store::SqlKeyStoreError>),
199 #[error("proposal error: {0}")]
203 Proposal(#[from] ProposalError<sql_key_store::SqlKeyStoreError>),
204 #[error("commit to pending proposals error: {0}")]
208 CommitToPendingProposals(
209 #[from] CommitToPendingProposalsError<sql_key_store::SqlKeyStoreError>,
210 ),
211 #[error("merge pending commit error: {0}")]
215 MergePendingCommit(
216 #[from] openmls::group::MergePendingCommitError<sql_key_store::SqlKeyStoreError>,
217 ),
218 #[error("Proposals not supported: {0}")]
222 ProposalsNotSupported(String),
223 #[error("min_version {requested} exceeds own pkg_version {own}")]
228 MinVersionExceedsOwnVersion { requested: String, own: String },
229 #[error("min_version {requested} would downgrade existing floor {current}")]
234 MinVersionDowngrade { requested: String, current: String },
235 #[error("invalid min_version {value:?}: {reason}")]
239 InvalidMinVersion { value: String, reason: String },
240 #[error("component source error: {0}")]
245 ComponentSource(#[from] xmtp_mls_common::app_data::component_source::ComponentSourceError),
246 #[error("metadata field error: {0}")]
250 MetadataField(#[from] xmtp_mls_common::app_data::fields::FieldError),
251 #[error("app data commit error: {0}")]
258 AppDataCommit(#[from] super::app_data::GroupAppDataError<sql_key_store::SqlKeyStoreError>),
259 #[error("bootstrap synthesis error: {0}")]
268 BootstrapSynthesis(#[from] super::app_data::migration::BootstrapSynthesisError),
269 #[error("bootstrap commit error: {0}")]
275 BootstrapCommit(
276 #[from] super::app_data::migration::BootstrapCommitError<sql_key_store::SqlKeyStoreError>,
277 ),
278 #[error("Credential error")]
282 CredentialError(#[from] BasicCredentialError),
283 #[error("LeafNode error")]
287 LeafNodeError(#[from] LibraryError),
288 #[error("Installation diff error: {0}")]
292 InstallationDiff(#[from] InstallationDiffError),
293 #[error("PSKs are not support")]
297 NoPSKSupport,
298 #[error("sql key store error: {0}")]
302 SqlKeyStore(#[from] sql_key_store::SqlKeyStoreError),
303 #[error("Sync failed to wait for intent: {}", _0)]
307 SyncFailedToWait(#[source] Box<SyncSummary>),
308 #[error(transparent)]
310 #[error_code(inherit)]
311 StreamBarrier(#[from] crate::subscriptions::barrier::BarrierError),
312 #[error("Intent {intent_id} was published but processing is not confirmed")]
314 PublishedButUnconfirmed {
315 intent_id: i32,
316 #[source]
317 cause: Option<Box<crate::subscriptions::barrier::BarrierError>>,
318 },
319 #[error("Missing pending commit")]
323 MissingPendingCommit,
324 #[error(transparent)]
328 ProcessIntent(#[from] ProcessIntentError),
329 #[error("Failed to load lock")]
333 LockUnavailable,
334 #[error("Exceeded max characters for this field. Must be under: {length}")]
338 TooManyCharacters { length: usize },
339 #[error(
346 "app data update was superseded; expected {expected:?} but the committed value is {actual:?}"
347 )]
348 AppDataSuperseded { expected: String, actual: String },
349 #[error("Group is paused until version {0} is available")]
353 GroupPausedUntilUpdate(String),
354 #[error("Group is inactive")]
358 GroupInactive,
359 #[error("{}", _0.to_string())]
363 Sync(#[from] Box<SyncSummary>),
364 #[error(transparent)]
368 Db(#[from] xmtp_db::ConnectionError),
369 #[error(transparent)]
373 MlsStore(#[from] MlsStoreError),
374 #[error(transparent)]
378 MetadataPermissionsError(#[from] MetadataPermissionsError),
379 #[error("Failed to verify all installations: failedInstallations={0}")]
383 FailedToVerifyInstallations(FailedInstallationIds),
384 #[error("no welcomes to send")]
388 NoWelcomesToSend,
389 #[error("Codec error: {0}")]
393 CodecError(#[from] CodecError),
394 #[error(transparent)]
398 WrapWelcome(#[from] WrapPayloadError),
399 #[error(transparent)]
403 UnwrapWelcome(#[from] UnwrapPayloadError),
404 #[error("Failed to retrieve welcome data from topic {0}")]
408 WelcomeDataNotFound(String),
409 #[error("Welcome requires group {group_id} to process through {anchor}")]
411 WelcomeGroupPrefixPending { group_id: GroupId, anchor: u64 },
412 #[error("Welcome join metadata is invalid")]
414 InvalidWelcomeMetadata,
415 #[error("Welcome requires client version {0}")]
417 UnsupportedWelcomeVersion(String),
418 #[error("Result was not initialized")]
422 UninitializedResult,
423 #[error(transparent)]
427 Diesel(#[from] xmtp_db::diesel::result::Error),
428 #[error(transparent)]
432 UninitializedField(#[from] derive_builder::UninitializedFieldError),
433 #[error(transparent)]
437 DeleteMessage(#[from] DeleteMessageError),
438 #[error(transparent)]
442 DeviceSync(#[from] Box<DeviceSyncError>),
443}
444
445impl From<xmtp_mls_validation::commit::CommitRuleError> for GroupError {
446 fn from(error: xmtp_mls_validation::commit::CommitRuleError) -> Self {
447 Self::CommitValidation(error.into())
448 }
449}
450
451#[derive(Error, Debug)]
452pub enum DeleteMessageError {
453 #[error("Message not found: {0}")]
454 MessageNotFound(String),
455 #[error("Not authorized to delete this message")]
456 NotAuthorized,
457 #[error("Cannot delete this message type")]
458 NonDeletableMessage,
459 #[error("Message already deleted")]
460 MessageAlreadyDeleted,
461}
462
463impl RetryableError for DeleteMessageError {
464 fn is_retryable(&self) -> bool {
465 false
466 }
467}
468
469impl From<prost::EncodeError> for GroupError {
470 fn from(value: prost::EncodeError) -> Self {
471 GroupError::ConversionError(value.into())
472 }
473}
474
475impl From<prost::DecodeError> for GroupError {
476 fn from(value: prost::DecodeError) -> Self {
477 GroupError::ConversionError(value.into())
478 }
479}
480
481impl From<SyncSummary> for GroupError {
482 fn from(value: SyncSummary) -> Self {
483 GroupError::Sync(Box::new(value))
484 }
485}
486
487#[derive(Error, Debug)]
488pub enum MetadataPermissionsError {
489 #[error(transparent)]
490 Permissions(#[from] GroupMutablePermissionsError),
491 #[error(transparent)]
492 Mutable(#[from] GroupMutableMetadataError),
493 #[error("Metadata error {0}")]
494 GroupMetadata(#[from] GroupMetadataError),
495 #[error("Invalid permission update")]
496 InvalidPermissionUpdate,
497 #[error("cannot change metadata of DM")]
498 DmGroupMetadataForbidden,
499 #[error(transparent)]
500 DmValidation(#[from] DmValidationError),
501 #[error("Invalid extension: {0}")]
502 InvalidExtension(#[from] openmls::prelude::InvalidExtensionError),
503 #[error(transparent)]
509 ComponentSource(#[from] xmtp_mls_common::app_data::component_source::ComponentSourceError),
510}
511
512impl RetryableError for MetadataPermissionsError {
513 fn is_retryable(&self) -> bool {
514 false
515 }
516}
517
518#[derive(Error, Debug)]
519pub enum GroupLeaveValidationError {
520 #[error("cannot leave a DM conversation")]
521 DmLeaveForbidden,
522 #[error("cannot leave a group that has only one member")]
523 SingleMemberLeaveRejected,
524 #[error("super-admin cannot leave a group; must be demoted first")]
525 SuperAdminLeaveForbidden,
526 #[error("inbox ID already exists in the pending leave list")]
527 InboxAlreadyInPendingList,
528 #[error("inbox ID does not exist in the pending leave list")]
529 InboxNotInPendingList,
530 #[error("only a member of the group can send a leave request or retract a leave request")]
531 NotAGroupMember,
532}
533
534impl RetryableError for GroupLeaveValidationError {
535 fn is_retryable(&self) -> bool {
536 false
537 }
538}
539
540#[derive(Error, Debug)]
541pub enum DmValidationError {
542 #[error("DM group must include our inbox in its member pair")]
543 OurInboxMustBeMember,
544 #[error("DM group must have DM_MEMBERS set")]
545 MustHaveMembersSet,
546 #[error("Invalid conversation type for DM group")]
547 InvalidConversationType,
548 #[error("DM members do not match expected inboxes")]
549 ExpectedInboxesDoNotMatch,
550 #[error("DM group must have empty admin and super admin lists")]
551 MustHaveEmptyAdminAndSuperAdmin,
552 #[error("Invalid permissions for DM group")]
553 InvalidPermissions,
554 #[error("DM group membership is invalid")]
555 InvalidMembership,
556 #[error("DM group member credential is invalid")]
557 InvalidMemberCredential,
558 #[error("DM group includes an inbox outside its pair")]
559 MemberOutsidePair,
560 #[error("stored DM identifier does not match its validated pair")]
561 StoredDmIdMismatch,
562}
563
564impl RetryableError for DmValidationError {
565 fn is_retryable(&self) -> bool {
566 match self {
567 Self::OurInboxMustBeMember
568 | Self::MustHaveMembersSet
569 | Self::InvalidConversationType
570 | Self::ExpectedInboxesDoNotMatch
571 | Self::MustHaveEmptyAdminAndSuperAdmin
572 | Self::InvalidPermissions
573 | Self::InvalidMembership
574 | Self::InvalidMemberCredential
575 | Self::MemberOutsidePair
576 | Self::StoredDmIdMismatch => false,
577 }
578 }
579}
580
581impl RetryableError for GroupError {
582 fn is_retryable(&self) -> bool {
583 match self {
584 Self::OutgoingPreparation(error) => error.is_retryable(),
585 Self::Client(client_error) => client_error.is_retryable(),
586 Self::Storage(storage) => storage.is_retryable(),
587 Self::ReceiveError(msg) => msg.is_retryable(),
588 Self::Identity(identity) => identity.is_retryable(),
589 Self::UpdateGroupMembership(update) => update.is_retryable(),
590 Self::GroupCreate(group) => group.is_retryable(),
591 Self::SelfUpdate(update) => update.is_retryable(),
592 Self::WelcomeError(
593 openmls::prelude::WelcomeError::UnsupportedMlsVersion
594 | openmls::prelude::WelcomeError::UnsupportedExtensions,
595 ) => true,
596 Self::WelcomeError(welcome) => welcome.is_retryable(),
597 Self::SqlKeyStore(sql) => sql.is_retryable(),
598 Self::InstallationDiff(diff) => diff.is_retryable(),
599 Self::CreateGroupContextExtProposalError(create) => create.is_retryable(),
600 Self::ProposeAddMember(e) => e.is_retryable(),
601 Self::ProposeRemoveMember(e) => e.is_retryable(),
602 Self::Proposal(e) => e.is_retryable(),
603 Self::CommitToPendingProposals(e) => e.is_retryable(),
604 Self::ProposalsNotSupported(_) => false,
605 Self::MinVersionExceedsOwnVersion { .. } => false,
606 Self::MinVersionDowngrade { .. } => false,
607 Self::InvalidMinVersion { .. } => false,
608 Self::ComponentSource(_) => false,
609 Self::MetadataField(_) => false,
610 Self::AppDataCommit(e) => e.is_retryable(),
611 Self::BootstrapSynthesis(e) => e.is_retryable(),
616 Self::BootstrapCommit(_) => false,
617 Self::CommitValidation(err) => err.is_retryable(),
618 Self::WrappedApi(err) => err.is_retryable(),
619 Self::ProcessIntent(err) => err.is_retryable(),
620 Self::LocalEvent(err) => err.is_retryable(),
621 Self::LockUnavailable => true,
622 Self::WelcomeGroupPrefixPending { .. } | Self::UnsupportedWelcomeVersion(_) => true,
623 Self::SyncFailedToWait(_) => true,
624 Self::StreamBarrier(error) => error.is_retryable(),
625 Self::PublishedButUnconfirmed { .. } => true,
626 Self::CodecError(_) => true,
627 Self::Sync(s) => s.is_retryable(),
628 Self::Db(e) => e.is_retryable(),
629 Self::MlsStore(e) => e.is_retryable(),
630 Self::MetadataPermissionsError(e) => e.is_retryable(),
631 Self::WrapWelcome(e) => e.is_retryable(),
632 Self::UnwrapWelcome(e) => e.is_retryable(),
633 Self::Diesel(e) => e.is_retryable(),
634 Self::LeaveCantProcessed(e) => e.is_retryable(),
635 Self::DeleteMessage(e) => e.is_retryable(),
636 Self::DeviceSync(e) => e.is_retryable(),
637 Self::MergePendingCommit(e) => e.is_retryable(),
638 Self::MissingSequenceId => true,
645 Self::NotFound(_)
646 | Self::InvalidWelcomeMetadata
647 | Self::UserLimitExceeded
648 | Self::InvalidGroupMembership
649 | Self::Intent(_)
650 | Self::CreateMessage(_)
651 | Self::TlsError(_)
652 | Self::AddressNotFound(_)
653 | Self::InvalidExtension(_)
654 | Self::Signature(_)
655 | Self::LeafNodeError(_)
656 | Self::NoPSKSupport
657 | Self::MissingPendingCommit
658 | Self::AddressValidation(_)
659 | Self::InvalidPublicKeys(_)
660 | Self::CredentialError(_)
661 | Self::ConversionError(_)
662 | Self::CryptoError(_)
663 | Self::TooManyCharacters { .. }
664 | Self::AppDataSuperseded { .. }
665 | Self::GroupPausedUntilUpdate(_)
666 | Self::GroupInactive
667 | Self::ReservedTranscriptContentType
668 | Self::FailedToVerifyInstallations(_)
669 | Self::NoWelcomesToSend
670 | Self::WelcomeDataNotFound(_)
671 | Self::UninitializedField(_)
672 | Self::UninitializedResult => false,
673 }
674 }
675}
676
677impl crate::worker::NeedsDbReconnect for GroupError {
678 fn needs_db_reconnect(&self) -> bool {
681 match self {
682 Self::Storage(s) => s.db_needs_connection(),
683 Self::Client(c) => c.db_needs_connection(),
684 Self::Db(c) => c.db_needs_connection(),
685 Self::MlsStore(s) => s.needs_db_reconnect(),
686 Self::Identity(i) => i.needs_db_reconnect(),
687 Self::DeviceSync(d) => d.needs_db_reconnect(),
688 Self::StreamBarrier(error) => error.needs_db_reconnect(),
689 Self::PublishedButUnconfirmed { cause, .. } => cause
690 .as_ref()
691 .is_some_and(|error| error.needs_db_reconnect()),
692 Self::ReceiveError(error) => error.needs_db_reconnect(),
693 Self::Sync(summary) | Self::SyncFailedToWait(summary) => summary.needs_db_reconnect(),
694 _ => false,
695 }
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[xmtp_common::test]
704 fn missing_sequence_id_is_retryable() {
705 assert!(GroupError::MissingSequenceId.is_retryable());
710 }
711
712 #[xmtp_common::test]
713 fn failed_to_verify_installations_includes_hex_ids() {
714 let err = GroupError::FailedToVerifyInstallations(FailedInstallationIds(vec![
715 vec![0xAB; 32],
716 vec![0xCD; 32],
717 ]));
718 let message = err.to_string();
719 assert!(message.contains("failedInstallations="));
720 assert!(message.contains(&hex::encode([0xAB; 32])));
721 assert!(message.contains(&hex::encode([0xCD; 32])));
722 assert!(!err.is_retryable());
723 }
724}