diff --git a/protos/Backups.proto b/protos/Backups.proto index 261a8c202296..06cd4f7e760b 100644 --- a/protos/Backups.proto +++ b/protos/Backups.proto @@ -77,6 +77,7 @@ message AccountData { bool hasSeenGroupStoryEducationSheet = 15; bool hasCompletedUsernameOnboarding = 16; PhoneNumberSharingMode phoneNumberSharingMode = 17; + ChatStyle defaultChatStyle = 18; } bytes profileKey = 1; @@ -228,7 +229,7 @@ message Chat { uint64 muteUntilMs = 6; bool markedUnread = 7; bool dontNotifyForMentionsIfMuted = 8; - FilePointer wallpaper = 9; + ChatStyle style = 9; } /** @@ -725,6 +726,7 @@ message SimpleChatUpdate { BAD_DECRYPT = 9; PAYMENTS_ACTIVATED = 10; PAYMENT_ACTIVATION_REQUEST = 11; + UNSUPPORTED_PROTOCOL_MESSAGE = 12; } Type type = 1; @@ -1008,3 +1010,79 @@ message StickerPackSticker { string emoji = 1; uint32 id = 2; } + + +message ChatStyle { + message Gradient { + uint32 angle = 1; // degrees + repeated uint32 colors = 2; + repeated float positions = 3; // percent from 0 to 1 + } + + message AutomaticBubbleColor { + } + + enum WallpaperPreset { + UNKNOWN_WALLPAPER_PRESET = 0; + SOLID_BLUSH = 1; + SOLID_COPPER = 2; + SOLID_DUST = 3; + SOLID_CELADON = 4; + SOLID_RAINFOREST = 5; + SOLID_PACIFIC = 6; + SOLID_FROST = 7; + SOLID_NAVY = 8; + SOLID_LILAC = 9; + SOLID_PINK = 10; + SOLID_EGGPLANT = 11; + SOLID_SILVER = 12; + GRADIENT_SUNSET = 13; + GRADIENT_NOIR = 14; + GRADIENT_HEATMAP = 15; + GRADIENT_AQUA = 16; + GRADIENT_IRIDESCENT = 17; + GRADIENT_MONSTERA = 18; + GRADIENT_BLISS = 19; + GRADIENT_SKY = 20; + GRADIENT_PEACH = 21; + } + + enum BubbleColorPreset { + UNKNOWN_BUBBLE_COLOR_PRESET = 0; + SOLID_ULTRAMARINE = 1; + SOLID_CRIMSON = 2; + SOLID_VERMILION = 3; + SOLID_BURLAP = 4; + SOLID_FOREST = 5; + SOLID_WINTERGREEN = 6; + SOLID_TEAL = 7; + SOLID_BLUE = 8; + SOLID_INDIGO = 9; + SOLID_VIOLET = 10; + SOLID_PLUM = 11; + SOLID_TAUPE = 12; + SOLID_STEEL = 13; + GRADIENT_EMBER = 14; + GRADIENT_MIDNIGHT = 15; + GRADIENT_INFRARED = 16; + GRADIENT_LAGOON = 17; + GRADIENT_FLUORESCENT = 18; + GRADIENT_BASIL = 19; + GRADIENT_SUBLIME = 20; + GRADIENT_SEA = 21; + GRADIENT_TANGERINE = 22; + } + + oneof wallpaper { + WallpaperPreset wallpaperPreset = 1; + FilePointer wallpaperPhoto = 2; + } + + oneof bubbleColor { + BubbleColorPreset bubbleColorPreset = 3; + Gradient bubbleGradient = 4; + uint32 bubbleSolidColor = 5; + // Bubble setting is automatically determined based on the wallpaper setting. + AutomaticBubbleColor autoBubbleColor = 6; + } +} \ No newline at end of file diff --git a/ts/services/backups/export.ts b/ts/services/backups/export.ts index af6ae97355f5..2ac19a82df9f 100644 --- a/ts/services/backups/export.ts +++ b/ts/services/backups/export.ts @@ -620,12 +620,8 @@ export class BackupExportStream extends Readable { const isOutgoing = message.type === 'outgoing'; const isIncoming = message.type === 'incoming'; - if (isOutgoing) { - authorId = this.getOrPushPrivateRecipient({ - serviceId: aboutMe.aci, - }); - // Pacify typescript - } else if (message.sourceServiceId) { + // Pacify typescript + if (message.sourceServiceId) { authorId = this.getOrPushPrivateRecipient({ serviceId: message.sourceServiceId, e164: message.source, @@ -635,7 +631,15 @@ export class BackupExportStream extends Readable { serviceId: message.sourceServiceId, e164: message.source, }); + } else { + strictAssert(!isIncoming, 'Incoming message must have source'); + + // Author must be always present, even if we are directionless + authorId = this.getOrPushPrivateRecipient({ + serviceId: aboutMe.aci, + }); } + if (isOutgoing || isIncoming) { strictAssert(authorId, 'Incoming/outgoing messages require an author'); } @@ -1088,7 +1092,15 @@ export class BackupExportStream extends Readable { } if (isPhoneNumberDiscovery(message)) { - // TODO (DESKTOP-6964): need to add to protos + const e164 = message.phoneNumberDiscovery?.e164; + if (!e164) { + return { kind: NonBubbleResultKind.Drop }; + } + + updateMessage.sessionSwitchover = { + e164: Long.fromString(e164), + }; + return { kind: NonBubbleResultKind.Directionless, patch }; } if (isUniversalTimerNotification(message)) { @@ -1097,7 +1109,9 @@ export class BackupExportStream extends Readable { } if (isContactRemovedNotification(message)) { - // TODO (DESKTOP-6964): this doesn't appear to be in the protos at all + // Transient, drop it + // TODO: DESKTOP-7124 + return { kind: NonBubbleResultKind.Drop }; } if (isGiftBadge(message)) { @@ -1111,10 +1125,14 @@ export class BackupExportStream extends Readable { } if (isUnsupportedMessage(message)) { - // TODO (DESKTOP-6964): need to add to protos - } + const simpleUpdate = new Backups.SimpleChatUpdate(); + simpleUpdate.type = + Backups.SimpleChatUpdate.Type.UNSUPPORTED_PROTOCOL_MESSAGE; - // TODO (DESKTOP-6964): session switchover + updateMessage.simpleUpdate = simpleUpdate; + + return { kind: NonBubbleResultKind.Directed, patch }; + } if (isGroupV1Migration(message)) { const { groupMigration } = message; @@ -1177,7 +1195,7 @@ export class BackupExportStream extends Readable { updateMessage.simpleUpdate = simpleUpdate; - return { kind: NonBubbleResultKind.Directionless, patch }; + return { kind: NonBubbleResultKind.Directed, patch }; } if (isChatSessionRefreshed(message)) { diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts index 3f4729acc7f4..e9022633a287 100644 --- a/ts/services/backups/import.ts +++ b/ts/services/backups/import.ts @@ -671,12 +671,14 @@ export class BackupImportStream extends Writable { conversation.isArchived = chat.archived === true; conversation.isPinned = chat.pinnedOrder != null; - conversation.expireTimer = chat.expirationTimerMs - ? DurationInSeconds.fromMillis(chat.expirationTimerMs.toNumber()) - : undefined; - conversation.muteExpiresAt = chat.muteUntilMs - ? getTimestampFromLong(chat.muteUntilMs) - : undefined; + conversation.expireTimer = + chat.expirationTimerMs && !chat.expirationTimerMs.isZero() + ? DurationInSeconds.fromMillis(chat.expirationTimerMs.toNumber()) + : undefined; + conversation.muteExpiresAt = + chat.muteUntilMs && !chat.muteUntilMs.isZero() + ? getTimestampFromLong(chat.muteUntilMs) + : undefined; conversation.markedUnread = chat.markedUnread === true; conversation.dontNotifyForMentionsIfMuted = chat.dontNotifyForMentionsIfMuted === true; @@ -715,7 +717,6 @@ export class BackupImportStream extends Writable { let attributes: MessageAttributesType = { id: generateUuid(), - canReplyToStory: false, conversationId: chatConvo.id, received_at: incrementMessageCounter(), sent_at: timestamp, @@ -723,13 +724,14 @@ export class BackupImportStream extends Writable { sourceServiceId: authorConvo?.serviceId, timestamp, type: item.outgoing != null ? 'outgoing' : 'incoming', - unidentifiedDeliveryReceived: false, - expirationStartTimestamp: item.expireStartDate - ? getTimestampFromLong(item.expireStartDate) - : undefined, - expireTimer: item.expiresInMs - ? DurationInSeconds.fromMillis(item.expiresInMs.toNumber()) - : undefined, + expirationStartTimestamp: + item.expireStartDate && !item.expireStartDate.isZero() + ? getTimestampFromLong(item.expireStartDate) + : undefined, + expireTimer: + item.expiresInMs && !item.expiresInMs.isZero() + ? DurationInSeconds.fromMillis(item.expiresInMs.toNumber()) + : undefined, }; const additionalMessages: Array = []; @@ -1110,9 +1112,10 @@ export class BackupImportStream extends Writable { const { expiresInMs } = updateMessage.expirationTimerChange; const sourceServiceId = author?.serviceId ?? aboutMe.aci; - const expireTimer = isNumber(expiresInMs) - ? DurationInSeconds.fromMillis(expiresInMs) - : DurationInSeconds.fromSeconds(0); + const expireTimer = + isNumber(expiresInMs) && expiresInMs + ? DurationInSeconds.fromMillis(expiresInMs) + : DurationInSeconds.fromSeconds(0); return { message: { @@ -1178,8 +1181,21 @@ export class BackupImportStream extends Writable { }; } + if (updateMessage.sessionSwitchover) { + const { e164 } = updateMessage.sessionSwitchover; + strictAssert(e164 != null, 'sessionSwitchover must have an old e164'); + return { + message: { + type: 'phone-number-discovery', + phoneNumberDiscovery: { + e164: `+${e164}`, + }, + }, + additionalMessages: [], + }; + } + // TODO (DESKTOP-6964): check these fields - // updateMessage.sessionSwitchover // updateMessage.callingMessage return undefined; @@ -1703,9 +1719,10 @@ export class BackupImportStream extends Writable { ); } const sourceServiceId = fromAciObject(Aci.fromUuidBytes(updaterAci)); - const expireTimer = isNumber(expiresInMs) - ? DurationInSeconds.fromMillis(expiresInMs) - : undefined; + const expireTimer = + isNumber(expiresInMs) && expiresInMs + ? DurationInSeconds.fromMillis(expiresInMs) + : undefined; additionalMessages.push({ type: 'timer-notification', sourceServiceId, @@ -1841,6 +1858,13 @@ export class BackupImportStream extends Writable { kind: PaymentEventKind.ActivationRequest, }, }; + case Type.UNSUPPORTED_PROTOCOL_MESSAGE: + return { + supportedVersionAtReceive: + SignalService.DataMessage.ProtocolVersion.CURRENT - 2, + requiredProtocolVersion: + SignalService.DataMessage.ProtocolVersion.CURRENT - 1, + }; default: throw new Error('Not implemented'); } diff --git a/ts/test-electron/backup/backup_groupv2_notifications_test.ts b/ts/test-electron/backup/backup_groupv2_notifications_test.ts index 62c656f30611..87ea4d678fa2 100644 --- a/ts/test-electron/backup/backup_groupv2_notifications_test.ts +++ b/ts/test-electron/backup/backup_groupv2_notifications_test.ts @@ -7,6 +7,7 @@ import Data from '../../sql/Client'; import { SignalService as Proto } from '../../protobuf'; import { generateAci, generatePni } from '../../types/ServiceId'; +import type { ServiceIdString } from '../../types/ServiceId'; import type { MessageAttributesType } from '../../model-types'; import type { GroupV2ChangeType } from '../../groups'; import { getRandomBytes } from '../../Crypto'; @@ -42,7 +43,13 @@ let counter = 0; function createMessage( change: GroupV2ChangeType, - { disableIncrement }: { disableIncrement: boolean } = { + { + disableIncrement = false, + sourceServiceId = change.from || OUR_ACI, + }: { + disableIncrement?: boolean; + sourceServiceId?: ServiceIdString; + } = { disableIncrement: false, } ): MessageAttributesType { @@ -60,6 +67,7 @@ function createMessage( sent_at: counter, timestamp: counter, type: 'group-v2-change', + sourceServiceId, }; } @@ -695,16 +703,19 @@ describe('backup/groupv2/notifications', () => { }); it('MemberAddFromInvited items', async () => { - const firstBefore = createMessage({ - from: OUR_PNI, - details: [ - { - type: 'member-add-from-invite', - aci: OUR_ACI, - inviter: CONTACT_B, - }, - ], - }); + const firstBefore = createMessage( + { + from: OUR_PNI, + details: [ + { + type: 'member-add-from-invite', + aci: OUR_ACI, + inviter: CONTACT_B, + }, + ], + }, + { sourceServiceId: OUR_ACI } + ); const firstAfter = createMessage( { from: OUR_ACI, @@ -719,15 +730,18 @@ describe('backup/groupv2/notifications', () => { { disableIncrement: true } ); - const secondBefore = createMessage({ - from: OUR_PNI, - details: [ - { - type: 'member-add-from-invite', - aci: OUR_ACI, - }, - ], - }); + const secondBefore = createMessage( + { + from: OUR_PNI, + details: [ + { + type: 'member-add-from-invite', + aci: OUR_ACI, + }, + ], + }, + { sourceServiceId: OUR_ACI } + ); const secondAfter = createMessage( { from: OUR_ACI, @@ -741,15 +755,18 @@ describe('backup/groupv2/notifications', () => { { disableIncrement: true } ); - const thirdBefore = createMessage({ - from: CONTACT_A_PNI, - details: [ - { - type: 'member-add-from-invite', - aci: CONTACT_A, - }, - ], - }); + const thirdBefore = createMessage( + { + from: CONTACT_A_PNI, + details: [ + { + type: 'member-add-from-invite', + aci: CONTACT_A, + }, + ], + }, + { sourceServiceId: CONTACT_A } + ); const thirdAfter = createMessage( { from: CONTACT_A, @@ -763,16 +780,19 @@ describe('backup/groupv2/notifications', () => { { disableIncrement: true } ); - const fourthBefore = createMessage({ - from: CONTACT_A_PNI, - details: [ - { - type: 'member-add-from-invite', - aci: CONTACT_A, - pni: CONTACT_A_PNI, - }, - ], - }); + const fourthBefore = createMessage( + { + from: CONTACT_A_PNI, + details: [ + { + type: 'member-add-from-invite', + aci: CONTACT_A, + pni: CONTACT_A_PNI, + }, + ], + }, + { sourceServiceId: CONTACT_A } + ); const fourthAfter = createMessage( { from: CONTACT_A, @@ -829,14 +849,17 @@ describe('backup/groupv2/notifications', () => { it('MemberAddFromLink items asymmetric', async () => { const before: Array = [ - createMessage({ - details: [ - { - type: 'member-add-from-link', - aci: CONTACT_A, - }, - ], - }), + createMessage( + { + details: [ + { + type: 'member-add-from-link', + aci: CONTACT_A, + }, + ], + }, + { sourceServiceId: CONTACT_A } + ), ]; const after: Array = [ createMessage( @@ -2008,6 +2031,7 @@ describe('backup/groupv2/notifications', () => { received_at: counter, sent_at: counter, timestamp: counter, + sourceServiceId: CONTACT_A, }; counter += 1; @@ -2023,6 +2047,7 @@ describe('backup/groupv2/notifications', () => { received_at: counter, sent_at: counter, timestamp: counter, + sourceServiceId: CONTACT_A, }; const messages: Array = [ @@ -2099,6 +2124,7 @@ describe('backup/groupv2/notifications', () => { received_at: counter, sent_at: counter, timestamp: counter, + sourceServiceId: OUR_ACI, }; counter += 1; @@ -2114,6 +2140,7 @@ describe('backup/groupv2/notifications', () => { received_at: counter, sent_at: counter, timestamp: counter, + sourceServiceId: OUR_ACI, }; counter += 1; @@ -2129,6 +2156,7 @@ describe('backup/groupv2/notifications', () => { received_at: counter, sent_at: counter, timestamp: counter, + sourceServiceId: OUR_ACI, }; const messages: Array = [ @@ -2160,6 +2188,7 @@ describe('backup/groupv2/notifications', () => { received_at: counter, sent_at: counter, timestamp: counter, + sourceServiceId: OUR_ACI, }; const legacyAfter = { id: generateGuid(), @@ -2173,6 +2202,7 @@ describe('backup/groupv2/notifications', () => { received_at: counter, sent_at: counter, timestamp: counter, + sourceServiceId: OUR_ACI, }; counter += 1; @@ -2191,6 +2221,7 @@ describe('backup/groupv2/notifications', () => { received_at: counter, sent_at: counter, timestamp: counter, + sourceServiceId: OUR_ACI, }; const allDataAfter = { id: generateGuid(), @@ -2204,6 +2235,7 @@ describe('backup/groupv2/notifications', () => { received_at: counter, sent_at: counter, timestamp: counter, + sourceServiceId: OUR_ACI, }; const before = [legacyBefore, allDataBefore]; diff --git a/ts/test-electron/backup/helpers.ts b/ts/test-electron/backup/helpers.ts index e15b3bf03e1e..e64a87444425 100644 --- a/ts/test-electron/backup/helpers.ts +++ b/ts/test-electron/backup/helpers.ts @@ -4,13 +4,18 @@ import { assert } from 'chai'; import path from 'path'; import { tmpdir } from 'os'; -import { pick, sortBy } from 'lodash'; +import { sortBy } from 'lodash'; import { createReadStream } from 'fs'; import { mkdtemp, rm } from 'fs/promises'; import type { MessageAttributesType } from '../../model-types'; +import type { + SendStateByConversationId, + SendState, +} from '../../messages/MessageSendState'; import { backupsService } from '../../services/backups'; +import { isUnsupportedMessage } from '../../state/selectors/message'; import { generateAci, generatePni } from '../../types/ServiceId'; import Data from '../../sql/Client'; import { getRandomBytes } from '../../Crypto'; @@ -37,37 +42,67 @@ function sortAndNormalize( messages: Array ): Array { return sortBy(messages, 'sent_at').map(message => { - const shallow = pick( - message, - 'contact', - 'conversationMerge', - 'droppedGV2MemberIds', - 'expirationTimerUpdate', - 'flags', - 'groupMigration', - 'groupV2Change', - 'invitedGV2Members', - 'isErased', - 'payment', - 'profileChange', - 'sent_at', - 'sticker', - 'timestamp', - 'type', - 'verified' - ); + const { + changedId, + conversationId, + editHistory, + key_changed: keyChanged, + reactions, + sendStateByConversationId, + verifiedChanged, + + // This is not in the backup + // eslint-disable-next-line @typescript-eslint/no-unused-vars + id: _id, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + received_at: _receivedAt, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + sourceDevice: _sourceDevice, + + ...rest + } = message; + + function mapSendState( + sendState?: SendStateByConversationId + ): SendStateByConversationId | undefined { + if (sendState == null) { + return undefined; + } + + const result: Record = {}; + for (const [id, state] of Object.entries(sendState)) { + result[mapConvoId(id) ?? id] = state; + } + return result; + } return { - ...shallow, - reactions: message.reactions?.map(({ fromId, ...rest }) => { + ...rest, + conversationId: mapConvoId(conversationId), + reactions: reactions?.map(({ fromId, ...restOfReaction }) => { return { from: mapConvoId(fromId), - ...rest, + ...restOfReaction, }; }), - changedId: mapConvoId(message.changedId), - key_changed: mapConvoId(message.key_changed), - verifiedChanged: mapConvoId(message.verifiedChanged), + changedId: mapConvoId(changedId), + key_changed: mapConvoId(keyChanged), + verifiedChanged: mapConvoId(verifiedChanged), + sendStateByConverationId: mapSendState(sendStateByConversationId), + editHistory: editHistory?.map(history => { + const { + sendStateByConversationId: historySendState, + ...restOfHistory + } = history; + + return { + ...restOfHistory, + sendStateByConversationId: mapSendState(historySendState), + }; + }), + + // Not an original property, but useful + isUnsupported: isUnsupportedMessage(message), }; }); } @@ -83,7 +118,7 @@ async function updateConvoIdToTitle() { for (const convo of all) { CONVO_ID_TO_STABLE_ID.set( convo.id, - convo.serviceId ?? convo.e164 ?? convo.id + convo.serviceId ?? convo.e164 ?? convo.masterKey ?? convo.id ); } } diff --git a/ts/test-electron/backup/non_bubble_test.ts b/ts/test-electron/backup/non_bubble_test.ts index d3c492304c32..b40e5bf02842 100644 --- a/ts/test-electron/backup/non_bubble_test.ts +++ b/ts/test-electron/backup/non_bubble_test.ts @@ -14,8 +14,10 @@ import { generateAci } from '../../types/ServiceId'; import { PaymentEventKind } from '../../types/Payment'; import { ContactFormType } from '../../types/EmbeddedContact'; import { DurationInSeconds } from '../../util/durations'; +import { ReadStatus } from '../../messages/MessageReadStatus'; +import { SeenStatus } from '../../MessageSeenStatus'; import { loadCallsHistory } from '../../services/callHistoryLoader'; -import { setupBasics, symmetricRoundtripHarness } from './helpers'; +import { setupBasics, symmetricRoundtripHarness, OUR_ACI } from './helpers'; const CONTACT_A = generateAci(); const GROUP_ID = Bytes.toBase64(getRandomBytes(32)); @@ -57,10 +59,13 @@ describe('backup/non-bubble messages', () => { id: generateGuid(), type: 'incoming', received_at: 1, + received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, + readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, flags: Proto.DataMessage.Flags.END_SESSION, }, ]); @@ -75,6 +80,7 @@ describe('backup/non-bubble messages', () => { received_at: 1, sent_at: 1, timestamp: 1, + sourceServiceId: OUR_ACI, }, ]); }); @@ -88,6 +94,7 @@ describe('backup/non-bubble messages', () => { received_at: 1, sent_at: 1, timestamp: 1, + sourceServiceId: CONTACT_A, }, ]); }); @@ -102,6 +109,7 @@ describe('backup/non-bubble messages', () => { received_at: 1, sent_at: 1, timestamp: 1, + sourceServiceId: CONTACT_A, }, ]); }); @@ -117,6 +125,7 @@ describe('backup/non-bubble messages', () => { received_at: 1, sent_at: 1, timestamp: 1, + sourceServiceId: CONTACT_A, }, ]); }); @@ -132,6 +141,7 @@ describe('backup/non-bubble messages', () => { received_at: 1, sent_at: 1, timestamp: 1, + sourceServiceId: CONTACT_A, }, ]); }); @@ -142,10 +152,10 @@ describe('backup/non-bubble messages', () => { conversationId: contactA.id, id: generateGuid(), type: 'change-number-notification', - sourceServiceId: CONTACT_A, received_at: 1, sent_at: 1, timestamp: 1, + sourceServiceId: CONTACT_A, }, ]); }); @@ -159,6 +169,7 @@ describe('backup/non-bubble messages', () => { received_at: 1, sent_at: 1, timestamp: 1, + sourceServiceId: CONTACT_A, }, ]); }); @@ -169,10 +180,10 @@ describe('backup/non-bubble messages', () => { conversationId: contactA.id, id: generateGuid(), type: 'delivery-issue', - sourceServiceId: CONTACT_A, received_at: 1, sent_at: 1, timestamp: 1, + sourceServiceId: CONTACT_A, }, ]); }); @@ -188,8 +199,11 @@ describe('backup/non-bubble messages', () => { kind: PaymentEventKind.Activation, }, received_at: 1, + received_at_ms: 1, sent_at: 1, timestamp: 1, + readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, }, ]); }); @@ -205,8 +219,11 @@ describe('backup/non-bubble messages', () => { kind: PaymentEventKind.ActivationRequest, }, received_at: 1, + received_at_ms: 1, sent_at: 1, timestamp: 1, + readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, }, ]); }); @@ -219,10 +236,13 @@ describe('backup/non-bubble messages', () => { id: generateGuid(), type: 'incoming', received_at: 1, + received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, + readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, payment: { kind: PaymentEventKind.Notification, note: 'note with text', @@ -239,10 +259,13 @@ describe('backup/non-bubble messages', () => { id: generateGuid(), type: 'incoming', received_at: 1, + received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, + readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, payment: { kind: PaymentEventKind.Notification, note: 'note with text', @@ -267,10 +290,13 @@ describe('backup/non-bubble messages', () => { id: generateGuid(), type: 'incoming', received_at: 1, + received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, + readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, contact: [ { name: { @@ -306,10 +332,13 @@ describe('backup/non-bubble messages', () => { id: generateGuid(), type: 'incoming', received_at: 1, + received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, + readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, // TODO (DESKTOP-6845): properly handle data FilePointer sticker: { emoji: '👍', @@ -337,10 +366,13 @@ describe('backup/non-bubble messages', () => { id: generateGuid(), type: 'incoming', received_at: 1, + received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, + readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, isErased: true, }, ]); @@ -375,9 +407,8 @@ describe('backup/non-bubble messages', () => { received_at: 1, sent_at: 1, timestamp: 1, - sourceServiceId: CONTACT_A, - sourceDevice: 1, changedId: contactA.id, + sourceServiceId: CONTACT_A, profileChange: { type: 'name', oldName: 'Old Name', @@ -393,11 +424,10 @@ describe('backup/non-bubble messages', () => { conversationId: contactA.id, id: generateGuid(), type: 'conversation-merge', + sourceServiceId: CONTACT_A, received_at: 1, sent_at: 1, timestamp: 1, - sourceServiceId: CONTACT_A, - sourceDevice: 1, conversationMerge: { renderInfo: { type: 'private', @@ -407,4 +437,42 @@ describe('backup/non-bubble messages', () => { }, ]); }); + + it('roundtrips session switchover', async () => { + await symmetricRoundtripHarness([ + { + conversationId: contactA.id, + id: generateGuid(), + type: 'phone-number-discovery', + received_at: 1, + sent_at: 1, + timestamp: 1, + sourceServiceId: CONTACT_A, + phoneNumberDiscovery: { + e164: '+12125551234', + }, + }, + ]); + }); + + // TODO: DESKTOP-7122 + it.skip('roundtrips unsupported message', async () => { + await symmetricRoundtripHarness([ + { + conversationId: contactA.id, + id: generateGuid(), + type: 'incoming', + received_at: 1, + received_at_ms: 1, + sourceServiceId: CONTACT_A, + sourceDevice: 1, + sent_at: 1, + timestamp: 1, + readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, + supportedVersionAtReceive: 1, + requiredProtocolVersion: 2, + }, + ]); + }); }); diff --git a/ts/windows/main/preload_test.ts b/ts/windows/main/preload_test.ts index ee5d64205935..1831e93cff84 100644 --- a/ts/windows/main/preload_test.ts +++ b/ts/windows/main/preload_test.ts @@ -5,9 +5,12 @@ import { ipcRenderer as ipc } from 'electron'; import { sync } from 'fast-glob'; +import { inspect } from 'util'; // eslint-disable-next-line import/no-extraneous-dependencies -import { assert } from 'chai'; +import { assert, config as chaiConfig } from 'chai'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { reporters } from 'mocha'; import { getSignalProtocolStore } from '../../SignalProtocolStore'; import { initMessageCleanup } from '../../services/messageStateCleanup'; @@ -16,6 +19,28 @@ import { initializeRedux } from '../../state/initializeRedux'; import * as Stickers from '../../types/Stickers'; import { ThemeType } from '../../types/Util'; +// Show actual objects instead of abbreviated errors +chaiConfig.truncateThreshold = 0; + +function patchDeepEqual(method: 'deepEqual' | 'deepStrictEqual'): void { + const originalFn = assert[method]; + assert[method] = (...args) => { + try { + return originalFn(...args); + } catch (error) { + reporters.base.useColors = false; + error.message = reporters.base.generateDiff( + inspect(error.actual, { depth: Infinity, sorted: true }), + inspect(error.expected, { depth: Infinity, sorted: true }) + ); + throw error; + } + }; +} + +patchDeepEqual('deepEqual'); +patchDeepEqual('deepStrictEqual'); + window.assert = assert; // This is a hack to let us run TypeScript tests in the renderer process. See the