Fix some backup integration tests

This commit is contained in:
Fedor Indutny 2024-09-16 15:12:41 -07:00 committed by GitHub
parent aa75ec13a6
commit f91ec886a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 450 additions and 231 deletions

View file

@ -4290,7 +4290,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see
``` ```
## attest 0.1.0, libsignal-ffi 0.55.1, libsignal-jni 0.55.1, libsignal-jni-testing 0.55.1, libsignal-node 0.55.1, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-bridge-testing 0.1.0, libsignal-bridge-types 0.1.0, libsignal-core 0.1.0, signal-crypto 0.1.0, device-transfer 0.1.0, libsignal-keytrans 0.0.1, signal-media 0.1.0, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, signal-pin 0.1.0, poksho 0.7.0, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0 ## attest 0.1.0, libsignal-ffi 0.56.1, libsignal-jni 0.56.1, libsignal-jni-testing 0.56.1, libsignal-node 0.56.1, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-bridge-testing 0.1.0, libsignal-bridge-types 0.1.0, libsignal-core 0.1.0, signal-crypto 0.1.0, device-transfer 0.1.0, libsignal-keytrans 0.0.1, signal-media 0.1.0, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, signal-pin 0.1.0, poksho 0.7.0, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0
``` ```
GNU AFFERO GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE
@ -9518,6 +9518,33 @@ SOFTWARE.
``` ```
## tokio-socks 0.5.2
```
MIT License
Copyright (c) 2018 Yilin Chen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## zeroize 1.8.1 ## zeroize 1.8.1
``` ```

View file

@ -997,7 +997,9 @@ async function createWindow() {
mainWindow.webContents.send('ci:event', 'db-initialized', {}); mainWindow.webContents.send('ci:event', 'db-initialized', {});
const shouldShowWindow = const shouldShowWindow =
!app.getLoginItemSettings().wasOpenedAsHidden && !startInTray; !app.getLoginItemSettings().wasOpenedAsHidden &&
!startInTray &&
!config.get<boolean>('ciIsBackupIntegration');
if (shouldShowWindow) { if (shouldShowWindow) {
getLogger().info('showing main window'); getLogger().info('showing main window');
@ -2724,7 +2726,7 @@ ipc.on('get-config', async event => {
dnsFallback: await getDNSFallback(), dnsFallback: await getDNSFallback(),
disableIPv6: DISABLE_IPV6, disableIPv6: DISABLE_IPV6,
ciBackupPath: config.get<string | null>('ciBackupPath') || undefined, ciBackupPath: config.get<string | null>('ciBackupPath') || undefined,
ciIsPlaintextBackup: config.get<boolean>('ciIsPlaintextBackup'), ciIsBackupIntegration: config.get<boolean>('ciIsBackupIntegration'),
nodeVersion: process.versions.node, nodeVersion: process.versions.node,
hostname: os.hostname(), hostname: os.hostname(),
osRelease: os.release(), osRelease: os.release(),

View file

@ -18,7 +18,7 @@
"updatesEnabled": false, "updatesEnabled": false,
"ciMode": false, "ciMode": false,
"ciBackupPath": null, "ciBackupPath": null,
"ciIsPlaintextBackup": false, "ciIsBackupIntegration": false,
"forcePreloadBundle": false, "forcePreloadBundle": false,
"openDevTools": false, "openDevTools": false,
"buildCreation": 0, "buildCreation": 0,

9
package-lock.json generated
View file

@ -21,7 +21,7 @@
"@react-aria/utils": "3.16.0", "@react-aria/utils": "3.16.0",
"@react-spring/web": "9.5.5", "@react-spring/web": "9.5.5",
"@signalapp/better-sqlite3": "8.8.1", "@signalapp/better-sqlite3": "8.8.1",
"@signalapp/libsignal-client": "0.55.1", "@signalapp/libsignal-client": "0.56.1",
"@signalapp/ringrtc": "2.47.1", "@signalapp/ringrtc": "2.47.1",
"@types/fabric": "4.5.3", "@types/fabric": "4.5.3",
"backbone": "1.4.0", "backbone": "1.4.0",
@ -7230,10 +7230,11 @@
} }
}, },
"node_modules/@signalapp/libsignal-client": { "node_modules/@signalapp/libsignal-client": {
"version": "0.55.1", "version": "0.56.1",
"resolved": "https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.55.1.tgz", "resolved": "https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.56.1.tgz",
"integrity": "sha512-qa2sztxNy5QyXYg9Z8xH9zdYikwNORyWr/95HnLAdzf4YFGsee/8JS74L+2kAn55lE7CVD+EVpgXJYFFw2Gu/w==", "integrity": "sha512-lgOdtf/63G1LuiHTrpxfwumwERuqSd1FM5Q5ZyBo8NUDQoQinG8nyza7XUXggaC/+VFggJUvAfZZvRNkJS7RTA==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"node-gyp-build": "^4.2.3", "node-gyp-build": "^4.2.3",
"type-fest": "^3.5.0", "type-fest": "^3.5.0",

View file

@ -105,7 +105,7 @@
"@react-aria/utils": "3.16.0", "@react-aria/utils": "3.16.0",
"@react-spring/web": "9.5.5", "@react-spring/web": "9.5.5",
"@signalapp/better-sqlite3": "8.8.1", "@signalapp/better-sqlite3": "8.8.1",
"@signalapp/libsignal-client": "0.55.1", "@signalapp/libsignal-client": "0.56.1",
"@signalapp/ringrtc": "2.47.1", "@signalapp/ringrtc": "2.47.1",
"@types/fabric": "4.5.3", "@types/fabric": "4.5.3",
"backbone": "1.4.0", "backbone": "1.4.0",

View file

@ -276,7 +276,7 @@ message AdHocCall {
} }
uint64 callId = 1; uint64 callId = 1;
// Refers to a Recipient with the `callLink` field set // Refers to a `CallLink` recipient.
uint64 recipientId = 2; uint64 recipientId = 2;
State state = 3; State state = 3;
uint64 callTimestamp = 4; uint64 callTimestamp = 4;
@ -477,7 +477,6 @@ message ContactAttachment {
optional string prefix = 3; optional string prefix = 3;
optional string suffix = 4; optional string suffix = 4;
optional string middleName = 5; optional string middleName = 5;
optional string displayName = 6;
} }
message Phone { message Phone {
@ -652,10 +651,9 @@ message Quote {
optional uint64 targetSentTimestamp = 1; // null if the target message could not be found at time of quote insert optional uint64 targetSentTimestamp = 1; // null if the target message could not be found at time of quote insert
uint64 authorId = 2; uint64 authorId = 2;
optional string text = 3; optional Text text = 3;
repeated QuotedAttachment attachments = 4; repeated QuotedAttachment attachments = 4;
repeated BodyRange bodyRanges = 5; Type type = 5;
Type type = 6;
} }
message BodyRange { message BodyRange {
@ -681,11 +679,9 @@ message Reaction {
string emoji = 1; string emoji = 1;
uint64 authorId = 2; uint64 authorId = 2;
uint64 sentTimestamp = 3; uint64 sentTimestamp = 3;
// Optional because some clients may not track this data
optional uint64 receivedTimestamp = 4;
// A higher sort order means that a reaction is more recent. Some clients may export this as // A higher sort order means that a reaction is more recent. Some clients may export this as
// incrementing numbers (e.g. 1, 2, 3), others as timestamps. // incrementing numbers (e.g. 1, 2, 3), others as timestamps.
uint64 sortOrder = 5; uint64 sortOrder = 4;
} }
message ChatUpdateMessage { message ChatUpdateMessage {
@ -1069,7 +1065,7 @@ message StickerPack {
message ChatStyle { message ChatStyle {
message Gradient { message Gradient {
uint32 angle = 1; // degrees uint32 angle = 1; // degrees
repeated fixed32 colors = 2; repeated fixed32 colors = 2; // 0xAARRGGBB
repeated float positions = 3; // percent from 0 to 1 repeated float positions = 3; // percent from 0 to 1
} }
@ -1077,7 +1073,7 @@ message ChatStyle {
uint64 id = 1; uint64 id = 1;
oneof color { oneof color {
fixed32 solid = 2; fixed32 solid = 2; // 0xAARRGGBB
Gradient gradient = 3; Gradient gradient = 3;
} }
} }

View file

@ -1,7 +1,9 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { format } from 'node:util';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { BackupLevel } from '@signalapp/libsignal-client/zkgroup';
import type { IPCResponse as ChallengeResponseType } from './challenge'; import type { IPCResponse as ChallengeResponseType } from './challenge';
import type { MessageAttributesType } from './model-types.d'; import type { MessageAttributesType } from './model-types.d';
@ -18,7 +20,7 @@ type ResolveType = (data: unknown) => void;
export type CIType = { export type CIType = {
deviceName: string; deviceName: string;
backupData?: Uint8Array; backupData?: Uint8Array;
isPlaintextBackup?: boolean; isBackupIntegration?: boolean;
getConversationId: (address: string | null) => string | null; getConversationId: (address: string | null) => string | null;
getMessagesBySentAt( getMessagesBySentAt(
sentAt: number sentAt: number
@ -38,18 +40,19 @@ export type CIType = {
exportBackupToDisk(path: string): Promise<void>; exportBackupToDisk(path: string): Promise<void>;
exportPlaintextBackupToDisk(path: string): Promise<void>; exportPlaintextBackupToDisk(path: string): Promise<void>;
unlink: () => void; unlink: () => void;
print: (...args: ReadonlyArray<unknown>) => void;
}; };
export type GetCIOptionsType = Readonly<{ export type GetCIOptionsType = Readonly<{
deviceName: string; deviceName: string;
backupData?: Uint8Array; backupData?: Uint8Array;
isPlaintextBackup?: boolean; isBackupIntegration?: boolean;
}>; }>;
export function getCI({ export function getCI({
deviceName, deviceName,
backupData, backupData,
isPlaintextBackup, isBackupIntegration,
}: GetCIOptionsType): CIType { }: GetCIOptionsType): CIType {
const eventListeners = new Map<string, Array<ResolveType>>(); const eventListeners = new Map<string, Array<ResolveType>>();
const completedEvents = new Map<string, Array<unknown>>(); const completedEvents = new Map<string, Array<unknown>>();
@ -174,13 +177,13 @@ export function getCI({
} }
async function exportBackupToDisk(path: string) { async function exportBackupToDisk(path: string) {
await backupsService.exportToDisk(path); await backupsService.exportToDisk(path, BackupLevel.Media);
} }
async function exportPlaintextBackupToDisk(path: string) { async function exportPlaintextBackupToDisk(path: string) {
await backupsService.exportToDisk( await backupsService.exportToDisk(
path, path,
undefined, BackupLevel.Media,
BackupType.TestOnlyPlaintext BackupType.TestOnlyPlaintext
); );
} }
@ -189,10 +192,14 @@ export function getCI({
window.Whisper.events.trigger('unlinkAndDisconnect'); window.Whisper.events.trigger('unlinkAndDisconnect');
} }
function print(...args: ReadonlyArray<unknown>) {
handleEvent('print', format(...args));
}
return { return {
deviceName, deviceName,
backupData, backupData,
isPlaintextBackup, isBackupIntegration,
getConversationId, getConversationId,
getMessagesBySentAt, getMessagesBySentAt,
handleEvent, handleEvent,
@ -204,5 +211,6 @@ export function getCI({
exportPlaintextBackupToDisk, exportPlaintextBackupToDisk,
unlink, unlink,
getPendingEventCount, getPendingEventCount,
print,
}; };
} }

View file

@ -182,6 +182,10 @@ export class ConversationController {
// we can reset the mute state on the model. If the mute has already expired // we can reset the mute state on the model. If the mute has already expired
// then we reset the state right away. // then we reset the state right away.
this._conversations.on('add', (model: ConversationModel): void => { this._conversations.on('add', (model: ConversationModel): void => {
// Don't modify conversations in backup integration testing
if (window.SignalCI?.isBackupIntegration) {
return;
}
model.startMuteTimer(); model.startMuteTimer();
}); });
} }

View file

@ -20,7 +20,10 @@ import type { ConversationType } from '../../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges'; import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
import { groupBy } from '../../util/mapUtil'; import { groupBy } from '../../util/mapUtil';
import type { ContactNameColorType } from '../../types/Colors'; import type { ContactNameColorType } from '../../types/Colors';
import { SendStatus } from '../../messages/MessageSendState'; import {
SendStatus,
type VisibleSendStatus,
} from '../../messages/MessageSendState';
import { WidthBreakpoint } from '../_util'; import { WidthBreakpoint } from '../_util';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { formatDateTimeLong } from '../../util/timestamp'; import { formatDateTimeLong } from '../../util/timestamp';
@ -234,7 +237,7 @@ export function MessageDetail({
} }
function renderContactGroupHeaderText( function renderContactGroupHeaderText(
sendStatus: undefined | SendStatus sendStatus: undefined | VisibleSendStatus
): string { ): string {
if (sendStatus === undefined) { if (sendStatus === undefined) {
return i18n('icu:from'); return i18n('icu:from');
@ -259,7 +262,7 @@ export function MessageDetail({
} }
function renderContactGroup( function renderContactGroup(
sendStatus: undefined | SendStatus, sendStatus: undefined | VisibleSendStatus,
statusContacts: undefined | ReadonlyArray<Contact> statusContacts: undefined | ReadonlyArray<Contact>
): ReactNode { ): ReactNode {
if (!statusContacts || !statusContacts.length) { if (!statusContacts || !statusContacts.length) {
@ -295,7 +298,8 @@ export function MessageDetail({
return ( return (
<div className="module-message-detail__contact-container"> <div className="module-message-detail__contact-container">
{[ {(
[
undefined, undefined,
SendStatus.Failed, SendStatus.Failed,
SendStatus.Viewed, SendStatus.Viewed,
@ -303,7 +307,8 @@ export function MessageDetail({
SendStatus.Delivered, SendStatus.Delivered,
SendStatus.Sent, SendStatus.Sent,
SendStatus.Pending, SendStatus.Pending,
].map(sendStatus => ] as Array<VisibleSendStatus | undefined>
).map(sendStatus =>
renderContactGroup(sendStatus, contactsBySendStatus.get(sendStatus)) renderContactGroup(sendStatus, contactsBySendStatus.get(sendStatus))
)} )}
</div> </div>

View file

@ -5685,7 +5685,7 @@ export async function applyNewAvatar(
// Group has avatar; has it changed? // Group has avatar; has it changed?
if ( if (
newAvatarUrl && newAvatarUrl &&
(!attributes.avatar || attributes.avatar.url !== newAvatarUrl) (!attributes.avatar?.path || attributes.avatar.url !== newAvatarUrl)
) { ) {
if (!attributes.secretParams) { if (!attributes.secretParams) {
throw new Error('applyNewAvatar: group was missing secretParams!'); throw new Error('applyNewAvatar: group was missing secretParams!');

View file

@ -219,6 +219,9 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
} }
static async start(): Promise<void> { static async start(): Promise<void> {
if (window.SignalCI?.isBackupIntegration) {
return;
}
await AttachmentDownloadManager.instance.start(); await AttachmentDownloadManager.instance.start();
} }

View file

@ -33,6 +33,7 @@ export enum SendStatus {
Delivered = 'Delivered', Delivered = 'Delivered',
Read = 'Read', Read = 'Read',
Viewed = 'Viewed', Viewed = 'Viewed',
Skipped = 'Skipped',
} }
export const parseMessageSendStatus = makeEnumParser( export const parseMessageSendStatus = makeEnumParser(
@ -45,6 +46,14 @@ export const UNDELIVERED_SEND_STATUSES = [
SendStatus.Failed, SendStatus.Failed,
]; ];
export type VisibleSendStatus =
| SendStatus.Failed
| SendStatus.Pending
| SendStatus.Sent
| SendStatus.Delivered
| SendStatus.Read
| SendStatus.Viewed;
const STATUS_NUMBERS: Record<SendStatus, number> = { const STATUS_NUMBERS: Record<SendStatus, number> = {
[SendStatus.Failed]: 0, [SendStatus.Failed]: 0,
[SendStatus.Pending]: 1, [SendStatus.Pending]: 1,
@ -52,6 +61,7 @@ const STATUS_NUMBERS: Record<SendStatus, number> = {
[SendStatus.Delivered]: 3, [SendStatus.Delivered]: 3,
[SendStatus.Read]: 4, [SendStatus.Read]: 4,
[SendStatus.Viewed]: 5, [SendStatus.Viewed]: 5,
[SendStatus.Skipped]: 6,
}; };
export const maxStatus = (a: SendStatus, b: SendStatus): SendStatus => export const maxStatus = (a: SendStatus, b: SendStatus): SendStatus =>
@ -69,6 +79,8 @@ export const isSent = (status: SendStatus): boolean =>
STATUS_NUMBERS[status] >= STATUS_NUMBERS[SendStatus.Sent]; STATUS_NUMBERS[status] >= STATUS_NUMBERS[SendStatus.Sent];
export const isFailed = (status: SendStatus): boolean => export const isFailed = (status: SendStatus): boolean =>
status === SendStatus.Failed; status === SendStatus.Failed;
export const isSkipped = (status: SendStatus): boolean =>
status === SendStatus.Skipped;
/** /**
* `SendState` combines `SendStatus` and a timestamp. You can use it to show things to the * `SendState` combines `SendStatus` and a timestamp. You can use it to show things to the
@ -88,7 +100,8 @@ export type SendState = Readonly<{
| SendStatus.Sent | SendStatus.Sent
| SendStatus.Delivered | SendStatus.Delivered
| SendStatus.Read | SendStatus.Read
| SendStatus.Viewed; | SendStatus.Viewed
| SendStatus.Skipped;
updatedAt?: number; updatedAt?: number;
}>; }>;

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

@ -119,7 +119,6 @@ export type MessageReactionType = {
fromId: string; fromId: string;
targetTimestamp: number; targetTimestamp: number;
timestamp: number; timestamp: number;
receivedAtDate: undefined | number;
isSentByConversationId?: Record<string, boolean>; isSentByConversationId?: Record<string, boolean>;
}; };
@ -337,6 +336,7 @@ export type ConversationAttributesType = {
wallpaperPhotoPointerBase64?: string; wallpaperPhotoPointerBase64?: string;
wallpaperPreset?: number; wallpaperPreset?: number;
dimWallpaperInDarkMode?: boolean; dimWallpaperInDarkMode?: boolean;
autoBubbleColor?: boolean;
discoveredUnregisteredAt?: number; discoveredUnregisteredAt?: number;
firstUnregisteredAt?: number; firstUnregisteredAt?: number;

View file

@ -2265,7 +2265,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
fromId: reaction.fromId, fromId: reaction.fromId,
targetTimestamp: reaction.targetTimestamp, targetTimestamp: reaction.targetTimestamp,
timestamp: reaction.timestamp, timestamp: reaction.timestamp,
receivedAtDate: reaction.receivedAtDate,
isSentByConversationId: isFromThisDevice isSentByConversationId: isFromThisDevice
? zipObject(conversation.getMemberConversationIds(), repeat(false)) ? zipObject(conversation.getMemberConversationIds(), repeat(false))
: undefined, : undefined,

View file

@ -49,7 +49,7 @@ import {
} from '../../util/whatTypeOfConversation'; } from '../../util/whatTypeOfConversation';
import { isConversationUnregistered } from '../../util/isConversationUnregistered'; import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import { uuidToBytes } from '../../util/uuidToBytes'; import { uuidToBytes } from '../../util/uuidToBytes';
import { assertDev, strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { getSafeLongFromTimestamp } from '../../util/timestampLongUtils'; import { getSafeLongFromTimestamp } from '../../util/timestampLongUtils';
import { DAY, MINUTE, SECOND, DurationInSeconds } from '../../util/durations'; import { DAY, MINUTE, SECOND, DurationInSeconds } from '../../util/durations';
import { import {
@ -89,7 +89,10 @@ import * as Bytes from '../../Bytes';
import { canBeSynced as canPreferredReactionEmojiBeSynced } from '../../reactions/preferredReactionEmoji'; import { canBeSynced as canPreferredReactionEmojiBeSynced } from '../../reactions/preferredReactionEmoji';
import { SendStatus } from '../../messages/MessageSendState'; import { SendStatus } from '../../messages/MessageSendState';
import { BACKUP_VERSION } from './constants'; import { BACKUP_VERSION } from './constants';
import { getMessageIdForLogging } from '../../util/idForLogging'; import {
getMessageIdForLogging,
getConversationIdForLogging,
} from '../../util/idForLogging';
import { makeLookup } from '../../util/makeLookup'; import { makeLookup } from '../../util/makeLookup';
import type { import type {
CallHistoryDetails, CallHistoryDetails,
@ -197,6 +200,8 @@ export class BackupExportStream extends Readable {
private readonly backupTimeMs = getSafeLongFromTimestamp(this.now); private readonly backupTimeMs = getSafeLongFromTimestamp(this.now);
private readonly convoIdToRecipientId = new Map<string, number>(); private readonly convoIdToRecipientId = new Map<string, number>();
private readonly serviceIdToRecipientId = new Map<string, number>();
private readonly e164ToRecipientId = new Map<string, number>();
private readonly roomIdToRecipientId = new Map<string, number>(); private readonly roomIdToRecipientId = new Map<string, number>();
private attachmentBackupJobs: Array<CoreAttachmentBackupJobType> = []; private attachmentBackupJobs: Array<CoreAttachmentBackupJobType> = [];
private buffers = new Array<Uint8Array>(); private buffers = new Array<Uint8Array>();
@ -222,12 +227,14 @@ export class BackupExportStream extends Readable {
// TODO (DESKTOP-7344): Clear & add backup jobs in a single transaction // TODO (DESKTOP-7344): Clear & add backup jobs in a single transaction
await DataWriter.clearAllAttachmentBackupJobs(); await DataWriter.clearAllAttachmentBackupJobs();
if (!window.SignalCI?.isBackupIntegration) {
await Promise.all( await Promise.all(
this.attachmentBackupJobs.map(job => this.attachmentBackupJobs.map(job =>
AttachmentBackupManager.addJobAndMaybeThumbnailJob(job) AttachmentBackupManager.addJobAndMaybeThumbnailJob(job)
) )
); );
drop(AttachmentBackupManager.start()); drop(AttachmentBackupManager.start());
}
log.info('BackupExportStream: finished'); log.info('BackupExportStream: finished');
} }
})() })()
@ -407,7 +414,17 @@ export class BackupExportStream extends Readable {
let pinnedOrder: number | null = null; let pinnedOrder: number | null = null;
if (attributes.isPinned) { if (attributes.isPinned) {
pinnedOrder = Math.max(0, pinnedConversationIds.indexOf(attributes.id)); const index = pinnedConversationIds.indexOf(attributes.id);
if (index === -1) {
const convoId = getConversationIdForLogging(attributes);
log.warn(`backups: ${convoId} is pinned, but is not on the list`);
}
pinnedOrder = Math.max(1, index + 1);
}
// Skip conversations that have no presence in left pane (no chats)
if (!attributes.isPinned && !attributes.active_at) {
continue;
} }
this.pushFrame({ this.pushFrame({
@ -438,6 +455,7 @@ export class BackupExportStream extends Readable {
color: attributes.conversationColor, color: attributes.conversationColor,
customColorId: attributes.customColorId, customColorId: attributes.customColorId,
dimWallpaperInDarkMode: attributes.dimWallpaperInDarkMode, dimWallpaperInDarkMode: attributes.dimWallpaperInDarkMode,
autoBubbleColor: attributes.autoBubbleColor,
}), }),
}, },
}); });
@ -686,22 +704,29 @@ export class BackupExportStream extends Readable {
}; };
} }
private getRecipientIdentifier({ private getExistingRecipientId(
id, options: GetRecipientIdOptionsType
serviceId, ): Long | undefined {
e164, let existing: number | undefined;
}: GetRecipientIdOptionsType): string { if (options.serviceId != null) {
const identifier = serviceId ?? e164 ?? id; existing = this.serviceIdToRecipientId.get(options.serviceId);
assertDev(identifier, 'Identifier cannot be blank'); }
return identifier; if (existing === undefined && options.e164 != null) {
existing = this.e164ToRecipientId.get(options.e164);
}
if (existing === undefined && options.id != null) {
existing = this.convoIdToRecipientId.get(options.id);
}
if (existing !== undefined) {
return Long.fromNumber(existing);
}
return undefined;
} }
private getRecipientId(options: GetRecipientIdOptionsType): Long { private getRecipientId(options: GetRecipientIdOptionsType): Long {
const identifier = this.getRecipientIdentifier(options); const existing = this.getExistingRecipientId(options);
const existing = this.convoIdToRecipientId.get(identifier);
if (existing !== undefined) { if (existing !== undefined) {
return Long.fromNumber(existing); return existing;
} }
const { id, serviceId, e164 } = options; const { id, serviceId, e164 } = options;
@ -713,10 +738,10 @@ export class BackupExportStream extends Readable {
this.convoIdToRecipientId.set(id, recipientId); this.convoIdToRecipientId.set(id, recipientId);
} }
if (serviceId !== undefined) { if (serviceId !== undefined) {
this.convoIdToRecipientId.set(serviceId, recipientId); this.serviceIdToRecipientId.set(serviceId, recipientId);
} }
if (e164 !== undefined) { if (e164 !== undefined) {
this.convoIdToRecipientId.set(e164, recipientId); this.e164ToRecipientId.set(e164, recipientId);
} }
const result = Long.fromNumber(recipientId); const result = Long.fromNumber(recipientId);
@ -724,8 +749,8 @@ export class BackupExportStream extends Readable {
} }
private getOrPushPrivateRecipient(options: GetRecipientIdOptionsType): Long { private getOrPushPrivateRecipient(options: GetRecipientIdOptionsType): Long {
const identifier = this.getRecipientIdentifier(options); const existing = this.getExistingRecipientId(options);
const needsPush = !this.convoIdToRecipientId.has(identifier); const needsPush = existing == null;
const result = this.getRecipientId(options); const result = this.getRecipientId(options);
if (needsPush) { if (needsPush) {
@ -832,9 +857,12 @@ export class BackupExportStream extends Readable {
title: { title: {
title: convo.name ?? '', title: convo.name ?? '',
}, },
description: { description:
descriptionText: convo.description ?? '', convo.description != null
}, ? {
descriptionText: convo.description,
}
: null,
avatarUrl: convo.avatar?.url, avatarUrl: convo.avatar?.url,
disappearingMessagesTimer: disappearingMessagesTimer:
convo.expireTimer != null convo.expireTimer != null
@ -848,9 +876,7 @@ export class BackupExportStream extends Readable {
version: convo.revision || 0, version: convo.revision || 0,
members: convo.membersV2?.map(member => { members: convo.membersV2?.map(member => {
const memberConvo = window.ConversationController.get(member.aci); const memberConvo = window.ConversationController.get(member.aci);
strictAssert(memberConvo, 'Missing GV2 member'); const { profileKey } = memberConvo?.attributes ?? {};
const { profileKey } = memberConvo.attributes;
return { return {
userId: this.aciToBytes(member.aci), userId: this.aciToBytes(member.aci),
@ -876,9 +902,7 @@ export class BackupExportStream extends Readable {
membersPendingAdminApproval: convo.pendingAdminApprovalV2?.map( membersPendingAdminApproval: convo.pendingAdminApprovalV2?.map(
member => { member => {
const memberConvo = window.ConversationController.get(member.aci); const memberConvo = window.ConversationController.get(member.aci);
strictAssert(memberConvo, 'Missing GV2 member pending approval'); const { profileKey } = memberConvo?.attributes ?? {};
const { profileKey } = memberConvo.attributes;
return { return {
userId: this.aciToBytes(member.aci), userId: this.aciToBytes(member.aci),
profileKey: profileKey profileKey: profileKey
@ -2024,7 +2048,15 @@ export class BackupExportStream extends Readable {
return { return {
targetSentTimestamp: Long.fromNumber(quote.id), targetSentTimestamp: Long.fromNumber(quote.id),
authorId, authorId,
text: quote.text, text:
quote.text != null
? {
body: quote.text,
bodyRanges: quote.bodyRanges?.map(range =>
this.toBodyRange(range)
),
}
: null,
attachments: await Promise.all( attachments: await Promise.all(
quote.attachments.map( quote.attachments.map(
async ( async (
@ -2044,7 +2076,6 @@ export class BackupExportStream extends Readable {
} }
) )
), ),
bodyRanges: quote.bodyRanges?.map(range => this.toBodyRange(range)),
type: quote.isGiftBadge type: quote.isGiftBadge
? Backups.Quote.Type.GIFTBADGE ? Backups.Quote.Type.GIFTBADGE
: Backups.Quote.Type.NORMAL, : Backups.Quote.Type.NORMAL,
@ -2132,6 +2163,9 @@ export class BackupExportStream extends Readable {
// new keys so that we don't try to re-upload it again on the next export // new keys so that we don't try to re-upload it again on the next export
} }
// We don't download attachments during integration tests and thus have no
// "iv" for an attachment and can't create a job
if (!window.SignalCI?.isBackupIntegration) {
const backupJob = await maybeGetBackupJobForAttachmentAndFilePointer({ const backupJob = await maybeGetBackupJobForAttachmentAndFilePointer({
attachment: updatedAttachment ?? attachment, attachment: updatedAttachment ?? attachment,
filePointer, filePointer,
@ -2142,6 +2176,7 @@ export class BackupExportStream extends Readable {
if (backupJob) { if (backupJob) {
this.attachmentBackupJobs.push(backupJob); this.attachmentBackupJobs.push(backupJob);
} }
}
return filePointer; return filePointer;
} }
@ -2162,9 +2197,6 @@ export class BackupExportStream extends Readable {
id: reaction.fromId, id: reaction.fromId,
}), }),
sentTimestamp: getSafeLongFromTimestamp(reaction.timestamp), sentTimestamp: getSafeLongFromTimestamp(reaction.timestamp),
receivedTimestamp: getSafeLongFromTimestamp(
reaction.receivedAtDate ?? reaction.timestamp
),
sortOrder: Long.fromNumber(sortOrder), sortOrder: Long.fromNumber(sortOrder),
}; };
}); });
@ -2259,6 +2291,9 @@ export class BackupExportStream extends Readable {
sealedSender, sealedSender,
}); });
break; break;
case SendStatus.Skipped:
sendStatus.skipped = {};
break;
case SendStatus.Failed: { case SendStatus.Failed: {
sendStatus.failed = new Backups.SendStatus.Failed(); sendStatus.failed = new Backups.SendStatus.Failed();
if (!serviceId) { if (!serviceId) {
@ -2273,6 +2308,10 @@ export class BackupExportStream extends Readable {
if (identityKeyMismatch) { if (identityKeyMismatch) {
sendStatus.failed.reason = sendStatus.failed.reason =
Backups.SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH; Backups.SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH;
} else if (errorName === 'UnknownError') {
// See ts/backups/import.ts
sendStatus.failed.reason =
Backups.SendStatus.Failed.FailureReason.UNKNOWN;
} else { } else {
sendStatus.failed.reason = sendStatus.failed.reason =
Backups.SendStatus.Failed.FailureReason.NETWORK; Backups.SendStatus.Failed.FailureReason.NETWORK;
@ -2430,7 +2469,7 @@ export class BackupExportStream extends Readable {
const result = new Array<Backups.ChatStyle.ICustomChatColor>(); const result = new Array<Backups.ChatStyle.ICustomChatColor>();
for (const [uuid, color] of map.entries()) { for (const [uuid, color] of map.entries()) {
const id = Long.fromNumber(result.length); const id = Long.fromNumber(result.length + 1);
this.customColorIdByUuid.set(uuid, id); this.customColorIdByUuid.set(uuid, id);
const start = hslToRGBInt( const start = hslToRGBInt(
@ -2465,18 +2504,25 @@ export class BackupExportStream extends Readable {
return result; return result;
} }
private toDefaultChatStyle(): Backups.IChatStyle { private toDefaultChatStyle(): Backups.IChatStyle | null {
const defaultColor = window.storage.get('defaultConversationColor'); const defaultColor = window.storage.get('defaultConversationColor');
const wallpaperPhotoPointer = window.storage.get(
return this.toChatStyle({ 'defaultWallpaperPhotoPointer'
wallpaperPhotoPointer: window.storage.get('defaultWallpaperPhotoPointer'), );
wallpaperPreset: window.storage.get('defaultWallpaperPreset'), const wallpaperPreset = window.storage.get('defaultWallpaperPreset');
color: defaultColor?.color, const dimWallpaperInDarkMode = window.storage.get(
customColorId: defaultColor?.customColorData?.id,
dimWallpaperInDarkMode: window.storage.get(
'defaultDimWallpaperInDarkMode', 'defaultDimWallpaperInDarkMode',
false false
), );
const autoBubbleColor = window.storage.get('defaultAutoBubbleColor');
return this.toChatStyle({
wallpaperPhotoPointer,
wallpaperPreset,
color: defaultColor?.color,
customColorId: defaultColor?.customColorData?.id,
dimWallpaperInDarkMode,
autoBubbleColor,
}); });
} }
@ -2486,18 +2532,30 @@ export class BackupExportStream extends Readable {
color, color,
customColorId, customColorId,
dimWallpaperInDarkMode, dimWallpaperInDarkMode,
}: LocalChatStyle): Backups.IChatStyle { autoBubbleColor,
}: LocalChatStyle): Backups.IChatStyle | null {
const result: Backups.IChatStyle = { const result: Backups.IChatStyle = {
dimWallpaperInDarkMode, dimWallpaperInDarkMode,
}; };
// The defaults
if (
(color == null || color === 'ultramarine') &&
wallpaperPhotoPointer == null &&
wallpaperPreset == null &&
!dimWallpaperInDarkMode &&
(autoBubbleColor === true || autoBubbleColor == null)
) {
return null;
}
if (Bytes.isNotEmpty(wallpaperPhotoPointer)) { if (Bytes.isNotEmpty(wallpaperPhotoPointer)) {
result.wallpaperPhoto = Backups.FilePointer.decode(wallpaperPhotoPointer); result.wallpaperPhoto = Backups.FilePointer.decode(wallpaperPhotoPointer);
} else if (wallpaperPreset) { } else if (wallpaperPreset) {
result.wallpaperPreset = wallpaperPreset; result.wallpaperPreset = wallpaperPreset;
} }
if (color == null) { if (color == null || autoBubbleColor) {
result.autoBubbleColor = {}; result.autoBubbleColor = {};
return result; return result;
} }

View file

@ -380,8 +380,10 @@ export class BackupImportStream extends Writable {
// Schedule group avatar download. // Schedule group avatar download.
await pMap( await pMap(
[...this.pendingGroupAvatars.entries()], [...this.pendingGroupAvatars.entries()],
([conversationId, newAvatarUrl]) => { async ([conversationId, newAvatarUrl]) => {
return groupAvatarJobQueue.add({ conversationId, newAvatarUrl }); if (!window.SignalCI?.isBackupIntegration) {
await groupAvatarJobQueue.add({ conversationId, newAvatarUrl });
}
}, },
{ concurrency: MAX_CONCURRENCY } { concurrency: MAX_CONCURRENCY }
); );
@ -403,7 +405,9 @@ export class BackupImportStream extends Writable {
await DataReader.getSizeOfPendingBackupAttachmentDownloadJobs() await DataReader.getSizeOfPendingBackupAttachmentDownloadJobs()
); );
if (!window.SignalCI?.isBackupIntegration) {
await AttachmentDownloadManager.start(); await AttachmentDownloadManager.start();
}
done(); done();
} catch (error) { } catch (error) {
@ -724,6 +728,12 @@ export class BackupImportStream extends Writable {
defaultChatStyle.dimWallpaperInDarkMode defaultChatStyle.dimWallpaperInDarkMode
); );
} }
if (defaultChatStyle.autoBubbleColor != null) {
await window.storage.put(
'defaultAutoBubbleColor',
defaultChatStyle.autoBubbleColor
);
}
this.updateConversation(me); this.updateConversation(me);
} }
@ -855,6 +865,11 @@ export class BackupImportStream extends Writable {
profileSharing: group.whitelisted === true, profileSharing: group.whitelisted === true,
hideStory: group.hideStory === true, hideStory: group.hideStory === true,
storySendMode, storySendMode,
avatar: avatarUrl
? {
url: avatarUrl,
}
: undefined,
// Snapshot // Snapshot
name: dropNull(title?.title), name: dropNull(title?.title),
@ -1098,8 +1113,12 @@ export class BackupImportStream extends Writable {
this.chatIdToConvo.set(chat.id.toNumber(), conversation); this.chatIdToConvo.set(chat.id.toNumber(), conversation);
// Make sure conversation appears in left pane
if (conversation.active_at == null) {
conversation.active_at = Math.max(chat.id.toNumber(), 1);
}
conversation.isArchived = chat.archived === true; conversation.isArchived = chat.archived === true;
conversation.isPinned = chat.pinnedOrder != null; conversation.isPinned = (chat.pinnedOrder || 0) !== 0;
conversation.expireTimer = conversation.expireTimer =
chat.expirationTimerMs && !chat.expirationTimerMs.isZero() chat.expirationTimerMs && !chat.expirationTimerMs.isZero()
@ -1134,6 +1153,9 @@ export class BackupImportStream extends Writable {
if (chatStyle.dimWallpaperInDarkMode != null) { if (chatStyle.dimWallpaperInDarkMode != null) {
conversation.dimWallpaperInDarkMode = chatStyle.dimWallpaperInDarkMode; conversation.dimWallpaperInDarkMode = chatStyle.dimWallpaperInDarkMode;
} }
if (chatStyle.autoBubbleColor) {
conversation.autoBubbleColor = chatStyle.autoBubbleColor;
}
this.updateConversation(conversation); this.updateConversation(conversation);
@ -1334,10 +1356,6 @@ export class BackupImportStream extends Writable {
'status target conversation not found' 'status target conversation not found'
); );
// Desktop does not keep track of users we did not attempt to send to
if (status.skipped) {
continue;
}
const { serviceId } = target; const { serviceId } = target;
let sendStatus: SendStatus; let sendStatus: SendStatus;
@ -1379,7 +1397,6 @@ export class BackupImportStream extends Writable {
}); });
break; break;
case Backups.SendStatus.Failed.FailureReason.NETWORK: case Backups.SendStatus.Failed.FailureReason.NETWORK:
case Backups.SendStatus.Failed.FailureReason.UNKNOWN:
errors.push({ errors.push({
serviceId, serviceId,
name: 'OutgoingMessageError', name: 'OutgoingMessageError',
@ -1387,9 +1404,19 @@ export class BackupImportStream extends Writable {
message: 'no http error', message: 'no http error',
}); });
break; break;
case Backups.SendStatus.Failed.FailureReason.UNKNOWN:
errors.push({
serviceId,
name: 'UnknownError',
message: 'unknown error',
});
break;
default: default:
throw missingCaseError(status.failed.reason); throw missingCaseError(status.failed.reason);
} }
// Desktop does not keep track of users we did not attempt to send to
} else if (status.skipped) {
sendStatus = SendStatus.Skipped;
} else { } else {
throw new Error(`Unknown sendStatus received: ${status}`); throw new Error(`Unknown sendStatus received: ${status}`);
} }
@ -1416,7 +1443,8 @@ export class BackupImportStream extends Writable {
}; };
} }
if (incoming) { if (incoming) {
const receivedAtMs = incoming.dateReceived?.toNumber() ?? this.now; const receivedAtMs = incoming.dateReceived?.toNumber() || this.now;
const serverTimestamp = incoming.dateServerSent?.toNumber() || undefined;
const unidentifiedDeliveryReceived = incoming.sealedSender === true; const unidentifiedDeliveryReceived = incoming.sealedSender === true;
@ -1426,6 +1454,7 @@ export class BackupImportStream extends Writable {
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen, seenStatus: SeenStatus.Seen,
received_at_ms: receivedAtMs, received_at_ms: receivedAtMs,
serverTimestamp,
unidentifiedDeliveryReceived, unidentifiedDeliveryReceived,
}, },
newActiveAt: receivedAtMs, newActiveAt: receivedAtMs,
@ -1437,6 +1466,7 @@ export class BackupImportStream extends Writable {
readStatus: ReadStatus.Unread, readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen, seenStatus: SeenStatus.Unseen,
received_at_ms: receivedAtMs, received_at_ms: receivedAtMs,
serverTimestamp,
unidentifiedDeliveryReceived, unidentifiedDeliveryReceived,
}, },
newActiveAt: receivedAtMs, newActiveAt: receivedAtMs,
@ -1574,10 +1604,10 @@ export class BackupImportStream extends Writable {
{ {
id: getTimestampFromLong(quote.targetSentTimestamp), id: getTimestampFromLong(quote.targetSentTimestamp),
authorAci: authorConvo.serviceId, authorAci: authorConvo.serviceId,
text: dropNull(quote.text), text: dropNull(quote.text?.body),
bodyRanges: quote.bodyRanges?.length bodyRanges: quote.text?.bodyRanges?.length
? filterAndClean( ? filterAndClean(
quote.bodyRanges.map(range => ({ quote.text?.bodyRanges.map(range => ({
...range, ...range,
mentionAci: range.mentionAci mentionAci: range.mentionAci
? Aci.parseFromServiceIdBinary( ? Aci.parseFromServiceIdBinary(
@ -1623,17 +1653,13 @@ export class BackupImportStream extends Writable {
} }
return 0; return 0;
}) })
.map(({ emoji, authorId, sentTimestamp, receivedTimestamp }) => { .map(({ emoji, authorId, sentTimestamp }) => {
strictAssert(emoji != null, 'reaction must have an emoji'); strictAssert(emoji != null, 'reaction must have an emoji');
strictAssert(authorId != null, 'reaction must have authorId'); strictAssert(authorId != null, 'reaction must have authorId');
strictAssert( strictAssert(
sentTimestamp != null, sentTimestamp != null,
'reaction must have a sentTimestamp' 'reaction must have a sentTimestamp'
); );
strictAssert(
receivedTimestamp != null,
'reaction must have a receivedTimestamp'
);
const authorConvo = this.recipientIdToConvo.get(authorId.toNumber()); const authorConvo = this.recipientIdToConvo.get(authorId.toNumber());
strictAssert( strictAssert(
@ -1645,7 +1671,6 @@ export class BackupImportStream extends Writable {
emoji, emoji,
fromId: authorConvo.id, fromId: authorConvo.id,
targetTimestamp: getTimestampFromLong(sentTimestamp), targetTimestamp: getTimestampFromLong(sentTimestamp),
receivedAtDate: getTimestampFromLong(receivedTimestamp),
timestamp: getTimestampFromLong(sentTimestamp), timestamp: getTimestampFromLong(sentTimestamp),
}; };
}); });
@ -1681,7 +1706,6 @@ export class BackupImportStream extends Writable {
prefix: dropNull(name.prefix), prefix: dropNull(name.prefix),
suffix: dropNull(name.suffix), suffix: dropNull(name.suffix),
middleName: dropNull(name.middleName), middleName: dropNull(name.middleName),
displayName: dropNull(name.displayName),
} }
: undefined, : undefined,
number: number?.length number: number?.length
@ -2909,9 +2933,10 @@ export class BackupImportStream extends Writable {
return { return {
wallpaperPhotoPointer: undefined, wallpaperPhotoPointer: undefined,
wallpaperPreset: undefined, wallpaperPreset: undefined,
color: 'ultramarine', color: undefined,
customColorData: undefined, customColorData: undefined,
dimWallpaperInDarkMode: undefined, dimWallpaperInDarkMode: undefined,
autoBubbleColor: true,
}; };
} }
@ -2929,11 +2954,13 @@ export class BackupImportStream extends Writable {
let color: ConversationColorType | undefined; let color: ConversationColorType | undefined;
let customColorData: CustomColorDataType | undefined; let customColorData: CustomColorDataType | undefined;
let autoBubbleColor = false;
if (chatStyle.autoBubbleColor) { if (chatStyle.autoBubbleColor) {
autoBubbleColor = true;
if (wallpaperPreset != null) { if (wallpaperPreset != null) {
color = WALLPAPER_TO_BUBBLE_COLOR.get(wallpaperPreset) || 'ultramarine'; color = WALLPAPER_TO_BUBBLE_COLOR.get(wallpaperPreset);
} else { } else {
color = 'ultramarine'; color = undefined;
} }
} else if (chatStyle.bubbleColorPreset != null) { } else if (chatStyle.bubbleColorPreset != null) {
const { BubbleColorPreset } = Backups.ChatStyle; const { BubbleColorPreset } = Backups.ChatStyle;
@ -3025,6 +3052,7 @@ export class BackupImportStream extends Writable {
color, color,
customColorData, customColorData,
dimWallpaperInDarkMode, dimWallpaperInDarkMode,
autoBubbleColor,
}; };
} }
} }

View file

@ -15,4 +15,5 @@ export type LocalChatStyle = Readonly<{
color: ConversationColorType | undefined; color: ConversationColorType | undefined;
customColorId: string | undefined; customColorId: string | undefined;
dimWallpaperInDarkMode: boolean | undefined; dimWallpaperInDarkMode: boolean | undefined;
autoBubbleColor: boolean | undefined;
}>; }>;

View file

@ -308,7 +308,7 @@ function startInstaller(): ThunkAction<
finishInstall({ finishInstall({
deviceName: SignalCI.deviceName, deviceName: SignalCI.deviceName,
backupFile: SignalCI.backupData, backupFile: SignalCI.backupData,
isPlaintextBackup: SignalCI.isPlaintextBackup, isBackupIntegration: SignalCI.isBackupIntegration,
}) })
); );
} }

View file

@ -26,7 +26,6 @@ describe('reaction utilities', () => {
fromId: OUR_CONVO_ID, fromId: OUR_CONVO_ID,
targetTimestamp: Date.now(), targetTimestamp: Date.now(),
timestamp: Date.now(), timestamp: Date.now(),
receivedAtDate: Date.now(),
...(isPending ? { isSentByConversationId: { [uuid()]: false } } : {}), ...(isPending ? { isSentByConversationId: { [uuid()]: false } } : {}),
}); });

View file

@ -48,7 +48,7 @@ describe('backup/attachments', () => {
contactA = await window.ConversationController.getOrCreateAndWait( contactA = await window.ConversationController.getOrCreateAndWait(
CONTACT_A, CONTACT_A,
'private', 'private',
{ systemGivenName: 'CONTACT_A' } { systemGivenName: 'CONTACT_A', active_at: 1 }
); );
await loadAll(); await loadAll();

View file

@ -86,32 +86,35 @@ describe('backup/groupv2/notifications', () => {
await window.ConversationController.getOrCreateAndWait( await window.ConversationController.getOrCreateAndWait(
CONTACT_A, CONTACT_A,
'private', 'private',
{ pni: CONTACT_A_PNI, systemGivenName: 'CONTACT_A' } { pni: CONTACT_A_PNI, systemGivenName: 'CONTACT_A', active_at: 1 }
); );
await window.ConversationController.getOrCreateAndWait( await window.ConversationController.getOrCreateAndWait(
CONTACT_B, CONTACT_B,
'private', 'private',
{ systemGivenName: 'CONTACT_B' } { systemGivenName: 'CONTACT_B', active_at: 1 }
); );
await window.ConversationController.getOrCreateAndWait( await window.ConversationController.getOrCreateAndWait(
CONTACT_C, CONTACT_C,
'private', 'private',
{ systemGivenName: 'CONTACT_C' } { systemGivenName: 'CONTACT_C', active_at: 1 }
); );
await window.ConversationController.getOrCreateAndWait(ADMIN_A, 'private', { await window.ConversationController.getOrCreateAndWait(ADMIN_A, 'private', {
systemGivenName: 'ADMIN_A', systemGivenName: 'ADMIN_A',
active_at: 1,
}); });
await window.ConversationController.getOrCreateAndWait( await window.ConversationController.getOrCreateAndWait(
INVITEE_A, INVITEE_A,
'private', 'private',
{ {
systemGivenName: 'INVITEE_A', systemGivenName: 'INVITEE_A',
active_at: 1,
} }
); );
await window.ConversationController.getOrCreateAndWait(GROUP_ID, 'group', { await window.ConversationController.getOrCreateAndWait(GROUP_ID, 'group', {
groupVersion: 2, groupVersion: 2,
masterKey: Bytes.toBase64(getRandomBytes(32)), masterKey: Bytes.toBase64(getRandomBytes(32)),
name: 'Rock Enthusiasts', name: 'Rock Enthusiasts',
active_at: 1,
}); });
await loadAll(); await loadAll();

View file

@ -51,12 +51,12 @@ describe('backup/bubble messages', () => {
contactA = await window.ConversationController.getOrCreateAndWait( contactA = await window.ConversationController.getOrCreateAndWait(
CONTACT_A, CONTACT_A,
'private', 'private',
{ systemGivenName: 'CONTACT_A' } { systemGivenName: 'CONTACT_A', active_at: 1 }
); );
contactB = await window.ConversationController.getOrCreateAndWait( contactB = await window.ConversationController.getOrCreateAndWait(
CONTACT_B, CONTACT_B,
'private', 'private',
{ systemGivenName: 'CONTACT_B' } { systemGivenName: 'CONTACT_B', active_at: 1 }
); );
gv1 = await window.ConversationController.getOrCreateAndWait( gv1 = await window.ConversationController.getOrCreateAndWait(
@ -64,6 +64,7 @@ describe('backup/bubble messages', () => {
'group', 'group',
{ {
groupVersion: 1, groupVersion: 1,
active_at: 1,
} }
); );

View file

@ -51,7 +51,7 @@ describe('backup/calling', () => {
contactA = await window.ConversationController.getOrCreateAndWait( contactA = await window.ConversationController.getOrCreateAndWait(
CONTACT_A, CONTACT_A,
'private', 'private',
{ systemGivenName: 'CONTACT_A' } { systemGivenName: 'CONTACT_A', active_at: 1 }
); );
groupA = await window.ConversationController.getOrCreateAndWait( groupA = await window.ConversationController.getOrCreateAndWait(
GROUP_ID_STRING, GROUP_ID_STRING,
@ -60,6 +60,7 @@ describe('backup/calling', () => {
groupVersion: 2, groupVersion: 2,
masterKey: Bytes.toBase64(GROUP_MASTER_KEY), masterKey: Bytes.toBase64(GROUP_MASTER_KEY),
name: 'Rock Enthusiasts', name: 'Rock Enthusiasts',
active_at: 1,
} }
); );

View file

@ -43,7 +43,7 @@ describe('backup/non-bubble messages', () => {
contactA = await window.ConversationController.getOrCreateAndWait( contactA = await window.ConversationController.getOrCreateAndWait(
CONTACT_A, CONTACT_A,
'private', 'private',
{ systemGivenName: 'CONTACT_A' } { systemGivenName: 'CONTACT_A', active_at: 1 }
); );
group = await window.ConversationController.getOrCreateAndWait( group = await window.ConversationController.getOrCreateAndWait(
@ -53,6 +53,7 @@ describe('backup/non-bubble messages', () => {
groupVersion: 2, groupVersion: 2,
masterKey: Bytes.toBase64(getRandomBytes(32)), masterKey: Bytes.toBase64(getRandomBytes(32)),
name: 'Rock Enthusiasts', name: 'Rock Enthusiasts',
active_at: 1,
} }
); );
@ -345,7 +346,6 @@ describe('backup/non-bubble messages', () => {
fromId: contactA.id, fromId: contactA.id,
targetTimestamp: 1, targetTimestamp: 1,
timestamp: 1, timestamp: 1,
receivedAtDate: 1,
}, },
], ],
}, },
@ -385,7 +385,6 @@ describe('backup/non-bubble messages', () => {
fromId: contactA.id, fromId: contactA.id,
targetTimestamp: 1, targetTimestamp: 1,
timestamp: 1, timestamp: 1,
receivedAtDate: 1,
}, },
], ],
}, },

View file

@ -0,0 +1,119 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-console */
import { cpus } from 'node:os';
import { inspect } from 'node:util';
import { basename } from 'node:path';
import { reporters } from 'mocha';
import pMap from 'p-map';
import logSymbols from 'log-symbols';
import {
ComparableBackup,
Purpose,
} from '@signalapp/libsignal-client/dist/MessageBackup';
import { FileStream } from '../../services/backups/util/FileStream';
import type { App } from '../playwright';
import { Bootstrap } from '../bootstrap';
const WORKER_COUNT = process.env.WORKER_COUNT
? parseInt(process.env.WORKER_COUNT, 10)
: Math.min(8, cpus().length);
(reporters.base as unknown as { maxDiffSize: number }).maxDiffSize = Infinity;
const testFiles = process.argv.slice(2);
let total = 0;
let passed = 0;
let failed = 0;
function pass(): void {
process.stdout.write(`${logSymbols.success}`);
total += 1;
passed += 1;
}
function fail(filePath: string, error: string): void {
total += 1;
failed += 1;
console.log(`\n${logSymbols.error} ${basename(filePath)}`);
console.error(error);
}
async function runOne(filePath: string): Promise<void> {
const bootstrap = new Bootstrap({ contactCount: 0 });
let app: App | undefined;
try {
await bootstrap.init();
app = await bootstrap.link({
ciBackupPath: filePath,
ciIsBackupIntegration: true,
});
const backupPath = bootstrap.getBackupPath('backup.bin');
await app.exportPlaintextBackupToDisk(backupPath);
await app.close();
app = undefined;
const actualStream = new FileStream(backupPath);
const expectedStream = new FileStream(filePath);
try {
const actual = await ComparableBackup.fromUnencrypted(
Purpose.RemoteBackup,
actualStream,
BigInt(await actualStream.size())
);
const expected = await ComparableBackup.fromUnencrypted(
Purpose.RemoteBackup,
expectedStream,
BigInt(await expectedStream.size())
);
const actualString = actual.comparableString();
const expectedString = expected.comparableString();
if (actualString === expectedString) {
pass();
} else {
fail(
filePath,
reporters.base.generateDiff(
inspect(actualString, { depth: Infinity, sorted: true }),
inspect(expectedString, { depth: Infinity, sorted: true })
)
);
await bootstrap.saveLogs(app, basename(filePath));
}
} finally {
await actualStream.close();
await expectedStream.close();
}
} catch (error) {
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);
}
}
}
async function main(): Promise<void> {
await pMap(testFiles, runOne, { concurrency: WORKER_COUNT });
console.log(`${passed}/${total} (${failed} failures)`);
if (failed !== 0) {
process.exit(0);
}
}
main().catch(error => {
console.error(error);
process.exit(1);
});

View file

@ -1,86 +0,0 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { join } from 'node:path';
import createDebug from 'debug';
import fastGlob from 'fast-glob';
import {
ComparableBackup,
Purpose,
} from '@signalapp/libsignal-client/dist/MessageBackup';
import * as durations from '../../util/durations';
import { FileStream } from '../../services/backups/util/FileStream';
import type { App } from '../playwright';
import { Bootstrap } from '../bootstrap';
export const debug = createDebug('mock:test:backups');
const TEST_FOLDER = process.env.BACKUP_TEST_FOLDER;
describe('backups/integration', async function (this: Mocha.Suite) {
this.timeout(100 * durations.MINUTE);
if (!TEST_FOLDER) {
return;
}
let bootstrap: Bootstrap;
let app: App | undefined;
beforeEach(async () => {
bootstrap = new Bootstrap();
await bootstrap.init();
});
afterEach(async function (this: Mocha.Context) {
if (!bootstrap) {
return;
}
await bootstrap.maybeSaveLogs(this.currentTest, app);
await app?.close();
await bootstrap.teardown();
});
const testFiles = fastGlob.sync(join(TEST_FOLDER, '*.binproto'), {
onlyFiles: true,
});
testFiles.forEach(fullPath => {
it(`passes ${fullPath}`, async () => {
app = await bootstrap.link({
ciBackupPath: fullPath,
ciIsPlaintextBackup: true,
});
const backupPath = bootstrap.getBackupPath('backup.bin');
await app.exportPlaintextBackupToDisk(backupPath);
await app.close();
const actualStream = new FileStream(backupPath);
const expectedStream = new FileStream(fullPath);
try {
const actual = await ComparableBackup.fromUnencrypted(
Purpose.RemoteBackup,
actualStream,
BigInt(await actualStream.size())
);
const expected = await ComparableBackup.fromUnencrypted(
Purpose.RemoteBackup,
expectedStream,
BigInt(await expectedStream.size())
);
assert.strictEqual(
actual.comparableString(),
expected.comparableString()
);
} finally {
await actualStream.close();
await expectedStream.close();
}
});
});
});

View file

@ -164,7 +164,7 @@ export class Bootstrap {
private readonly randomId = crypto.randomBytes(8).toString('hex'); private readonly randomId = crypto.randomBytes(8).toString('hex');
constructor(options: BootstrapOptions = {}) { constructor(options: BootstrapOptions = {}) {
this.cdn3Path = path.join(os.tmpdir(), 'mock-signal-cdn3-'); this.cdn3Path = path.join(os.tmpdir(), `mock-signal-cdn3-${this.randomId}`);
this.server = new Server({ this.server = new Server({
// Limit number of storage read keys for easier testing // Limit number of storage read keys for easier testing
maxStorageReadKeys: MAX_STORAGE_READ_KEYS, maxStorageReadKeys: MAX_STORAGE_READ_KEYS,

View file

@ -3,7 +3,7 @@
import type { ElectronApplication, Page } from 'playwright'; import type { ElectronApplication, Page } from 'playwright';
import { _electron as electron } from 'playwright'; import { _electron as electron } from 'playwright';
import { EventEmitter } from 'events'; import { EventEmitter, once } from 'events';
import pTimeout from 'p-timeout'; import pTimeout from 'p-timeout';
import type { import type {
@ -12,6 +12,7 @@ import type {
} from '../challenge'; } from '../challenge';
import type { ReceiptType } from '../types/Receipt'; import type { ReceiptType } from '../types/Receipt';
import { SECOND } from '../util/durations'; import { SECOND } from '../util/durations';
import { drop } from '../util/drop';
export type AppLoadedInfoType = Readonly<{ export type AppLoadedInfoType = Readonly<{
loadTime: number; loadTime: number;
@ -87,6 +88,8 @@ export class App extends EventEmitter {
} }
this.privApp.on('close', () => this.emit('close')); this.privApp.on('close', () => this.emit('close'));
drop(this.printLoop());
} }
public async waitForProvisionURL(): Promise<string> { public async waitForProvisionURL(): Promise<string> {
@ -255,4 +258,36 @@ export class App extends EventEmitter {
return this.privApp; return this.privApp;
} }
private async printLoop(): Promise<void> {
const kClosed: unique symbol = Symbol('kClosed');
const onClose = (async (): Promise<typeof kClosed> => {
try {
await once(this, 'close');
} catch {
// Ignore
}
return kClosed;
})();
// eslint-disable-next-line no-constant-condition
while (true) {
try {
// eslint-disable-next-line no-await-in-loop
const value = await Promise.race([
this.waitForEvent<string>('print', 0),
onClose,
]);
if (value === kClosed) {
break;
}
// eslint-disable-next-line no-console
console.error(`CI.print: ${value}`);
} catch {
// Ignore errors
}
}
}
} }

View file

@ -126,7 +126,7 @@ type CreateAccountSharedOptionsType = Readonly<{
// Test-only // Test-only
backupFile?: Uint8Array; backupFile?: Uint8Array;
isPlaintextBackup?: boolean; isBackupIntegration?: boolean;
}>; }>;
type CreatePrimaryDeviceOptionsType = Readonly<{ type CreatePrimaryDeviceOptionsType = Readonly<{
@ -220,7 +220,7 @@ function signedPreKeyToUploadSignedPreKey({
export type ConfirmNumberResultType = Readonly<{ export type ConfirmNumberResultType = Readonly<{
deviceName: string; deviceName: string;
backupFile: Uint8Array | undefined; backupFile: Uint8Array | undefined;
isPlaintextBackup: boolean; isBackupIntegration: boolean;
}>; }>;
export default class AccountManager extends EventTarget { export default class AccountManager extends EventTarget {
@ -923,7 +923,7 @@ export default class AccountManager extends EventTarget {
readReceipts, readReceipts,
userAgent, userAgent,
backupFile, backupFile,
isPlaintextBackup, isBackupIntegration,
} = options; } = options;
const { storage } = window.textsecure; const { storage } = window.textsecure;
@ -969,7 +969,7 @@ export default class AccountManager extends EventTarget {
if (backupFile !== undefined) { if (backupFile !== undefined) {
log.warn( log.warn(
'createAccount: Restoring from ' + 'createAccount: Restoring from ' +
`${isPlaintextBackup ? 'plaintext' : 'ciphertext'} backup; ` + `${isBackupIntegration ? 'plaintext' : 'ciphertext'} backup; ` +
'deleting all previous data' 'deleting all previous data'
); );
} }
@ -1231,7 +1231,9 @@ export default class AccountManager extends EventTarget {
if (backupFile !== undefined) { if (backupFile !== undefined) {
await backupsService.importBackup( await backupsService.importBackup(
() => Readable.from([backupFile]), () => Readable.from([backupFile]),
isPlaintextBackup ? BackupType.TestOnlyPlaintext : BackupType.Ciphertext isBackupIntegration
? BackupType.TestOnlyPlaintext
: BackupType.Ciphertext
); );
} }
} }

View file

@ -69,7 +69,7 @@ type StateType = Readonly<
export type PrepareLinkDataOptionsType = Readonly<{ export type PrepareLinkDataOptionsType = Readonly<{
deviceName: string; deviceName: string;
backupFile?: Uint8Array; backupFile?: Uint8Array;
isPlaintextBackup?: boolean; isBackupIntegration?: boolean;
}>; }>;
export class Provisioner { export class Provisioner {
@ -153,7 +153,7 @@ export class Provisioner {
public prepareLinkData({ public prepareLinkData({
deviceName, deviceName,
backupFile, backupFile,
isPlaintextBackup, isBackupIntegration,
}: PrepareLinkDataOptionsType): CreateLinkedDeviceOptionsType { }: PrepareLinkDataOptionsType): CreateLinkedDeviceOptionsType {
strictAssert( strictAssert(
this.state.step === Step.ReadyToLink, this.state.step === Step.ReadyToLink,
@ -211,7 +211,7 @@ export class Provisioner {
MAX_DEVICE_NAME_LENGTH MAX_DEVICE_NAME_LENGTH
), ),
backupFile, backupFile,
isPlaintextBackup, isBackupIntegration,
userAgent, userAgent,
ourAci, ourAci,
ourPni, ourPni,

View file

@ -43,7 +43,7 @@ export const rendererConfigSchema = z.object({
disableIPv6: z.boolean(), disableIPv6: z.boolean(),
dnsFallback: DNSFallbackSchema, dnsFallback: DNSFallbackSchema,
ciBackupPath: configOptionalStringSchema, ciBackupPath: configOptionalStringSchema,
ciIsPlaintextBackup: z.boolean(), ciIsBackupIntegration: z.boolean(),
environment: environmentSchema, environment: environmentSchema,
isMockTestEnvironment: z.boolean(), isMockTestEnvironment: z.boolean(),
homePath: configRequiredStringSchema, homePath: configRequiredStringSchema,

View file

@ -69,6 +69,7 @@ export type StorageAccessType = {
defaultWallpaperPhotoPointer: Uint8Array; defaultWallpaperPhotoPointer: Uint8Array;
defaultWallpaperPreset: number; defaultWallpaperPreset: number;
defaultDimWallpaperInDarkMode: boolean; defaultDimWallpaperInDarkMode: boolean;
defaultAutoBubbleColor: boolean;
customColors: CustomColorsItemType; customColors: CustomColorsItemType;
device_name: string; device_name: string;

View file

@ -25,6 +25,6 @@ if (config.ciMode) {
backupData: config.ciBackupPath backupData: config.ciBackupPath
? fs.readFileSync(config.ciBackupPath) ? fs.readFileSync(config.ciBackupPath)
: undefined, : undefined,
isPlaintextBackup: config.ciIsPlaintextBackup === true, isBackupIntegration: config.ciIsBackupIntegration === true,
}); });
} }