diff --git a/ts/background.ts b/ts/background.ts index 3b1298ac859..fa407a18fb0 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -19,6 +19,8 @@ import { DEFAULT_CONVERSATION_COLOR } from './types/Colors'; import { ChallengeHandler } from './challenge'; import { isWindowDragElement } from './util/isWindowDragElement'; import { assert } from './util/assert'; +import { filter } from './util/iterables'; +import { isNotNil } from './util/isNotNil'; import { senderCertificateService } from './services/senderCertificate'; import { routineProfileRefresh } from './routineProfileRefresh'; import { isMoreRecentThan, isOlderThan } from './util/timestamp'; @@ -43,7 +45,7 @@ import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation'; import { getSendOptions } from './util/getSendOptions'; import { BackOff } from './util/BackOff'; import { AppViewType } from './state/ducks/app'; -import { hasErrors, isIncoming } from './state/selectors/message'; +import { isIncoming } from './state/selectors/message'; import { actionCreators } from './state/actions'; import { Deletes } from './messageModifiers/Deletes'; import { DeliveryReceipts } from './messageModifiers/DeliveryReceipts'; @@ -1652,51 +1654,43 @@ export async function startApp(): Promise { window.dispatchEvent(new Event('storage_ready')); - window.log.info('Cleanup: starting...'); - const messagesForCleanup = await window.Signal.Data.getOutgoingWithoutExpirationStartTimestamp( - { - MessageCollection: window.Whisper.MessageCollection, - } - ); + window.log.info('Expiration start timestamp cleanup: starting...'); + const messagesUnexpectedlyMissingExpirationStartTimestamp = await window.Signal.Data.getMessagesUnexpectedlyMissingExpirationStartTimestamp(); window.log.info( - `Cleanup: Found ${messagesForCleanup.length} messages for cleanup` + `Expiration start timestamp cleanup: Found ${messagesUnexpectedlyMissingExpirationStartTimestamp.length} messages for cleanup` ); - 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'); - - if (hasErrors(message.attributes)) { - return; - } - - if (delivered) { - window.log.info( - `Cleanup: Starting timer for delivered message ${sentAt}` + if (messagesUnexpectedlyMissingExpirationStartTimestamp.length) { + const newMessageAttributes = messagesUnexpectedlyMissingExpirationStartTimestamp.map( + message => { + const expirationStartTimestamp = Math.min( + ...filter( + [ + // These messages should always have a sent_at, but we have fallbacks + // just in case. + message.sent_at, + Date.now(), + // The query shouldn't return messages with expiration start timestamps, + // but we're trying to be extra careful. + message.expirationStartTimestamp, + ], + isNotNil + ) ); - message.set('expirationStartTimestamp', sentAt); - return; + window.log.info( + `Expiration start timestamp cleanup: starting timer for ${message.type} message sent at ${message.sent_at}. Starting timer at ${message.expirationStartTimestamp}` + ); + return { + ...message, + expirationStartTimestamp, + }; } + ); - window.log.info(`Cleanup: Deleting unsent message ${sentAt}`); - await window.Signal.Data.removeMessage(message.id, { - Message: window.Whisper.Message, - }); - const conversation = message.getConversation(); - if (conversation) { - await conversation.updateLastMessage(); - } - }) - ); - if (messagesForCleanup.length) { - window.Whisper.ExpiringMessagesListener.update(); + await window.Signal.Data.saveMessages(newMessageAttributes, { + Message: MessageModel, + }); } - window.log.info('Cleanup: complete'); + window.log.info('Expiration start timestamp cleanup: complete'); window.log.info('listening for registration events'); window.Whisper.events.on('registration_done', () => { diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index 28dd0d905e2..7febee751b5 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -189,7 +189,7 @@ const dataInterface: ClientInterface = { getAllMessageIds, getMessagesBySentAt, getExpiredMessages, - getOutgoingWithoutExpirationStartTimestamp, + getMessagesUnexpectedlyMissingExpirationStartTimestamp, getSoonestMessageExpiry, getNextTapToViewMessageTimestampToAgeOut, getTapToViewMessagesNeedingErase, @@ -1301,14 +1301,8 @@ async function getExpiredMessages({ return new MessageCollection(messages); } -async function getOutgoingWithoutExpirationStartTimestamp({ - MessageCollection, -}: { - MessageCollection: typeof MessageModelCollectionType; -}) { - const messages = await channels.getOutgoingWithoutExpirationStartTimestamp(); - - return new MessageCollection(messages); +function getMessagesUnexpectedlyMissingExpirationStartTimestamp() { + return channels.getMessagesUnexpectedlyMissingExpirationStartTimestamp(); } function getSoonestMessageExpiry() { diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index eeaf95183de..0bf60674c03 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -322,6 +322,9 @@ export type DataInterface = { getMessageServerGuidsForSpam: ( conversationId: string ) => Promise>; + getMessagesUnexpectedlyMissingExpirationStartTimestamp: () => Promise< + Array + >; getSoonestMessageExpiry: () => Promise; getJobsInQueue(queueType: string): Promise>; @@ -380,7 +383,6 @@ export type ServerInterface = DataInterface & { conversationId: string; ourConversationId: string; }) => Promise; - getOutgoingWithoutExpirationStartTimestamp: () => Promise>; getTapToViewMessagesNeedingErase: () => Promise>; getUnreadCountForConversation: (conversationId: string) => Promise; getUnreadByConversationAndMarkRead: ( @@ -518,9 +520,6 @@ export type ClientInterface = DataInterface & { ourConversationId: string; Message: typeof MessageModel; }) => Promise; - getOutgoingWithoutExpirationStartTimestamp: (options: { - MessageCollection: typeof MessageModelCollectionType; - }) => Promise; getTapToViewMessagesNeedingErase: (options: { MessageCollection: typeof MessageModelCollectionType; }) => Promise; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index cbacc2abf97..0cf11577541 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -178,7 +178,7 @@ const dataInterface: ServerInterface = { getAllMessageIds, getMessagesBySentAt, getExpiredMessages, - getOutgoingWithoutExpirationStartTimestamp, + getMessagesUnexpectedlyMissingExpirationStartTimestamp, getSoonestMessageExpiry, getNextTapToViewMessageTimestampToAgeOut, getTapToViewMessagesNeedingErase, @@ -1889,6 +1889,27 @@ function updateToSchemaVersion33(currentVersion: number, db: Database) { console.log('updateToSchemaVersion33: success!'); } +function updateToSchemaVersion34(currentVersion: number, db: Database) { + if (currentVersion >= 34) { + return; + } + + db.transaction(() => { + db.exec(` + -- This index should exist, but we add "IF EXISTS" for safety. + DROP INDEX IF EXISTS outgoing_messages_without_expiration_start_timestamp; + + CREATE INDEX messages_unexpectedly_missing_expiration_start_timestamp ON messages ( + expireTimer, expirationStartTimestamp, type + ) + WHERE expireTimer IS NOT NULL AND expirationStartTimestamp IS NULL; + `); + + db.pragma('user_version = 34'); + })(); + console.log('updateToSchemaVersion34: success!'); +} + const SCHEMA_VERSIONS = [ updateToSchemaVersion1, updateToSchemaVersion2, @@ -1923,6 +1944,7 @@ const SCHEMA_VERSIONS = [ updateToSchemaVersion31, updateToSchemaVersion32, updateToSchemaVersion33, + updateToSchemaVersion34, ]; function updateSchema(db: Database): void { @@ -3906,7 +3928,7 @@ async function getExpiredMessages(): Promise> { return rows.map(row => jsonToObject(row.json)); } -async function getOutgoingWithoutExpirationStartTimestamp(): Promise< +async function getMessagesUnexpectedlyMissingExpirationStartTimestamp(): Promise< Array > { const db = getInstance(); @@ -3914,11 +3936,17 @@ async function getOutgoingWithoutExpirationStartTimestamp(): Promise< .prepare( ` SELECT json FROM messages - INDEXED BY outgoing_messages_without_expiration_start_timestamp + INDEXED BY messages_unexpectedly_missing_expiration_start_timestamp WHERE expireTimer > 0 AND expirationStartTimestamp IS NULL AND - type IS 'outgoing'; + ( + type IS 'outgoing' OR + (type IS 'incoming' AND ( + unread = 0 OR + unread IS NULL + )) + ); ` ) .all();