diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b374c8a6..5b93dbe33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,7 +194,7 @@ jobs: uses: actions/checkout@v4 with: repository: 'signalapp/Signal-Message-Backup-Tests' - ref: '996daf691d4162ce854845dc883c62adb1a3fe55' + ref: 'c84390838f65c6c0927c9bbcc3a240f986fc4a80' path: 'backup-integration-tests' - run: xvfb-run --auto-servernum npm run test-electron diff --git a/protos/Backups.proto b/protos/Backups.proto index 03bcaf948..4facb2cfa 100644 --- a/protos/Backups.proto +++ b/protos/Backups.proto @@ -342,6 +342,7 @@ message ChatItem { ChatUpdateMessage updateMessage = 15; PaymentNotification paymentNotification = 16; GiftBadge giftBadge = 17; + ViewOnceMessage viewOnceMessage = 18; } } @@ -470,6 +471,12 @@ message GiftBadge { State state = 2; } +message ViewOnceMessage { + // Will be null for viewed messages + MessageAttachment attachment = 1; + repeated Reaction reactions = 2; +} + message ContactAttachment { message Name { optional string givenName = 1; diff --git a/ts/services/backups/export.ts b/ts/services/backups/export.ts index df128c7a9..b96cf5be4 100644 --- a/ts/services/backups/export.ts +++ b/ts/services/backups/export.ts @@ -77,6 +77,7 @@ import { isNormalBubble, isPhoneNumberDiscovery, isProfileChange, + isTapToView, isUniversalTimerNotification, isUnsupportedMessage, isVerifiedChange, @@ -1060,7 +1061,12 @@ export class BackupExportStream extends Readable { } const { contact, sticker } = message; - if (message.isErased) { + if (isTapToView(message)) { + result.viewOnceMessage = await this.toViewOnceMessage({ + message, + backupLevel, + }); + } else if (message.isErased) { result.remoteDeletedMessage = {}; } else if (messageHasPaymentEvent(message)) { const { payment } = message; @@ -2265,7 +2271,7 @@ export class BackupExportStream extends Readable { serverTimestamp != null ? getSafeLongFromTimestamp(serverTimestamp) : null, - read: readStatus === ReadStatus.Read, + read: readStatus === ReadStatus.Read || readStatus === ReadStatus.Viewed, sealedSender: unidentifiedDeliveryReceived === true, }; } @@ -2443,6 +2449,30 @@ export class BackupExportStream extends Readable { }; } + private async toViewOnceMessage({ + message, + backupLevel, + }: { + message: Pick< + MessageAttributesType, + 'attachments' | 'received_at' | 'reactions' + >; + backupLevel: BackupLevel; + }): Promise { + const attachment = message.attachments?.at(0); + return { + attachment: + attachment == null + ? null + : await this.processMessageAttachment({ + attachment, + backupLevel, + messageReceivedAt: message.received_at, + }), + reactions: this.getMessageReactions(message), + }; + } + private async toChatItemRevisions( parent: Backups.IChatItem, message: MessageAttributesType, diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts index 6e8c600ba..0b2464508 100644 --- a/ts/services/backups/import.ts +++ b/ts/services/backups/import.ts @@ -1303,6 +1303,11 @@ export class BackupImportStream extends Writable { ...attributes, ...(await this.fromStandardMessage(item.standardMessage, chatConvo.id)), }; + } else if (item.viewOnceMessage) { + attributes = { + ...attributes, + ...(await this.fromViewOnceMessage(item.viewOnceMessage)), + }; } else { const result = await this.fromNonBubbleChatItem(item, { aboutMe, @@ -1566,6 +1571,27 @@ export class BackupImportStream extends Writable { }; } + private async fromViewOnceMessage({ + attachment, + reactions, + }: Backups.IViewOnceMessage): Promise> { + return { + ...(attachment + ? { + attachments: [ + convertBackupMessageAttachmentToAttachment(attachment), + ].filter(isNotNil), + } + : { + attachments: undefined, + readStatus: ReadStatus.Viewed, + isErased: true, + }), + reactions: this.fromReactions(reactions), + isViewOnce: true, + }; + } + private async fromRevisions( mainMessage: MessageAttributesType, revisions: ReadonlyArray