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,
// 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;
}

View file

@ -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 {

View file

@ -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(