Fix some backup integration tests
This commit is contained in:
parent
aa75ec13a6
commit
f91ec886a2
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
|
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
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
9
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
ts/CI.ts
20
ts/CI.ts
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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!');
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
2
ts/model-types.d.ts
vendored
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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;
|
color: ConversationColorType | undefined;
|
||||||
customColorId: string | undefined;
|
customColorId: string | undefined;
|
||||||
dimWallpaperInDarkMode: boolean | undefined;
|
dimWallpaperInDarkMode: boolean | undefined;
|
||||||
|
autoBubbleColor: boolean | undefined;
|
||||||
}>;
|
}>;
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 } } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
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');
|
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,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue