Fix more import/export tests for backups

This commit is contained in:
Fedor Indutny 2024-09-16 17:40:52 -07:00 committed by GitHub
parent 8dabe4fbe4
commit 84c562d0b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 225 additions and 60 deletions

View file

@ -196,14 +196,22 @@ export enum GiftBadgeStates {
Unopened = 'Unopened', Unopened = 'Unopened',
Opened = 'Opened', Opened = 'Opened',
Redeemed = 'Redeemed', Redeemed = 'Redeemed',
Failed = 'Failed',
} }
export type GiftBadgeType = { export type GiftBadgeType =
expiration: number; | {
id: string | undefined; state:
level: number; | GiftBadgeStates.Unopened
state: GiftBadgeStates; | GiftBadgeStates.Opened
}; | GiftBadgeStates.Redeemed;
expiration: number;
id: string | undefined;
level: number;
}
| {
state: GiftBadgeStates.Failed;
};
export type PropsData = { export type PropsData = {
id: string; id: string;
@ -1385,7 +1393,10 @@ export class Message extends React.PureComponent<Props, State> {
return null; return null;
} }
if (giftBadge.state === GiftBadgeStates.Unopened) { if (
giftBadge.state === GiftBadgeStates.Unopened ||
giftBadge.state === GiftBadgeStates.Failed
) {
const description = const description =
direction === 'incoming' direction === 'incoming'
? i18n('icu:message--donation--unopened--incoming') ? i18n('icu:message--donation--unopened--incoming')

View file

@ -2009,6 +2009,13 @@ GiftBadgeUnopened.args = {
}, },
}; };
export const GiftBadgeFailed = Template.bind({});
GiftBadgeFailed.args = {
giftBadge: {
state: GiftBadgeStates.Failed,
},
};
const getPreferredBadge = () => ({ const getPreferredBadge = () => ({
category: BadgeCategory.Donor, category: BadgeCategory.Donor,
descriptionTemplate: 'This is a description of the badge', descriptionTemplate: 'This is a description of the badge',

View file

@ -138,7 +138,7 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
} }
const giftBadge = message.get('giftBadge'); const giftBadge = message.get('giftBadge');
if (giftBadge) { if (giftBadge && giftBadge.state !== GiftBadgeStates.Failed) {
didChangeMessage = true; didChangeMessage = true;
message.set({ message.set({
giftBadge: { giftBadge: {

24
ts/model-types.d.ts vendored
View file

@ -136,6 +136,9 @@ export type EditHistoryType = {
timestamp: number; timestamp: number;
received_at: number; received_at: number;
received_at_ms?: number; received_at_ms?: number;
serverTimestamp?: number;
readStatus?: ReadStatus;
unidentifiedDeliveryReceived?: boolean;
}; };
type MessageType = type MessageType =
@ -225,13 +228,20 @@ export type MessageAttributesType = {
targetAuthorAci: AciString; targetAuthorAci: AciString;
targetTimestamp: number; targetTimestamp: number;
}; };
giftBadge?: { giftBadge?:
expiration: number; | {
level: number; state:
id: string | undefined; | GiftBadgeStates.Unopened
receiptCredentialPresentation: string; | GiftBadgeStates.Opened
state: GiftBadgeStates; | GiftBadgeStates.Redeemed;
}; expiration: number;
level: number;
id: string | undefined;
receiptCredentialPresentation: string;
}
| {
state: GiftBadgeStates.Failed;
};
expirationTimerUpdate?: { expirationTimerUpdate?: {
expireTimer?: DurationInSeconds; expireTimer?: DurationInSeconds;

View file

@ -158,6 +158,7 @@ import {
} from '../messages/copyQuote'; } from '../messages/copyQuote';
import { getRoomIdFromCallLink } from '../util/callLinksRingrtc'; import { getRoomIdFromCallLink } from '../util/callLinksRingrtc';
import { explodePromise } from '../util/explodePromise'; import { explodePromise } from '../util/explodePromise';
import { GiftBadgeStates } from '../components/conversation/Message';
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
@ -2076,7 +2077,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
await DataWriter.updateConversation(conversation.attributes); await DataWriter.updateConversation(conversation.attributes);
const giftBadge = message.get('giftBadge'); const giftBadge = message.get('giftBadge');
if (giftBadge) { if (giftBadge && giftBadge.state !== GiftBadgeStates.Failed) {
const { level } = giftBadge; const { level } = giftBadge;
const { updatesUrl } = window.SignalContext.config; const { updatesUrl } = window.SignalContext.config;
strictAssert( strictAssert(

View file

@ -22,6 +22,7 @@ import * as log from '../../logging/log';
import { GiftBadgeStates } from '../../components/conversation/Message'; import { GiftBadgeStates } from '../../components/conversation/Message';
import { type CustomColorType } from '../../types/Colors'; import { type CustomColorType } from '../../types/Colors';
import { StorySendMode, MY_STORY_ID } from '../../types/Stories'; import { StorySendMode, MY_STORY_ID } from '../../types/Stories';
import { getStickerPacksForBackup } from '../../types/Stickers';
import { import {
isPniString, isPniString,
type AciString, type AciString,
@ -384,7 +385,7 @@ export class BackupExportStream extends Readable {
stats.callLinks += 1; stats.callLinks += 1;
} }
const stickerPacks = await DataReader.getInstalledStickerPacks(); const stickerPacks = await getStickerPacksForBackup();
for (const { id, key } of stickerPacks) { for (const { id, key } of stickerPacks) {
this.pushFrame({ this.pushFrame({
@ -1142,27 +1143,33 @@ export class BackupExportStream extends Readable {
const { giftBadge } = message; const { giftBadge } = message;
strictAssert(giftBadge != null, 'Message must have gift badge'); strictAssert(giftBadge != null, 'Message must have gift badge');
let state: Backups.GiftBadge.State; if (giftBadge.state === GiftBadgeStates.Failed) {
switch (giftBadge.state) { result.giftBadge = {
case GiftBadgeStates.Unopened: state: Backups.GiftBadge.State.FAILED,
state = Backups.GiftBadge.State.UNOPENED; };
break; } else {
case GiftBadgeStates.Opened: let state: Backups.GiftBadge.State;
state = Backups.GiftBadge.State.OPENED; switch (giftBadge.state) {
break; case GiftBadgeStates.Unopened:
case GiftBadgeStates.Redeemed: state = Backups.GiftBadge.State.UNOPENED;
state = Backups.GiftBadge.State.REDEEMED; break;
break; case GiftBadgeStates.Opened:
default: state = Backups.GiftBadge.State.OPENED;
throw missingCaseError(giftBadge.state); break;
} case GiftBadgeStates.Redeemed:
state = Backups.GiftBadge.State.REDEEMED;
break;
default:
throw missingCaseError(giftBadge);
}
result.giftBadge = { result.giftBadge = {
receiptCredentialPresentation: Bytes.fromBase64( receiptCredentialPresentation: Bytes.fromBase64(
giftBadge.receiptCredentialPresentation giftBadge.receiptCredentialPresentation
), ),
state, state,
}; };
}
} else { } else {
result.standardMessage = await this.toStandardMessage( result.standardMessage = await this.toStandardMessage(
message, message,

View file

@ -35,7 +35,8 @@ import {
import { import {
STICKERPACK_ID_BYTE_LEN, STICKERPACK_ID_BYTE_LEN,
STICKERPACK_KEY_BYTE_LEN, STICKERPACK_KEY_BYTE_LEN,
downloadStickerPack, createPacksFromBackup,
type StickerPackPointerType,
} from '../../types/Stickers'; } from '../../types/Stickers';
import type { import type {
ConversationColorType, ConversationColorType,
@ -285,6 +286,7 @@ export class BackupImportStream extends Writable {
return processMessagesBatch(ourAci, batch); return processMessagesBatch(ourAci, batch);
}, },
}); });
private readonly stickerPacks = new Array<StickerPackPointerType>();
private ourConversation?: ConversationAttributesType; private ourConversation?: ConversationAttributesType;
private pinnedConversations = new Array<[number, string]>(); private pinnedConversations = new Array<[number, string]>();
private customColorById = new Map<number, CustomColorDataType>(); private customColorById = new Map<number, CustomColorDataType>();
@ -357,6 +359,9 @@ export class BackupImportStream extends Writable {
await this.conversationOpBatcher.flushAndWait(); await this.conversationOpBatcher.flushAndWait();
await this.saveMessageBatcher.flushAndWait(); await this.saveMessageBatcher.flushAndWait();
// Store sticker packs and schedule downloads
await createPacksFromBackup(this.stickerPacks);
// Reset and reload conversations and storage again // Reset and reload conversations and storage again
window.ConversationController.reset(); window.ConversationController.reset();
@ -1537,8 +1542,14 @@ export class BackupImportStream extends Writable {
const timestamp = getTimestampFromLong(rev.dateSent); const timestamp = getTimestampFromLong(rev.dateSent);
const { const {
// eslint-disable-next-line camelcase patch: {
patch: { sendStateByConversationId, received_at_ms }, sendStateByConversationId,
// eslint-disable-next-line camelcase
received_at_ms,
serverTimestamp,
readStatus,
unidentifiedDeliveryReceived,
},
} = this.fromDirectionDetails(rev, timestamp); } = this.fromDirectionDetails(rev, timestamp);
return { return {
@ -1551,6 +1562,9 @@ export class BackupImportStream extends Writable {
sendStateByConversationId, sendStateByConversationId,
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
received_at_ms, received_at_ms,
serverTimestamp,
readStatus,
unidentifiedDeliveryReceived,
}; };
}) })
// Fix order: from newest to oldest // Fix order: from newest to oldest
@ -1572,6 +1586,9 @@ export class BackupImportStream extends Writable {
timestamp: mainMessage.timestamp, timestamp: mainMessage.timestamp,
received_at: mainMessage.received_at, received_at: mainMessage.received_at,
received_at_ms: mainMessage.received_at_ms, received_at_ms: mainMessage.received_at_ms,
serverTimestamp: mainMessage.serverTimestamp,
readStatus: mainMessage.readStatus,
unidentifiedDeliveryReceived: mainMessage.unidentifiedDeliveryReceived,
}); });
return result; return result;
@ -1863,6 +1880,17 @@ export class BackupImportStream extends Writable {
} }
if (chatItem.giftBadge) { if (chatItem.giftBadge) {
const { giftBadge } = chatItem; const { giftBadge } = chatItem;
if (giftBadge.state === Backups.GiftBadge.State.FAILED) {
return {
message: {
giftBadge: {
state: GiftBadgeStates.Failed,
},
},
additionalMessages: [],
};
}
strictAssert( strictAssert(
Bytes.isNotEmpty(giftBadge.receiptCredentialPresentation), Bytes.isNotEmpty(giftBadge.receiptCredentialPresentation),
'Gift badge must have a presentation' 'Gift badge must have a presentation'
@ -1874,15 +1902,11 @@ export class BackupImportStream extends Writable {
state = GiftBadgeStates.Opened; state = GiftBadgeStates.Opened;
break; break;
case Backups.GiftBadge.State.FAILED:
case Backups.GiftBadge.State.REDEEMED: case Backups.GiftBadge.State.REDEEMED:
state = GiftBadgeStates.Redeemed; state = GiftBadgeStates.Redeemed;
break; break;
case Backups.GiftBadge.State.UNOPENED: case Backups.GiftBadge.State.UNOPENED:
state = GiftBadgeStates.Unopened;
break;
default: default:
state = GiftBadgeStates.Unopened; state = GiftBadgeStates.Unopened;
break; break;
@ -2842,25 +2866,23 @@ export class BackupImportStream extends Writable {
} }
private async fromStickerPack({ private async fromStickerPack({
packId: id, packId: packIdBytes,
packKey: key, packKey: packKeyBytes,
}: Backups.IStickerPack): Promise<void> { }: Backups.IStickerPack): Promise<void> {
strictAssert( strictAssert(
id?.length === STICKERPACK_ID_BYTE_LEN, packIdBytes?.length === STICKERPACK_ID_BYTE_LEN,
'Sticker pack must have a valid pack id' '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( strictAssert(
key?.length === STICKERPACK_KEY_BYTE_LEN, packKeyBytes?.length === STICKERPACK_KEY_BYTE_LEN,
`${logId}: must have a valid pack key` `${logId}: must have a valid pack key`
); );
const key = Bytes.toBase64(packKeyBytes);
drop( this.stickerPacks.push({ id, key });
downloadStickerPack(Bytes.toHex(id), Bytes.toBase64(key), {
fromBackup: true,
})
);
} }
private async fromAdHocCall({ private async fromAdHocCall({

View file

@ -875,6 +875,7 @@ type WritableInterface = {
) => void; ) => void;
createOrUpdateStickerPack: (pack: StickerPackType) => void; createOrUpdateStickerPack: (pack: StickerPackType) => void;
createOrUpdateStickerPacks: (packs: ReadonlyArray<StickerPackType>) => void;
updateStickerPackStatus: ( updateStickerPackStatus: (
id: string, id: string,
status: StickerPackStatusType, status: StickerPackStatusType,
@ -895,6 +896,9 @@ type WritableInterface = {
) => ReadonlyArray<string> | undefined; ) => ReadonlyArray<string> | undefined;
deleteStickerPack: (packId: string) => Array<string>; deleteStickerPack: (packId: string) => Array<string>;
addUninstalledStickerPack: (pack: UninstalledStickerPackType) => void; addUninstalledStickerPack: (pack: UninstalledStickerPackType) => void;
addUninstalledStickerPacks: (
pack: ReadonlyArray<UninstalledStickerPackType>
) => void;
removeUninstalledStickerPack: (packId: string) => void; removeUninstalledStickerPack: (packId: string) => void;
installStickerPack: (packId: string, timestamp: number) => void; installStickerPack: (packId: string, timestamp: number) => void;
uninstallStickerPack: (packId: string, timestamp: number) => void; uninstallStickerPack: (packId: string, timestamp: number) => void;

View file

@ -486,6 +486,7 @@ export const DataWriter: ServerWritableInterface = {
saveBackupCdnObjectMetadata, saveBackupCdnObjectMetadata,
createOrUpdateStickerPack, createOrUpdateStickerPack,
createOrUpdateStickerPacks,
updateStickerPackStatus, updateStickerPackStatus,
updateStickerPackInfo, updateStickerPackInfo,
createOrUpdateSticker, createOrUpdateSticker,
@ -495,6 +496,7 @@ export const DataWriter: ServerWritableInterface = {
deleteStickerPackReference, deleteStickerPackReference,
deleteStickerPack, deleteStickerPack,
addUninstalledStickerPack, addUninstalledStickerPack,
addUninstalledStickerPacks,
removeUninstalledStickerPack, removeUninstalledStickerPack,
installStickerPack, installStickerPack,
uninstallStickerPack, uninstallStickerPack,
@ -5236,6 +5238,16 @@ function createOrUpdateStickerPack(
` `
).run(payload); ).run(payload);
} }
function createOrUpdateStickerPacks(
db: WritableDB,
packs: ReadonlyArray<StickerPackType>
): void {
db.transaction(() => {
for (const pack of packs) {
createOrUpdateStickerPack(db, pack);
}
})();
}
function updateStickerPackStatus( function updateStickerPackStatus(
db: WritableDB, db: WritableDB,
id: string, id: string,
@ -5630,6 +5642,16 @@ function addUninstalledStickerPack(
storageNeedsSync: pack.storageNeedsSync ? 1 : 0, 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 { function removeUninstalledStickerPack(db: WritableDB, packId: string): void {
db.prepare<Query>( db.prepare<Query>(
'DELETE FROM uninstalled_sticker_packs WHERE id IS $id' 'DELETE FROM uninstalled_sticker_packs WHERE id IS $id'

View file

@ -94,18 +94,24 @@ describe('backup/bubble messages', () => {
timestamp: 5, timestamp: 5,
received_at: 5, received_at: 5,
received_at_ms: 5, received_at_ms: 5,
readStatus: ReadStatus.Unread,
unidentifiedDeliveryReceived: true,
}, },
{ {
body: 'c', body: 'c',
timestamp: 4, timestamp: 4,
received_at: 4, received_at: 4,
received_at_ms: 4, received_at_ms: 4,
readStatus: ReadStatus.Unread,
unidentifiedDeliveryReceived: false,
}, },
{ {
body: 'b', body: 'b',
timestamp: 3, timestamp: 3,
received_at: 3, received_at: 3,
received_at_ms: 3, received_at_ms: 3,
readStatus: ReadStatus.Read,
unidentifiedDeliveryReceived: false,
}, },
], ],
}, },

View file

@ -14,6 +14,7 @@ import {
} from '@signalapp/libsignal-client/dist/MessageBackup'; } from '@signalapp/libsignal-client/dist/MessageBackup';
import { FileStream } from '../../services/backups/util/FileStream'; import { FileStream } from '../../services/backups/util/FileStream';
import { drop } from '../../util/drop';
import type { App } from '../playwright'; import type { App } from '../playwright';
import { Bootstrap } from '../bootstrap'; import { Bootstrap } from '../bootstrap';
@ -96,11 +97,16 @@ async function runOne(filePath: string): Promise<void> {
await bootstrap.saveLogs(app, basename(filePath)); await bootstrap.saveLogs(app, basename(filePath));
fail(filePath, error.stack); fail(filePath, error.stack);
} finally { } finally {
try { // No need to block on this
await bootstrap.teardown(); drop(
} catch (error) { (async () => {
console.error(`Failed to teardown ${basename(filePath)}`, error); try {
} await bootstrap.teardown();
} catch (error) {
console.error(`Failed to teardown ${basename(filePath)}`, error);
}
})()
);
} }
} }

View file

@ -19,6 +19,7 @@ import type {
StickerType as StickerFromDBType, StickerType as StickerFromDBType,
StickerPackType, StickerPackType,
StickerPackStatusType, StickerPackStatusType,
UninstalledStickerPackType,
} from '../sql/Interface'; } from '../sql/Interface';
import { DataReader, DataWriter } from '../sql/Client'; import { DataReader, DataWriter } from '../sql/Client';
import { SignalService as Proto } from '../protobuf'; 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_ID_BYTE_LEN = 16;
export const STICKERPACK_KEY_BYTE_LEN = 32; export const STICKERPACK_KEY_BYTE_LEN = 32;
@ -135,9 +141,65 @@ export async function load(): Promise<void> {
packsToDownload = capturePacksToDownload(packs); 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( export function getDataFromLink(
link: string link: string
): undefined | { id: string; key: string } { ): undefined | StickerPackPointerType {
const url = maybeParseUrl(link); const url = maybeParseUrl(link);
if (!url) { if (!url) {
return undefined; return undefined;

View file

@ -124,6 +124,9 @@ export async function handleEditMessage(
timestamp: mainMessage.timestamp, timestamp: mainMessage.timestamp,
received_at: mainMessage.received_at, received_at: mainMessage.received_at,
received_at_ms: mainMessage.received_at_ms, 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, timestamp: upgradedEditedMessageData.timestamp,
received_at: upgradedEditedMessageData.received_at, received_at: upgradedEditedMessageData.received_at,
received_at_ms: upgradedEditedMessageData.received_at_ms, received_at_ms: upgradedEditedMessageData.received_at_ms,
serverTimestamp: upgradedEditedMessageData.serverTimestamp,
readStatus: upgradedEditedMessageData.readStatus,
unidentifiedDeliveryReceived:
upgradedEditedMessageData.unidentifiedDeliveryReceived,
quote: nextEditedMessageQuote, quote: nextEditedMessageQuote,
}; };