Backup support for link preview and contact attachments
This commit is contained in:
parent
4aa898f495
commit
534029d2e6
7 changed files with 357 additions and 54 deletions
|
@ -479,7 +479,7 @@ message ContactAttachment {
|
||||||
repeated Phone number = 3;
|
repeated Phone number = 3;
|
||||||
repeated Email email = 4;
|
repeated Email email = 4;
|
||||||
repeated PostalAddress address = 5;
|
repeated PostalAddress address = 5;
|
||||||
optional string avatarUrlPath = 6;
|
optional FilePointer avatar = 6;
|
||||||
optional string organization = 7;
|
optional string organization = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,7 @@ import {
|
||||||
import type { CoreAttachmentBackupJobType } from '../../types/AttachmentBackup';
|
import type { CoreAttachmentBackupJobType } from '../../types/AttachmentBackup';
|
||||||
import { AttachmentBackupManager } from '../../jobs/AttachmentBackupManager';
|
import { AttachmentBackupManager } from '../../jobs/AttachmentBackupManager';
|
||||||
import { getBackupCdnInfo } from './util/mediaId';
|
import { getBackupCdnInfo } from './util/mediaId';
|
||||||
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
|
|
||||||
const MAX_CONCURRENCY = 10;
|
const MAX_CONCURRENCY = 10;
|
||||||
|
|
||||||
|
@ -760,23 +761,30 @@ export class BackupExportStream extends Readable {
|
||||||
} else if (contact && contact[0]) {
|
} else if (contact && contact[0]) {
|
||||||
const contactMessage = new Backups.ContactMessage();
|
const contactMessage = new Backups.ContactMessage();
|
||||||
|
|
||||||
// TODO (DESKTOP-6845): properly handle avatarUrlPath
|
contactMessage.contact = await Promise.all(
|
||||||
|
contact.map(async contactDetails => ({
|
||||||
contactMessage.contact = contact.map(contactDetails => ({
|
...contactDetails,
|
||||||
...contactDetails,
|
number: contactDetails.number?.map(number => ({
|
||||||
number: contactDetails.number?.map(number => ({
|
...number,
|
||||||
...number,
|
type: numberToPhoneType(number.type),
|
||||||
type: numberToPhoneType(number.type),
|
})),
|
||||||
})),
|
email: contactDetails.email?.map(email => ({
|
||||||
email: contactDetails.email?.map(email => ({
|
...email,
|
||||||
...email,
|
type: numberToPhoneType(email.type),
|
||||||
type: numberToPhoneType(email.type),
|
})),
|
||||||
})),
|
address: contactDetails.address?.map(address => ({
|
||||||
address: contactDetails.address?.map(address => ({
|
...address,
|
||||||
...address,
|
type: numberToAddressType(address.type),
|
||||||
type: numberToAddressType(address.type),
|
})),
|
||||||
})),
|
avatar: contactDetails.avatar?.avatar
|
||||||
}));
|
? await this.processAttachment({
|
||||||
|
attachment: contactDetails.avatar.avatar,
|
||||||
|
backupLevel,
|
||||||
|
messageReceivedAt: message.received_at,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const reactions = this.getMessageReactions(message);
|
const reactions = this.getMessageReactions(message);
|
||||||
if (reactions != null) {
|
if (reactions != null) {
|
||||||
|
@ -790,7 +798,13 @@ export class BackupExportStream extends Readable {
|
||||||
stickerProto.packId = Bytes.fromHex(sticker.packId);
|
stickerProto.packId = Bytes.fromHex(sticker.packId);
|
||||||
stickerProto.packKey = Bytes.fromBase64(sticker.packKey);
|
stickerProto.packKey = Bytes.fromBase64(sticker.packKey);
|
||||||
stickerProto.stickerId = sticker.stickerId;
|
stickerProto.stickerId = sticker.stickerId;
|
||||||
// TODO (DESKTOP-6845): properly handle data FilePointer
|
stickerProto.data = sticker.data
|
||||||
|
? await this.processAttachment({
|
||||||
|
attachment: sticker.data,
|
||||||
|
backupLevel,
|
||||||
|
messageReceivedAt: message.received_at,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
result.stickerMessage = {
|
result.stickerMessage = {
|
||||||
sticker: stickerProto,
|
sticker: stickerProto,
|
||||||
|
@ -817,14 +831,25 @@ export class BackupExportStream extends Readable {
|
||||||
bodyRanges: message.bodyRanges?.map(range => this.toBodyRange(range)),
|
bodyRanges: message.bodyRanges?.map(range => this.toBodyRange(range)),
|
||||||
},
|
},
|
||||||
|
|
||||||
linkPreview: message.preview?.map(preview => {
|
linkPreview: message.preview
|
||||||
return {
|
? await Promise.all(
|
||||||
url: preview.url,
|
message.preview.map(async preview => {
|
||||||
title: preview.title,
|
return {
|
||||||
description: preview.description,
|
url: preview.url,
|
||||||
date: getSafeLongFromTimestamp(preview.date),
|
title: preview.title,
|
||||||
};
|
description: preview.description,
|
||||||
}),
|
date: getSafeLongFromTimestamp(preview.date),
|
||||||
|
image: preview.image
|
||||||
|
? await this.processAttachment({
|
||||||
|
attachment: preview.image,
|
||||||
|
backupLevel,
|
||||||
|
messageReceivedAt: message.received_at,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
reactions: this.getMessageReactions(message),
|
reactions: this.getMessageReactions(message),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1785,7 +1810,7 @@ export class BackupExportStream extends Readable {
|
||||||
private getIncomingMessageDetails({
|
private getIncomingMessageDetails({
|
||||||
received_at_ms: receivedAtMs,
|
received_at_ms: receivedAtMs,
|
||||||
serverTimestamp,
|
serverTimestamp,
|
||||||
readAt,
|
readStatus,
|
||||||
}: MessageAttributesType): Backups.ChatItem.IIncomingMessageDetails {
|
}: MessageAttributesType): Backups.ChatItem.IIncomingMessageDetails {
|
||||||
return {
|
return {
|
||||||
dateReceived:
|
dateReceived:
|
||||||
|
@ -1794,7 +1819,7 @@ export class BackupExportStream extends Readable {
|
||||||
serverTimestamp != null
|
serverTimestamp != null
|
||||||
? getSafeLongFromTimestamp(serverTimestamp)
|
? getSafeLongFromTimestamp(serverTimestamp)
|
||||||
: null,
|
: null,
|
||||||
read: Boolean(readAt),
|
read: readStatus === ReadStatus.Read,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -873,15 +873,32 @@ export class BackupImportStream extends Writable {
|
||||||
data: Backups.IStandardMessage
|
data: Backups.IStandardMessage
|
||||||
): Partial<MessageAttributesType> {
|
): Partial<MessageAttributesType> {
|
||||||
return {
|
return {
|
||||||
body: data.text?.body ?? '',
|
body: data.text?.body || undefined,
|
||||||
attachments: data.attachments
|
attachments: data.attachments?.length
|
||||||
?.map(attachment => {
|
? data.attachments
|
||||||
if (!attachment.pointer) {
|
.map(attachment => {
|
||||||
return null;
|
if (!attachment.pointer) {
|
||||||
}
|
return null;
|
||||||
return convertFilePointerToAttachment(attachment.pointer);
|
}
|
||||||
})
|
return convertFilePointerToAttachment(attachment.pointer);
|
||||||
.filter(isNotNil),
|
})
|
||||||
|
.filter(isNotNil)
|
||||||
|
: undefined,
|
||||||
|
preview: data.linkPreview?.length
|
||||||
|
? data.linkPreview.map(preview => {
|
||||||
|
const { url } = preview;
|
||||||
|
strictAssert(url, 'preview must have a URL');
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
title: dropNull(preview.title),
|
||||||
|
description: dropNull(preview.description),
|
||||||
|
date: getTimestampFromLong(preview.date),
|
||||||
|
image: preview.image
|
||||||
|
? convertFilePointerToAttachment(preview.image)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
reactions: this.fromReactions(data.reactions),
|
reactions: this.fromReactions(data.reactions),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -889,6 +906,9 @@ export class BackupImportStream extends Writable {
|
||||||
private fromReactions(
|
private fromReactions(
|
||||||
reactions: ReadonlyArray<Backups.IReaction> | null | undefined
|
reactions: ReadonlyArray<Backups.IReaction> | null | undefined
|
||||||
): Array<MessageReactionType> | undefined {
|
): Array<MessageReactionType> | undefined {
|
||||||
|
if (!reactions?.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return reactions?.map(
|
return reactions?.map(
|
||||||
({ emoji, authorId, sentTimestamp, receivedTimestamp }) => {
|
({ emoji, authorId, sentTimestamp, receivedTimestamp }) => {
|
||||||
strictAssert(emoji != null, 'reaction must have an emoji');
|
strictAssert(emoji != null, 'reaction must have an emoji');
|
||||||
|
@ -938,14 +958,8 @@ export class BackupImportStream extends Writable {
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
contact: (chatItem.contactMessage.contact ?? []).map(details => {
|
contact: (chatItem.contactMessage.contact ?? []).map(details => {
|
||||||
const {
|
const { avatar, name, number, email, address, organization } =
|
||||||
name,
|
details;
|
||||||
number,
|
|
||||||
email,
|
|
||||||
address,
|
|
||||||
// TODO (DESKTOP-6845): properly handle avatarUrlPath
|
|
||||||
organization,
|
|
||||||
} = details;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: name
|
name: name
|
||||||
|
@ -1016,6 +1030,12 @@ export class BackupImportStream extends Writable {
|
||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
organization: dropNull(organization),
|
organization: dropNull(organization),
|
||||||
|
avatar: avatar
|
||||||
|
? {
|
||||||
|
avatar: convertFilePointerToAttachment(avatar),
|
||||||
|
isProfile: false,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
reactions: this.fromReactions(chatItem.contactMessage.reactions),
|
reactions: this.fromReactions(chatItem.contactMessage.reactions),
|
||||||
|
@ -1038,7 +1058,7 @@ export class BackupImportStream extends Writable {
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
stickerMessage: {
|
stickerMessage: {
|
||||||
sticker: { emoji, packId, packKey, stickerId },
|
sticker: { emoji, packId, packKey, stickerId, data },
|
||||||
},
|
},
|
||||||
} = chatItem;
|
} = chatItem;
|
||||||
strictAssert(emoji != null, 'stickerMessage must have an emoji');
|
strictAssert(emoji != null, 'stickerMessage must have an emoji');
|
||||||
|
@ -1059,6 +1079,7 @@ export class BackupImportStream extends Writable {
|
||||||
packId: Bytes.toHex(packId),
|
packId: Bytes.toHex(packId),
|
||||||
packKey: Bytes.toBase64(packKey),
|
packKey: Bytes.toBase64(packKey),
|
||||||
stickerId,
|
stickerId,
|
||||||
|
data: data ? convertFilePointerToAttachment(data) : undefined,
|
||||||
},
|
},
|
||||||
reactions: this.fromReactions(chatItem.stickerMessage.reactions),
|
reactions: this.fromReactions(chatItem.stickerMessage.reactions),
|
||||||
},
|
},
|
||||||
|
|
253
ts/test-electron/backup/attachments_test.ts
Normal file
253
ts/test-electron/backup/attachments_test.ts
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { v4 as generateGuid } from 'uuid';
|
||||||
|
import { BackupLevel } from '@signalapp/libsignal-client/zkgroup';
|
||||||
|
import { omit } from 'lodash';
|
||||||
|
|
||||||
|
import type { ConversationModel } from '../../models/conversations';
|
||||||
|
import * as Bytes from '../../Bytes';
|
||||||
|
import Data from '../../sql/Client';
|
||||||
|
import { generateAci } from '../../types/ServiceId';
|
||||||
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
|
import { SeenStatus } from '../../MessageSeenStatus';
|
||||||
|
import { loadCallsHistory } from '../../services/callHistoryLoader';
|
||||||
|
import { setupBasics, asymmetricRoundtripHarness } from './helpers';
|
||||||
|
import { IMAGE_JPEG } from '../../types/MIME';
|
||||||
|
import type { MessageAttributesType } from '../../model-types';
|
||||||
|
import type { AttachmentType } from '../../types/Attachment';
|
||||||
|
import { strictAssert } from '../../util/assert';
|
||||||
|
|
||||||
|
const CONTACT_A = generateAci();
|
||||||
|
|
||||||
|
describe('backup/attachments', () => {
|
||||||
|
let contactA: ConversationModel;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await Data._removeAllMessages();
|
||||||
|
await Data._removeAllConversations();
|
||||||
|
window.storage.reset();
|
||||||
|
|
||||||
|
await setupBasics();
|
||||||
|
|
||||||
|
contactA = await window.ConversationController.getOrCreateAndWait(
|
||||||
|
CONTACT_A,
|
||||||
|
'private',
|
||||||
|
{ systemGivenName: 'CONTACT_A' }
|
||||||
|
);
|
||||||
|
|
||||||
|
await loadCallsHistory();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getBase64(str: string): string {
|
||||||
|
return Bytes.toBase64(Bytes.fromString(str));
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeAttachment(
|
||||||
|
index: number,
|
||||||
|
overrides?: Partial<AttachmentType>
|
||||||
|
): AttachmentType {
|
||||||
|
return {
|
||||||
|
cdnKey: `cdnKey${index}`,
|
||||||
|
cdnNumber: 3,
|
||||||
|
key: getBase64(`key${index}`),
|
||||||
|
digest: getBase64(`digest${index}`),
|
||||||
|
iv: getBase64(`iv${index}`),
|
||||||
|
size: 100,
|
||||||
|
contentType: IMAGE_JPEG,
|
||||||
|
path: `/path/to/file${index}.png`,
|
||||||
|
uploadTimestamp: index,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeMessage(
|
||||||
|
timestamp: number,
|
||||||
|
overrides?: Partial<MessageAttributesType>
|
||||||
|
): MessageAttributesType {
|
||||||
|
return {
|
||||||
|
conversationId: contactA.id,
|
||||||
|
id: generateGuid(),
|
||||||
|
type: 'incoming',
|
||||||
|
received_at: timestamp,
|
||||||
|
received_at_ms: timestamp,
|
||||||
|
sourceServiceId: CONTACT_A,
|
||||||
|
sourceDevice: timestamp,
|
||||||
|
sent_at: timestamp,
|
||||||
|
timestamp,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('normal attachments', () => {
|
||||||
|
it('BackupLevel.Messages, roundtrips normal attachments', async () => {
|
||||||
|
const attachment1 = composeAttachment(1);
|
||||||
|
const attachment2 = composeAttachment(2);
|
||||||
|
|
||||||
|
await asymmetricRoundtripHarness(
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
attachments: [attachment1, attachment2],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
// path & iv will not be roundtripped
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
attachments: [
|
||||||
|
omit(attachment1, ['path', 'iv']),
|
||||||
|
omit(attachment2, ['path', 'iv']),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
BackupLevel.Messages
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('BackupLevel.Media, roundtrips normal attachments', async () => {
|
||||||
|
const attachment = composeAttachment(1);
|
||||||
|
strictAssert(attachment.digest, 'digest exists');
|
||||||
|
|
||||||
|
await asymmetricRoundtripHarness(
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
attachments: [attachment],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
// path, iv, and uploadTimestamp will not be roundtripped,
|
||||||
|
// but there will be a backupLocator
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
...omit(attachment, ['path', 'iv', 'uploadTimestamp']),
|
||||||
|
backupLocator: { mediaName: attachment.digest },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
BackupLevel.Media
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Preview attachments', () => {
|
||||||
|
it('BackupLevel.Messages, roundtrips preview attachments', async () => {
|
||||||
|
const attachment = composeAttachment(1);
|
||||||
|
|
||||||
|
await asymmetricRoundtripHarness(
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
preview: [{ url: 'url', date: 1, image: attachment }],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
// path & iv will not be roundtripped
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
preview: [
|
||||||
|
{ url: 'url', date: 1, image: omit(attachment, ['path', 'iv']) },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
BackupLevel.Messages
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('BackupLevel.Media, roundtrips preview attachments', async () => {
|
||||||
|
const attachment = composeAttachment(1);
|
||||||
|
strictAssert(attachment.digest, 'digest exists');
|
||||||
|
|
||||||
|
await asymmetricRoundtripHarness(
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
preview: [
|
||||||
|
{
|
||||||
|
url: 'url',
|
||||||
|
date: 1,
|
||||||
|
title: 'title',
|
||||||
|
description: 'description',
|
||||||
|
image: attachment,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
preview: [
|
||||||
|
{
|
||||||
|
url: 'url',
|
||||||
|
date: 1,
|
||||||
|
title: 'title',
|
||||||
|
description: 'description',
|
||||||
|
image: {
|
||||||
|
// path, iv, and uploadTimestamp will not be roundtripped,
|
||||||
|
// but there will be a backupLocator
|
||||||
|
...omit(attachment, ['path', 'iv', 'uploadTimestamp']),
|
||||||
|
backupLocator: { mediaName: attachment.digest },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
BackupLevel.Media
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('contact attachments', () => {
|
||||||
|
it('BackupLevel.Messages, roundtrips contact attachments', async () => {
|
||||||
|
const attachment = composeAttachment(1);
|
||||||
|
|
||||||
|
await asymmetricRoundtripHarness(
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
contact: [{ avatar: { avatar: attachment, isProfile: false } }],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
// path & iv will not be roundtripped
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
avatar: {
|
||||||
|
avatar: omit(attachment, ['path', 'iv']),
|
||||||
|
isProfile: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
BackupLevel.Messages
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('BackupLevel.Media, roundtrips contact attachments', async () => {
|
||||||
|
const attachment = composeAttachment(1);
|
||||||
|
strictAssert(attachment.digest, 'digest exists');
|
||||||
|
|
||||||
|
await asymmetricRoundtripHarness(
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
contact: [{ avatar: { avatar: attachment, isProfile: false } }],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
// path, iv, and uploadTimestamp will not be roundtripped,
|
||||||
|
// but there will be a backupLocator
|
||||||
|
[
|
||||||
|
composeMessage(1, {
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
avatar: {
|
||||||
|
avatar: {
|
||||||
|
...omit(attachment, ['path', 'iv', 'uploadTimestamp']),
|
||||||
|
backupLocator: { mediaName: attachment.digest },
|
||||||
|
},
|
||||||
|
isProfile: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
BackupLevel.Media
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -8,6 +8,7 @@ import { sortBy } from 'lodash';
|
||||||
import { createReadStream } from 'fs';
|
import { createReadStream } from 'fs';
|
||||||
import { mkdtemp, rm } from 'fs/promises';
|
import { mkdtemp, rm } from 'fs/promises';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
|
import { BackupLevel } from '@signalapp/libsignal-client/zkgroup';
|
||||||
|
|
||||||
import type { MessageAttributesType } from '../../model-types';
|
import type { MessageAttributesType } from '../../model-types';
|
||||||
import type {
|
import type {
|
||||||
|
@ -109,9 +110,10 @@ function sortAndNormalize(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function symmetricRoundtripHarness(
|
export async function symmetricRoundtripHarness(
|
||||||
messages: Array<MessageAttributesType>
|
messages: Array<MessageAttributesType>,
|
||||||
|
backupLevel: BackupLevel = BackupLevel.Messages
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return asymmetricRoundtripHarness(messages, messages);
|
return asymmetricRoundtripHarness(messages, messages, backupLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateConvoIdToTitle() {
|
async function updateConvoIdToTitle() {
|
||||||
|
@ -126,7 +128,8 @@ async function updateConvoIdToTitle() {
|
||||||
|
|
||||||
export async function asymmetricRoundtripHarness(
|
export async function asymmetricRoundtripHarness(
|
||||||
before: Array<MessageAttributesType>,
|
before: Array<MessageAttributesType>,
|
||||||
after: Array<MessageAttributesType>
|
after: Array<MessageAttributesType>,
|
||||||
|
backupLevel: BackupLevel = BackupLevel.Messages
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const outDir = await mkdtemp(path.join(tmpdir(), 'signal-temp-'));
|
const outDir = await mkdtemp(path.join(tmpdir(), 'signal-temp-'));
|
||||||
const fetchAndSaveBackupCdnObjectMetadata = sinon.stub(
|
const fetchAndSaveBackupCdnObjectMetadata = sinon.stub(
|
||||||
|
@ -138,7 +141,7 @@ export async function asymmetricRoundtripHarness(
|
||||||
|
|
||||||
await Data.saveMessages(before, { forceSave: true, ourAci: OUR_ACI });
|
await Data.saveMessages(before, { forceSave: true, ourAci: OUR_ACI });
|
||||||
|
|
||||||
await backupsService.exportToDisk(targetOutputFile);
|
await backupsService.exportToDisk(targetOutputFile, backupLevel);
|
||||||
|
|
||||||
await updateConvoIdToTitle();
|
await updateConvoIdToTitle();
|
||||||
|
|
||||||
|
|
|
@ -152,6 +152,9 @@ describe('backups', function (this: Mocha.Suite) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backupPath = bootstrap.getBackupPath('backup.bin');
|
||||||
|
await app.exportBackupToDisk(backupPath);
|
||||||
|
|
||||||
const comparator = await bootstrap.createScreenshotComparator(
|
const comparator = await bootstrap.createScreenshotComparator(
|
||||||
app,
|
app,
|
||||||
async (window, snapshot) => {
|
async (window, snapshot) => {
|
||||||
|
@ -185,8 +188,6 @@ describe('backups', function (this: Mocha.Suite) {
|
||||||
this.test
|
this.test
|
||||||
);
|
);
|
||||||
|
|
||||||
const backupPath = bootstrap.getBackupPath('backup.bin');
|
|
||||||
await app.exportBackupToDisk(backupPath);
|
|
||||||
await app.close();
|
await app.close();
|
||||||
|
|
||||||
// Restart
|
// Restart
|
||||||
|
|
|
@ -9,7 +9,7 @@ type GenericLinkPreviewType<Image> = {
|
||||||
domain?: string;
|
domain?: string;
|
||||||
url: string;
|
url: string;
|
||||||
isStickerPack?: boolean;
|
isStickerPack?: boolean;
|
||||||
isCallLink: boolean;
|
isCallLink?: boolean;
|
||||||
image?: Readonly<Image>;
|
image?: Readonly<Image>;
|
||||||
date?: number;
|
date?: number;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue