diff --git a/protos/Backups.proto b/protos/Backups.proto index 8cde79590..e93f29aea 100644 --- a/protos/Backups.proto +++ b/protos/Backups.proto @@ -31,6 +31,7 @@ message BackupInfo { // For example, Chats may all be together at the beginning, // or may each immediately precede its first ChatItem. message Frame { + // If unset, importers should skip this frame without throwing an error. oneof item { AccountData account = 1; Recipient recipient = 2; @@ -45,13 +46,13 @@ message Frame { message AccountData { enum PhoneNumberSharingMode { - UNKNOWN = 0; + UNKNOWN = 0; // Interpret as "Nobody" EVERYBODY = 1; NOBODY = 2; } message UsernameLink { enum Color { - UNKNOWN = 0; + UNKNOWN = 0; // Interpret as "Blue" BLUE = 1; WHITE = 2; GREY = 3; @@ -98,6 +99,7 @@ message AccountData { message IAPSubscriberData { bytes subscriberId = 1; + // If unset, importers should ignore the subscriber data without throwing an error. oneof iapSubscriptionId { // Identifies an Android Play Store IAP subscription. string purchaseToken = 2; @@ -120,6 +122,7 @@ message AccountData { message Recipient { uint64 id = 1; // generated id for reference only within this file + // If unset, importers should skip this frame without throwing an error. oneof destination { Contact contact = 2; Group group = 3; @@ -132,9 +135,9 @@ message Recipient { message Contact { enum IdentityState { - DEFAULT = 0; + DEFAULT = 0; // A valid value -- indicates unset by the user VERIFIED = 1; - UNVERIFIED = 2; + UNVERIFIED = 2; // Was once verified and is now unverified } message Registered {} @@ -143,7 +146,7 @@ message Contact { } enum Visibility { - VISIBLE = 0; + VISIBLE = 0; // A valid value -- the contact is not hidden HIDDEN = 1; HIDDEN_MESSAGE_REQUEST = 2; } @@ -160,6 +163,7 @@ message Contact { bool blocked = 5; Visibility visibility = 6; + // If unset, consider the user to be registered oneof registration { Registered registered = 7; NotRegistered notRegistered = 8; @@ -178,7 +182,7 @@ message Contact { message Group { enum StorySendMode { - DEFAULT = 0; + DEFAULT = 0; // A valid value -- indicates unset by the user DISABLED = 1; ENABLED = 2; } @@ -212,6 +216,7 @@ message Group { } message GroupAttributeBlob { + // If unset, consider the field it represents to not be present oneof content { string title = 1; bytes avatar = 2; @@ -222,7 +227,7 @@ message Group { message Member { enum Role { - UNKNOWN = 0; + UNKNOWN = 0; // Intepret as "Default" DEFAULT = 1; ADMINISTRATOR = 2; } @@ -254,7 +259,7 @@ message Group { message AccessControl { enum AccessRequired { - UNKNOWN = 0; + UNKNOWN = 0; // Intepret as "Unsatisfiable" ANY = 1; MEMBER = 2; ADMINISTRATOR = 3; @@ -294,7 +299,7 @@ message Chat { */ message CallLink { enum Restrictions { - UNKNOWN = 0; + UNKNOWN = 0; // Interpret as "Admin Approval" NONE = 1; ADMIN_APPROVAL = 2; } @@ -308,7 +313,7 @@ message CallLink { message AdHocCall { enum State { - UNKNOWN_STATE = 0; + UNKNOWN_STATE = 0; // Interpret as "Generic" GENERIC = 1; } @@ -324,6 +329,7 @@ message DistributionListItem { // by an all-0 UUID (00000000-0000-0000-0000-000000000000). bytes distributionId = 1; // distribution list ids are uuids + // If unset, importers should skip the item entirely without showing an error. oneof item { uint64 deletionTimestamp = 2; DistributionList distributionList = 3; @@ -332,7 +338,7 @@ message DistributionListItem { message DistributionList { enum PrivacyMode { - UNKNOWN = 0; + UNKNOWN = 0; // Interpret as "Only with" ONLY_WITH = 1; ALL_EXCEPT = 2; ALL = 3; @@ -367,12 +373,14 @@ message ChatItem { repeated ChatItem revisions = 6; // ordered from oldest to newest bool sms = 7; + // If unset, importers should skip this item without throwing an error. oneof directionalDetails { IncomingMessageDetails incoming = 8; OutgoingMessageDetails outgoing = 9; DirectionlessMessageDetails directionless = 10; } + // If unset, importers should skip this item without throwing an error. oneof item { StandardMessage standardMessage = 11; ContactMessage contactMessage = 12; @@ -421,6 +429,7 @@ message SendStatus { uint64 recipientId = 1; uint64 timestamp = 2; // the time the status was last updated -- if from a receipt, it should be the sentTime of the receipt + // If unset, importers should consider the status to be "pending" oneof deliveryStatus { Pending pending = 3; Sent sent = 4; @@ -457,6 +466,7 @@ message DirectStoryReplyMessage { FilePointer longText = 2; } + // If unset, importers should ignore the message without throwing an error. oneof reply { TextReply textReply = 1; string emoji = 2; @@ -475,7 +485,7 @@ message PaymentNotification { message FailedTransaction { // Failed payments can't be synced from the ledger enum FailureReason { - GENERIC = 0; + GENERIC = 0; // A valid value -- reason unknown NETWORK = 1; INSUFFICIENT_FUNDS = 2; } @@ -484,7 +494,7 @@ message PaymentNotification { message Transaction { enum Status { - INITIAL = 0; + INITIAL = 0; // A valid value -- state unconfirmed SUBMITTED = 1; SUCCESSFUL = 2; } @@ -501,6 +511,7 @@ message PaymentNotification { optional bytes receipt = 7; // mobile coin blobs } + // If unset, importers should treat the transaction as successful with no metadata. oneof payment { Transaction transaction = 1; FailedTransaction failedTransaction = 2; @@ -515,7 +526,7 @@ message PaymentNotification { message GiftBadge { enum State { - UNOPENED = 0; + UNOPENED = 0; // A valid state OPENED = 1; REDEEMED = 2; FAILED = 3; @@ -543,7 +554,7 @@ message ContactAttachment { message Phone { enum Type { - UNKNOWN = 0; + UNKNOWN = 0; // Interpret as "Home" HOME = 1; MOBILE = 2; WORK = 3; @@ -557,7 +568,7 @@ message ContactAttachment { message Email { enum Type { - UNKNOWN = 0; + UNKNOWN = 0; // Intepret as "Home" HOME = 1; MOBILE = 2; WORK = 3; @@ -571,7 +582,7 @@ message ContactAttachment { message PostalAddress { enum Type { - UNKNOWN = 0; + UNKNOWN = 0; // Interpret as "Home" HOME = 1; WORK = 2; CUSTOM = 3; @@ -631,7 +642,7 @@ message MessageAttachment { // but explicitly mutually exclusive. Note the different raw values // (non-zero starting values are not supported in proto3.) enum Flag { - NONE = 0; + NONE = 0; // A valid value -- no flag applied VOICE_MESSAGE = 1; BORDERLESS = 2; GIF = 3; @@ -682,6 +693,7 @@ message FilePointer { message InvalidAttachmentLocator { } + // If unset, importers should consider it to be an InvalidAttachmentLocator without throwing an error. oneof locator { BackupLocator backupLocator = 1; AttachmentLocator attachmentLocator = 2; @@ -700,7 +712,7 @@ message FilePointer { message Quote { enum Type { - UNKNOWN = 0; + UNKNOWN = 0; // Interpret as "Normal" NORMAL = 1; GIFT_BADGE = 2; VIEW_ONCE = 3; @@ -721,7 +733,7 @@ message Quote { message BodyRange { enum Style { - NONE = 0; + NONE = 0; // Importers should ignore the body range without throwing an error. BOLD = 1; ITALIC = 2; SPOILER = 3; @@ -734,6 +746,7 @@ message BodyRange { uint32 start = 1; uint32 length = 2; + // If unset, importers should ignore the body range without throwing an error. oneof associatedValue { bytes mentionAci = 3; Style style = 4; @@ -750,6 +763,7 @@ message Reaction { } message ChatUpdateMessage { + // If unset, importers should ignore the update message without throwing an error. oneof update { SimpleChatUpdate simpleUpdate = 1; GroupChangeChatUpdate groupChange = 2; @@ -765,19 +779,19 @@ message ChatUpdateMessage { message IndividualCall { enum Type { - UNKNOWN_TYPE = 0; + UNKNOWN_TYPE = 0; // Interpret as "Audio call" AUDIO_CALL = 1; VIDEO_CALL = 2; } enum Direction { - UNKNOWN_DIRECTION = 0; + UNKNOWN_DIRECTION = 0; // Interpret as "Incoming" INCOMING = 1; OUTGOING = 2; } enum State { - UNKNOWN_STATE = 0; + UNKNOWN_STATE = 0; // Interpret as "Accepted" ACCEPTED = 1; NOT_ACCEPTED = 2; // An incoming call that is no longer ongoing, which we neither accepted @@ -798,7 +812,7 @@ message IndividualCall { message GroupCall { enum State { - UNKNOWN_STATE = 0; + UNKNOWN_STATE = 0; // Interpret as "Generic" // A group call was started without ringing. GENERIC = 1; // We joined a group call that was started without ringing. @@ -829,7 +843,7 @@ message GroupCall { message SimpleChatUpdate { enum Type { - UNKNOWN = 0; + UNKNOWN = 0; // Importers should skip the update without throwing an error. JOINED_SIGNAL = 1; IDENTITY_UPDATE = 2; IDENTITY_VERIFIED = 3; @@ -863,6 +877,7 @@ message ProfileChangeChatUpdate { } message LearnedProfileChatUpdate { + // If unset, importers should consider the previous name to be an empty string. oneof previousName { uint64 e164 = 1; string username = 2; @@ -879,6 +894,7 @@ message SessionSwitchoverChatUpdate { message GroupChangeChatUpdate { message Update { + // If unset, importers should consider it to be a GenericGroupUpdate with unset updaterAci oneof update { GenericGroupUpdate genericGroupUpdate = 1; GroupCreationUpdate groupCreationUpdate = 2; @@ -948,7 +964,7 @@ message GroupDescriptionUpdate { } enum GroupV2AccessLevel { - UNKNOWN = 0; + UNKNOWN = 0; // Interpret as "Unsatisfiable" ANY = 1; MEMBER = 2; ADMINISTRATOR = 3; @@ -1138,6 +1154,7 @@ message ChatStyle { message CustomChatColor { uint64 id = 1; + // If unset, use the default chat color oneof color { fixed32 solid = 2; // 0xAARRGGBB Gradient gradient = 3; @@ -1148,7 +1165,7 @@ message ChatStyle { } enum WallpaperPreset { - UNKNOWN_WALLPAPER_PRESET = 0; + UNKNOWN_WALLPAPER_PRESET = 0; // Interpret as the wallpaper being unset SOLID_BLUSH = 1; SOLID_COPPER = 2; SOLID_DUST = 3; @@ -1173,7 +1190,7 @@ message ChatStyle { } enum BubbleColorPreset { - UNKNOWN_BUBBLE_COLOR_PRESET = 0; + UNKNOWN_BUBBLE_COLOR_PRESET = 0; // Interpret as the user's default chat bubble color SOLID_ULTRAMARINE = 1; SOLID_CRIMSON = 2; SOLID_VERMILION = 3; @@ -1198,6 +1215,7 @@ message ChatStyle { GRADIENT_TANGERINE = 22; } + // If unset, importers should consider there to be no wallpaper. oneof wallpaper { WallpaperPreset wallpaperPreset = 1; // This `FilePointer` is expected not to contain a `fileName`, `width`, @@ -1205,6 +1223,7 @@ message ChatStyle { FilePointer wallpaperPhoto = 2; } + // If unset, importers should consider it to be AutomaticBubbleColor oneof bubbleColor { // Bubble setting is automatically determined based on the wallpaper setting, // or `SOLID_ULTRAMARINE` for `noWallpaper` @@ -1220,7 +1239,7 @@ message ChatStyle { message NotificationProfile { enum DayOfWeek { - UNKNOWN = 0; + UNKNOWN = 0; // Interpret as "Monday" MONDAY = 1; TUESDAY = 2; WEDNESDAY = 3; @@ -1246,7 +1265,7 @@ message NotificationProfile { message ChatFolder { // Represents the default "All chats" folder record vs all other custom folders enum FolderType { - UNKNOWN = 0; + UNKNOWN = 0; // Interpret as "Custom" ALL = 1; CUSTOM = 2; } diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts index 956e089f9..7ea79116e 100644 --- a/ts/services/backups/import.ts +++ b/ts/services/backups/import.ts @@ -480,8 +480,8 @@ export class BackupImportStream extends Writable { // Not a conversation return; } else { - log.warn(`${this.#logId}: unsupported recipient item`); - return; + log.warn(`${this.#logId}: unsupported recipient destination`); + throw new Error('Unsupported recipient destination'); } if (convo !== this.#ourConversation) { @@ -505,6 +505,7 @@ export class BackupImportStream extends Writable { await this.#fromAdHocCall(frame.adHocCall); } else { log.warn(`${this.#logId}: unsupported frame item ${frame.item}`); + throw new Error('Unsupported frame type'); } } catch (error) { this.#frameErrorCount += 1; @@ -940,11 +941,12 @@ export class BackupImportStream extends Writable { ); attrs.discoveredUnregisteredAt = timestamp || this.#now; attrs.firstUnregisteredAt = timestamp || undefined; - } else { - strictAssert( + } else if (!contact.registered) { + log.error( contact.registered, - 'contact is either registered or unregistered' + 'contact is neither registered nor unregistered; treating as registered' ); + this.#frameErrorCount += 1; } if (contact.blocked) { @@ -1649,7 +1651,12 @@ export class BackupImportStream extends Writable { } else if (status.skipped) { sendStatus = SendStatus.Skipped; } else { - throw new Error(`Unknown sendStatus received: ${status}`); + log.error( + `${timestamp}: Unknown sendStatus received: ${status}, falling back to Pending` + ); + // We fallback to pending for unknown send statuses + sendStatus = SendStatus.Pending; + this.#frameErrorCount += 1; } sendStateByConversationId[target.id] = { @@ -1896,6 +1903,10 @@ export class BackupImportStream extends Writable { targetAuthorAci: storyAuthorAci, targetTimestamp: 0, // stories are never imported }; + } else { + throw new Error( + 'Direct story reply message missing both textReply and emoji' + ); } return result; @@ -2413,10 +2424,12 @@ export class BackupImportStream extends Writable { if (updateMessage.learnedProfileChange) { const { e164, username } = updateMessage.learnedProfileChange; - strictAssert( - e164 != null || username != null, - 'learnedProfileChange must have an old name' - ); + if (e164 == null && username == null) { + log.error( + `${options.timestamp}: learnedProfileChange had no previous e164 or username` + ); + this.#frameErrorCount += 1; + } return { message: { type: 'title-transition-notification', @@ -3392,7 +3405,7 @@ export class BackupImportStream extends Writable { value = { start: rgbIntToDesktopHSL(color.solid), }; - } else { + } else if (color.gradient) { strictAssert(color.gradient != null, 'Either solid or gradient'); strictAssert(color.gradient.colors != null, 'Missing gradient colors'); @@ -3409,6 +3422,12 @@ export class BackupImportStream extends Writable { end: rgbIntToDesktopHSL(end), deg, }; + } else { + log.error( + 'CustomChatColor missing both solid and gradient fields, dropping' + ); + this.#frameErrorCount += 1; + continue; } customColors.colors[uuid] = value; @@ -3532,16 +3551,23 @@ export class BackupImportStream extends Writable { color = 'ultramarine'; break; } - } else { - strictAssert(chatStyle.customColorId != null, 'Missing custom color id'); - + } else if (chatStyle.customColorId != null) { const entry = this.#customColorById.get( chatStyle.customColorId.toNumber() ); - strictAssert(entry != null, 'Missing custom color'); - color = 'custom'; - customColorData = entry; + if (entry) { + color = 'custom'; + customColorData = entry; + } else { + log.error('Chat style referenced missing custom color'); + this.#frameErrorCount += 1; + autoBubbleColor = true; + } + } else { + log.error('ChatStyle has no recognized field'); + this.#frameErrorCount += 1; + autoBubbleColor = true; } return { diff --git a/ts/services/backups/util/filePointers.ts b/ts/services/backups/util/filePointers.ts index 638972264..8375ab9b5 100644 --- a/ts/services/backups/util/filePointers.ts +++ b/ts/services/backups/util/filePointers.ts @@ -120,15 +120,15 @@ export function convertFilePointerToAttachment( }; } - if (invalidAttachmentLocator) { - return { - ...omit(commonProps, 'downloadPath'), - error: true, - size: 0, - }; + if (!invalidAttachmentLocator) { + log.error('convertFilePointerToAttachment: filePointer had no locator'); } - throw new Error('convertFilePointerToAttachment: mising locator'); + return { + ...omit(commonProps, 'downloadPath'), + error: true, + size: 0, + }; } export function convertBackupMessageAttachmentToAttachment(