Update behavior during import for missing oneof fields

This commit is contained in:
trevor-signal 2025-02-04 12:55:38 -05:00 committed by GitHub
parent 22d30ec4eb
commit d30ad37262
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 99 additions and 54 deletions

View file

@ -31,6 +31,7 @@ message BackupInfo {
// For example, Chats may all be together at the beginning, // For example, Chats may all be together at the beginning,
// or may each immediately precede its first ChatItem. // or may each immediately precede its first ChatItem.
message Frame { message Frame {
// If unset, importers should skip this frame without throwing an error.
oneof item { oneof item {
AccountData account = 1; AccountData account = 1;
Recipient recipient = 2; Recipient recipient = 2;
@ -45,13 +46,13 @@ message Frame {
message AccountData { message AccountData {
enum PhoneNumberSharingMode { enum PhoneNumberSharingMode {
UNKNOWN = 0; UNKNOWN = 0; // Interpret as "Nobody"
EVERYBODY = 1; EVERYBODY = 1;
NOBODY = 2; NOBODY = 2;
} }
message UsernameLink { message UsernameLink {
enum Color { enum Color {
UNKNOWN = 0; UNKNOWN = 0; // Interpret as "Blue"
BLUE = 1; BLUE = 1;
WHITE = 2; WHITE = 2;
GREY = 3; GREY = 3;
@ -98,6 +99,7 @@ message AccountData {
message IAPSubscriberData { message IAPSubscriberData {
bytes subscriberId = 1; bytes subscriberId = 1;
// If unset, importers should ignore the subscriber data without throwing an error.
oneof iapSubscriptionId { oneof iapSubscriptionId {
// Identifies an Android Play Store IAP subscription. // Identifies an Android Play Store IAP subscription.
string purchaseToken = 2; string purchaseToken = 2;
@ -120,6 +122,7 @@ message AccountData {
message Recipient { message Recipient {
uint64 id = 1; // generated id for reference only within this file uint64 id = 1; // generated id for reference only within this file
// If unset, importers should skip this frame without throwing an error.
oneof destination { oneof destination {
Contact contact = 2; Contact contact = 2;
Group group = 3; Group group = 3;
@ -132,9 +135,9 @@ message Recipient {
message Contact { message Contact {
enum IdentityState { enum IdentityState {
DEFAULT = 0; DEFAULT = 0; // A valid value -- indicates unset by the user
VERIFIED = 1; VERIFIED = 1;
UNVERIFIED = 2; UNVERIFIED = 2; // Was once verified and is now unverified
} }
message Registered {} message Registered {}
@ -143,7 +146,7 @@ message Contact {
} }
enum Visibility { enum Visibility {
VISIBLE = 0; VISIBLE = 0; // A valid value -- the contact is not hidden
HIDDEN = 1; HIDDEN = 1;
HIDDEN_MESSAGE_REQUEST = 2; HIDDEN_MESSAGE_REQUEST = 2;
} }
@ -160,6 +163,7 @@ message Contact {
bool blocked = 5; bool blocked = 5;
Visibility visibility = 6; Visibility visibility = 6;
// If unset, consider the user to be registered
oneof registration { oneof registration {
Registered registered = 7; Registered registered = 7;
NotRegistered notRegistered = 8; NotRegistered notRegistered = 8;
@ -178,7 +182,7 @@ message Contact {
message Group { message Group {
enum StorySendMode { enum StorySendMode {
DEFAULT = 0; DEFAULT = 0; // A valid value -- indicates unset by the user
DISABLED = 1; DISABLED = 1;
ENABLED = 2; ENABLED = 2;
} }
@ -212,6 +216,7 @@ message Group {
} }
message GroupAttributeBlob { message GroupAttributeBlob {
// If unset, consider the field it represents to not be present
oneof content { oneof content {
string title = 1; string title = 1;
bytes avatar = 2; bytes avatar = 2;
@ -222,7 +227,7 @@ message Group {
message Member { message Member {
enum Role { enum Role {
UNKNOWN = 0; UNKNOWN = 0; // Intepret as "Default"
DEFAULT = 1; DEFAULT = 1;
ADMINISTRATOR = 2; ADMINISTRATOR = 2;
} }
@ -254,7 +259,7 @@ message Group {
message AccessControl { message AccessControl {
enum AccessRequired { enum AccessRequired {
UNKNOWN = 0; UNKNOWN = 0; // Intepret as "Unsatisfiable"
ANY = 1; ANY = 1;
MEMBER = 2; MEMBER = 2;
ADMINISTRATOR = 3; ADMINISTRATOR = 3;
@ -294,7 +299,7 @@ message Chat {
*/ */
message CallLink { message CallLink {
enum Restrictions { enum Restrictions {
UNKNOWN = 0; UNKNOWN = 0; // Interpret as "Admin Approval"
NONE = 1; NONE = 1;
ADMIN_APPROVAL = 2; ADMIN_APPROVAL = 2;
} }
@ -308,7 +313,7 @@ message CallLink {
message AdHocCall { message AdHocCall {
enum State { enum State {
UNKNOWN_STATE = 0; UNKNOWN_STATE = 0; // Interpret as "Generic"
GENERIC = 1; GENERIC = 1;
} }
@ -324,6 +329,7 @@ message DistributionListItem {
// by an all-0 UUID (00000000-0000-0000-0000-000000000000). // by an all-0 UUID (00000000-0000-0000-0000-000000000000).
bytes distributionId = 1; // distribution list ids are uuids bytes distributionId = 1; // distribution list ids are uuids
// If unset, importers should skip the item entirely without showing an error.
oneof item { oneof item {
uint64 deletionTimestamp = 2; uint64 deletionTimestamp = 2;
DistributionList distributionList = 3; DistributionList distributionList = 3;
@ -332,7 +338,7 @@ message DistributionListItem {
message DistributionList { message DistributionList {
enum PrivacyMode { enum PrivacyMode {
UNKNOWN = 0; UNKNOWN = 0; // Interpret as "Only with"
ONLY_WITH = 1; ONLY_WITH = 1;
ALL_EXCEPT = 2; ALL_EXCEPT = 2;
ALL = 3; ALL = 3;
@ -367,12 +373,14 @@ message ChatItem {
repeated ChatItem revisions = 6; // ordered from oldest to newest repeated ChatItem revisions = 6; // ordered from oldest to newest
bool sms = 7; bool sms = 7;
// If unset, importers should skip this item without throwing an error.
oneof directionalDetails { oneof directionalDetails {
IncomingMessageDetails incoming = 8; IncomingMessageDetails incoming = 8;
OutgoingMessageDetails outgoing = 9; OutgoingMessageDetails outgoing = 9;
DirectionlessMessageDetails directionless = 10; DirectionlessMessageDetails directionless = 10;
} }
// If unset, importers should skip this item without throwing an error.
oneof item { oneof item {
StandardMessage standardMessage = 11; StandardMessage standardMessage = 11;
ContactMessage contactMessage = 12; ContactMessage contactMessage = 12;
@ -421,6 +429,7 @@ message SendStatus {
uint64 recipientId = 1; 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 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 { oneof deliveryStatus {
Pending pending = 3; Pending pending = 3;
Sent sent = 4; Sent sent = 4;
@ -457,6 +466,7 @@ message DirectStoryReplyMessage {
FilePointer longText = 2; FilePointer longText = 2;
} }
// If unset, importers should ignore the message without throwing an error.
oneof reply { oneof reply {
TextReply textReply = 1; TextReply textReply = 1;
string emoji = 2; string emoji = 2;
@ -475,7 +485,7 @@ message PaymentNotification {
message FailedTransaction { // Failed payments can't be synced from the ledger message FailedTransaction { // Failed payments can't be synced from the ledger
enum FailureReason { enum FailureReason {
GENERIC = 0; GENERIC = 0; // A valid value -- reason unknown
NETWORK = 1; NETWORK = 1;
INSUFFICIENT_FUNDS = 2; INSUFFICIENT_FUNDS = 2;
} }
@ -484,7 +494,7 @@ message PaymentNotification {
message Transaction { message Transaction {
enum Status { enum Status {
INITIAL = 0; INITIAL = 0; // A valid value -- state unconfirmed
SUBMITTED = 1; SUBMITTED = 1;
SUCCESSFUL = 2; SUCCESSFUL = 2;
} }
@ -501,6 +511,7 @@ message PaymentNotification {
optional bytes receipt = 7; // mobile coin blobs optional bytes receipt = 7; // mobile coin blobs
} }
// If unset, importers should treat the transaction as successful with no metadata.
oneof payment { oneof payment {
Transaction transaction = 1; Transaction transaction = 1;
FailedTransaction failedTransaction = 2; FailedTransaction failedTransaction = 2;
@ -515,7 +526,7 @@ message PaymentNotification {
message GiftBadge { message GiftBadge {
enum State { enum State {
UNOPENED = 0; UNOPENED = 0; // A valid state
OPENED = 1; OPENED = 1;
REDEEMED = 2; REDEEMED = 2;
FAILED = 3; FAILED = 3;
@ -543,7 +554,7 @@ message ContactAttachment {
message Phone { message Phone {
enum Type { enum Type {
UNKNOWN = 0; UNKNOWN = 0; // Interpret as "Home"
HOME = 1; HOME = 1;
MOBILE = 2; MOBILE = 2;
WORK = 3; WORK = 3;
@ -557,7 +568,7 @@ message ContactAttachment {
message Email { message Email {
enum Type { enum Type {
UNKNOWN = 0; UNKNOWN = 0; // Intepret as "Home"
HOME = 1; HOME = 1;
MOBILE = 2; MOBILE = 2;
WORK = 3; WORK = 3;
@ -571,7 +582,7 @@ message ContactAttachment {
message PostalAddress { message PostalAddress {
enum Type { enum Type {
UNKNOWN = 0; UNKNOWN = 0; // Interpret as "Home"
HOME = 1; HOME = 1;
WORK = 2; WORK = 2;
CUSTOM = 3; CUSTOM = 3;
@ -631,7 +642,7 @@ message MessageAttachment {
// but explicitly mutually exclusive. Note the different raw values // but explicitly mutually exclusive. Note the different raw values
// (non-zero starting values are not supported in proto3.) // (non-zero starting values are not supported in proto3.)
enum Flag { enum Flag {
NONE = 0; NONE = 0; // A valid value -- no flag applied
VOICE_MESSAGE = 1; VOICE_MESSAGE = 1;
BORDERLESS = 2; BORDERLESS = 2;
GIF = 3; GIF = 3;
@ -682,6 +693,7 @@ message FilePointer {
message InvalidAttachmentLocator { message InvalidAttachmentLocator {
} }
// If unset, importers should consider it to be an InvalidAttachmentLocator without throwing an error.
oneof locator { oneof locator {
BackupLocator backupLocator = 1; BackupLocator backupLocator = 1;
AttachmentLocator attachmentLocator = 2; AttachmentLocator attachmentLocator = 2;
@ -700,7 +712,7 @@ message FilePointer {
message Quote { message Quote {
enum Type { enum Type {
UNKNOWN = 0; UNKNOWN = 0; // Interpret as "Normal"
NORMAL = 1; NORMAL = 1;
GIFT_BADGE = 2; GIFT_BADGE = 2;
VIEW_ONCE = 3; VIEW_ONCE = 3;
@ -721,7 +733,7 @@ message Quote {
message BodyRange { message BodyRange {
enum Style { enum Style {
NONE = 0; NONE = 0; // Importers should ignore the body range without throwing an error.
BOLD = 1; BOLD = 1;
ITALIC = 2; ITALIC = 2;
SPOILER = 3; SPOILER = 3;
@ -734,6 +746,7 @@ message BodyRange {
uint32 start = 1; uint32 start = 1;
uint32 length = 2; uint32 length = 2;
// If unset, importers should ignore the body range without throwing an error.
oneof associatedValue { oneof associatedValue {
bytes mentionAci = 3; bytes mentionAci = 3;
Style style = 4; Style style = 4;
@ -750,6 +763,7 @@ message Reaction {
} }
message ChatUpdateMessage { message ChatUpdateMessage {
// If unset, importers should ignore the update message without throwing an error.
oneof update { oneof update {
SimpleChatUpdate simpleUpdate = 1; SimpleChatUpdate simpleUpdate = 1;
GroupChangeChatUpdate groupChange = 2; GroupChangeChatUpdate groupChange = 2;
@ -765,19 +779,19 @@ message ChatUpdateMessage {
message IndividualCall { message IndividualCall {
enum Type { enum Type {
UNKNOWN_TYPE = 0; UNKNOWN_TYPE = 0; // Interpret as "Audio call"
AUDIO_CALL = 1; AUDIO_CALL = 1;
VIDEO_CALL = 2; VIDEO_CALL = 2;
} }
enum Direction { enum Direction {
UNKNOWN_DIRECTION = 0; UNKNOWN_DIRECTION = 0; // Interpret as "Incoming"
INCOMING = 1; INCOMING = 1;
OUTGOING = 2; OUTGOING = 2;
} }
enum State { enum State {
UNKNOWN_STATE = 0; UNKNOWN_STATE = 0; // Interpret as "Accepted"
ACCEPTED = 1; ACCEPTED = 1;
NOT_ACCEPTED = 2; NOT_ACCEPTED = 2;
// An incoming call that is no longer ongoing, which we neither accepted // An incoming call that is no longer ongoing, which we neither accepted
@ -798,7 +812,7 @@ message IndividualCall {
message GroupCall { message GroupCall {
enum State { enum State {
UNKNOWN_STATE = 0; UNKNOWN_STATE = 0; // Interpret as "Generic"
// A group call was started without ringing. // A group call was started without ringing.
GENERIC = 1; GENERIC = 1;
// We joined a group call that was started without ringing. // We joined a group call that was started without ringing.
@ -829,7 +843,7 @@ message GroupCall {
message SimpleChatUpdate { message SimpleChatUpdate {
enum Type { enum Type {
UNKNOWN = 0; UNKNOWN = 0; // Importers should skip the update without throwing an error.
JOINED_SIGNAL = 1; JOINED_SIGNAL = 1;
IDENTITY_UPDATE = 2; IDENTITY_UPDATE = 2;
IDENTITY_VERIFIED = 3; IDENTITY_VERIFIED = 3;
@ -863,6 +877,7 @@ message ProfileChangeChatUpdate {
} }
message LearnedProfileChatUpdate { message LearnedProfileChatUpdate {
// If unset, importers should consider the previous name to be an empty string.
oneof previousName { oneof previousName {
uint64 e164 = 1; uint64 e164 = 1;
string username = 2; string username = 2;
@ -879,6 +894,7 @@ message SessionSwitchoverChatUpdate {
message GroupChangeChatUpdate { message GroupChangeChatUpdate {
message Update { message Update {
// If unset, importers should consider it to be a GenericGroupUpdate with unset updaterAci
oneof update { oneof update {
GenericGroupUpdate genericGroupUpdate = 1; GenericGroupUpdate genericGroupUpdate = 1;
GroupCreationUpdate groupCreationUpdate = 2; GroupCreationUpdate groupCreationUpdate = 2;
@ -948,7 +964,7 @@ message GroupDescriptionUpdate {
} }
enum GroupV2AccessLevel { enum GroupV2AccessLevel {
UNKNOWN = 0; UNKNOWN = 0; // Interpret as "Unsatisfiable"
ANY = 1; ANY = 1;
MEMBER = 2; MEMBER = 2;
ADMINISTRATOR = 3; ADMINISTRATOR = 3;
@ -1138,6 +1154,7 @@ message ChatStyle {
message CustomChatColor { message CustomChatColor {
uint64 id = 1; uint64 id = 1;
// If unset, use the default chat color
oneof color { oneof color {
fixed32 solid = 2; // 0xAARRGGBB fixed32 solid = 2; // 0xAARRGGBB
Gradient gradient = 3; Gradient gradient = 3;
@ -1148,7 +1165,7 @@ message ChatStyle {
} }
enum WallpaperPreset { enum WallpaperPreset {
UNKNOWN_WALLPAPER_PRESET = 0; UNKNOWN_WALLPAPER_PRESET = 0; // Interpret as the wallpaper being unset
SOLID_BLUSH = 1; SOLID_BLUSH = 1;
SOLID_COPPER = 2; SOLID_COPPER = 2;
SOLID_DUST = 3; SOLID_DUST = 3;
@ -1173,7 +1190,7 @@ message ChatStyle {
} }
enum BubbleColorPreset { 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_ULTRAMARINE = 1;
SOLID_CRIMSON = 2; SOLID_CRIMSON = 2;
SOLID_VERMILION = 3; SOLID_VERMILION = 3;
@ -1198,6 +1215,7 @@ message ChatStyle {
GRADIENT_TANGERINE = 22; GRADIENT_TANGERINE = 22;
} }
// If unset, importers should consider there to be no wallpaper.
oneof wallpaper { oneof wallpaper {
WallpaperPreset wallpaperPreset = 1; WallpaperPreset wallpaperPreset = 1;
// This `FilePointer` is expected not to contain a `fileName`, `width`, // This `FilePointer` is expected not to contain a `fileName`, `width`,
@ -1205,6 +1223,7 @@ message ChatStyle {
FilePointer wallpaperPhoto = 2; FilePointer wallpaperPhoto = 2;
} }
// If unset, importers should consider it to be AutomaticBubbleColor
oneof bubbleColor { oneof bubbleColor {
// Bubble setting is automatically determined based on the wallpaper setting, // Bubble setting is automatically determined based on the wallpaper setting,
// or `SOLID_ULTRAMARINE` for `noWallpaper` // or `SOLID_ULTRAMARINE` for `noWallpaper`
@ -1220,7 +1239,7 @@ message ChatStyle {
message NotificationProfile { message NotificationProfile {
enum DayOfWeek { enum DayOfWeek {
UNKNOWN = 0; UNKNOWN = 0; // Interpret as "Monday"
MONDAY = 1; MONDAY = 1;
TUESDAY = 2; TUESDAY = 2;
WEDNESDAY = 3; WEDNESDAY = 3;
@ -1246,7 +1265,7 @@ message NotificationProfile {
message ChatFolder { message ChatFolder {
// Represents the default "All chats" folder record vs all other custom folders // Represents the default "All chats" folder record vs all other custom folders
enum FolderType { enum FolderType {
UNKNOWN = 0; UNKNOWN = 0; // Interpret as "Custom"
ALL = 1; ALL = 1;
CUSTOM = 2; CUSTOM = 2;
} }

View file

@ -480,8 +480,8 @@ export class BackupImportStream extends Writable {
// Not a conversation // Not a conversation
return; return;
} else { } else {
log.warn(`${this.#logId}: unsupported recipient item`); log.warn(`${this.#logId}: unsupported recipient destination`);
return; throw new Error('Unsupported recipient destination');
} }
if (convo !== this.#ourConversation) { if (convo !== this.#ourConversation) {
@ -505,6 +505,7 @@ export class BackupImportStream extends Writable {
await this.#fromAdHocCall(frame.adHocCall); await this.#fromAdHocCall(frame.adHocCall);
} else { } else {
log.warn(`${this.#logId}: unsupported frame item ${frame.item}`); log.warn(`${this.#logId}: unsupported frame item ${frame.item}`);
throw new Error('Unsupported frame type');
} }
} catch (error) { } catch (error) {
this.#frameErrorCount += 1; this.#frameErrorCount += 1;
@ -940,11 +941,12 @@ export class BackupImportStream extends Writable {
); );
attrs.discoveredUnregisteredAt = timestamp || this.#now; attrs.discoveredUnregisteredAt = timestamp || this.#now;
attrs.firstUnregisteredAt = timestamp || undefined; attrs.firstUnregisteredAt = timestamp || undefined;
} else { } else if (!contact.registered) {
strictAssert( log.error(
contact.registered, contact.registered,
'contact is either registered or unregistered' 'contact is neither registered nor unregistered; treating as registered'
); );
this.#frameErrorCount += 1;
} }
if (contact.blocked) { if (contact.blocked) {
@ -1649,7 +1651,12 @@ export class BackupImportStream extends Writable {
} else if (status.skipped) { } else if (status.skipped) {
sendStatus = SendStatus.Skipped; sendStatus = SendStatus.Skipped;
} else { } 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] = { sendStateByConversationId[target.id] = {
@ -1896,6 +1903,10 @@ export class BackupImportStream extends Writable {
targetAuthorAci: storyAuthorAci, targetAuthorAci: storyAuthorAci,
targetTimestamp: 0, // stories are never imported targetTimestamp: 0, // stories are never imported
}; };
} else {
throw new Error(
'Direct story reply message missing both textReply and emoji'
);
} }
return result; return result;
@ -2413,10 +2424,12 @@ export class BackupImportStream extends Writable {
if (updateMessage.learnedProfileChange) { if (updateMessage.learnedProfileChange) {
const { e164, username } = updateMessage.learnedProfileChange; const { e164, username } = updateMessage.learnedProfileChange;
strictAssert( if (e164 == null && username == null) {
e164 != null || username != null, log.error(
'learnedProfileChange must have an old name' `${options.timestamp}: learnedProfileChange had no previous e164 or username`
); );
this.#frameErrorCount += 1;
}
return { return {
message: { message: {
type: 'title-transition-notification', type: 'title-transition-notification',
@ -3392,7 +3405,7 @@ export class BackupImportStream extends Writable {
value = { value = {
start: rgbIntToDesktopHSL(color.solid), start: rgbIntToDesktopHSL(color.solid),
}; };
} else { } else if (color.gradient) {
strictAssert(color.gradient != null, 'Either solid or gradient'); strictAssert(color.gradient != null, 'Either solid or gradient');
strictAssert(color.gradient.colors != null, 'Missing gradient colors'); strictAssert(color.gradient.colors != null, 'Missing gradient colors');
@ -3409,6 +3422,12 @@ export class BackupImportStream extends Writable {
end: rgbIntToDesktopHSL(end), end: rgbIntToDesktopHSL(end),
deg, deg,
}; };
} else {
log.error(
'CustomChatColor missing both solid and gradient fields, dropping'
);
this.#frameErrorCount += 1;
continue;
} }
customColors.colors[uuid] = value; customColors.colors[uuid] = value;
@ -3532,16 +3551,23 @@ export class BackupImportStream extends Writable {
color = 'ultramarine'; color = 'ultramarine';
break; break;
} }
} else { } else if (chatStyle.customColorId != null) {
strictAssert(chatStyle.customColorId != null, 'Missing custom color id');
const entry = this.#customColorById.get( const entry = this.#customColorById.get(
chatStyle.customColorId.toNumber() chatStyle.customColorId.toNumber()
); );
strictAssert(entry != null, 'Missing custom color');
if (entry) {
color = 'custom'; color = 'custom';
customColorData = entry; 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 { return {

View file

@ -120,15 +120,15 @@ export function convertFilePointerToAttachment(
}; };
} }
if (invalidAttachmentLocator) { if (!invalidAttachmentLocator) {
log.error('convertFilePointerToAttachment: filePointer had no locator');
}
return { return {
...omit(commonProps, 'downloadPath'), ...omit(commonProps, 'downloadPath'),
error: true, error: true,
size: 0, size: 0,
}; };
}
throw new Error('convertFilePointerToAttachment: mising locator');
} }
export function convertBackupMessageAttachmentToAttachment( export function convertBackupMessageAttachmentToAttachment(