diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts index 69f58beaca..0c04e07e92 100644 --- a/ts/services/backups/import.ts +++ b/ts/services/backups/import.ts @@ -374,7 +374,6 @@ export class BackupImportStream extends Writable { toastType: ToastType.FailedToImportBackup, }); } - // TODO (DESKTOP-7934): throw in tests if we cannot process a frame } else { log.info(`${this.logId}: successfully processed all frames.`); } diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index 0a01d2c590..beb6825f27 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -305,6 +305,9 @@ export class BackupsService { log.info(`importBackup: starting ${backupType}...`); this.isRunning = 'import'; const importStart = Date.now(); + + await DataWriter.disableMessageInsertTriggers(); + try { const importStream = await BackupImportStream.create(backupType); if (backupType === BackupType.Ciphertext) { @@ -397,6 +400,8 @@ export class BackupsService { throw error; } finally { this.isRunning = false; + await DataWriter.enableMessageInsertTriggersAndBackfill(); + window.IPC.stopTrackingQueryStats({ epochName: 'Backup Import' }); if (window.SignalCI) { window.SignalCI.handleEvent('backupImportComplete', { diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 5d8ad2f9cc..be83d6164a 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -952,6 +952,10 @@ type WritableInterface = { insertJob(job: Readonly): void; deleteJob(id: string): void; + disableMessageInsertTriggers(): void; + enableMessageInsertTriggersAndBackfill(): void; + ensureMessageInsertTriggersAreEnabled(): void; + processGroupCallRingCancellation(ringId: bigint): void; cleanExpiredGroupCallRingCancellations(): void; }; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index f8ec9703e0..9982b762f1 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -543,6 +543,10 @@ export const DataWriter: ServerWritableInterface = { processGroupCallRingCancellation, cleanExpiredGroupCallRingCancellations, + disableMessageInsertTriggers, + enableMessageInsertTriggersAndBackfill, + ensureMessageInsertTriggersAreEnabled, + // Server-only removeKnownStickers, @@ -7463,3 +7467,84 @@ function getUnreadEditedMessagesAndMarkRead( }); })(); } + +function disableMessageInsertTriggers(db: WritableDB): void { + db.transaction(() => { + createOrUpdateItem(db, { + id: 'messageInsertTriggersDisabled', + value: true, + }); + db.exec('DROP TRIGGER IF EXISTS messages_on_insert;'); + db.exec('DROP TRIGGER IF EXISTS messages_on_insert_insert_mentions;'); + })(); +} + +const selectMentionsFromMessages = ` + SELECT messages.id, bodyRanges.value ->> 'mentionAci' as mentionAci, + bodyRanges.value ->> 'start' as start, + bodyRanges.value ->> 'length' as length + FROM messages, json_each(messages.json ->> 'bodyRanges') as bodyRanges + WHERE bodyRanges.value ->> 'mentionAci' IS NOT NULL +`; + +function enableMessageInsertTriggersAndBackfill(db: WritableDB): void { + const createTriggersQuery = ` + DROP TRIGGER IF EXISTS messages_on_insert; + CREATE TRIGGER messages_on_insert AFTER INSERT ON messages + WHEN new.isViewOnce IS NOT 1 AND new.storyId IS NULL + BEGIN + INSERT INTO messages_fts + (rowid, body) + VALUES + (new.rowid, new.body); + END; + + DROP TRIGGER IF EXISTS messages_on_insert_insert_mentions; + CREATE TRIGGER messages_on_insert_insert_mentions AFTER INSERT ON messages + BEGIN + INSERT INTO mentions (messageId, mentionAci, start, length) + ${selectMentionsFromMessages} + AND messages.id = new.id; + END; + `; + db.transaction(() => { + backfillMentionsTable(db); + backfillMessagesFtsTable(db); + db.exec(createTriggersQuery); + createOrUpdateItem(db, { + id: 'messageInsertTriggersDisabled', + value: false, + }); + })(); +} + +function backfillMessagesFtsTable(db: WritableDB): void { + db.exec(` + DELETE FROM messages_fts; + INSERT OR REPLACE INTO messages_fts (rowid, body) + SELECT rowid, body + FROM messages + WHERE isViewOnce IS NOT 1 AND storyId IS NULL; + `); +} + +function backfillMentionsTable(db: WritableDB): void { + db.exec(` + DELETE FROM mentions; + INSERT INTO mentions (messageId, mentionAci, start, length) + ${selectMentionsFromMessages}; + `); +} + +function ensureMessageInsertTriggersAreEnabled(db: WritableDB): void { + db.transaction(() => { + const storedItem = getItemById(db, 'messageInsertTriggersDisabled'); + const triggersDisabled = storedItem?.value; + if (triggersDisabled) { + logger.warn( + 'Message insert triggers were disabled; reenabling and backfilling data' + ); + enableMessageInsertTriggersAndBackfill(db); + } + })(); +} diff --git a/ts/sql/migrations/45-stories.ts b/ts/sql/migrations/45-stories.ts index 0bc0ec16fd..6c39ca8537 100644 --- a/ts/sql/migrations/45-stories.ts +++ b/ts/sql/migrations/45-stories.ts @@ -51,6 +51,8 @@ export default function updateToSchemaVersion45( --- Message insert/update triggers to exclude stories and story replies DROP TRIGGER messages_on_insert; + -- Note: any changes to this trigger must be reflected in + -- Server.ts: enableMessageInsertTriggersAndBackfill CREATE TRIGGER messages_on_insert AFTER INSERT ON messages WHEN new.isViewOnce IS NOT 1 AND new.storyId IS NULL BEGIN diff --git a/ts/sql/migrations/84-all-mentions.ts b/ts/sql/migrations/84-all-mentions.ts index 632ff0a067..607261eda6 100644 --- a/ts/sql/migrations/84-all-mentions.ts +++ b/ts/sql/migrations/84-all-mentions.ts @@ -35,6 +35,8 @@ export default function updateToSchemaVersion84( INSERT INTO mentions (messageId, mentionUuid, start, length) ${selectMentionsFromMessages}; + -- Note: any changes to this trigger must be reflected in + -- Server.ts: enableMessageInsertTriggersAndBackfill CREATE TRIGGER messages_on_insert_insert_mentions AFTER INSERT ON messages BEGIN INSERT INTO mentions (messageId, mentionUuid, start, length) diff --git a/ts/sql/migrations/index.ts b/ts/sql/migrations/index.ts index 678600e16a..cc08e26c10 100644 --- a/ts/sql/migrations/index.ts +++ b/ts/sql/migrations/index.ts @@ -105,6 +105,7 @@ import { updateToSchemaVersion1250, version as MAX_VERSION, } from './1250-defunct-call-links-storage'; +import { DataWriter } from '../Server'; function updateToSchemaVersion1( currentVersion: number, @@ -2132,6 +2133,7 @@ export function updateSchema(db: WritableDB, logger: LoggerType): void { runSchemaUpdate(startingVersion, db, logger); } + DataWriter.ensureMessageInsertTriggersAreEnabled(db); enableFTS5SecureDelete(db, logger); if (startingVersion !== MAX_VERSION) { diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts index 3dd7325392..93da2d23ea 100644 --- a/ts/types/Storage.d.ts +++ b/ts/types/Storage.d.ts @@ -152,6 +152,7 @@ export type StorageAccessType = { backupMediaDownloadPaused: boolean; backupMediaDownloadBannerDismissed: boolean; backupMediaDownloadIdle: boolean; + messageInsertTriggersDisabled: boolean; setBackupMessagesSignatureKey: boolean; setBackupMediaSignatureKey: boolean; lastReceivedAtCounter: number;