From 03a187097f9c8f415c4fc6981669cf118163c2be Mon Sep 17 00:00:00 2001 From: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com> Date: Wed, 16 Jun 2021 17:20:17 -0500 Subject: [PATCH] Disappearing message cleanups --- js/delivery_receipts.js | 7 +- js/expiring_messages.js | 27 ++-- js/read_receipts.js | 7 +- js/read_syncs.js | 6 +- ts/background.ts | 27 ++-- ts/components/conversation/Message.tsx | 5 +- ts/groups.ts | 2 + ts/model-types.d.ts | 1 - ts/models/conversations.ts | 3 - ts/models/messages.ts | 35 +---- ts/services/MessageUpdater.ts | 43 ------ ts/sql/Client.ts | 25 +--- ts/sql/Interface.ts | 21 +-- ts/sql/Server.ts | 196 +++++++++++-------------- ts/types/Message.ts | 3 +- ts/util/messageBatcher.ts | 9 +- 16 files changed, 149 insertions(+), 268 deletions(-) diff --git a/js/delivery_receipts.js b/js/delivery_receipts.js index 0335bfc6eab..560ffd3ae10 100644 --- a/js/delivery_receipts.js +++ b/js/delivery_receipts.js @@ -1,4 +1,4 @@ -// Copyright 2016-2020 Signal Messenger, LLC +// Copyright 2016-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only /* global @@ -96,11 +96,6 @@ sent: true, }); - if (message.isExpiring() && !expirationStartTimestamp) { - // TODO DESKTOP-1509: use setToExpire once this is TS - await message.setToExpire(false, { skipSave: true }); - } - window.Signal.Util.queueUpdateMessage(message.attributes); // notify frontend listeners diff --git a/js/expiring_messages.js b/js/expiring_messages.js index caf9fe45b69..512fe042aba 100644 --- a/js/expiring_messages.js +++ b/js/expiring_messages.js @@ -1,4 +1,4 @@ -// Copyright 2016-2020 Signal Messenger, LLC +// Copyright 2016-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only /* global @@ -17,6 +17,9 @@ const messages = await window.Signal.Data.getExpiredMessages({ MessageCollection: Whisper.MessageCollection, }); + window.log.info( + `destroyExpiredMessages: found ${messages.length} messages to expire` + ); const messageIds = []; const inMemoryMessages = []; @@ -59,20 +62,15 @@ let timeout; async function checkExpiringMessages() { - // Look up the next expiring message and set a timer to destroy it - const message = await window.Signal.Data.getNextExpiringMessage({ - Message: Whisper.Message, - }); + window.log.info('checkExpiringMessages: checking for expiring messages'); - if (!message) { + const soonestExpiry = await window.Signal.Data.getSoonestMessageExpiry(); + if (!soonestExpiry) { + window.log.info('checkExpiringMessages: found no messages to expire'); return; } - const expiresAt = message.get('expires_at'); - Whisper.ExpiringMessagesListener.nextExpiration = expiresAt; - window.log.info('next message expires', new Date(expiresAt).toISOString()); - - let wait = expiresAt - Date.now(); + let wait = soonestExpiry - Date.now(); // In the past if (wait < 0) { @@ -84,6 +82,12 @@ wait = 2147483647; } + window.log.info( + `checkExpiringMessages: next message expires ${new Date( + soonestExpiry + ).toISOString()}; waiting ${wait} ms before clearing` + ); + clearTimeout(timeout); timeout = setTimeout(destroyExpiredMessages, wait); } @@ -93,7 +97,6 @@ ); Whisper.ExpiringMessagesListener = { - nextExpiration: null, init(events) { checkExpiringMessages(); events.on('timetravel', debouncedCheckExpiringMessages); diff --git a/js/read_receipts.js b/js/read_receipts.js index 4582e84f1ff..0aba0699e72 100644 --- a/js/read_receipts.js +++ b/js/read_receipts.js @@ -1,4 +1,4 @@ -// Copyright 2016-2020 Signal Messenger, LLC +// Copyright 2016-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only /* global @@ -97,11 +97,6 @@ sent: true, }); - if (message.isExpiring() && !expirationStartTimestamp) { - // TODO DESKTOP-1509: use setToExpire once this is TS - await message.setToExpire(false, { skipSave: true }); - } - window.Signal.Util.queueUpdateMessage(message.attributes); // notify frontend listeners diff --git a/js/read_syncs.js b/js/read_syncs.js index bb2458ed7a9..299446d627b 100644 --- a/js/read_syncs.js +++ b/js/read_syncs.js @@ -1,4 +1,4 @@ -// Copyright 2017-2020 Signal Messenger, LLC +// Copyright 2017-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only /* global @@ -120,10 +120,6 @@ ); message.set({ expirationStartTimestamp }); - const force = true; - // TODO DESKTOP-1509: use setToExpire once this is TS - await message.setToExpire(force, { skipSave: true }); - const conversation = message.getConversation(); if (conversation) { conversation.trigger('expiration-change', message); diff --git a/ts/background.ts b/ts/background.ts index e974feb63fb..f94f4746747 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -24,14 +24,13 @@ import { routineProfileRefresh } from './routineProfileRefresh'; import { isMoreRecentThan, isOlderThan } from './util/timestamp'; import { isValidReactionEmoji } from './reactions/isValidReactionEmoji'; import { ConversationModel } from './models/conversations'; -import { getMessageById } from './models/messages'; +import { getMessageById, MessageModel } from './models/messages'; import { createBatcher } from './util/batcher'; import { updateConversationsWithUuidLookup } from './updateConversationsWithUuidLookup'; import { initializeAllJobQueues } from './jobs/initializeAllJobQueues'; import { removeStorageKeyJobQueue } from './jobs/removeStorageKeyJobQueue'; import { ourProfileKeyService } from './services/ourProfileKey'; import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey'; -import { setToExpire } from './services/MessageUpdater'; import { LatestQueue } from './util/LatestQueue'; import { parseIntOrThrow } from './util/parseIntOrThrow'; import { @@ -1642,7 +1641,7 @@ export async function startApp(): Promise { window.dispatchEvent(new Event('storage_ready')); window.log.info('Cleanup: starting...'); - const messagesForCleanup = await window.Signal.Data.getOutgoingWithoutExpiresAt( + const messagesForCleanup = await window.Signal.Data.getOutgoingWithoutExpirationStartTimestamp( { MessageCollection: window.Whisper.MessageCollection, } @@ -1652,11 +1651,13 @@ export async function startApp(): Promise { ); await Promise.all( messagesForCleanup.map(async message => { + assert( + !message.get('expirationStartTimestamp'), + 'Cleanup should not have messages with an expirationStartTimestamp' + ); + const delivered = message.get('delivered'); const sentAt = message.get('sent_at'); - const expirationStartTimestamp = message.get( - 'expirationStartTimestamp' - ); if (message.hasErrors()) { return; @@ -1666,12 +1667,7 @@ export async function startApp(): Promise { window.log.info( `Cleanup: Starting timer for delivered message ${sentAt}` ); - message.set( - setToExpire({ - ...message.attributes, - expirationStartTimestamp: expirationStartTimestamp || sentAt, - }) - ); + message.set('expirationStartTimestamp', sentAt); return; } @@ -1685,6 +1681,9 @@ export async function startApp(): Promise { } }) ); + if (messagesForCleanup.length) { + window.Whisper.ExpiringMessagesListener.update(); + } window.log.info('Cleanup: complete'); window.log.info('listening for registration events'); @@ -2389,7 +2388,9 @@ export async function startApp(): Promise { messagesToSave.push(message.attributes); } }); - await window.Signal.Data.saveMessages(messagesToSave, {}); + await window.Signal.Data.saveMessages(messagesToSave, { + Message: MessageModel, + }); } function onReconnect() { // We disable notifications on first connect, but the same applies to reconnect. In diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 3872cfbef2d..398260b1c34 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -152,7 +152,6 @@ export type PropsData = { isViewOnce: boolean; }; previews: Array; - isExpired?: boolean; isTapToView?: boolean; isTapToViewExpired?: boolean; @@ -497,7 +496,7 @@ export class Message extends React.Component { public checkExpired(): void { const now = Date.now(); - const { isExpired, expirationTimestamp, expirationLength } = this.props; + const { expirationTimestamp, expirationLength } = this.props; if (!expirationTimestamp || !expirationLength) { return; @@ -506,7 +505,7 @@ export class Message extends React.Component { return; } - if (isExpired || now >= expirationTimestamp) { + if (now >= expirationTimestamp) { this.setState({ expiring: true, }); diff --git a/ts/groups.ts b/ts/groups.ts index 9124bd906c9..3f15a98ed4b 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -1717,6 +1717,7 @@ export async function createGroupV2({ }; await window.Signal.Data.saveMessages([createdTheGroupMessage], { forceSave: true, + Message: window.Whisper.Message, }); const model = new window.Whisper.Message(createdTheGroupMessage); window.MessageController.register(model.id, model); @@ -2831,6 +2832,7 @@ async function updateGroup( if (changeMessagesToSave.length > 0) { await window.Signal.Data.saveMessages(changeMessagesToSave, { forceSave: true, + Message: window.Whisper.Message, }); changeMessagesToSave.forEach(changeMessage => { const model = new window.Whisper.Message(changeMessage); diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index dac5df51ec7..9ec2b984a00 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -90,7 +90,6 @@ export type MessageAttributesType = { errors?: Array; expirationStartTimestamp: number | null; expireTimer: number; - expires_at: number; groupMigration?: GroupMigrationType; group_update: { avatarUpdated: boolean; diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 60ef31c1f00..acbf84a4d40 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -1234,9 +1234,6 @@ export class ConversationModel extends window.Backbone } addSingleMessage(message: MessageModel): MessageModel { - // TODO use MessageUpdater.setToExpire - message.setToExpire(); - const { messagesAdded } = window.reduxActions.conversations; const isNewMessage = true; messagesAdded( diff --git a/ts/models/messages.ts b/ts/models/messages.ts index ebea782c729..37b8145ff6b 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -56,7 +56,7 @@ import { AttachmentType, isImage, isVideo } from '../types/Attachment'; import { MIMEType } from '../types/MIME'; import { LinkPreviewType } from '../types/message/LinkPreviews'; import { ourProfileKeyService } from '../services/ourProfileKey'; -import { markRead, setToExpire } from '../services/MessageUpdater'; +import { markRead } from '../services/MessageUpdater'; import { isDirectConversation, isGroupV1, @@ -256,8 +256,6 @@ export class MessageModel extends window.Backbone.Model { isSelected?: boolean; - hasExpired?: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any quotedMessage: any; @@ -281,10 +279,7 @@ export class MessageModel extends window.Backbone.Model { this.OUR_UUID = window.textsecure.storage.user.getUuid(); this.on('destroy', this.onDestroy); - this.on('change:expirationStartTimestamp', this.setToExpire); - this.on('change:expireTimer', this.setToExpire); this.on('unload', this.unload); - this.setToExpire(); this.on('change', this.notifyRedux); } @@ -1035,7 +1030,6 @@ export class MessageModel extends window.Backbone.Model { attachments: this.getAttachmentsForMessage(), previews: this.getPropsForPreview(), quote: this.getPropsForQuote(), - isExpired: this.hasExpired, expirationLength, expirationTimestamp, reactions, @@ -2028,10 +2022,6 @@ export class MessageModel extends window.Backbone.Model { } } - onExpired(): void { - this.hasExpired = true; - } - isUnidentifiedDelivery( contactId: string, lookup: Record @@ -2163,29 +2153,6 @@ export class MessageModel extends window.Backbone.Model { return this.get('expireTimer') && this.get('expirationStartTimestamp'); } - setToExpire(force = false, options = {}): void { - this.set(setToExpire(this.attributes, { ...options, force })); - } - - isExpired(): boolean { - return this.msTilExpire() <= 0; - } - - msTilExpire(): number { - if (!this.isExpiring()) { - return Infinity; - } - const now = Date.now(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const start = this.get('expirationStartTimestamp')!; - const delta = this.get('expireTimer') * 1000; - let msFromNow = start + delta - now; - if (msFromNow < 0) { - msFromNow = 0; - } - return msFromNow; - } - getIncomingContact(): ConversationModel | undefined | null { if (!this.isIncoming()) { return null; diff --git a/ts/services/MessageUpdater.ts b/ts/services/MessageUpdater.ts index 849ddfbf4f0..7a34629468c 100644 --- a/ts/services/MessageUpdater.ts +++ b/ts/services/MessageUpdater.ts @@ -42,46 +42,3 @@ export function getExpiresAt( ? messageAttrs.expirationStartTimestamp + expireTimerMs : undefined; } - -export function setToExpire( - messageAttrs: MessageAttributesType, - { force = false, skipSave = false } = {} -): MessageAttributesType { - if (!isExpiring(messageAttrs) || (!force && messageAttrs.expires_at)) { - return messageAttrs; - } - - const expiresAt = getExpiresAt(messageAttrs); - - if (!expiresAt) { - return messageAttrs; - } - - const nextMessageAttributes = { - ...messageAttrs, - expires_at: expiresAt, - }; - - window.log.info('Set message expiration', { - start: messageAttrs.expirationStartTimestamp, - expiresAt, - sentAt: messageAttrs.sent_at, - }); - - if (messageAttrs.id && !skipSave) { - window.Signal.Util.queueUpdateMessage(nextMessageAttributes); - } - - return nextMessageAttributes; -} - -function isExpiring( - messageAttrs: Pick< - MessageAttributesType, - 'expireTimer' | 'expirationStartTimestamp' - > -): boolean { - return Boolean( - messageAttrs.expireTimer && messageAttrs.expirationStartTimestamp - ); -} diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index 64c29e3e514..28dd0d905e2 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -189,8 +189,8 @@ const dataInterface: ClientInterface = { getAllMessageIds, getMessagesBySentAt, getExpiredMessages, - getOutgoingWithoutExpiresAt, - getNextExpiringMessage, + getOutgoingWithoutExpirationStartTimestamp, + getSoonestMessageExpiry, getNextTapToViewMessageTimestampToAgeOut, getTapToViewMessagesNeedingErase, getOlderMessagesByConversation, @@ -997,12 +997,13 @@ async function saveMessage( async function saveMessages( arrayOfMessages: Array, - { forceSave }: { forceSave?: boolean } = {} + { forceSave, Message }: { forceSave?: boolean; Message: typeof MessageModel } ) { await channels.saveMessages( arrayOfMessages.map(message => _cleanMessageData(message)), { forceSave } ); + Message.updateTimers(); } async function removeMessage( @@ -1300,28 +1301,18 @@ async function getExpiredMessages({ return new MessageCollection(messages); } -async function getOutgoingWithoutExpiresAt({ +async function getOutgoingWithoutExpirationStartTimestamp({ MessageCollection, }: { MessageCollection: typeof MessageModelCollectionType; }) { - const messages = await channels.getOutgoingWithoutExpiresAt(); + const messages = await channels.getOutgoingWithoutExpirationStartTimestamp(); return new MessageCollection(messages); } -async function getNextExpiringMessage({ - Message, -}: { - Message: typeof MessageModel; -}) { - const message = await channels.getNextExpiringMessage(); - - if (message) { - return new Message(message); - } - - return null; +function getSoonestMessageExpiry() { + return channels.getSoonestMessageExpiry(); } async function getNextTapToViewMessageTimestampToAgeOut() { diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 5b168d2e5df..eeef07b217c 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -221,10 +221,6 @@ export type DataInterface = { getMessageCount: (conversationId?: string) => Promise; hasUserInitiatedMessages: (conversationId: string) => Promise; - saveMessages: ( - arrayOfMessages: Array, - options: { forceSave?: boolean } - ) => Promise; getAllMessageIds: () => Promise>; getMessageMetricsForConversation: ( conversationId: string @@ -313,6 +309,7 @@ export type DataInterface = { getMessageServerGuidsForSpam: ( conversationId: string ) => Promise>; + getSoonestMessageExpiry: () => Promise; getJobsInQueue(queueType: string): Promise>; insertJob(job: Readonly): Promise; @@ -370,8 +367,7 @@ export type ServerInterface = DataInterface & { conversationId: string; ourConversationId: string; }) => Promise; - getNextExpiringMessage: () => Promise; - getOutgoingWithoutExpiresAt: () => Promise>; + getOutgoingWithoutExpirationStartTimestamp: () => Promise>; getTapToViewMessagesNeedingErase: () => Promise>; getUnreadCountForConversation: (conversationId: string) => Promise; getUnreadByConversationAndMarkRead: ( @@ -416,6 +412,10 @@ export type ServerInterface = DataInterface & { data: MessageType, options: { forceSave?: boolean } ) => Promise; + saveMessages: ( + arrayOfMessages: Array, + options: { forceSave?: boolean } + ) => Promise; updateConversation: (data: ConversationType) => Promise; // For testing only @@ -505,10 +505,7 @@ export type ClientInterface = DataInterface & { ourConversationId: string; Message: typeof MessageModel; }) => Promise; - getNextExpiringMessage: (options: { - Message: typeof MessageModel; - }) => Promise; - getOutgoingWithoutExpiresAt: (options: { + getOutgoingWithoutExpirationStartTimestamp: (options: { MessageCollection: typeof MessageModelCollectionType; }) => Promise; getTapToViewMessagesNeedingErase: (options: { @@ -557,6 +554,10 @@ export type ClientInterface = DataInterface & { data: MessageType, options: { forceSave?: boolean; Message: typeof MessageModel } ) => Promise; + saveMessages: ( + arrayOfMessages: Array, + options: { forceSave?: boolean; Message: typeof MessageModel } + ) => Promise; searchMessages: ( query: string, options?: { limit?: number } diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 2d8eea4ff59..96d6949b8c1 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -33,10 +33,8 @@ import { ReactionType } from '../types/Reactions'; import { StoredJob } from '../jobs/types'; import { assert } from '../util/assert'; import { combineNames } from '../util/combineNames'; -import { getExpiresAt } from '../services/MessageUpdater'; import { isNormalNumber } from '../util/isNormalNumber'; import { isNotNil } from '../util/isNotNil'; -import * as iterables from '../util/iterables'; import { ConversationColorType, CustomColorType } from '../types/Colors'; import { @@ -180,8 +178,8 @@ const dataInterface: ServerInterface = { getAllMessageIds, getMessagesBySentAt, getExpiredMessages, - getOutgoingWithoutExpiresAt, - getNextExpiringMessage, + getOutgoingWithoutExpirationStartTimestamp, + getSoonestMessageExpiry, getNextTapToViewMessageTimestampToAgeOut, getTapToViewMessagesNeedingErase, getOlderMessagesByConversation, @@ -1859,6 +1857,38 @@ function updateToSchemaVersion32(currentVersion: number, db: Database) { console.log('updateToSchemaVersion32: success!'); } +function updateToSchemaVersion33(currentVersion: number, db: Database) { + if (currentVersion >= 33) { + return; + } + + db.transaction(() => { + db.exec(` + -- These indexes should exist, but we add "IF EXISTS" for safety. + DROP INDEX IF EXISTS messages_expires_at; + DROP INDEX IF EXISTS messages_without_timer; + + ALTER TABLE messages + ADD COLUMN + expiresAt INT + GENERATED ALWAYS + AS (expirationStartTimestamp + (expireTimer * 1000)); + + CREATE INDEX message_expires_at ON messages ( + expiresAt + ); + + CREATE INDEX outgoing_messages_without_expiration_start_timestamp ON messages ( + expireTimer, expirationStartTimestamp, type + ) + WHERE expireTimer IS NOT NULL AND expirationStartTimestamp IS NULL; + `); + + db.pragma('user_version = 33'); + })(); + console.log('updateToSchemaVersion33: success!'); +} + const SCHEMA_VERSIONS = [ updateToSchemaVersion1, updateToSchemaVersion2, @@ -1892,6 +1922,7 @@ const SCHEMA_VERSIONS = [ updateToSchemaVersion30, updateToSchemaVersion31, updateToSchemaVersion32, + updateToSchemaVersion33, ]; function updateSchema(db: Database): void { @@ -2997,7 +3028,6 @@ function saveMessageSync( const { body, conversationId, - expires_at, hasAttachments, hasFileAttachments, hasVisualMediaAttachments, @@ -3024,7 +3054,6 @@ function saveMessageSync( body: body || null, conversationId, expirationStartTimestamp: expirationStartTimestamp || null, - expires_at: expires_at || null, expireTimer: expireTimer || null, hasAttachments: hasAttachments ? 1 : 0, hasFileAttachments: hasFileAttachments ? 1 : 0, @@ -3053,7 +3082,6 @@ function saveMessageSync( body = $body, conversationId = $conversationId, expirationStartTimestamp = $expirationStartTimestamp, - expires_at = $expires_at, expireTimer = $expireTimer, hasAttachments = $hasAttachments, hasFileAttachments = $hasFileAttachments, @@ -3091,7 +3119,6 @@ function saveMessageSync( body, conversationId, expirationStartTimestamp, - expires_at, expireTimer, hasAttachments, hasFileAttachments, @@ -3114,7 +3141,6 @@ function saveMessageSync( $body, $conversationId, $expirationStartTimestamp, - $expires_at, $expireTimer, $hasAttachments, $hasFileAttachments, @@ -3243,31 +3269,6 @@ async function getMessageBySender({ return rows.map(row => jsonToObject(row.json)); } -function getExpireData( - expireTimer: number, - readAt?: number -): { - expirationStartTimestamp: number; - expiresAt: number; -} { - const expirationStartTimestamp = Math.min(Date.now(), readAt || Date.now()); - const expiresAt = getExpiresAt({ - expireTimer, - expirationStartTimestamp, - }); - - // We are guaranteeing an expirationStartTimestamp above so this should - // definitely return a number. - if (!expiresAt || typeof expiresAt !== 'number') { - assert(false, 'Expected expiresAt to be a number'); - } - - return { - expirationStartTimestamp, - expiresAt, - }; -} - async function getUnreadCountForConversation( conversationId: string ): Promise { @@ -3296,10 +3297,33 @@ async function getUnreadByConversationAndMarkRead( > { const db = getInstance(); return db.transaction(() => { + const expirationStartTimestamp = Math.min(Date.now(), readAt ?? Infinity); + db.prepare( + ` + UPDATE messages + SET + expirationStartTimestamp = $expirationStartTimestamp, + json = json_patch(json, $jsonPatch) + WHERE + ( + expirationStartTimestamp IS NULL OR + expirationStartTimestamp > $expirationStartTimestamp + ) AND + expireTimer IS NOT NULL AND + conversationId = $conversationId AND + received_at <= $newestUnreadId; + ` + ).run({ + conversationId, + expirationStartTimestamp, + jsonPatch: JSON.stringify({ expirationStartTimestamp }), + newestUnreadId, + }); + const rows = db .prepare( ` - SELECT id, expires_at, expireTimer, expirationStartTimestamp, json + SELECT id, json FROM messages WHERE unread = $unread AND conversationId = $conversationId AND @@ -3313,10 +3337,6 @@ async function getUnreadByConversationAndMarkRead( newestUnreadId, }); - if (!rows.length) { - return []; - } - db.prepare( ` UPDATE messages @@ -3335,58 +3355,18 @@ async function getUnreadByConversationAndMarkRead( unread: 1, }); - const rowsWithExpireTimers = iterables.filter(rows, row => row.expireTimer); - const rowsNeedingExpirationUpdates = iterables.filter( - rowsWithExpireTimers, - row => - !row.expirationStartTimestamp || - !row.expires_at || - getExpireData(row.expireTimer, readAt).expirationStartTimestamp < - row.expirationStartTimestamp - ); - const expirationStartTimestampUpdates: Iterable<{ - id: string; - expirationStartTimestamp: number; - expiresAt: number; - }> = iterables.map(rowsNeedingExpirationUpdates, row => ({ - id: row.id, - ...getExpireData(row.expireTimer, readAt), - })); - const stmt = db.prepare( - ` - UPDATE messages - SET - expirationStartTimestamp = $expirationStartTimestamp, - expires_at = $expiresAt - WHERE - id = $id; - ` - ); - const updatedExpireDataByRowId = new Map< - string, - { - expirationStartTimestamp: number; - expiresAt: number; - } - >(); - for (const update of expirationStartTimestampUpdates) { - stmt.run(update); - updatedExpireDataByRowId.set(update.id, update); - } - return rows.map(row => { const json = jsonToObject(row.json); - const updatedExpireData = updatedExpireDataByRowId.get(row.id); return { unread: false, - ...pick(json, ['id', 'sent_at', 'source', 'sourceUuid', 'type']), - ...(updatedExpireData - ? { - expirationStartTimestamp: - updatedExpireData.expirationStartTimestamp, - expires_at: updatedExpireData.expiresAt, - } - : {}), + ...pick(json, [ + 'expirationStartTimestamp', + 'id', + 'sent_at', + 'source', + 'sourceUuid', + 'type', + ]), }; }); })(); @@ -3916,30 +3896,29 @@ async function getExpiredMessages(): Promise> { .prepare( ` SELECT json FROM messages WHERE - expires_at IS NOT NULL AND - expires_at <= $expires_at - ORDER BY expires_at ASC; + expiresAt IS NOT NULL AND + expiresAt <= $now + ORDER BY expiresAt ASC; ` ) - .all({ - expires_at: now, - }); + .all({ now }); return rows.map(row => jsonToObject(row.json)); } -async function getOutgoingWithoutExpiresAt(): Promise> { +async function getOutgoingWithoutExpirationStartTimestamp(): Promise< + Array +> { const db = getInstance(); const rows: JSONRows = db .prepare( ` SELECT json FROM messages - INDEXED BY messages_without_timer + INDEXED BY outgoing_messages_without_expiration_start_timestamp WHERE expireTimer > 0 AND - expires_at IS NULL AND - type IS 'outgoing' - ORDER BY expires_at ASC; + expirationStartTimestamp IS NULL AND + type IS 'outgoing'; ` ) .all(); @@ -3947,26 +3926,21 @@ async function getOutgoingWithoutExpiresAt(): Promise> { return rows.map(row => jsonToObject(row.json)); } -async function getNextExpiringMessage(): Promise { +async function getSoonestMessageExpiry(): Promise { const db = getInstance(); - // Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index - const rows: JSONRows = db + // Note: we use `pluck` to only get the first column. + const result: null | number = db .prepare( ` - SELECT json FROM messages - WHERE expires_at > 0 - ORDER BY expires_at ASC - LIMIT 1; + SELECT MIN(expiresAt) + FROM messages; ` ) - .all(); + .pluck(true) + .get(); - if (!rows || rows.length < 1) { - return undefined; - } - - return jsonToObject(rows[0].json); + return result || undefined; } async function getNextTapToViewMessageTimestampToAgeOut(): Promise< diff --git a/ts/types/Message.ts b/ts/types/Message.ts index af5f250b85f..cfac752fb7a 100644 --- a/ts/types/Message.ts +++ b/ts/types/Message.ts @@ -1,4 +1,4 @@ -// Copyright 2018-2020 Signal Messenger, LLC +// Copyright 2018-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only /* eslint-disable camelcase */ @@ -56,7 +56,6 @@ export type OutgoingMessage = Readonly< // Optional body?: string; - expires_at?: number; expireTimer?: number; messageTimer?: number; // deprecated isViewOnce?: number; diff --git a/ts/util/messageBatcher.ts b/ts/util/messageBatcher.ts index 5c2fbbf2049..22790ba930d 100644 --- a/ts/util/messageBatcher.ts +++ b/ts/util/messageBatcher.ts @@ -11,7 +11,9 @@ const updateMessageBatcher = createBatcher({ maxSize: 50, processBatch: async (messageAttrs: Array) => { window.log.info('updateMessageBatcher', messageAttrs.length); - await window.Signal.Data.saveMessages(messageAttrs, {}); + await window.Signal.Data.saveMessages(messageAttrs, { + Message: window.Whisper.Message, + }); }, }); @@ -37,6 +39,9 @@ export const saveNewMessageBatcher = createWaitBatcher({ maxSize: 30, processBatch: async (messageAttrs: Array) => { window.log.info('saveNewMessageBatcher', messageAttrs.length); - await window.Signal.Data.saveMessages(messageAttrs, { forceSave: true }); + await window.Signal.Data.saveMessages(messageAttrs, { + forceSave: true, + Message: window.Whisper.Message, + }); }, });