Fix more import/export tests for backups
This commit is contained in:
parent
8dabe4fbe4
commit
84c562d0b2
13 changed files with 225 additions and 60 deletions
|
@ -196,13 +196,21 @@ export enum GiftBadgeStates {
|
|||
Unopened = 'Unopened',
|
||||
Opened = 'Opened',
|
||||
Redeemed = 'Redeemed',
|
||||
Failed = 'Failed',
|
||||
}
|
||||
|
||||
export type GiftBadgeType = {
|
||||
export type GiftBadgeType =
|
||||
| {
|
||||
state:
|
||||
| GiftBadgeStates.Unopened
|
||||
| GiftBadgeStates.Opened
|
||||
| GiftBadgeStates.Redeemed;
|
||||
expiration: number;
|
||||
id: string | undefined;
|
||||
level: number;
|
||||
state: GiftBadgeStates;
|
||||
}
|
||||
| {
|
||||
state: GiftBadgeStates.Failed;
|
||||
};
|
||||
|
||||
export type PropsData = {
|
||||
|
@ -1385,7 +1393,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (giftBadge.state === GiftBadgeStates.Unopened) {
|
||||
if (
|
||||
giftBadge.state === GiftBadgeStates.Unopened ||
|
||||
giftBadge.state === GiftBadgeStates.Failed
|
||||
) {
|
||||
const description =
|
||||
direction === 'incoming'
|
||||
? i18n('icu:message--donation--unopened--incoming')
|
||||
|
|
|
@ -2009,6 +2009,13 @@ GiftBadgeUnopened.args = {
|
|||
},
|
||||
};
|
||||
|
||||
export const GiftBadgeFailed = Template.bind({});
|
||||
GiftBadgeFailed.args = {
|
||||
giftBadge: {
|
||||
state: GiftBadgeStates.Failed,
|
||||
},
|
||||
};
|
||||
|
||||
const getPreferredBadge = () => ({
|
||||
category: BadgeCategory.Donor,
|
||||
descriptionTemplate: 'This is a description of the badge',
|
||||
|
|
|
@ -138,7 +138,7 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
|
|||
}
|
||||
|
||||
const giftBadge = message.get('giftBadge');
|
||||
if (giftBadge) {
|
||||
if (giftBadge && giftBadge.state !== GiftBadgeStates.Failed) {
|
||||
didChangeMessage = true;
|
||||
message.set({
|
||||
giftBadge: {
|
||||
|
|
14
ts/model-types.d.ts
vendored
14
ts/model-types.d.ts
vendored
|
@ -136,6 +136,9 @@ export type EditHistoryType = {
|
|||
timestamp: number;
|
||||
received_at: number;
|
||||
received_at_ms?: number;
|
||||
serverTimestamp?: number;
|
||||
readStatus?: ReadStatus;
|
||||
unidentifiedDeliveryReceived?: boolean;
|
||||
};
|
||||
|
||||
type MessageType =
|
||||
|
@ -225,12 +228,19 @@ export type MessageAttributesType = {
|
|||
targetAuthorAci: AciString;
|
||||
targetTimestamp: number;
|
||||
};
|
||||
giftBadge?: {
|
||||
giftBadge?:
|
||||
| {
|
||||
state:
|
||||
| GiftBadgeStates.Unopened
|
||||
| GiftBadgeStates.Opened
|
||||
| GiftBadgeStates.Redeemed;
|
||||
expiration: number;
|
||||
level: number;
|
||||
id: string | undefined;
|
||||
receiptCredentialPresentation: string;
|
||||
state: GiftBadgeStates;
|
||||
}
|
||||
| {
|
||||
state: GiftBadgeStates.Failed;
|
||||
};
|
||||
|
||||
expirationTimerUpdate?: {
|
||||
|
|
|
@ -158,6 +158,7 @@ import {
|
|||
} from '../messages/copyQuote';
|
||||
import { getRoomIdFromCallLink } from '../util/callLinksRingrtc';
|
||||
import { explodePromise } from '../util/explodePromise';
|
||||
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
|
@ -2076,7 +2077,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
await DataWriter.updateConversation(conversation.attributes);
|
||||
|
||||
const giftBadge = message.get('giftBadge');
|
||||
if (giftBadge) {
|
||||
if (giftBadge && giftBadge.state !== GiftBadgeStates.Failed) {
|
||||
const { level } = giftBadge;
|
||||
const { updatesUrl } = window.SignalContext.config;
|
||||
strictAssert(
|
||||
|
|
|
@ -22,6 +22,7 @@ import * as log from '../../logging/log';
|
|||
import { GiftBadgeStates } from '../../components/conversation/Message';
|
||||
import { type CustomColorType } from '../../types/Colors';
|
||||
import { StorySendMode, MY_STORY_ID } from '../../types/Stories';
|
||||
import { getStickerPacksForBackup } from '../../types/Stickers';
|
||||
import {
|
||||
isPniString,
|
||||
type AciString,
|
||||
|
@ -384,7 +385,7 @@ export class BackupExportStream extends Readable {
|
|||
stats.callLinks += 1;
|
||||
}
|
||||
|
||||
const stickerPacks = await DataReader.getInstalledStickerPacks();
|
||||
const stickerPacks = await getStickerPacksForBackup();
|
||||
|
||||
for (const { id, key } of stickerPacks) {
|
||||
this.pushFrame({
|
||||
|
@ -1142,6 +1143,11 @@ export class BackupExportStream extends Readable {
|
|||
const { giftBadge } = message;
|
||||
strictAssert(giftBadge != null, 'Message must have gift badge');
|
||||
|
||||
if (giftBadge.state === GiftBadgeStates.Failed) {
|
||||
result.giftBadge = {
|
||||
state: Backups.GiftBadge.State.FAILED,
|
||||
};
|
||||
} else {
|
||||
let state: Backups.GiftBadge.State;
|
||||
switch (giftBadge.state) {
|
||||
case GiftBadgeStates.Unopened:
|
||||
|
@ -1154,7 +1160,7 @@ export class BackupExportStream extends Readable {
|
|||
state = Backups.GiftBadge.State.REDEEMED;
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(giftBadge.state);
|
||||
throw missingCaseError(giftBadge);
|
||||
}
|
||||
|
||||
result.giftBadge = {
|
||||
|
@ -1163,6 +1169,7 @@ export class BackupExportStream extends Readable {
|
|||
),
|
||||
state,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
result.standardMessage = await this.toStandardMessage(
|
||||
message,
|
||||
|
|
|
@ -35,7 +35,8 @@ import {
|
|||
import {
|
||||
STICKERPACK_ID_BYTE_LEN,
|
||||
STICKERPACK_KEY_BYTE_LEN,
|
||||
downloadStickerPack,
|
||||
createPacksFromBackup,
|
||||
type StickerPackPointerType,
|
||||
} from '../../types/Stickers';
|
||||
import type {
|
||||
ConversationColorType,
|
||||
|
@ -285,6 +286,7 @@ export class BackupImportStream extends Writable {
|
|||
return processMessagesBatch(ourAci, batch);
|
||||
},
|
||||
});
|
||||
private readonly stickerPacks = new Array<StickerPackPointerType>();
|
||||
private ourConversation?: ConversationAttributesType;
|
||||
private pinnedConversations = new Array<[number, string]>();
|
||||
private customColorById = new Map<number, CustomColorDataType>();
|
||||
|
@ -357,6 +359,9 @@ export class BackupImportStream extends Writable {
|
|||
await this.conversationOpBatcher.flushAndWait();
|
||||
await this.saveMessageBatcher.flushAndWait();
|
||||
|
||||
// Store sticker packs and schedule downloads
|
||||
await createPacksFromBackup(this.stickerPacks);
|
||||
|
||||
// Reset and reload conversations and storage again
|
||||
window.ConversationController.reset();
|
||||
|
||||
|
@ -1537,8 +1542,14 @@ export class BackupImportStream extends Writable {
|
|||
const timestamp = getTimestampFromLong(rev.dateSent);
|
||||
|
||||
const {
|
||||
patch: {
|
||||
sendStateByConversationId,
|
||||
// eslint-disable-next-line camelcase
|
||||
patch: { sendStateByConversationId, received_at_ms },
|
||||
received_at_ms,
|
||||
serverTimestamp,
|
||||
readStatus,
|
||||
unidentifiedDeliveryReceived,
|
||||
},
|
||||
} = this.fromDirectionDetails(rev, timestamp);
|
||||
|
||||
return {
|
||||
|
@ -1551,6 +1562,9 @@ export class BackupImportStream extends Writable {
|
|||
sendStateByConversationId,
|
||||
// eslint-disable-next-line camelcase
|
||||
received_at_ms,
|
||||
serverTimestamp,
|
||||
readStatus,
|
||||
unidentifiedDeliveryReceived,
|
||||
};
|
||||
})
|
||||
// Fix order: from newest to oldest
|
||||
|
@ -1572,6 +1586,9 @@ export class BackupImportStream extends Writable {
|
|||
timestamp: mainMessage.timestamp,
|
||||
received_at: mainMessage.received_at,
|
||||
received_at_ms: mainMessage.received_at_ms,
|
||||
serverTimestamp: mainMessage.serverTimestamp,
|
||||
readStatus: mainMessage.readStatus,
|
||||
unidentifiedDeliveryReceived: mainMessage.unidentifiedDeliveryReceived,
|
||||
});
|
||||
|
||||
return result;
|
||||
|
@ -1863,6 +1880,17 @@ export class BackupImportStream extends Writable {
|
|||
}
|
||||
if (chatItem.giftBadge) {
|
||||
const { giftBadge } = chatItem;
|
||||
if (giftBadge.state === Backups.GiftBadge.State.FAILED) {
|
||||
return {
|
||||
message: {
|
||||
giftBadge: {
|
||||
state: GiftBadgeStates.Failed,
|
||||
},
|
||||
},
|
||||
additionalMessages: [],
|
||||
};
|
||||
}
|
||||
|
||||
strictAssert(
|
||||
Bytes.isNotEmpty(giftBadge.receiptCredentialPresentation),
|
||||
'Gift badge must have a presentation'
|
||||
|
@ -1874,15 +1902,11 @@ export class BackupImportStream extends Writable {
|
|||
state = GiftBadgeStates.Opened;
|
||||
break;
|
||||
|
||||
case Backups.GiftBadge.State.FAILED:
|
||||
case Backups.GiftBadge.State.REDEEMED:
|
||||
state = GiftBadgeStates.Redeemed;
|
||||
break;
|
||||
|
||||
case Backups.GiftBadge.State.UNOPENED:
|
||||
state = GiftBadgeStates.Unopened;
|
||||
break;
|
||||
|
||||
default:
|
||||
state = GiftBadgeStates.Unopened;
|
||||
break;
|
||||
|
@ -2842,25 +2866,23 @@ export class BackupImportStream extends Writable {
|
|||
}
|
||||
|
||||
private async fromStickerPack({
|
||||
packId: id,
|
||||
packKey: key,
|
||||
packId: packIdBytes,
|
||||
packKey: packKeyBytes,
|
||||
}: Backups.IStickerPack): Promise<void> {
|
||||
strictAssert(
|
||||
id?.length === STICKERPACK_ID_BYTE_LEN,
|
||||
packIdBytes?.length === STICKERPACK_ID_BYTE_LEN,
|
||||
'Sticker pack must have a valid pack id'
|
||||
);
|
||||
|
||||
const logId = `fromStickerPack(${Bytes.toHex(id).slice(-2)})`;
|
||||
const id = Bytes.toHex(packIdBytes);
|
||||
const logId = `fromStickerPack(${id.slice(-2)})`;
|
||||
strictAssert(
|
||||
key?.length === STICKERPACK_KEY_BYTE_LEN,
|
||||
packKeyBytes?.length === STICKERPACK_KEY_BYTE_LEN,
|
||||
`${logId}: must have a valid pack key`
|
||||
);
|
||||
const key = Bytes.toBase64(packKeyBytes);
|
||||
|
||||
drop(
|
||||
downloadStickerPack(Bytes.toHex(id), Bytes.toBase64(key), {
|
||||
fromBackup: true,
|
||||
})
|
||||
);
|
||||
this.stickerPacks.push({ id, key });
|
||||
}
|
||||
|
||||
private async fromAdHocCall({
|
||||
|
|
|
@ -875,6 +875,7 @@ type WritableInterface = {
|
|||
) => void;
|
||||
|
||||
createOrUpdateStickerPack: (pack: StickerPackType) => void;
|
||||
createOrUpdateStickerPacks: (packs: ReadonlyArray<StickerPackType>) => void;
|
||||
updateStickerPackStatus: (
|
||||
id: string,
|
||||
status: StickerPackStatusType,
|
||||
|
@ -895,6 +896,9 @@ type WritableInterface = {
|
|||
) => ReadonlyArray<string> | undefined;
|
||||
deleteStickerPack: (packId: string) => Array<string>;
|
||||
addUninstalledStickerPack: (pack: UninstalledStickerPackType) => void;
|
||||
addUninstalledStickerPacks: (
|
||||
pack: ReadonlyArray<UninstalledStickerPackType>
|
||||
) => void;
|
||||
removeUninstalledStickerPack: (packId: string) => void;
|
||||
installStickerPack: (packId: string, timestamp: number) => void;
|
||||
uninstallStickerPack: (packId: string, timestamp: number) => void;
|
||||
|
|
|
@ -486,6 +486,7 @@ export const DataWriter: ServerWritableInterface = {
|
|||
saveBackupCdnObjectMetadata,
|
||||
|
||||
createOrUpdateStickerPack,
|
||||
createOrUpdateStickerPacks,
|
||||
updateStickerPackStatus,
|
||||
updateStickerPackInfo,
|
||||
createOrUpdateSticker,
|
||||
|
@ -495,6 +496,7 @@ export const DataWriter: ServerWritableInterface = {
|
|||
deleteStickerPackReference,
|
||||
deleteStickerPack,
|
||||
addUninstalledStickerPack,
|
||||
addUninstalledStickerPacks,
|
||||
removeUninstalledStickerPack,
|
||||
installStickerPack,
|
||||
uninstallStickerPack,
|
||||
|
@ -5236,6 +5238,16 @@ function createOrUpdateStickerPack(
|
|||
`
|
||||
).run(payload);
|
||||
}
|
||||
function createOrUpdateStickerPacks(
|
||||
db: WritableDB,
|
||||
packs: ReadonlyArray<StickerPackType>
|
||||
): void {
|
||||
db.transaction(() => {
|
||||
for (const pack of packs) {
|
||||
createOrUpdateStickerPack(db, pack);
|
||||
}
|
||||
})();
|
||||
}
|
||||
function updateStickerPackStatus(
|
||||
db: WritableDB,
|
||||
id: string,
|
||||
|
@ -5630,6 +5642,16 @@ function addUninstalledStickerPack(
|
|||
storageNeedsSync: pack.storageNeedsSync ? 1 : 0,
|
||||
});
|
||||
}
|
||||
function addUninstalledStickerPacks(
|
||||
db: WritableDB,
|
||||
packs: ReadonlyArray<UninstalledStickerPackType>
|
||||
): void {
|
||||
return db.transaction(() => {
|
||||
for (const pack of packs) {
|
||||
addUninstalledStickerPack(db, pack);
|
||||
}
|
||||
})();
|
||||
}
|
||||
function removeUninstalledStickerPack(db: WritableDB, packId: string): void {
|
||||
db.prepare<Query>(
|
||||
'DELETE FROM uninstalled_sticker_packs WHERE id IS $id'
|
||||
|
|
|
@ -94,18 +94,24 @@ describe('backup/bubble messages', () => {
|
|||
timestamp: 5,
|
||||
received_at: 5,
|
||||
received_at_ms: 5,
|
||||
readStatus: ReadStatus.Unread,
|
||||
unidentifiedDeliveryReceived: true,
|
||||
},
|
||||
{
|
||||
body: 'c',
|
||||
timestamp: 4,
|
||||
received_at: 4,
|
||||
received_at_ms: 4,
|
||||
readStatus: ReadStatus.Unread,
|
||||
unidentifiedDeliveryReceived: false,
|
||||
},
|
||||
{
|
||||
body: 'b',
|
||||
timestamp: 3,
|
||||
received_at: 3,
|
||||
received_at_ms: 3,
|
||||
readStatus: ReadStatus.Read,
|
||||
unidentifiedDeliveryReceived: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from '@signalapp/libsignal-client/dist/MessageBackup';
|
||||
|
||||
import { FileStream } from '../../services/backups/util/FileStream';
|
||||
import { drop } from '../../util/drop';
|
||||
import type { App } from '../playwright';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
|
||||
|
@ -96,11 +97,16 @@ async function runOne(filePath: string): Promise<void> {
|
|||
await bootstrap.saveLogs(app, basename(filePath));
|
||||
fail(filePath, error.stack);
|
||||
} finally {
|
||||
// No need to block on this
|
||||
drop(
|
||||
(async () => {
|
||||
try {
|
||||
await bootstrap.teardown();
|
||||
} catch (error) {
|
||||
console.error(`Failed to teardown ${basename(filePath)}`, error);
|
||||
}
|
||||
})()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import type {
|
|||
StickerType as StickerFromDBType,
|
||||
StickerPackType,
|
||||
StickerPackStatusType,
|
||||
UninstalledStickerPackType,
|
||||
} from '../sql/Interface';
|
||||
import { DataReader, DataWriter } from '../sql/Client';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
@ -62,6 +63,11 @@ export type DownloadMap = Record<
|
|||
}
|
||||
>;
|
||||
|
||||
export type StickerPackPointerType = Readonly<{
|
||||
id: string;
|
||||
key: string;
|
||||
}>;
|
||||
|
||||
export const STICKERPACK_ID_BYTE_LEN = 16;
|
||||
export const STICKERPACK_KEY_BYTE_LEN = 32;
|
||||
|
||||
|
@ -135,9 +141,65 @@ export async function load(): Promise<void> {
|
|||
packsToDownload = capturePacksToDownload(packs);
|
||||
}
|
||||
|
||||
export async function createPacksFromBackup(
|
||||
packs: ReadonlyArray<StickerPackPointerType>
|
||||
): Promise<void> {
|
||||
const known = new Set(packs.map(({ id }) => id));
|
||||
const pairs = packs.slice();
|
||||
const uninstalled = new Array<UninstalledStickerPackType>();
|
||||
|
||||
for (const [id, { key }] of Object.entries(BLESSED_PACKS)) {
|
||||
if (known.has(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blessed packs that are not in the backup were uninstalled
|
||||
pairs.push({ id, key });
|
||||
uninstalled.push({
|
||||
id,
|
||||
key: undefined,
|
||||
uninstalledAt: Date.now(),
|
||||
storageNeedsSync: false,
|
||||
});
|
||||
}
|
||||
|
||||
const packsToStore = pairs.map(
|
||||
({ id, key }): StickerPackType => ({
|
||||
...STICKER_PACK_DEFAULTS,
|
||||
|
||||
id,
|
||||
key,
|
||||
status: 'known' as const,
|
||||
})
|
||||
);
|
||||
|
||||
await DataWriter.createOrUpdateStickerPacks(packsToStore);
|
||||
await DataWriter.addUninstalledStickerPacks(uninstalled);
|
||||
|
||||
packsToDownload = capturePacksToDownload(makeLookup(packsToStore, 'id'));
|
||||
}
|
||||
|
||||
export async function getStickerPacksForBackup(): Promise<
|
||||
Array<StickerPackPointerType>
|
||||
> {
|
||||
const result = new Array<StickerPackPointerType>();
|
||||
const stickerPacks = await DataReader.getAllStickerPacks();
|
||||
const uninstalled = new Set(
|
||||
(await DataReader.getUninstalledStickerPacks()).map(({ id }) => id)
|
||||
);
|
||||
for (const { id, key } of stickerPacks) {
|
||||
if (uninstalled.has(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({ id, key });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getDataFromLink(
|
||||
link: string
|
||||
): undefined | { id: string; key: string } {
|
||||
): undefined | StickerPackPointerType {
|
||||
const url = maybeParseUrl(link);
|
||||
if (!url) {
|
||||
return undefined;
|
||||
|
|
|
@ -124,6 +124,9 @@ export async function handleEditMessage(
|
|||
timestamp: mainMessage.timestamp,
|
||||
received_at: mainMessage.received_at,
|
||||
received_at_ms: mainMessage.received_at_ms,
|
||||
serverTimestamp: mainMessage.serverTimestamp,
|
||||
readStatus: mainMessage.readStatus,
|
||||
unidentifiedDeliveryReceived: mainMessage.unidentifiedDeliveryReceived,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -258,6 +261,10 @@ export async function handleEditMessage(
|
|||
timestamp: upgradedEditedMessageData.timestamp,
|
||||
received_at: upgradedEditedMessageData.received_at,
|
||||
received_at_ms: upgradedEditedMessageData.received_at_ms,
|
||||
serverTimestamp: upgradedEditedMessageData.serverTimestamp,
|
||||
readStatus: upgradedEditedMessageData.readStatus,
|
||||
unidentifiedDeliveryReceived:
|
||||
upgradedEditedMessageData.unidentifiedDeliveryReceived,
|
||||
quote: nextEditedMessageQuote,
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue