From 9f5602af056ebfa9b3c6f3a5f54e9fa0245a2e6a Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:16:43 -0600 Subject: [PATCH] Disallow conversation model creation during import Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> --- ts/ConversationController.ts | 15 +++++++ ts/background.ts | 8 +++- ts/services/backups/import.ts | 1 + ts/services/backups/index.ts | 3 ++ .../tapToViewMessagesDeletionService.ts | 41 ++++++++++++++++--- 5 files changed, 60 insertions(+), 8 deletions(-) diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index 39d3be9a57..d678b3a47a 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -150,6 +150,7 @@ export function start(): void { export class ConversationController { #_initialFetchComplete = false; + #isReadOnly = false; private _initialPromise: undefined | Promise; @@ -291,6 +292,10 @@ export class ConversationController { return conversation; } + if (this.#isReadOnly) { + throw new Error('ConversationController is read-only'); + } + const id = generateUuid(); if (type === 'group') { @@ -1299,6 +1304,16 @@ export class ConversationController { ); } + setReadOnly(value: boolean): void { + if (this.#isReadOnly === value) { + log.warn(`ConversationController: already at readOnly=${value}`); + return; + } + + log.info(`ConversationController: readOnly=${value}`); + this.#isReadOnly = value; + } + reset(): void { delete this._initialPromise; this.#_initialFetchComplete = false; diff --git a/ts/background.ts b/ts/background.ts index 0655366b27..1c198da463 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1513,10 +1513,10 @@ export async function startApp(): Promise { }); void updateExpiringMessagesService(); - void tapToViewMessagesDeletionService.update(); + tapToViewMessagesDeletionService.update(); window.Whisper.events.on('timetravel', () => { void updateExpiringMessagesService(); - void tapToViewMessagesDeletionService.update(); + tapToViewMessagesDeletionService.update(); }); const isCoreDataValid = Boolean( @@ -1652,6 +1652,8 @@ export async function startApp(): Promise { const backupDownloadPath = window.storage.get('backupDownloadPath'); if (backupDownloadPath) { + tapToViewMessagesDeletionService.pause(); + // Download backup before enabling request handler and storage service try { await backupsService.downloadAndImport({ @@ -1670,6 +1672,8 @@ export async function startApp(): Promise { log.error('afterStart: backup download failed, rejecting'); backupReady.reject(error); throw error; + } finally { + tapToViewMessagesDeletionService.resume(); } } else { backupReady.resolve(); diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts index d530652df3..742c2f14de 100644 --- a/ts/services/backups/import.ts +++ b/ts/services/backups/import.ts @@ -342,6 +342,7 @@ export class BackupImportStream extends Writable { } // Reset and reload conversations and storage again + window.ConversationController.setReadOnly(false); window.ConversationController.reset(); await window.ConversationController.load(); diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index e078415708..ca2adf4475 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -350,6 +350,8 @@ export class BackupsService { await DataWriter.disableMessageInsertTriggers(); try { + window.ConversationController.setReadOnly(true); + const importStream = await BackupImportStream.create(backupType); if (backupType === BackupType.Ciphertext) { const { aesKey, macKey } = getKeyMaterial( @@ -440,6 +442,7 @@ export class BackupsService { throw error; } finally { + window.ConversationController.setReadOnly(false); this.#isRunning = false; await DataWriter.enableMessageInsertTriggersAndBackfill(); diff --git a/ts/services/tapToViewMessagesDeletionService.ts b/ts/services/tapToViewMessagesDeletionService.ts index 242836da85..421b808e4c 100644 --- a/ts/services/tapToViewMessagesDeletionService.ts +++ b/ts/services/tapToViewMessagesDeletionService.ts @@ -10,6 +10,7 @@ import { strictAssert } from '../util/assert'; import { toBoundedDate } from '../util/timestamp'; import { getMessageIdForLogging } from '../util/idForLogging'; import { eraseMessageContents } from '../util/cleanup'; +import { drop } from '../util/drop'; import { MessageModel } from '../models/messages'; async function eraseTapToViewMessages() { @@ -53,12 +54,38 @@ async function eraseTapToViewMessages() { } class TapToViewMessagesDeletionService { - public update: () => Promise; - #timeout?: ReturnType; + #isPaused = false; + #debouncedUpdate = debounce(this.#checkTapToViewMessages); - constructor() { - this.update = debounce(this.#checkTapToViewMessages, 1000); + update() { + drop(this.#debouncedUpdate()); + } + + pause(): void { + if (this.#isPaused) { + window.SignalContext.log.warn('checkTapToViewMessages: already paused'); + return; + } + + window.SignalContext.log.info('checkTapToViewMessages: pause'); + + this.#isPaused = true; + clearTimeoutIfNecessary(this.#timeout); + this.#timeout = undefined; + } + + resume(): void { + if (!this.#isPaused) { + window.SignalContext.log.warn('checkTapToViewMessages: not paused'); + return; + } + + window.SignalContext.log.info('checkTapToViewMessages: resuming'); + this.#isPaused = false; + + this.#debouncedUpdate.cancel(); + this.update(); } async #checkTapToViewMessages() { @@ -89,8 +116,10 @@ class TapToViewMessagesDeletionService { clearTimeoutIfNecessary(this.#timeout); this.#timeout = setTimeout(async () => { - await eraseTapToViewMessages(); - void this.update(); + if (!this.#isPaused && !window.SignalContext.isTestOrMockEnvironment()) { + await eraseTapToViewMessages(); + } + this.update(); }, wait); } }