Fix some backup integration tests
This commit is contained in:
parent
c68be0f86e
commit
3a991822c5
33 changed files with 450 additions and 231 deletions
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
@ -997,7 +997,9 @@ async function createWindow() {
|
|||
mainWindow.webContents.send('ci:event', 'db-initialized', {});
|
||||
|
||||
const shouldShowWindow =
|
||||
!app.getLoginItemSettings().wasOpenedAsHidden && !startInTray;
|
||||
!app.getLoginItemSettings().wasOpenedAsHidden &&
|
||||
!startInTray &&
|
||||
!config.get<boolean>('ciIsBackupIntegration');
|
||||
|
||||
if (shouldShowWindow) {
|
||||
getLogger().info('showing main window');
|
||||
|
@ -2724,7 +2726,7 @@ ipc.on('get-config', async event => {
|
|||
dnsFallback: await getDNSFallback(),
|
||||
disableIPv6: DISABLE_IPV6,
|
||||
ciBackupPath: config.get<string | null>('ciBackupPath') || undefined,
|
||||
ciIsPlaintextBackup: config.get<boolean>('ciIsPlaintextBackup'),
|
||||
ciIsBackupIntegration: config.get<boolean>('ciIsBackupIntegration'),
|
||||
nodeVersion: process.versions.node,
|
||||
hostname: os.hostname(),
|
||||
osRelease: os.release(),
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"updatesEnabled": false,
|
||||
"ciMode": false,
|
||||
"ciBackupPath": null,
|
||||
"ciIsPlaintextBackup": false,
|
||||
"ciIsBackupIntegration": false,
|
||||
"forcePreloadBundle": false,
|
||||
"openDevTools": false,
|
||||
"buildCreation": 0,
|
||||
|
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -21,7 +21,7 @@
|
|||
"@react-aria/utils": "3.16.0",
|
||||
"@react-spring/web": "9.5.5",
|
||||
"@signalapp/better-sqlite3": "8.8.1",
|
||||
"@signalapp/libsignal-client": "0.55.1",
|
||||
"@signalapp/libsignal-client": "0.56.1",
|
||||
"@signalapp/ringrtc": "2.47.0",
|
||||
"@types/fabric": "4.5.3",
|
||||
"backbone": "1.4.0",
|
||||
|
@ -7230,10 +7230,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@signalapp/libsignal-client": {
|
||||
"version": "0.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.55.1.tgz",
|
||||
"integrity": "sha512-qa2sztxNy5QyXYg9Z8xH9zdYikwNORyWr/95HnLAdzf4YFGsee/8JS74L+2kAn55lE7CVD+EVpgXJYFFw2Gu/w==",
|
||||
"version": "0.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.56.1.tgz",
|
||||
"integrity": "sha512-lgOdtf/63G1LuiHTrpxfwumwERuqSd1FM5Q5ZyBo8NUDQoQinG8nyza7XUXggaC/+VFggJUvAfZZvRNkJS7RTA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.2.3",
|
||||
"type-fest": "^3.5.0",
|
||||
|
|
|
@ -105,7 +105,7 @@
|
|||
"@react-aria/utils": "3.16.0",
|
||||
"@react-spring/web": "9.5.5",
|
||||
"@signalapp/better-sqlite3": "8.8.1",
|
||||
"@signalapp/libsignal-client": "0.55.1",
|
||||
"@signalapp/libsignal-client": "0.56.1",
|
||||
"@signalapp/ringrtc": "2.47.0",
|
||||
"@types/fabric": "4.5.3",
|
||||
"backbone": "1.4.0",
|
||||
|
|
|
@ -276,7 +276,7 @@ message AdHocCall {
|
|||
}
|
||||
|
||||
uint64 callId = 1;
|
||||
// Refers to a Recipient with the `callLink` field set
|
||||
// Refers to a `CallLink` recipient.
|
||||
uint64 recipientId = 2;
|
||||
State state = 3;
|
||||
uint64 callTimestamp = 4;
|
||||
|
@ -477,7 +477,6 @@ message ContactAttachment {
|
|||
optional string prefix = 3;
|
||||
optional string suffix = 4;
|
||||
optional string middleName = 5;
|
||||
optional string displayName = 6;
|
||||
}
|
||||
|
||||
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
|
||||
uint64 authorId = 2;
|
||||
optional string text = 3;
|
||||
optional Text text = 3;
|
||||
repeated QuotedAttachment attachments = 4;
|
||||
repeated BodyRange bodyRanges = 5;
|
||||
Type type = 6;
|
||||
Type type = 5;
|
||||
}
|
||||
|
||||
message BodyRange {
|
||||
|
@ -681,11 +679,9 @@ message Reaction {
|
|||
string emoji = 1;
|
||||
uint64 authorId = 2;
|
||||
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
|
||||
// incrementing numbers (e.g. 1, 2, 3), others as timestamps.
|
||||
uint64 sortOrder = 5;
|
||||
uint64 sortOrder = 4;
|
||||
}
|
||||
|
||||
message ChatUpdateMessage {
|
||||
|
@ -1069,7 +1065,7 @@ message StickerPack {
|
|||
message ChatStyle {
|
||||
message Gradient {
|
||||
uint32 angle = 1; // degrees
|
||||
repeated fixed32 colors = 2;
|
||||
repeated fixed32 colors = 2; // 0xAARRGGBB
|
||||
repeated float positions = 3; // percent from 0 to 1
|
||||
}
|
||||
|
||||
|
@ -1077,7 +1073,7 @@ message ChatStyle {
|
|||
uint64 id = 1;
|
||||
|
||||
oneof color {
|
||||
fixed32 solid = 2;
|
||||
fixed32 solid = 2; // 0xAARRGGBB
|
||||
Gradient gradient = 3;
|
||||
}
|
||||
}
|
||||
|
|
20
ts/CI.ts
20
ts/CI.ts
|
@ -1,7 +1,9 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { format } from 'node:util';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { BackupLevel } from '@signalapp/libsignal-client/zkgroup';
|
||||
|
||||
import type { IPCResponse as ChallengeResponseType } from './challenge';
|
||||
import type { MessageAttributesType } from './model-types.d';
|
||||
|
@ -18,7 +20,7 @@ type ResolveType = (data: unknown) => void;
|
|||
export type CIType = {
|
||||
deviceName: string;
|
||||
backupData?: Uint8Array;
|
||||
isPlaintextBackup?: boolean;
|
||||
isBackupIntegration?: boolean;
|
||||
getConversationId: (address: string | null) => string | null;
|
||||
getMessagesBySentAt(
|
||||
sentAt: number
|
||||
|
@ -38,18 +40,19 @@ export type CIType = {
|
|||
exportBackupToDisk(path: string): Promise<void>;
|
||||
exportPlaintextBackupToDisk(path: string): Promise<void>;
|
||||
unlink: () => void;
|
||||
print: (...args: ReadonlyArray<unknown>) => void;
|
||||
};
|
||||
|
||||
export type GetCIOptionsType = Readonly<{
|
||||
deviceName: string;
|
||||
backupData?: Uint8Array;
|
||||
isPlaintextBackup?: boolean;
|
||||
isBackupIntegration?: boolean;
|
||||
}>;
|
||||
|
||||
export function getCI({
|
||||
deviceName,
|
||||
backupData,
|
||||
isPlaintextBackup,
|
||||
isBackupIntegration,
|
||||
}: GetCIOptionsType): CIType {
|
||||
const eventListeners = new Map<string, Array<ResolveType>>();
|
||||
const completedEvents = new Map<string, Array<unknown>>();
|
||||
|
@ -174,13 +177,13 @@ export function getCI({
|
|||
}
|
||||
|
||||
async function exportBackupToDisk(path: string) {
|
||||
await backupsService.exportToDisk(path);
|
||||
await backupsService.exportToDisk(path, BackupLevel.Media);
|
||||
}
|
||||
|
||||
async function exportPlaintextBackupToDisk(path: string) {
|
||||
await backupsService.exportToDisk(
|
||||
path,
|
||||
undefined,
|
||||
BackupLevel.Media,
|
||||
BackupType.TestOnlyPlaintext
|
||||
);
|
||||
}
|
||||
|
@ -189,10 +192,14 @@ export function getCI({
|
|||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
||||
}
|
||||
|
||||
function print(...args: ReadonlyArray<unknown>) {
|
||||
handleEvent('print', format(...args));
|
||||
}
|
||||
|
||||
return {
|
||||
deviceName,
|
||||
backupData,
|
||||
isPlaintextBackup,
|
||||
isBackupIntegration,
|
||||
getConversationId,
|
||||
getMessagesBySentAt,
|
||||
handleEvent,
|
||||
|
@ -204,5 +211,6 @@ export function getCI({
|
|||
exportPlaintextBackupToDisk,
|
||||
unlink,
|
||||
getPendingEventCount,
|
||||
print,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -182,6 +182,10 @@ export class ConversationController {
|
|||
// we can reset the mute state on the model. If the mute has already expired
|
||||
// then we reset the state right away.
|
||||
this._conversations.on('add', (model: ConversationModel): void => {
|
||||
// Don't modify conversations in backup integration testing
|
||||
if (window.SignalCI?.isBackupIntegration) {
|
||||
return;
|
||||
}
|
||||
model.startMuteTimer();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -20,7 +20,10 @@ import type { ConversationType } from '../../state/ducks/conversations';
|
|||
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
|
||||
import { groupBy } from '../../util/mapUtil';
|
||||
import type { ContactNameColorType } from '../../types/Colors';
|
||||
import { SendStatus } from '../../messages/MessageSendState';
|
||||
import {
|
||||
SendStatus,
|
||||
type VisibleSendStatus,
|
||||
} from '../../messages/MessageSendState';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
import * as log from '../../logging/log';
|
||||
import { formatDateTimeLong } from '../../util/timestamp';
|
||||
|
@ -234,7 +237,7 @@ export function MessageDetail({
|
|||
}
|
||||
|
||||
function renderContactGroupHeaderText(
|
||||
sendStatus: undefined | SendStatus
|
||||
sendStatus: undefined | VisibleSendStatus
|
||||
): string {
|
||||
if (sendStatus === undefined) {
|
||||
return i18n('icu:from');
|
||||
|
@ -259,7 +262,7 @@ export function MessageDetail({
|
|||
}
|
||||
|
||||
function renderContactGroup(
|
||||
sendStatus: undefined | SendStatus,
|
||||
sendStatus: undefined | VisibleSendStatus,
|
||||
statusContacts: undefined | ReadonlyArray<Contact>
|
||||
): ReactNode {
|
||||
if (!statusContacts || !statusContacts.length) {
|
||||
|
@ -295,15 +298,17 @@ export function MessageDetail({
|
|||
|
||||
return (
|
||||
<div className="module-message-detail__contact-container">
|
||||
{[
|
||||
undefined,
|
||||
SendStatus.Failed,
|
||||
SendStatus.Viewed,
|
||||
SendStatus.Read,
|
||||
SendStatus.Delivered,
|
||||
SendStatus.Sent,
|
||||
SendStatus.Pending,
|
||||
].map(sendStatus =>
|
||||
{(
|
||||
[
|
||||
undefined,
|
||||
SendStatus.Failed,
|
||||
SendStatus.Viewed,
|
||||
SendStatus.Read,
|
||||
SendStatus.Delivered,
|
||||
SendStatus.Sent,
|
||||
SendStatus.Pending,
|
||||
] as Array<VisibleSendStatus | undefined>
|
||||
).map(sendStatus =>
|
||||
renderContactGroup(sendStatus, contactsBySendStatus.get(sendStatus))
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -5669,7 +5669,7 @@ export async function applyNewAvatar(
|
|||
// Group has avatar; has it changed?
|
||||
if (
|
||||
newAvatarUrl &&
|
||||
(!attributes.avatar || attributes.avatar.url !== newAvatarUrl)
|
||||
(!attributes.avatar?.path || attributes.avatar.url !== newAvatarUrl)
|
||||
) {
|
||||
if (!attributes.secretParams) {
|
||||
throw new Error('applyNewAvatar: group was missing secretParams!');
|
||||
|
|
|
@ -215,6 +215,9 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
|||
}
|
||||
|
||||
static async start(): Promise<void> {
|
||||
if (window.SignalCI?.isBackupIntegration) {
|
||||
return;
|
||||
}
|
||||
await AttachmentDownloadManager.instance.start();
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ export enum SendStatus {
|
|||
Delivered = 'Delivered',
|
||||
Read = 'Read',
|
||||
Viewed = 'Viewed',
|
||||
Skipped = 'Skipped',
|
||||
}
|
||||
|
||||
export const parseMessageSendStatus = makeEnumParser(
|
||||
|
@ -45,6 +46,14 @@ export const UNDELIVERED_SEND_STATUSES = [
|
|||
SendStatus.Failed,
|
||||
];
|
||||
|
||||
export type VisibleSendStatus =
|
||||
| SendStatus.Failed
|
||||
| SendStatus.Pending
|
||||
| SendStatus.Sent
|
||||
| SendStatus.Delivered
|
||||
| SendStatus.Read
|
||||
| SendStatus.Viewed;
|
||||
|
||||
const STATUS_NUMBERS: Record<SendStatus, number> = {
|
||||
[SendStatus.Failed]: 0,
|
||||
[SendStatus.Pending]: 1,
|
||||
|
@ -52,6 +61,7 @@ const STATUS_NUMBERS: Record<SendStatus, number> = {
|
|||
[SendStatus.Delivered]: 3,
|
||||
[SendStatus.Read]: 4,
|
||||
[SendStatus.Viewed]: 5,
|
||||
[SendStatus.Skipped]: 6,
|
||||
};
|
||||
|
||||
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];
|
||||
export const isFailed = (status: SendStatus): boolean =>
|
||||
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
|
||||
|
@ -88,7 +100,8 @@ export type SendState = Readonly<{
|
|||
| SendStatus.Sent
|
||||
| SendStatus.Delivered
|
||||
| SendStatus.Read
|
||||
| SendStatus.Viewed;
|
||||
| SendStatus.Viewed
|
||||
| SendStatus.Skipped;
|
||||
updatedAt?: number;
|
||||
}>;
|
||||
|
||||
|
|
2
ts/model-types.d.ts
vendored
2
ts/model-types.d.ts
vendored
|
@ -119,7 +119,6 @@ export type MessageReactionType = {
|
|||
fromId: string;
|
||||
targetTimestamp: number;
|
||||
timestamp: number;
|
||||
receivedAtDate: undefined | number;
|
||||
isSentByConversationId?: Record<string, boolean>;
|
||||
};
|
||||
|
||||
|
@ -337,6 +336,7 @@ export type ConversationAttributesType = {
|
|||
wallpaperPhotoPointerBase64?: string;
|
||||
wallpaperPreset?: number;
|
||||
dimWallpaperInDarkMode?: boolean;
|
||||
autoBubbleColor?: boolean;
|
||||
|
||||
discoveredUnregisteredAt?: number;
|
||||
firstUnregisteredAt?: number;
|
||||
|
|
|
@ -2257,7 +2257,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
fromId: reaction.fromId,
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
timestamp: reaction.timestamp,
|
||||
receivedAtDate: reaction.receivedAtDate,
|
||||
isSentByConversationId: isFromThisDevice
|
||||
? zipObject(conversation.getMemberConversationIds(), repeat(false))
|
||||
: undefined,
|
||||
|
|
|
@ -49,7 +49,7 @@ import {
|
|||
} from '../../util/whatTypeOfConversation';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||
import { uuidToBytes } from '../../util/uuidToBytes';
|
||||
import { assertDev, strictAssert } from '../../util/assert';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { getSafeLongFromTimestamp } from '../../util/timestampLongUtils';
|
||||
import { DAY, MINUTE, SECOND, DurationInSeconds } from '../../util/durations';
|
||||
import {
|
||||
|
@ -89,7 +89,10 @@ import * as Bytes from '../../Bytes';
|
|||
import { canBeSynced as canPreferredReactionEmojiBeSynced } from '../../reactions/preferredReactionEmoji';
|
||||
import { SendStatus } from '../../messages/MessageSendState';
|
||||
import { BACKUP_VERSION } from './constants';
|
||||
import { getMessageIdForLogging } from '../../util/idForLogging';
|
||||
import {
|
||||
getMessageIdForLogging,
|
||||
getConversationIdForLogging,
|
||||
} from '../../util/idForLogging';
|
||||
import { makeLookup } from '../../util/makeLookup';
|
||||
import type {
|
||||
CallHistoryDetails,
|
||||
|
@ -197,6 +200,8 @@ export class BackupExportStream extends Readable {
|
|||
|
||||
private readonly backupTimeMs = getSafeLongFromTimestamp(this.now);
|
||||
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 attachmentBackupJobs: Array<CoreAttachmentBackupJobType> = [];
|
||||
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
|
||||
await DataWriter.clearAllAttachmentBackupJobs();
|
||||
await Promise.all(
|
||||
this.attachmentBackupJobs.map(job =>
|
||||
AttachmentBackupManager.addJobAndMaybeThumbnailJob(job)
|
||||
)
|
||||
);
|
||||
drop(AttachmentBackupManager.start());
|
||||
if (!window.SignalCI?.isBackupIntegration) {
|
||||
await Promise.all(
|
||||
this.attachmentBackupJobs.map(job =>
|
||||
AttachmentBackupManager.addJobAndMaybeThumbnailJob(job)
|
||||
)
|
||||
);
|
||||
drop(AttachmentBackupManager.start());
|
||||
}
|
||||
log.info('BackupExportStream: finished');
|
||||
}
|
||||
})()
|
||||
|
@ -407,7 +414,17 @@ export class BackupExportStream extends Readable {
|
|||
|
||||
let pinnedOrder: number | null = null;
|
||||
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({
|
||||
|
@ -438,6 +455,7 @@ export class BackupExportStream extends Readable {
|
|||
color: attributes.conversationColor,
|
||||
customColorId: attributes.customColorId,
|
||||
dimWallpaperInDarkMode: attributes.dimWallpaperInDarkMode,
|
||||
autoBubbleColor: attributes.autoBubbleColor,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
@ -686,22 +704,29 @@ export class BackupExportStream extends Readable {
|
|||
};
|
||||
}
|
||||
|
||||
private getRecipientIdentifier({
|
||||
id,
|
||||
serviceId,
|
||||
e164,
|
||||
}: GetRecipientIdOptionsType): string {
|
||||
const identifier = serviceId ?? e164 ?? id;
|
||||
assertDev(identifier, 'Identifier cannot be blank');
|
||||
return identifier;
|
||||
private getExistingRecipientId(
|
||||
options: GetRecipientIdOptionsType
|
||||
): Long | undefined {
|
||||
let existing: number | undefined;
|
||||
if (options.serviceId != null) {
|
||||
existing = this.serviceIdToRecipientId.get(options.serviceId);
|
||||
}
|
||||
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 {
|
||||
const identifier = this.getRecipientIdentifier(options);
|
||||
|
||||
const existing = this.convoIdToRecipientId.get(identifier);
|
||||
const existing = this.getExistingRecipientId(options);
|
||||
if (existing !== undefined) {
|
||||
return Long.fromNumber(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
const { id, serviceId, e164 } = options;
|
||||
|
@ -713,10 +738,10 @@ export class BackupExportStream extends Readable {
|
|||
this.convoIdToRecipientId.set(id, recipientId);
|
||||
}
|
||||
if (serviceId !== undefined) {
|
||||
this.convoIdToRecipientId.set(serviceId, recipientId);
|
||||
this.serviceIdToRecipientId.set(serviceId, recipientId);
|
||||
}
|
||||
if (e164 !== undefined) {
|
||||
this.convoIdToRecipientId.set(e164, recipientId);
|
||||
this.e164ToRecipientId.set(e164, recipientId);
|
||||
}
|
||||
const result = Long.fromNumber(recipientId);
|
||||
|
||||
|
@ -724,8 +749,8 @@ export class BackupExportStream extends Readable {
|
|||
}
|
||||
|
||||
private getOrPushPrivateRecipient(options: GetRecipientIdOptionsType): Long {
|
||||
const identifier = this.getRecipientIdentifier(options);
|
||||
const needsPush = !this.convoIdToRecipientId.has(identifier);
|
||||
const existing = this.getExistingRecipientId(options);
|
||||
const needsPush = existing == null;
|
||||
const result = this.getRecipientId(options);
|
||||
|
||||
if (needsPush) {
|
||||
|
@ -832,9 +857,12 @@ export class BackupExportStream extends Readable {
|
|||
title: {
|
||||
title: convo.name ?? '',
|
||||
},
|
||||
description: {
|
||||
descriptionText: convo.description ?? '',
|
||||
},
|
||||
description:
|
||||
convo.description != null
|
||||
? {
|
||||
descriptionText: convo.description,
|
||||
}
|
||||
: null,
|
||||
avatarUrl: convo.avatar?.url,
|
||||
disappearingMessagesTimer:
|
||||
convo.expireTimer != null
|
||||
|
@ -848,9 +876,7 @@ export class BackupExportStream extends Readable {
|
|||
version: convo.revision || 0,
|
||||
members: convo.membersV2?.map(member => {
|
||||
const memberConvo = window.ConversationController.get(member.aci);
|
||||
strictAssert(memberConvo, 'Missing GV2 member');
|
||||
|
||||
const { profileKey } = memberConvo.attributes;
|
||||
const { profileKey } = memberConvo?.attributes ?? {};
|
||||
|
||||
return {
|
||||
userId: this.aciToBytes(member.aci),
|
||||
|
@ -876,9 +902,7 @@ export class BackupExportStream extends Readable {
|
|||
membersPendingAdminApproval: convo.pendingAdminApprovalV2?.map(
|
||||
member => {
|
||||
const memberConvo = window.ConversationController.get(member.aci);
|
||||
strictAssert(memberConvo, 'Missing GV2 member pending approval');
|
||||
|
||||
const { profileKey } = memberConvo.attributes;
|
||||
const { profileKey } = memberConvo?.attributes ?? {};
|
||||
return {
|
||||
userId: this.aciToBytes(member.aci),
|
||||
profileKey: profileKey
|
||||
|
@ -2024,7 +2048,15 @@ export class BackupExportStream extends Readable {
|
|||
return {
|
||||
targetSentTimestamp: Long.fromNumber(quote.id),
|
||||
authorId,
|
||||
text: quote.text,
|
||||
text:
|
||||
quote.text != null
|
||||
? {
|
||||
body: quote.text,
|
||||
bodyRanges: quote.bodyRanges?.map(range =>
|
||||
this.toBodyRange(range)
|
||||
),
|
||||
}
|
||||
: null,
|
||||
attachments: await Promise.all(
|
||||
quote.attachments.map(
|
||||
async (
|
||||
|
@ -2044,7 +2076,6 @@ export class BackupExportStream extends Readable {
|
|||
}
|
||||
)
|
||||
),
|
||||
bodyRanges: quote.bodyRanges?.map(range => this.toBodyRange(range)),
|
||||
type: quote.isGiftBadge
|
||||
? Backups.Quote.Type.GIFTBADGE
|
||||
: Backups.Quote.Type.NORMAL,
|
||||
|
@ -2132,15 +2163,19 @@ export class BackupExportStream extends Readable {
|
|||
// new keys so that we don't try to re-upload it again on the next export
|
||||
}
|
||||
|
||||
const backupJob = await maybeGetBackupJobForAttachmentAndFilePointer({
|
||||
attachment: updatedAttachment ?? attachment,
|
||||
filePointer,
|
||||
getBackupCdnInfo,
|
||||
messageReceivedAt,
|
||||
});
|
||||
// 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({
|
||||
attachment: updatedAttachment ?? attachment,
|
||||
filePointer,
|
||||
getBackupCdnInfo,
|
||||
messageReceivedAt,
|
||||
});
|
||||
|
||||
if (backupJob) {
|
||||
this.attachmentBackupJobs.push(backupJob);
|
||||
if (backupJob) {
|
||||
this.attachmentBackupJobs.push(backupJob);
|
||||
}
|
||||
}
|
||||
|
||||
return filePointer;
|
||||
|
@ -2162,9 +2197,6 @@ export class BackupExportStream extends Readable {
|
|||
id: reaction.fromId,
|
||||
}),
|
||||
sentTimestamp: getSafeLongFromTimestamp(reaction.timestamp),
|
||||
receivedTimestamp: getSafeLongFromTimestamp(
|
||||
reaction.receivedAtDate ?? reaction.timestamp
|
||||
),
|
||||
sortOrder: Long.fromNumber(sortOrder),
|
||||
};
|
||||
});
|
||||
|
@ -2259,6 +2291,9 @@ export class BackupExportStream extends Readable {
|
|||
sealedSender,
|
||||
});
|
||||
break;
|
||||
case SendStatus.Skipped:
|
||||
sendStatus.skipped = {};
|
||||
break;
|
||||
case SendStatus.Failed: {
|
||||
sendStatus.failed = new Backups.SendStatus.Failed();
|
||||
if (!serviceId) {
|
||||
|
@ -2273,6 +2308,10 @@ export class BackupExportStream extends Readable {
|
|||
if (identityKeyMismatch) {
|
||||
sendStatus.failed.reason =
|
||||
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 {
|
||||
sendStatus.failed.reason =
|
||||
Backups.SendStatus.Failed.FailureReason.NETWORK;
|
||||
|
@ -2430,7 +2469,7 @@ export class BackupExportStream extends Readable {
|
|||
|
||||
const result = new Array<Backups.ChatStyle.ICustomChatColor>();
|
||||
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);
|
||||
|
||||
const start = hslToRGBInt(
|
||||
|
@ -2465,18 +2504,25 @@ export class BackupExportStream extends Readable {
|
|||
return result;
|
||||
}
|
||||
|
||||
private toDefaultChatStyle(): Backups.IChatStyle {
|
||||
private toDefaultChatStyle(): Backups.IChatStyle | null {
|
||||
const defaultColor = window.storage.get('defaultConversationColor');
|
||||
const wallpaperPhotoPointer = window.storage.get(
|
||||
'defaultWallpaperPhotoPointer'
|
||||
);
|
||||
const wallpaperPreset = window.storage.get('defaultWallpaperPreset');
|
||||
const dimWallpaperInDarkMode = window.storage.get(
|
||||
'defaultDimWallpaperInDarkMode',
|
||||
false
|
||||
);
|
||||
const autoBubbleColor = window.storage.get('defaultAutoBubbleColor');
|
||||
|
||||
return this.toChatStyle({
|
||||
wallpaperPhotoPointer: window.storage.get('defaultWallpaperPhotoPointer'),
|
||||
wallpaperPreset: window.storage.get('defaultWallpaperPreset'),
|
||||
wallpaperPhotoPointer,
|
||||
wallpaperPreset,
|
||||
color: defaultColor?.color,
|
||||
customColorId: defaultColor?.customColorData?.id,
|
||||
dimWallpaperInDarkMode: window.storage.get(
|
||||
'defaultDimWallpaperInDarkMode',
|
||||
false
|
||||
),
|
||||
dimWallpaperInDarkMode,
|
||||
autoBubbleColor,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2486,18 +2532,30 @@ export class BackupExportStream extends Readable {
|
|||
color,
|
||||
customColorId,
|
||||
dimWallpaperInDarkMode,
|
||||
}: LocalChatStyle): Backups.IChatStyle {
|
||||
autoBubbleColor,
|
||||
}: LocalChatStyle): Backups.IChatStyle | null {
|
||||
const result: Backups.IChatStyle = {
|
||||
dimWallpaperInDarkMode,
|
||||
};
|
||||
|
||||
// The defaults
|
||||
if (
|
||||
(color == null || color === 'ultramarine') &&
|
||||
wallpaperPhotoPointer == null &&
|
||||
wallpaperPreset == null &&
|
||||
!dimWallpaperInDarkMode &&
|
||||
(autoBubbleColor === true || autoBubbleColor == null)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Bytes.isNotEmpty(wallpaperPhotoPointer)) {
|
||||
result.wallpaperPhoto = Backups.FilePointer.decode(wallpaperPhotoPointer);
|
||||
} else if (wallpaperPreset) {
|
||||
result.wallpaperPreset = wallpaperPreset;
|
||||
}
|
||||
|
||||
if (color == null) {
|
||||
if (color == null || autoBubbleColor) {
|
||||
result.autoBubbleColor = {};
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -380,8 +380,10 @@ export class BackupImportStream extends Writable {
|
|||
// Schedule group avatar download.
|
||||
await pMap(
|
||||
[...this.pendingGroupAvatars.entries()],
|
||||
([conversationId, newAvatarUrl]) => {
|
||||
return groupAvatarJobQueue.add({ conversationId, newAvatarUrl });
|
||||
async ([conversationId, newAvatarUrl]) => {
|
||||
if (!window.SignalCI?.isBackupIntegration) {
|
||||
await groupAvatarJobQueue.add({ conversationId, newAvatarUrl });
|
||||
}
|
||||
},
|
||||
{ concurrency: MAX_CONCURRENCY }
|
||||
);
|
||||
|
@ -403,7 +405,9 @@ export class BackupImportStream extends Writable {
|
|||
await DataReader.getSizeOfPendingBackupAttachmentDownloadJobs()
|
||||
);
|
||||
|
||||
await AttachmentDownloadManager.start();
|
||||
if (!window.SignalCI?.isBackupIntegration) {
|
||||
await AttachmentDownloadManager.start();
|
||||
}
|
||||
|
||||
done();
|
||||
} catch (error) {
|
||||
|
@ -724,6 +728,12 @@ export class BackupImportStream extends Writable {
|
|||
defaultChatStyle.dimWallpaperInDarkMode
|
||||
);
|
||||
}
|
||||
if (defaultChatStyle.autoBubbleColor != null) {
|
||||
await window.storage.put(
|
||||
'defaultAutoBubbleColor',
|
||||
defaultChatStyle.autoBubbleColor
|
||||
);
|
||||
}
|
||||
|
||||
this.updateConversation(me);
|
||||
}
|
||||
|
@ -855,6 +865,11 @@ export class BackupImportStream extends Writable {
|
|||
profileSharing: group.whitelisted === true,
|
||||
hideStory: group.hideStory === true,
|
||||
storySendMode,
|
||||
avatar: avatarUrl
|
||||
? {
|
||||
url: avatarUrl,
|
||||
}
|
||||
: undefined,
|
||||
|
||||
// Snapshot
|
||||
name: dropNull(title?.title),
|
||||
|
@ -1098,8 +1113,12 @@ export class BackupImportStream extends Writable {
|
|||
|
||||
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.isPinned = chat.pinnedOrder != null;
|
||||
conversation.isPinned = (chat.pinnedOrder || 0) !== 0;
|
||||
|
||||
conversation.expireTimer =
|
||||
chat.expirationTimerMs && !chat.expirationTimerMs.isZero()
|
||||
|
@ -1134,6 +1153,9 @@ export class BackupImportStream extends Writable {
|
|||
if (chatStyle.dimWallpaperInDarkMode != null) {
|
||||
conversation.dimWallpaperInDarkMode = chatStyle.dimWallpaperInDarkMode;
|
||||
}
|
||||
if (chatStyle.autoBubbleColor) {
|
||||
conversation.autoBubbleColor = chatStyle.autoBubbleColor;
|
||||
}
|
||||
|
||||
this.updateConversation(conversation);
|
||||
|
||||
|
@ -1334,10 +1356,6 @@ export class BackupImportStream extends Writable {
|
|||
'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;
|
||||
|
||||
let sendStatus: SendStatus;
|
||||
|
@ -1379,7 +1397,6 @@ export class BackupImportStream extends Writable {
|
|||
});
|
||||
break;
|
||||
case Backups.SendStatus.Failed.FailureReason.NETWORK:
|
||||
case Backups.SendStatus.Failed.FailureReason.UNKNOWN:
|
||||
errors.push({
|
||||
serviceId,
|
||||
name: 'OutgoingMessageError',
|
||||
|
@ -1387,9 +1404,19 @@ export class BackupImportStream extends Writable {
|
|||
message: 'no http error',
|
||||
});
|
||||
break;
|
||||
case Backups.SendStatus.Failed.FailureReason.UNKNOWN:
|
||||
errors.push({
|
||||
serviceId,
|
||||
name: 'UnknownError',
|
||||
message: 'unknown error',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
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 {
|
||||
throw new Error(`Unknown sendStatus received: ${status}`);
|
||||
}
|
||||
|
@ -1416,7 +1443,8 @@ export class BackupImportStream extends Writable {
|
|||
};
|
||||
}
|
||||
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;
|
||||
|
||||
|
@ -1426,6 +1454,7 @@ export class BackupImportStream extends Writable {
|
|||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
received_at_ms: receivedAtMs,
|
||||
serverTimestamp,
|
||||
unidentifiedDeliveryReceived,
|
||||
},
|
||||
newActiveAt: receivedAtMs,
|
||||
|
@ -1437,6 +1466,7 @@ export class BackupImportStream extends Writable {
|
|||
readStatus: ReadStatus.Unread,
|
||||
seenStatus: SeenStatus.Unseen,
|
||||
received_at_ms: receivedAtMs,
|
||||
serverTimestamp,
|
||||
unidentifiedDeliveryReceived,
|
||||
},
|
||||
newActiveAt: receivedAtMs,
|
||||
|
@ -1574,10 +1604,10 @@ export class BackupImportStream extends Writable {
|
|||
{
|
||||
id: getTimestampFromLong(quote.targetSentTimestamp),
|
||||
authorAci: authorConvo.serviceId,
|
||||
text: dropNull(quote.text),
|
||||
bodyRanges: quote.bodyRanges?.length
|
||||
text: dropNull(quote.text?.body),
|
||||
bodyRanges: quote.text?.bodyRanges?.length
|
||||
? filterAndClean(
|
||||
quote.bodyRanges.map(range => ({
|
||||
quote.text?.bodyRanges.map(range => ({
|
||||
...range,
|
||||
mentionAci: range.mentionAci
|
||||
? Aci.parseFromServiceIdBinary(
|
||||
|
@ -1623,17 +1653,13 @@ export class BackupImportStream extends Writable {
|
|||
}
|
||||
return 0;
|
||||
})
|
||||
.map(({ emoji, authorId, sentTimestamp, receivedTimestamp }) => {
|
||||
.map(({ emoji, authorId, sentTimestamp }) => {
|
||||
strictAssert(emoji != null, 'reaction must have an emoji');
|
||||
strictAssert(authorId != null, 'reaction must have authorId');
|
||||
strictAssert(
|
||||
sentTimestamp != null,
|
||||
'reaction must have a sentTimestamp'
|
||||
);
|
||||
strictAssert(
|
||||
receivedTimestamp != null,
|
||||
'reaction must have a receivedTimestamp'
|
||||
);
|
||||
|
||||
const authorConvo = this.recipientIdToConvo.get(authorId.toNumber());
|
||||
strictAssert(
|
||||
|
@ -1645,7 +1671,6 @@ export class BackupImportStream extends Writable {
|
|||
emoji,
|
||||
fromId: authorConvo.id,
|
||||
targetTimestamp: getTimestampFromLong(sentTimestamp),
|
||||
receivedAtDate: getTimestampFromLong(receivedTimestamp),
|
||||
timestamp: getTimestampFromLong(sentTimestamp),
|
||||
};
|
||||
});
|
||||
|
@ -1681,7 +1706,6 @@ export class BackupImportStream extends Writable {
|
|||
prefix: dropNull(name.prefix),
|
||||
suffix: dropNull(name.suffix),
|
||||
middleName: dropNull(name.middleName),
|
||||
displayName: dropNull(name.displayName),
|
||||
}
|
||||
: undefined,
|
||||
number: number?.length
|
||||
|
@ -2909,9 +2933,10 @@ export class BackupImportStream extends Writable {
|
|||
return {
|
||||
wallpaperPhotoPointer: undefined,
|
||||
wallpaperPreset: undefined,
|
||||
color: 'ultramarine',
|
||||
color: undefined,
|
||||
customColorData: undefined,
|
||||
dimWallpaperInDarkMode: undefined,
|
||||
autoBubbleColor: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2929,11 +2954,13 @@ export class BackupImportStream extends Writable {
|
|||
|
||||
let color: ConversationColorType | undefined;
|
||||
let customColorData: CustomColorDataType | undefined;
|
||||
let autoBubbleColor = false;
|
||||
if (chatStyle.autoBubbleColor) {
|
||||
autoBubbleColor = true;
|
||||
if (wallpaperPreset != null) {
|
||||
color = WALLPAPER_TO_BUBBLE_COLOR.get(wallpaperPreset) || 'ultramarine';
|
||||
color = WALLPAPER_TO_BUBBLE_COLOR.get(wallpaperPreset);
|
||||
} else {
|
||||
color = 'ultramarine';
|
||||
color = undefined;
|
||||
}
|
||||
} else if (chatStyle.bubbleColorPreset != null) {
|
||||
const { BubbleColorPreset } = Backups.ChatStyle;
|
||||
|
@ -3025,6 +3052,7 @@ export class BackupImportStream extends Writable {
|
|||
color,
|
||||
customColorData,
|
||||
dimWallpaperInDarkMode,
|
||||
autoBubbleColor,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
1
ts/services/backups/types.d.ts
vendored
1
ts/services/backups/types.d.ts
vendored
|
@ -15,4 +15,5 @@ export type LocalChatStyle = Readonly<{
|
|||
color: ConversationColorType | undefined;
|
||||
customColorId: string | undefined;
|
||||
dimWallpaperInDarkMode: boolean | undefined;
|
||||
autoBubbleColor: boolean | undefined;
|
||||
}>;
|
||||
|
|
|
@ -308,7 +308,7 @@ function startInstaller(): ThunkAction<
|
|||
finishInstall({
|
||||
deviceName: SignalCI.deviceName,
|
||||
backupFile: SignalCI.backupData,
|
||||
isPlaintextBackup: SignalCI.isPlaintextBackup,
|
||||
isBackupIntegration: SignalCI.isBackupIntegration,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ describe('reaction utilities', () => {
|
|||
fromId: OUR_CONVO_ID,
|
||||
targetTimestamp: Date.now(),
|
||||
timestamp: Date.now(),
|
||||
receivedAtDate: Date.now(),
|
||||
...(isPending ? { isSentByConversationId: { [uuid()]: false } } : {}),
|
||||
});
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ describe('backup/attachments', () => {
|
|||
contactA = await window.ConversationController.getOrCreateAndWait(
|
||||
CONTACT_A,
|
||||
'private',
|
||||
{ systemGivenName: 'CONTACT_A' }
|
||||
{ systemGivenName: 'CONTACT_A', active_at: 1 }
|
||||
);
|
||||
|
||||
await loadAll();
|
||||
|
|
|
@ -86,32 +86,35 @@ describe('backup/groupv2/notifications', () => {
|
|||
await window.ConversationController.getOrCreateAndWait(
|
||||
CONTACT_A,
|
||||
'private',
|
||||
{ pni: CONTACT_A_PNI, systemGivenName: 'CONTACT_A' }
|
||||
{ pni: CONTACT_A_PNI, systemGivenName: 'CONTACT_A', active_at: 1 }
|
||||
);
|
||||
await window.ConversationController.getOrCreateAndWait(
|
||||
CONTACT_B,
|
||||
'private',
|
||||
{ systemGivenName: 'CONTACT_B' }
|
||||
{ systemGivenName: 'CONTACT_B', active_at: 1 }
|
||||
);
|
||||
await window.ConversationController.getOrCreateAndWait(
|
||||
CONTACT_C,
|
||||
'private',
|
||||
{ systemGivenName: 'CONTACT_C' }
|
||||
{ systemGivenName: 'CONTACT_C', active_at: 1 }
|
||||
);
|
||||
await window.ConversationController.getOrCreateAndWait(ADMIN_A, 'private', {
|
||||
systemGivenName: 'ADMIN_A',
|
||||
active_at: 1,
|
||||
});
|
||||
await window.ConversationController.getOrCreateAndWait(
|
||||
INVITEE_A,
|
||||
'private',
|
||||
{
|
||||
systemGivenName: 'INVITEE_A',
|
||||
active_at: 1,
|
||||
}
|
||||
);
|
||||
await window.ConversationController.getOrCreateAndWait(GROUP_ID, 'group', {
|
||||
groupVersion: 2,
|
||||
masterKey: Bytes.toBase64(getRandomBytes(32)),
|
||||
name: 'Rock Enthusiasts',
|
||||
active_at: 1,
|
||||
});
|
||||
|
||||
await loadAll();
|
||||
|
|
|
@ -51,12 +51,12 @@ describe('backup/bubble messages', () => {
|
|||
contactA = await window.ConversationController.getOrCreateAndWait(
|
||||
CONTACT_A,
|
||||
'private',
|
||||
{ systemGivenName: 'CONTACT_A' }
|
||||
{ systemGivenName: 'CONTACT_A', active_at: 1 }
|
||||
);
|
||||
contactB = await window.ConversationController.getOrCreateAndWait(
|
||||
CONTACT_B,
|
||||
'private',
|
||||
{ systemGivenName: 'CONTACT_B' }
|
||||
{ systemGivenName: 'CONTACT_B', active_at: 1 }
|
||||
);
|
||||
|
||||
gv1 = await window.ConversationController.getOrCreateAndWait(
|
||||
|
@ -64,6 +64,7 @@ describe('backup/bubble messages', () => {
|
|||
'group',
|
||||
{
|
||||
groupVersion: 1,
|
||||
active_at: 1,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ describe('backup/calling', () => {
|
|||
contactA = await window.ConversationController.getOrCreateAndWait(
|
||||
CONTACT_A,
|
||||
'private',
|
||||
{ systemGivenName: 'CONTACT_A' }
|
||||
{ systemGivenName: 'CONTACT_A', active_at: 1 }
|
||||
);
|
||||
groupA = await window.ConversationController.getOrCreateAndWait(
|
||||
GROUP_ID_STRING,
|
||||
|
@ -60,6 +60,7 @@ describe('backup/calling', () => {
|
|||
groupVersion: 2,
|
||||
masterKey: Bytes.toBase64(GROUP_MASTER_KEY),
|
||||
name: 'Rock Enthusiasts',
|
||||
active_at: 1,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ describe('backup/non-bubble messages', () => {
|
|||
contactA = await window.ConversationController.getOrCreateAndWait(
|
||||
CONTACT_A,
|
||||
'private',
|
||||
{ systemGivenName: 'CONTACT_A' }
|
||||
{ systemGivenName: 'CONTACT_A', active_at: 1 }
|
||||
);
|
||||
|
||||
group = await window.ConversationController.getOrCreateAndWait(
|
||||
|
@ -53,6 +53,7 @@ describe('backup/non-bubble messages', () => {
|
|||
groupVersion: 2,
|
||||
masterKey: Bytes.toBase64(getRandomBytes(32)),
|
||||
name: 'Rock Enthusiasts',
|
||||
active_at: 1,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -345,7 +346,6 @@ describe('backup/non-bubble messages', () => {
|
|||
fromId: contactA.id,
|
||||
targetTimestamp: 1,
|
||||
timestamp: 1,
|
||||
receivedAtDate: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -385,7 +385,6 @@ describe('backup/non-bubble messages', () => {
|
|||
fromId: contactA.id,
|
||||
targetTimestamp: 1,
|
||||
timestamp: 1,
|
||||
receivedAtDate: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
119
ts/test-mock/backups/integration.ts
Normal file
119
ts/test-mock/backups/integration.ts
Normal 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);
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -164,7 +164,7 @@ export class Bootstrap {
|
|||
private readonly randomId = crypto.randomBytes(8).toString('hex');
|
||||
|
||||
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({
|
||||
// Limit number of storage read keys for easier testing
|
||||
maxStorageReadKeys: MAX_STORAGE_READ_KEYS,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import type { ElectronApplication, Page } from 'playwright';
|
||||
import { _electron as electron } from 'playwright';
|
||||
import { EventEmitter } from 'events';
|
||||
import { EventEmitter, once } from 'events';
|
||||
import pTimeout from 'p-timeout';
|
||||
|
||||
import type {
|
||||
|
@ -12,6 +12,7 @@ import type {
|
|||
} from '../challenge';
|
||||
import type { ReceiptType } from '../types/Receipt';
|
||||
import { SECOND } from '../util/durations';
|
||||
import { drop } from '../util/drop';
|
||||
|
||||
export type AppLoadedInfoType = Readonly<{
|
||||
loadTime: number;
|
||||
|
@ -87,6 +88,8 @@ export class App extends EventEmitter {
|
|||
}
|
||||
|
||||
this.privApp.on('close', () => this.emit('close'));
|
||||
|
||||
drop(this.printLoop());
|
||||
}
|
||||
|
||||
public async waitForProvisionURL(): Promise<string> {
|
||||
|
@ -239,4 +242,36 @@ export class App extends EventEmitter {
|
|||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,7 +126,7 @@ type CreateAccountSharedOptionsType = Readonly<{
|
|||
|
||||
// Test-only
|
||||
backupFile?: Uint8Array;
|
||||
isPlaintextBackup?: boolean;
|
||||
isBackupIntegration?: boolean;
|
||||
}>;
|
||||
|
||||
type CreatePrimaryDeviceOptionsType = Readonly<{
|
||||
|
@ -220,7 +220,7 @@ function signedPreKeyToUploadSignedPreKey({
|
|||
export type ConfirmNumberResultType = Readonly<{
|
||||
deviceName: string;
|
||||
backupFile: Uint8Array | undefined;
|
||||
isPlaintextBackup: boolean;
|
||||
isBackupIntegration: boolean;
|
||||
}>;
|
||||
|
||||
export default class AccountManager extends EventTarget {
|
||||
|
@ -923,7 +923,7 @@ export default class AccountManager extends EventTarget {
|
|||
readReceipts,
|
||||
userAgent,
|
||||
backupFile,
|
||||
isPlaintextBackup,
|
||||
isBackupIntegration,
|
||||
} = options;
|
||||
|
||||
const { storage } = window.textsecure;
|
||||
|
@ -969,7 +969,7 @@ export default class AccountManager extends EventTarget {
|
|||
if (backupFile !== undefined) {
|
||||
log.warn(
|
||||
'createAccount: Restoring from ' +
|
||||
`${isPlaintextBackup ? 'plaintext' : 'ciphertext'} backup; ` +
|
||||
`${isBackupIntegration ? 'plaintext' : 'ciphertext'} backup; ` +
|
||||
'deleting all previous data'
|
||||
);
|
||||
}
|
||||
|
@ -1231,7 +1231,9 @@ export default class AccountManager extends EventTarget {
|
|||
if (backupFile !== undefined) {
|
||||
await backupsService.importBackup(
|
||||
() => Readable.from([backupFile]),
|
||||
isPlaintextBackup ? BackupType.TestOnlyPlaintext : BackupType.Ciphertext
|
||||
isBackupIntegration
|
||||
? BackupType.TestOnlyPlaintext
|
||||
: BackupType.Ciphertext
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ type StateType = Readonly<
|
|||
export type PrepareLinkDataOptionsType = Readonly<{
|
||||
deviceName: string;
|
||||
backupFile?: Uint8Array;
|
||||
isPlaintextBackup?: boolean;
|
||||
isBackupIntegration?: boolean;
|
||||
}>;
|
||||
|
||||
export class Provisioner {
|
||||
|
@ -153,7 +153,7 @@ export class Provisioner {
|
|||
public prepareLinkData({
|
||||
deviceName,
|
||||
backupFile,
|
||||
isPlaintextBackup,
|
||||
isBackupIntegration,
|
||||
}: PrepareLinkDataOptionsType): CreateLinkedDeviceOptionsType {
|
||||
strictAssert(
|
||||
this.state.step === Step.ReadyToLink,
|
||||
|
@ -211,7 +211,7 @@ export class Provisioner {
|
|||
MAX_DEVICE_NAME_LENGTH
|
||||
),
|
||||
backupFile,
|
||||
isPlaintextBackup,
|
||||
isBackupIntegration,
|
||||
userAgent,
|
||||
ourAci,
|
||||
ourPni,
|
||||
|
|
|
@ -43,7 +43,7 @@ export const rendererConfigSchema = z.object({
|
|||
disableIPv6: z.boolean(),
|
||||
dnsFallback: DNSFallbackSchema,
|
||||
ciBackupPath: configOptionalStringSchema,
|
||||
ciIsPlaintextBackup: z.boolean(),
|
||||
ciIsBackupIntegration: z.boolean(),
|
||||
environment: environmentSchema,
|
||||
isMockTestEnvironment: z.boolean(),
|
||||
homePath: configRequiredStringSchema,
|
||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -69,6 +69,7 @@ export type StorageAccessType = {
|
|||
defaultWallpaperPhotoPointer: Uint8Array;
|
||||
defaultWallpaperPreset: number;
|
||||
defaultDimWallpaperInDarkMode: boolean;
|
||||
defaultAutoBubbleColor: boolean;
|
||||
|
||||
customColors: CustomColorsItemType;
|
||||
device_name: string;
|
||||
|
|
|
@ -25,6 +25,6 @@ if (config.ciMode) {
|
|||
backupData: config.ciBackupPath
|
||||
? fs.readFileSync(config.ciBackupPath)
|
||||
: undefined,
|
||||
isPlaintextBackup: config.ciIsPlaintextBackup === true,
|
||||
isBackupIntegration: config.ciIsBackupIntegration === true,
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue