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',
Opened = 'Opened',
Redeemed = 'Redeemed',
Failed = 'Failed',
}
export type GiftBadgeType = {
expiration: number;
id: string | undefined;
level: number;
state: GiftBadgeStates;
};
export type GiftBadgeType =
| {
state:
| GiftBadgeStates.Unopened
| GiftBadgeStates.Opened
| GiftBadgeStates.Redeemed;
expiration: number;
id: string | undefined;
level: number;
}
| {
state: GiftBadgeStates.Failed;
};
export type PropsData = {
id: string;
@ -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')

View file

@ -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',

View file

@ -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: {

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

@ -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,13 +228,20 @@ export type MessageAttributesType = {
targetAuthorAci: AciString;
targetTimestamp: number;
};
giftBadge?: {
expiration: number;
level: number;
id: string | undefined;
receiptCredentialPresentation: string;
state: GiftBadgeStates;
};
giftBadge?:
| {
state:
| GiftBadgeStates.Unopened
| GiftBadgeStates.Opened
| GiftBadgeStates.Redeemed;
expiration: number;
level: number;
id: string | undefined;
receiptCredentialPresentation: string;
}
| {
state: GiftBadgeStates.Failed;
};
expirationTimerUpdate?: {
expireTimer?: DurationInSeconds;

View file

@ -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(

View file

@ -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,27 +1143,33 @@ export class BackupExportStream extends Readable {
const { giftBadge } = message;
strictAssert(giftBadge != null, 'Message must have gift badge');
let state: Backups.GiftBadge.State;
switch (giftBadge.state) {
case GiftBadgeStates.Unopened:
state = Backups.GiftBadge.State.UNOPENED;
break;
case GiftBadgeStates.Opened:
state = Backups.GiftBadge.State.OPENED;
break;
case GiftBadgeStates.Redeemed:
state = Backups.GiftBadge.State.REDEEMED;
break;
default:
throw missingCaseError(giftBadge.state);
}
if (giftBadge.state === GiftBadgeStates.Failed) {
result.giftBadge = {
state: Backups.GiftBadge.State.FAILED,
};
} else {
let state: Backups.GiftBadge.State;
switch (giftBadge.state) {
case GiftBadgeStates.Unopened:
state = Backups.GiftBadge.State.UNOPENED;
break;
case GiftBadgeStates.Opened:
state = Backups.GiftBadge.State.OPENED;
break;
case GiftBadgeStates.Redeemed:
state = Backups.GiftBadge.State.REDEEMED;
break;
default:
throw missingCaseError(giftBadge);
}
result.giftBadge = {
receiptCredentialPresentation: Bytes.fromBase64(
giftBadge.receiptCredentialPresentation
),
state,
};
result.giftBadge = {
receiptCredentialPresentation: Bytes.fromBase64(
giftBadge.receiptCredentialPresentation
),
state,
};
}
} else {
result.standardMessage = await this.toStandardMessage(
message,

View file

@ -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 {
// eslint-disable-next-line camelcase
patch: { sendStateByConversationId, received_at_ms },
patch: {
sendStateByConversationId,
// eslint-disable-next-line camelcase
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({

View file

@ -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;

View file

@ -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'

View file

@ -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,
},
],
},

View file

@ -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 {
try {
await bootstrap.teardown();
} catch (error) {
console.error(`Failed to teardown ${basename(filePath)}`, error);
}
// No need to block on this
drop(
(async () => {
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,
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;

View file

@ -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,
};