From 60a53656aff07b64ceaa7541c3e4423095c5b88d Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Mon, 20 Dec 2021 13:04:02 -0800 Subject: [PATCH] Optimize a few queries --- js/modules/debug.js | 4 +- js/modules/messages_data_migrator.js | 8 +- ts/background.ts | 8 +- ts/groups.ts | 2 + ts/jobs/normalMessageSendJobQueue.ts | 4 +- ts/jobs/reactionJobQueue.ts | 7 +- ts/messageModifiers/AttachmentDownloads.ts | 4 +- ts/models/conversations.ts | 50 +- ts/models/messages.ts | 46 +- ts/sql/Client.ts | 8 +- ts/sql/Interface.ts | 5 +- ts/sql/Server.ts | 97 +--- ts/sql/migrations/41-uuid-keys.ts | 2 +- ts/sql/migrations/47-further-optimize.ts | 141 +++++ ts/sql/migrations/index.ts | 2 + ts/state/ducks/conversations.ts | 3 +- ts/test-electron/models/conversations_test.ts | 1 + ts/test-electron/sql/allMedia_test.ts | 24 +- .../sql/conversationSummary_test.ts | 548 ++++++++++++++++++ ts/test-electron/sql/fullTextSearch_test.ts | 24 +- ts/test-electron/sql/markRead_test.ts | 10 + ts/test-electron/sql/sendLog_test.ts | 101 ++-- ts/test-electron/sql/stories_test.ts | 5 + ts/test-electron/sql/timelineFetches_test.ts | 58 +- ts/test-node/sql_migrations_test.ts | 300 +++++++++- ts/util/messageBatcher.ts | 9 +- ts/views/conversation_view.ts | 3 +- 27 files changed, 1288 insertions(+), 186 deletions(-) create mode 100644 ts/sql/migrations/47-further-optimize.ts create mode 100644 ts/test-electron/sql/conversationSummary_test.ts diff --git a/js/modules/debug.js b/js/modules/debug.js index 90f26e399..8b0d2b9d9 100644 --- a/js/modules/debug.js +++ b/js/modules/debug.js @@ -60,7 +60,9 @@ exports.createConversation = async ({ await sleep(index * 100); window.SignalContext.log.info(`Create message ${index + 1}`); const message = await createRandomMessage({ conversationId }); - return Signal.Data.saveMessage(message); + return Signal.Data.saveMessage(message, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); }) ); }; diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index f30aae8bb..c63336887 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -65,7 +65,13 @@ exports.processNext = async ({ const upgradeDuration = Date.now() - upgradeStartTime; const saveStartTime = Date.now(); - await Promise.all(upgradedMessages.map(message => saveMessage(message))); + await Promise.all( + upgradedMessages.map(message => + saveMessage(message, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }) + ) + ); const saveDuration = Date.now() - saveStartTime; const totalDuration = Date.now() - startTime; diff --git a/ts/background.ts b/ts/background.ts index e4156aa13..4a1f0c858 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1701,7 +1701,9 @@ export async function startApp(): Promise { }; }); - await window.Signal.Data.saveMessages(newMessageAttributes); + await window.Signal.Data.saveMessages(newMessageAttributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } log.info('Expiration start timestamp cleanup: complete'); @@ -2369,7 +2371,9 @@ export async function startApp(): Promise { messagesToSave.push(message.attributes); } }); - await window.Signal.Data.saveMessages(messagesToSave); + await window.Signal.Data.saveMessages(messagesToSave, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } function onReconnect() { // We disable notifications on first connect, but the same applies to reconnect. In diff --git a/ts/groups.ts b/ts/groups.ts index 268ed8ac7..48a5897cf 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -1716,6 +1716,7 @@ export async function createGroupV2({ }; await window.Signal.Data.saveMessages([createdTheGroupMessage], { forceSave: true, + ourUuid, }); const model = new window.Whisper.Message(createdTheGroupMessage); window.MessageController.register(model.id, model); @@ -2861,6 +2862,7 @@ async function updateGroup( if (changeMessagesToSave.length > 0) { await window.Signal.Data.saveMessages(changeMessagesToSave, { forceSave: true, + ourUuid: ourUuid.toString(), }); changeMessagesToSave.forEach(changeMessage => { const model = new window.Whisper.Message(changeMessage); diff --git a/ts/jobs/normalMessageSendJobQueue.ts b/ts/jobs/normalMessageSendJobQueue.ts index a255bcf47..3592e0b6f 100644 --- a/ts/jobs/normalMessageSendJobQueue.ts +++ b/ts/jobs/normalMessageSendJobQueue.ts @@ -481,7 +481,9 @@ async function markMessageFailed( ): Promise { message.markFailed(); message.saveErrors(errors, { skipSave: true }); - await window.Signal.Data.saveMessage(message.attributes); + await window.Signal.Data.saveMessage(message.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } function didSendToEveryone(message: Readonly): boolean { diff --git a/ts/jobs/reactionJobQueue.ts b/ts/jobs/reactionJobQueue.ts index a2ada81a0..9e4e7d02e 100644 --- a/ts/jobs/reactionJobQueue.ts +++ b/ts/jobs/reactionJobQueue.ts @@ -58,6 +58,7 @@ export class ReactionJobQueue extends JobQueue { { attempt, log }: Readonly<{ attempt: number; log: LoggerType }> ): Promise { const { messageId } = data; + const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString(); const timeRemaining = timestamp + MAX_RETRY_TIME - Date.now(); const isFinalAttempt = attempt >= MAX_ATTEMPTS; @@ -100,7 +101,7 @@ export class ReactionJobQueue extends JobQueue { `could not react to ${messageId}. Removing this pending reaction` ); markReactionFailed(message, pendingReaction); - await window.Signal.Data.saveMessage(message.attributes); + await window.Signal.Data.saveMessage(message.attributes, { ourUuid }); return; } @@ -109,7 +110,7 @@ export class ReactionJobQueue extends JobQueue { `reacting to message ${messageId} ran out of time. Giving up on sending it` ); markReactionFailed(message, pendingReaction); - await window.Signal.Data.saveMessage(message.attributes); + await window.Signal.Data.saveMessage(message.attributes, { ourUuid }); return; } @@ -257,7 +258,7 @@ export class ReactionJobQueue extends JobQueue { } await handleCommonJobRequestError({ err, log, timeRemaining }); } finally { - await window.Signal.Data.saveMessage(message.attributes); + await window.Signal.Data.saveMessage(message.attributes, { ourUuid }); } } } diff --git a/ts/messageModifiers/AttachmentDownloads.ts b/ts/messageModifiers/AttachmentDownloads.ts index 018cc1f36..01e4dedfb 100644 --- a/ts/messageModifiers/AttachmentDownloads.ts +++ b/ts/messageModifiers/AttachmentDownloads.ts @@ -278,7 +278,9 @@ async function _finishJob( ): Promise { if (message) { logger.info(`attachment_downloads/_finishJob for job id: ${id}`); - await saveMessage(message.attributes); + await saveMessage(message.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } await removeAttachmentDownloadJob(id); diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index bec6e06c0..78b1ac312 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -1622,6 +1622,7 @@ export class ConversationModel extends window.Backbone if (eliminated > 0) { log.warn(`cleanModels: Eliminated ${eliminated} messages without an id`); } + const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString(); let upgraded = 0; for (let max = result.length, i = 0; i < max; i += 1) { @@ -1635,7 +1636,7 @@ export class ConversationModel extends window.Backbone const upgradedMessage = await upgradeMessageSchema(attributes); message.set(upgradedMessage); // eslint-disable-next-line no-await-in-loop - await window.Signal.Data.saveMessage(upgradedMessage); + await window.Signal.Data.saveMessage(upgradedMessage, { ourUuid }); upgraded += 1; } } @@ -1950,6 +1951,7 @@ export class ConversationModel extends window.Backbone options: { isLocalAction?: boolean } = {} ): Promise { const { isLocalAction } = options; + const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString(); let messages: Array | undefined; do { @@ -1991,7 +1993,9 @@ export class ConversationModel extends window.Backbone const registered = window.MessageController.register(m.id, m); const shouldSave = await registered.queueAttachmentDownloads(); if (shouldSave) { - await window.Signal.Data.saveMessage(registered.attributes); + await window.Signal.Data.saveMessage(registered.attributes, { + ourUuid, + }); } }) ); @@ -2838,7 +2842,9 @@ export class ConversationModel extends window.Backbone // this type does not fully implement the interface it is expected to } as unknown as MessageAttributesType; - const id = await window.Signal.Data.saveMessage(message); + const id = await window.Signal.Data.saveMessage(message, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); const model = window.MessageController.register( id, new window.Whisper.Message({ @@ -2878,7 +2884,9 @@ export class ConversationModel extends window.Backbone // this type does not fully implement the interface it is expected to } as unknown as MessageAttributesType; - const id = await window.Signal.Data.saveMessage(message); + const id = await window.Signal.Data.saveMessage(message, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); const model = window.MessageController.register( id, new window.Whisper.Message({ @@ -2914,7 +2922,9 @@ export class ConversationModel extends window.Backbone // this type does not fully implement the interface it is expected to } as unknown as MessageAttributesType; - const id = await window.Signal.Data.saveMessage(message); + const id = await window.Signal.Data.saveMessage(message, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); const model = window.MessageController.register( id, new window.Whisper.Message({ @@ -2970,7 +2980,9 @@ export class ConversationModel extends window.Backbone // TODO: DESKTOP-722 } as unknown as MessageAttributesType; - const id = await window.Signal.Data.saveMessage(message); + const id = await window.Signal.Data.saveMessage(message, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); const model = window.MessageController.register( id, new window.Whisper.Message({ @@ -3030,7 +3042,9 @@ export class ConversationModel extends window.Backbone // TODO: DESKTOP-722 } as unknown as MessageAttributesType; - const id = await window.Signal.Data.saveMessage(message); + const id = await window.Signal.Data.saveMessage(message, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); const model = window.MessageController.register( id, new window.Whisper.Message({ @@ -3090,7 +3104,9 @@ export class ConversationModel extends window.Backbone // TODO: DESKTOP-722 } as unknown as MessageAttributesType; - const id = await window.Signal.Data.saveMessage(message); + const id = await window.Signal.Data.saveMessage(message, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); const model = window.MessageController.register( id, new window.Whisper.Message({ @@ -3131,7 +3147,10 @@ export class ConversationModel extends window.Backbone const id = await window.Signal.Data.saveMessage( // TODO: DESKTOP-722 - message as MessageAttributesType + message as MessageAttributesType, + { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + } ); const model = window.MessageController.register( id, @@ -3948,6 +3967,7 @@ export class ConversationModel extends window.Backbone await window.Signal.Data.saveMessage(message.attributes, { jobToInsert, forceSave: true, + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), }); } ); @@ -4388,7 +4408,9 @@ export class ConversationModel extends window.Backbone // TODO: DESKTOP-722 } as unknown as MessageAttributesType); - const id = await window.Signal.Data.saveMessage(model.attributes); + const id = await window.Signal.Data.saveMessage(model.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); model.set({ id }); @@ -4484,7 +4506,9 @@ export class ConversationModel extends window.Backbone // TODO: DESKTOP-722 } as unknown as MessageAttributesType); - const id = await window.Signal.Data.saveMessage(model.attributes); + const id = await window.Signal.Data.saveMessage(model.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); model.set({ id }); @@ -4519,7 +4543,9 @@ export class ConversationModel extends window.Backbone // TODO: DESKTOP-722 } as unknown as MessageAttributesType); - const id = await window.Signal.Data.saveMessage(model.attributes); + const id = await window.Signal.Data.saveMessage(model.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); model.set({ id }); const message = window.MessageController.register(model.id, model); diff --git a/ts/models/messages.ts b/ts/models/messages.ts index a4cd1fc2f..bc8a960cd 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -965,7 +965,9 @@ export class MessageModel extends window.Backbone.Model { this.getConversation()?.debouncedUpdateLastMessage?.(); if (shouldPersist) { - await window.Signal.Data.saveMessage(this.attributes); + await window.Signal.Data.saveMessage(this.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } await window.Signal.Data.deleteSentProtoByMessageId(this.id); @@ -1099,7 +1101,9 @@ export class MessageModel extends window.Backbone.Model { } if (!skipSave && !this.doNotSave) { - await window.Signal.Data.saveMessage(this.attributes); + await window.Signal.Data.saveMessage(this.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } } @@ -1161,7 +1165,10 @@ export class MessageModel extends window.Backbone.Model { await normalMessageSendJobQueue.add( { messageId: this.id, conversationId: conversation.id }, async jobToInsert => { - await window.Signal.Data.saveMessage(this.attributes, { jobToInsert }); + await window.Signal.Data.saveMessage(this.attributes, { + jobToInsert, + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } ); } @@ -1256,7 +1263,9 @@ export class MessageModel extends window.Backbone.Model { } if (!this.doNotSave) { - await window.Signal.Data.saveMessage(this.attributes); + await window.Signal.Data.saveMessage(this.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } const sendStateByConversationId = { @@ -1403,7 +1412,9 @@ export class MessageModel extends window.Backbone.Model { } if (!this.doNotSave) { - await window.Signal.Data.saveMessage(this.attributes); + await window.Signal.Data.saveMessage(this.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } updateLeftPane(); @@ -1460,7 +1471,9 @@ export class MessageModel extends window.Backbone.Model { this.saveErrors(errors, { skipSave: true }); } } finally { - await window.Signal.Data.saveMessage(this.attributes); + await window.Signal.Data.saveMessage(this.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); if (updateLeftPane) { updateLeftPane(); @@ -1571,7 +1584,9 @@ export class MessageModel extends window.Backbone.Model { return result; } - await window.Signal.Data.saveMessage(this.attributes); + await window.Signal.Data.saveMessage(this.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); return result; }); }; @@ -2097,7 +2112,9 @@ export class MessageModel extends window.Backbone.Model { originalMessage.attributes ); originalMessage.set(upgradedMessage); - await window.Signal.Data.saveMessage(upgradedMessage); + await window.Signal.Data.saveMessage(upgradedMessage, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } } catch (error) { log.error( @@ -2264,7 +2281,9 @@ export class MessageModel extends window.Backbone.Model { sendStateByConversationId, unidentifiedDeliveries: [...unidentifiedDeliveriesSet], }); - await window.Signal.Data.saveMessage(toUpdate.attributes); + await window.Signal.Data.saveMessage(toUpdate.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); confirm(); return; @@ -3016,7 +3035,9 @@ export class MessageModel extends window.Backbone.Model { log.info( `modifyTargetMessage/${this.idForLogging()}: Changes in second run; saving.` ); - await window.Signal.Data.saveMessage(this.attributes); + await window.Signal.Data.saveMessage(this.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } } @@ -3166,13 +3187,16 @@ export class MessageModel extends window.Backbone.Model { ); await window.Signal.Data.saveMessage(this.attributes, { jobToInsert, + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), }); }); } else { await reactionJobQueue.add(jobData); } } else if (shouldPersist) { - await window.Signal.Data.saveMessage(this.attributes); + await window.Signal.Data.saveMessage(this.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } } diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index fa665d7d2..ec869912c 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -1076,7 +1076,11 @@ async function getMessageCount(conversationId?: string) { async function saveMessage( data: MessageType, - options: { jobToInsert?: Readonly; forceSave?: boolean } = {} + options: { + jobToInsert?: Readonly; + forceSave?: boolean; + ourUuid: UUIDStringType; + } ) { const id = await channels.saveMessage(_cleanMessageData(data), { ...options, @@ -1091,7 +1095,7 @@ async function saveMessage( async function saveMessages( arrayOfMessages: Array, - options?: { forceSave?: boolean } + options: { forceSave?: boolean; ourUuid: UUIDStringType } ) { await channels.saveMessages( arrayOfMessages.map(message => _cleanMessageData(message)), diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index af01a44fb..de3881cf6 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -371,14 +371,15 @@ export type DataInterface = { getMessageCount: (conversationId?: string) => Promise; saveMessage: ( data: MessageType, - options?: { + options: { jobToInsert?: StoredJob; forceSave?: boolean; + ourUuid: UUIDStringType; } ) => Promise; saveMessages: ( arrayOfMessages: Array, - options?: { forceSave?: boolean } + options: { forceSave?: boolean; ourUuid: UUIDStringType } ) => Promise; removeMessage: (id: string) => Promise; removeMessages: (ids: Array) => Promise; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index c9f0f07c9..e8264e402 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -1688,20 +1688,7 @@ function hasUserInitiatedMessages(conversationId: string): boolean { SELECT 1 FROM messages WHERE conversationId = $conversationId AND - (type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'group-v1-migration', - 'group-v2-change', - 'keychange', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ) + isUserInitiatedMessage = 1 LIMIT 1 ); ` @@ -1714,17 +1701,19 @@ function hasUserInitiatedMessages(conversationId: string): boolean { function saveMessageSync( data: MessageType, options: { - jobToInsert?: StoredJob; - forceSave?: boolean; alreadyInTransaction?: boolean; db?: Database; - } = {} + forceSave?: boolean; + jobToInsert?: StoredJob; + ourUuid: UUIDStringType; + } ): string { const { - jobToInsert, - forceSave, alreadyInTransaction, db = getInstance(), + forceSave, + jobToInsert, + ourUuid, } = options; if (!alreadyInTransaction) { @@ -1741,6 +1730,7 @@ function saveMessageSync( const { body, conversationId, + groupV2Change, hasAttachments, hasFileAttachments, hasVisualMediaAttachments, @@ -1772,6 +1762,7 @@ function saveMessageSync( hasAttachments: hasAttachments ? 1 : 0, hasFileAttachments: hasFileAttachments ? 1 : 0, hasVisualMediaAttachments: hasVisualMediaAttachments ? 1 : 0, + isChangeCreatedByUs: groupV2Change?.from === ourUuid ? 1 : 0, isErased: isErased ? 1 : 0, isViewOnce: isViewOnce ? 1 : 0, received_at: received_at || null, @@ -1801,6 +1792,7 @@ function saveMessageSync( hasAttachments = $hasAttachments, hasFileAttachments = $hasFileAttachments, hasVisualMediaAttachments = $hasVisualMediaAttachments, + isChangeCreatedByUs = $isChangeCreatedByUs, isErased = $isErased, isViewOnce = $isViewOnce, received_at = $received_at, @@ -1843,6 +1835,7 @@ function saveMessageSync( hasAttachments, hasFileAttachments, hasVisualMediaAttachments, + isChangeCreatedByUs, isErased, isViewOnce, received_at, @@ -1866,6 +1859,7 @@ function saveMessageSync( $hasAttachments, $hasFileAttachments, $hasVisualMediaAttachments, + $isChangeCreatedByUs, $isErased, $isViewOnce, $received_at, @@ -1895,10 +1889,11 @@ function saveMessageSync( async function saveMessage( data: MessageType, - options?: { + options: { jobToInsert?: StoredJob; forceSave?: boolean; alreadyInTransaction?: boolean; + ourUuid: UUIDStringType; } ): Promise { return saveMessageSync(data, options); @@ -1906,15 +1901,14 @@ async function saveMessage( async function saveMessages( arrayOfMessages: Array, - options?: { forceSave?: boolean } + options: { forceSave?: boolean; ourUuid: UUIDStringType } ): Promise { const db = getInstance(); - const { forceSave } = options || {}; db.transaction(() => { for (const message of arrayOfMessages) { assertSync( - saveMessageSync(message, { forceSave, alreadyInTransaction: true }) + saveMessageSync(message, { ...options, alreadyInTransaction: true }) ); } })(); @@ -2070,7 +2064,7 @@ async function getUnreadByConversationAndMarkRead({ expirationStartTimestamp IS NULL OR expirationStartTimestamp > $expirationStartTimestamp ) AND - expireTimer IS NOT NULL AND + expireTimer > 0 AND conversationId = $conversationId AND storyId IS $storyId AND received_at <= $newestUnreadAt; @@ -2161,7 +2155,7 @@ async function getUnreadReactionsAndMarkRead({ FROM reactions JOIN messages on messages.id IS reactions.messageId WHERE - unread IS NOT 0 AND + unread > 0 AND messages.conversationId IS $conversationId AND messages.received_at <= $newestUnreadAt AND messages.storyId IS $storyId @@ -2504,31 +2498,9 @@ function getLastConversationActivity({ SELECT json FROM messages WHERE conversationId = $conversationId AND - (type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'group-v1-migration', - 'keychange', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ) AND - ( - json_extract(json, '$.expirationTimerUpdate.fromSync') IS NULL - OR - json_extract(json, '$.expirationTimerUpdate.fromSync') != 1 - ) AND NOT - ( - type IS 'group-v2-change' AND - json_extract(json, '$.groupV2Change.from') IS NOT $ourUuid AND - json_extract(json, '$.groupV2Change.details.length') IS 1 AND - json_extract(json, '$.groupV2Change.details[0].type') IS 'member-remove' AND - json_extract(json, '$.groupV2Change.details[0].uuid') IS NOT $ourUuid - ) + shouldAffectActivity IS 1 AND + isTimerChangeFromSync IS 0 AND + isGroupLeaveEventFromOther IS 0 ORDER BY received_at DESC, sent_at DESC LIMIT 1; ` @@ -2557,29 +2529,12 @@ function getLastConversationPreview({ SELECT json FROM messages WHERE conversationId = $conversationId AND + shouldAffectPreview IS 1 AND + isGroupLeaveEventFromOther IS 0 AND ( - expiresAt IS NULL OR - (expiresAt > $now) - ) AND - ( - type IS NULL + expiresAt IS NULL OR - type NOT IN ( - 'change-number-notification', - 'group-v1-migration', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ) AND NOT - ( - type IS 'group-v2-change' AND - json_extract(json, '$.groupV2Change.from') IS NOT $ourUuid AND - json_extract(json, '$.groupV2Change.details.length') IS 1 AND - json_extract(json, '$.groupV2Change.details[0].type') IS 'member-remove' AND - json_extract(json, '$.groupV2Change.details[0].uuid') IS NOT $ourUuid + expiresAt > $now ) ORDER BY received_at DESC, sent_at DESC LIMIT 1; diff --git a/ts/sql/migrations/41-uuid-keys.ts b/ts/sql/migrations/41-uuid-keys.ts index 199bebfd2..c38f8a196 100644 --- a/ts/sql/migrations/41-uuid-keys.ts +++ b/ts/sql/migrations/41-uuid-keys.ts @@ -11,7 +11,7 @@ import { createOrUpdate, getById, removeById } from '../util'; import type { EmptyQuery, Query } from '../util'; import type { ItemKeyType } from '../Interface'; -function getOurUuid(db: Database): string | undefined { +export function getOurUuid(db: Database): string | undefined { const UUID_ID: ItemKeyType = 'uuid_id'; const row: { json: string } | undefined = db diff --git a/ts/sql/migrations/47-further-optimize.ts b/ts/sql/migrations/47-further-optimize.ts new file mode 100644 index 000000000..93f43e3d7 --- /dev/null +++ b/ts/sql/migrations/47-further-optimize.ts @@ -0,0 +1,141 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Database } from 'better-sqlite3'; + +import type { LoggerType } from '../../types/Logging'; +import { getOurUuid } from './41-uuid-keys'; +import type { Query } from '../util'; + +export default function updateToSchemaVersion47( + currentVersion: number, + db: Database, + logger: LoggerType +): void { + if (currentVersion >= 47) { + return; + } + + db.transaction(() => { + db.exec( + ` + DROP INDEX messages_conversation; + + ALTER TABLE messages + DROP COLUMN isStory; + ALTER TABLE messages + ADD COLUMN isStory INTEGER + GENERATED ALWAYS AS (type IS 'story'); + + ALTER TABLE messages + ADD COLUMN isChangeCreatedByUs INTEGER NOT NULL DEFAULT 0; + + ALTER TABLE messages + ADD COLUMN shouldAffectActivity INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'group-v1-migration', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change', + + 'keychange' + ) + ); + + ALTER TABLE messages + ADD COLUMN shouldAffectPreview INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'group-v1-migration', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + ); + + ALTER TABLE messages + ADD COLUMN isUserInitiatedMessage INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'group-v1-migration', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change', + + 'group-v2-change', + 'keychange' + ) + ); + + ALTER TABLE messages + ADD COLUMN isTimerChangeFromSync INTEGER + GENERATED ALWAYS AS ( + json_extract(json, '$.expirationTimerUpdate.fromSync') IS 1 + ); + + ALTER TABLE messages + ADD COLUMN isGroupLeaveEvent INTEGER + GENERATED ALWAYS AS ( + type IS 'group-v2-change' AND + json_array_length(json_extract(json, '$.groupV2Change.details')) IS 1 AND + json_extract(json, '$.groupV2Change.details[0].type') IS 'member-remove' AND + json_extract(json, '$.groupV2Change.from') IS NOT NULL AND + json_extract(json, '$.groupV2Change.from') IS json_extract(json, '$.groupV2Change.details[0].uuid') + ); + + ALTER TABLE messages + ADD COLUMN isGroupLeaveEventFromOther INTEGER + GENERATED ALWAYS AS ( + isGroupLeaveEvent IS 1 + AND + isChangeCreatedByUs IS 0 + ); + + CREATE INDEX messages_conversation ON messages + (conversationId, isStory, storyId, received_at, sent_at); + + CREATE INDEX messages_preview ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, expiresAt, received_at, sent_at); + + CREATE INDEX messages_activity ON messages + (conversationId, shouldAffectActivity, isTimerChangeFromSync, isGroupLeaveEventFromOther, received_at, sent_at); + + CREATE INDEX message_user_initiated ON messages (isUserInitiatedMessage); + ` + ); + + const ourUuid = getOurUuid(db); + if (!ourUuid) { + logger.warn('updateToSchemaVersion47: our UUID not found'); + } else { + db.prepare( + ` + UPDATE messages SET + isChangeCreatedByUs = json_extract(json, '$.groupV2Change.from') IS $ourUuid; + ` + ).run({ + ourUuid, + }); + } + + db.pragma('user_version = 47'); + })(); + + logger.info('updateToSchemaVersion47: success!'); +} diff --git a/ts/sql/migrations/index.ts b/ts/sql/migrations/index.ts index eb565dc46..92b6e930f 100644 --- a/ts/sql/migrations/index.ts +++ b/ts/sql/migrations/index.ts @@ -22,6 +22,7 @@ import updateToSchemaVersion43 from './43-gv2-uuid'; import updateToSchemaVersion44 from './44-badges'; import updateToSchemaVersion45 from './45-stories'; import updateToSchemaVersion46 from './46-optimize-stories'; +import updateToSchemaVersion47 from './47-further-optimize'; function updateToSchemaVersion1( currentVersion: number, @@ -1907,6 +1908,7 @@ export const SCHEMA_VERSIONS = [ updateToSchemaVersion44, updateToSchemaVersion45, updateToSchemaVersion46, + updateToSchemaVersion47, ]; export function updateSchema(db: Database, logger: LoggerType): void { diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index d5b5c4cf0..aebad3c3c 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -1333,7 +1333,8 @@ function cancelMessagesPendingConversationVerification(): ThunkAction< }); await window.Signal.Data.saveMessages( - messagesStopped.map(message => message.attributes) + messagesStopped.map(message => message.attributes), + { ourUuid: window.textsecure.storage.user.getCheckedUuid().toString() } ); }; } diff --git a/ts/test-electron/models/conversations_test.ts b/ts/test-electron/models/conversations_test.ts index 2eac06977..824efacc9 100644 --- a/ts/test-electron/models/conversations_test.ts +++ b/ts/test-electron/models/conversations_test.ts @@ -75,6 +75,7 @@ describe('Conversations', () => { // Saving to db and updating the convo's last message await window.Signal.Data.saveMessage(message.attributes, { forceSave: true, + ourUuid, }); message = window.MessageController.register(message.id, message); await window.Signal.Data.updateConversation(conversation.attributes); diff --git a/ts/test-electron/sql/allMedia_test.ts b/ts/test-electron/sql/allMedia_test.ts index f3fafcffa..0d8bb2171 100644 --- a/ts/test-electron/sql/allMedia_test.ts +++ b/ts/test-electron/sql/allMedia_test.ts @@ -32,6 +32,7 @@ describe('sql/allMedia', () => { const now = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -62,7 +63,10 @@ describe('sql/allMedia', () => { hasVisualMediaAttachments: true, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -79,6 +83,7 @@ describe('sql/allMedia', () => { const now = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -112,7 +117,10 @@ describe('sql/allMedia', () => { hasVisualMediaAttachments: true, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -131,6 +139,7 @@ describe('sql/allMedia', () => { const now = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -161,7 +170,10 @@ describe('sql/allMedia', () => { hasFileAttachments: true, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -178,6 +190,7 @@ describe('sql/allMedia', () => { const now = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -211,7 +224,10 @@ describe('sql/allMedia', () => { hasFileAttachments: true, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); diff --git a/ts/test-electron/sql/conversationSummary_test.ts b/ts/test-electron/sql/conversationSummary_test.ts new file mode 100644 index 000000000..9341066a7 --- /dev/null +++ b/ts/test-electron/sql/conversationSummary_test.ts @@ -0,0 +1,548 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import dataInterface from '../../sql/Client'; +import { UUID } from '../../types/UUID'; +import type { UUIDStringType } from '../../types/UUID'; + +import type { MessageAttributesType } from '../../model-types.d'; + +const { + removeAll, + _getAllMessages, + saveMessages, + getLastConversationMessages, +} = dataInterface; + +function getUuid(): UUIDStringType { + return UUID.generate().toString(); +} + +describe('sql/conversationSummary', () => { + beforeEach(async () => { + await removeAll(); + }); + + describe('getLastConversationMessages', () => { + it('returns the latest message in current conversation', async () => { + assert.lengthOf(await _getAllMessages(), 0); + + const now = Date.now(); + const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { + id: getUuid(), + body: 'message 1', + type: 'outgoing', + conversationId, + sent_at: now + 1, + received_at: now + 1, + timestamp: now + 1, + }; + const message2: MessageAttributesType = { + id: getUuid(), + body: 'message 2', + type: 'outgoing', + conversationId, + sent_at: now + 2, + received_at: now + 2, + timestamp: now + 2, + }; + const message3: MessageAttributesType = { + id: getUuid(), + body: 'message 3', + type: 'outgoing', + conversationId: getUuid(), + sent_at: now + 3, + received_at: now + 3, + timestamp: now + 3, + }; + + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); + + assert.lengthOf(await _getAllMessages(), 3); + + const messages = await getLastConversationMessages({ + conversationId, + ourUuid, + }); + + assert.strictEqual(messages.activity?.body, message2.body, 'activity'); + assert.strictEqual(messages.preview?.body, message2.body, 'preview'); + assert.isTrue(messages.hasUserInitiatedMessages); + }); + + it('preview excludes several message types, allows type = NULL', async () => { + assert.lengthOf(await _getAllMessages(), 0); + + const now = Date.now(); + const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { + id: getUuid(), + body: 'message 1', + // @ts-expect-error We're forcing a null type here for testing + type: null, + conversationId, + sent_at: now + 1, + received_at: now + 1, + timestamp: now + 1, + }; + const message2: MessageAttributesType = { + id: getUuid(), + body: 'message 2', + type: 'change-number-notification', + conversationId, + sent_at: now + 2, + received_at: now + 2, + timestamp: now + 2, + }; + const message3: MessageAttributesType = { + id: getUuid(), + body: 'message 3', + type: 'group-v1-migration', + conversationId, + sent_at: now + 3, + received_at: now + 3, + timestamp: now + 3, + }; + const message4: MessageAttributesType = { + id: getUuid(), + body: 'message 4', + type: 'message-history-unsynced', + conversationId, + sent_at: now + 4, + received_at: now + 4, + timestamp: now + 4, + }; + const message5: MessageAttributesType = { + id: getUuid(), + body: 'message 5', + type: 'profile-change', + conversationId, + sent_at: now + 5, + received_at: now + 5, + timestamp: now + 5, + }; + const message6: MessageAttributesType = { + id: getUuid(), + body: 'message 6', + type: 'story', + conversationId, + sent_at: now + 6, + received_at: now + 6, + timestamp: now + 6, + }; + const message7: MessageAttributesType = { + id: getUuid(), + body: 'message 7', + type: 'universal-timer-notification', + conversationId, + sent_at: now + 7, + received_at: now + 7, + timestamp: now + 7, + }; + const message8: MessageAttributesType = { + id: getUuid(), + body: 'message 8', + type: 'verified-change', + conversationId, + sent_at: now + 8, + received_at: now + 8, + timestamp: now + 8, + }; + + await saveMessages( + [ + message1, + message2, + message3, + message4, + message5, + message6, + message7, + message8, + ], + { + forceSave: true, + ourUuid, + } + ); + + assert.lengthOf(await _getAllMessages(), 8); + + const messages = await getLastConversationMessages({ + conversationId, + ourUuid, + }); + + assert.strictEqual(messages.preview?.body, message1.body); + }); + + it('activity excludes several message types, allows type = NULL', async () => { + assert.lengthOf(await _getAllMessages(), 0); + + const now = Date.now(); + const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { + id: getUuid(), + body: 'message 1', + // @ts-expect-error We're forcing a null type here for testing + type: null, + conversationId, + sent_at: now + 1, + received_at: now + 1, + timestamp: now + 1, + }; + const message2: MessageAttributesType = { + id: getUuid(), + body: 'message 2', + type: 'change-number-notification', + conversationId, + sent_at: now + 2, + received_at: now + 2, + timestamp: now + 2, + }; + const message3: MessageAttributesType = { + id: getUuid(), + body: 'message 3', + type: 'group-v1-migration', + conversationId, + sent_at: now + 3, + received_at: now + 3, + timestamp: now + 3, + }; + const message4: MessageAttributesType = { + id: getUuid(), + body: 'message 4', + type: 'keychange', + conversationId, + sent_at: now + 4, + received_at: now + 4, + timestamp: now + 4, + }; + const message5: MessageAttributesType = { + id: getUuid(), + body: 'message 5', + type: 'message-history-unsynced', + conversationId, + sent_at: now + 5, + received_at: now + 5, + timestamp: now + 5, + }; + const message6: MessageAttributesType = { + id: getUuid(), + body: 'message 6', + type: 'profile-change', + conversationId, + sent_at: now + 6, + received_at: now + 6, + timestamp: now + 6, + }; + const message7: MessageAttributesType = { + id: getUuid(), + body: 'message 7', + type: 'story', + conversationId, + sent_at: now + 7, + received_at: now + 7, + timestamp: now + 7, + }; + const message8: MessageAttributesType = { + id: getUuid(), + body: 'message 8', + type: 'universal-timer-notification', + conversationId, + sent_at: now + 8, + received_at: now + 8, + timestamp: now + 8, + }; + const message9: MessageAttributesType = { + id: getUuid(), + body: 'message 9', + type: 'verified-change', + conversationId, + sent_at: now + 9, + received_at: now + 9, + timestamp: now + 9, + }; + + await saveMessages( + [ + message1, + message2, + message3, + message4, + message5, + message6, + message7, + message8, + message9, + ], + { + forceSave: true, + ourUuid, + } + ); + + assert.lengthOf(await _getAllMessages(), 9); + + const messages = await getLastConversationMessages({ + conversationId, + ourUuid, + }); + + assert.strictEqual(messages.activity?.body, message1.body); + }); + + it('activity excludes expirationTimerUpdates with fromSync = true, includes fromSync = undefined', async () => { + assert.lengthOf(await _getAllMessages(), 0); + + const now = Date.now(); + const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { + id: getUuid(), + body: 'message 1', + type: 'outgoing', + conversationId, + expirationTimerUpdate: { + expireTimer: 10, + source: 'you', + }, + sent_at: now + 1, + received_at: now + 1, + timestamp: now + 1, + }; + const message2: MessageAttributesType = { + id: getUuid(), + body: 'message 2', + type: 'outgoing', + conversationId, + expirationTimerUpdate: { + expireTimer: 10, + fromSync: true, + }, + sent_at: now + 2, + received_at: now + 2, + timestamp: now + 2, + }; + + await saveMessages([message1, message2], { + forceSave: true, + ourUuid, + }); + + assert.lengthOf(await _getAllMessages(), 2); + + const messages = await getLastConversationMessages({ + conversationId, + ourUuid, + }); + + assert.strictEqual(messages.activity?.body, message1.body); + }); + + it('activity excludes expirationTimerUpdates with fromSync = true, includes fromSync = false', async () => { + assert.lengthOf(await _getAllMessages(), 0); + + const now = Date.now(); + const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { + id: getUuid(), + body: 'message 1', + type: 'outgoing', + conversationId, + expirationTimerUpdate: { + expireTimer: 10, + source: 'you', + fromSync: false, + }, + sent_at: now + 1, + received_at: now + 1, + timestamp: now + 1, + }; + const message2: MessageAttributesType = { + id: getUuid(), + body: 'message 2', + type: 'outgoing', + conversationId, + expirationTimerUpdate: { + expireTimer: 10, + fromSync: true, + }, + sent_at: now + 2, + received_at: now + 2, + timestamp: now + 2, + }; + + await saveMessages([message1, message2], { + forceSave: true, + ourUuid, + }); + + assert.lengthOf(await _getAllMessages(), 2); + + const messages = await getLastConversationMessages({ + conversationId, + ourUuid, + }); + + assert.strictEqual(messages.activity?.body, message1.body); + }); + + it('preview excludes expired message, includes non-disappearing message', async () => { + assert.lengthOf(await _getAllMessages(), 0); + + const now = Date.now(); + const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { + id: getUuid(), + body: 'message 1', + type: 'outgoing', + conversationId, + sent_at: now + 1, + received_at: now + 1, + timestamp: now + 1, + }; + const message2: MessageAttributesType = { + id: getUuid(), + body: 'message 2', + type: 'outgoing', + conversationId, + expirationStartTimestamp: now - 2 * 1000, + expireTimer: 1, + sent_at: now + 2, + received_at: now + 2, + timestamp: now + 2, + }; + + await saveMessages([message1, message2], { + forceSave: true, + ourUuid, + }); + + assert.lengthOf(await _getAllMessages(), 2); + + const messages = await getLastConversationMessages({ + conversationId, + ourUuid, + }); + + assert.strictEqual(messages.preview?.body, message1.body); + }); + + it('preview excludes expired message, includes non-disappearing message', async () => { + assert.lengthOf(await _getAllMessages(), 0); + + const now = Date.now(); + const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { + id: getUuid(), + body: 'message 1', + type: 'outgoing', + conversationId, + expirationStartTimestamp: now, + expireTimer: 30, + sent_at: now + 1, + received_at: now + 1, + timestamp: now + 1, + }; + const message2: MessageAttributesType = { + id: getUuid(), + body: 'message 2', + type: 'outgoing', + conversationId, + expirationStartTimestamp: now - 2 * 1000, + expireTimer: 1, + sent_at: now + 2, + received_at: now + 2, + timestamp: now + 2, + }; + + await saveMessages([message1, message2], { + forceSave: true, + ourUuid, + }); + + assert.lengthOf(await _getAllMessages(), 2); + + const messages = await getLastConversationMessages({ + conversationId, + ourUuid, + }); + + assert.strictEqual(messages.preview?.body, message1.body); + }); + + it('excludes group v2 change events where someone else leaves a group', async () => { + assert.lengthOf(await _getAllMessages(), 0); + + const now = Date.now(); + const conversationId = getUuid(); + const otherUuid = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { + id: getUuid(), + body: 'message 1 - removing ourselves', + type: 'group-v2-change', + conversationId, + groupV2Change: { + from: ourUuid, + details: [ + { + type: 'member-remove', + uuid: ourUuid, + }, + ], + }, + sent_at: now + 1, + received_at: now + 1, + timestamp: now + 1, + }; + const message2: MessageAttributesType = { + id: getUuid(), + body: 'message 2 - someone else leaving', + type: 'group-v2-change', + conversationId, + groupV2Change: { + from: otherUuid, + details: [ + { + type: 'member-remove', + uuid: otherUuid, + }, + ], + }, + sent_at: now + 2, + received_at: now + 2, + timestamp: now + 2, + }; + + await saveMessages([message1, message2], { + forceSave: true, + ourUuid, + }); + + assert.lengthOf(await _getAllMessages(), 2); + + const messages = await getLastConversationMessages({ + conversationId, + ourUuid, + }); + + assert.strictEqual(messages.activity?.body, message1.body, 'activity'); + assert.strictEqual(messages.preview?.body, message1.body, 'preview'); + assert.isFalse(messages.hasUserInitiatedMessages); + }); + }); +}); diff --git a/ts/test-electron/sql/fullTextSearch_test.ts b/ts/test-electron/sql/fullTextSearch_test.ts index acfdf72d8..22f80ad88 100644 --- a/ts/test-electron/sql/fullTextSearch_test.ts +++ b/ts/test-electron/sql/fullTextSearch_test.ts @@ -31,6 +31,7 @@ describe('sql/fullTextSearch', () => { const now = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), body: 'message 1 - generic string', @@ -59,7 +60,10 @@ describe('sql/fullTextSearch', () => { timestamp: now, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -68,7 +72,7 @@ describe('sql/fullTextSearch', () => { assert.strictEqual(searchResults[0].id, message2.id); message3.body = 'message 3 - unique string'; - await saveMessage(message3); + await saveMessage(message3, { ourUuid }); const searchResults2 = await searchMessages('unique'); assert.lengthOf(searchResults2, 2); @@ -81,6 +85,7 @@ describe('sql/fullTextSearch', () => { const now = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), body: 'message 1 - unique string', @@ -111,7 +116,10 @@ describe('sql/fullTextSearch', () => { isViewOnce: true, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -120,7 +128,7 @@ describe('sql/fullTextSearch', () => { assert.strictEqual(searchResults[0].id, message1.id); message1.body = 'message 3 - unique string'; - await saveMessage(message3); + await saveMessage(message3, { ourUuid }); const searchResults2 = await searchMessages('unique'); assert.lengthOf(searchResults2, 1); @@ -132,6 +140,7 @@ describe('sql/fullTextSearch', () => { const now = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), body: 'message 1 - unique string', @@ -162,7 +171,10 @@ describe('sql/fullTextSearch', () => { storyId: getUuid(), }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -171,7 +183,7 @@ describe('sql/fullTextSearch', () => { assert.strictEqual(searchResults[0].id, message1.id); message1.body = 'message 3 - unique string'; - await saveMessage(message3); + await saveMessage(message3, { ourUuid }); const searchResults2 = await searchMessages('unique'); assert.lengthOf(searchResults2, 1); diff --git a/ts/test-electron/sql/markRead_test.ts b/ts/test-electron/sql/markRead_test.ts index 7c7c9e02e..b50451f59 100644 --- a/ts/test-electron/sql/markRead_test.ts +++ b/ts/test-electron/sql/markRead_test.ts @@ -39,6 +39,7 @@ describe('sql/markRead', () => { const start = Date.now(); const readAt = start + 20; const conversationId = getUuid(); + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), @@ -117,6 +118,7 @@ describe('sql/markRead', () => { [message1, message2, message3, message4, message5, message6, message7], { forceSave: true, + ourUuid, } ); @@ -175,6 +177,7 @@ describe('sql/markRead', () => { const readAt = start + 20; const conversationId = getUuid(); const storyId = getUuid(); + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), @@ -258,6 +261,7 @@ describe('sql/markRead', () => { [message1, message2, message3, message4, message5, message6, message7], { forceSave: true, + ourUuid, } ); @@ -297,6 +301,7 @@ describe('sql/markRead', () => { const readAt = start + 20; const conversationId = getUuid(); const expireTimer = 15; + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), @@ -356,6 +361,7 @@ describe('sql/markRead', () => { await saveMessages([message1, message2, message3, message4, message5], { forceSave: true, + ourUuid, }); assert.strictEqual( @@ -409,6 +415,7 @@ describe('sql/markRead', () => { const start = Date.now(); const conversationId = getUuid(); const storyId = getUuid(); + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), @@ -459,6 +466,7 @@ describe('sql/markRead', () => { await saveMessages([message1, message2, message3, message4, message5], { forceSave: true, + ourUuid, }); assert.lengthOf(await _getAllMessages(), 5); @@ -553,6 +561,7 @@ describe('sql/markRead', () => { const start = Date.now(); const conversationId = getUuid(); const storyId = getUuid(); + const ourUuid = getUuid(); const message1: MessageAttributesType = { id: getUuid(), @@ -606,6 +615,7 @@ describe('sql/markRead', () => { await saveMessages([message1, message2, message3, message4, message5], { forceSave: true, + ourUuid, }); assert.lengthOf(await _getAllMessages(), 5); diff --git a/ts/test-electron/sql/sendLog_test.ts b/ts/test-electron/sql/sendLog_test.ts index 1f1f22e78..2fede1391 100644 --- a/ts/test-electron/sql/sendLog_test.ts +++ b/ts/test-electron/sql/sendLog_test.ts @@ -1,13 +1,17 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { v4 as getGuid } from 'uuid'; - import { assert } from 'chai'; import dataInterface from '../../sql/Client'; +import { UUID } from '../../types/UUID'; +import type { UUIDStringType } from '../../types/UUID'; import { constantTimeEqual, getRandomBytes } from '../../Crypto'; +function getUuid(): UUIDStringType { + return UUID.generate().toString(); +} + const { _getAllSentProtoMessageIds, _getAllSentProtoRecipients, @@ -37,9 +41,9 @@ describe('sql/sendLog', () => { timestamp, }; await insertSentProto(proto, { - messageIds: [getGuid()], + messageIds: [getUuid()], recipients: { - [getGuid()]: [1, 2], + [getUuid()]: [1, 2], }, }); const allProtos = await getAllSentProtos(); @@ -69,10 +73,10 @@ describe('sql/sendLog', () => { timestamp, }; await insertSentProto(proto, { - messageIds: [getGuid(), getGuid()], + messageIds: [getUuid(), getUuid()], recipients: { - [getGuid()]: [1, 2], - [getGuid()]: [1], + [getUuid()]: [1, 2], + [getUuid()]: [1], }, }); @@ -88,21 +92,22 @@ describe('sql/sendLog', () => { }); it('trigger deletes payload when referenced message is deleted', async () => { - const id = getGuid(); + const id = getUuid(); const timestamp = Date.now(); + const ourUuid = getUuid(); await saveMessage( { id, body: 'some text', - conversationId: getGuid(), + conversationId: getUuid(), received_at: timestamp, sent_at: timestamp, timestamp, type: 'outgoing', }, - { forceSave: true } + { forceSave: true, ourUuid } ); const bytes = getRandomBytes(128); @@ -114,7 +119,7 @@ describe('sql/sendLog', () => { await insertSentProto(proto, { messageIds: [id], recipients: { - [getGuid()]: [1, 2], + [getUuid()]: [1, 2], }, }); const allProtos = await getAllSentProtos(); @@ -133,9 +138,9 @@ describe('sql/sendLog', () => { it('supports adding duplicates', async () => { const timestamp = Date.now(); - const messageIds = [getGuid()]; + const messageIds = [getUuid()]; const recipients = { - [getGuid()]: [1], + [getUuid()]: [1], }; const proto1 = { contentHint: 7, @@ -170,7 +175,7 @@ describe('sql/sendLog', () => { it('handles duplicates, adding new recipients if needed', async () => { const timestamp = Date.now(); - const messageIds = [getGuid()]; + const messageIds = [getUuid()]; const proto = { contentHint: 1, proto: getRandomBytes(128), @@ -184,7 +189,7 @@ describe('sql/sendLog', () => { const id = await insertSentProto(proto, { messageIds, recipients: { - [getGuid()]: [1], + [getUuid()]: [1], }, }); @@ -192,7 +197,7 @@ describe('sql/sendLog', () => { assert.lengthOf(await _getAllSentProtoMessageIds(), 1); assert.lengthOf(await _getAllSentProtoRecipients(), 1); - const recipientUuid = getGuid(); + const recipientUuid = getUuid(); await insertProtoRecipients({ id, recipientUuid, @@ -225,21 +230,21 @@ describe('sql/sendLog', () => { timestamp: timestamp - 15, }; await insertSentProto(proto1, { - messageIds: [getGuid()], + messageIds: [getUuid()], recipients: { - [getGuid()]: [1], + [getUuid()]: [1], }, }); await insertSentProto(proto2, { - messageIds: [getGuid()], + messageIds: [getUuid()], recipients: { - [getGuid()]: [1, 2], + [getUuid()]: [1, 2], }, }); await insertSentProto(proto3, { - messageIds: [getGuid()], + messageIds: [getUuid()], recipients: { - [getGuid()]: [1, 2, 3], + [getUuid()]: [1, 2, 3], }, }); @@ -268,7 +273,7 @@ describe('sql/sendLog', () => { assert.lengthOf(await _getAllSentProtoMessageIds(), 0); assert.lengthOf(await _getAllSentProtoRecipients(), 0); - const messageId = getGuid(); + const messageId = getUuid(); const timestamp = Date.now(); const proto1 = { contentHint: 1, @@ -286,22 +291,22 @@ describe('sql/sendLog', () => { timestamp: timestamp - 20, }; await insertSentProto(proto1, { - messageIds: [messageId, getGuid()], + messageIds: [messageId, getUuid()], recipients: { - [getGuid()]: [1, 2], - [getGuid()]: [1], + [getUuid()]: [1, 2], + [getUuid()]: [1], }, }); await insertSentProto(proto2, { messageIds: [messageId], recipients: { - [getGuid()]: [1], + [getUuid()]: [1], }, }); await insertSentProto(proto3, { - messageIds: [getGuid()], + messageIds: [getUuid()], recipients: { - [getGuid()]: [1], + [getUuid()]: [1], }, }); @@ -321,15 +326,15 @@ describe('sql/sendLog', () => { it('does not delete payload if recipient remains', async () => { const timestamp = Date.now(); - const recipientUuid1 = getGuid(); - const recipientUuid2 = getGuid(); + const recipientUuid1 = getUuid(); + const recipientUuid2 = getUuid(); const proto = { contentHint: 1, proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { - messageIds: [getGuid()], + messageIds: [getUuid()], recipients: { [recipientUuid1]: [1, 2], [recipientUuid2]: [1], @@ -352,15 +357,15 @@ describe('sql/sendLog', () => { it('deletes payload if no recipients remain', async () => { const timestamp = Date.now(); - const recipientUuid1 = getGuid(); - const recipientUuid2 = getGuid(); + const recipientUuid1 = getUuid(); + const recipientUuid2 = getUuid(); const proto = { contentHint: 1, proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { - messageIds: [getGuid()], + messageIds: [getUuid()], recipients: { [recipientUuid1]: [1, 2], [recipientUuid2]: [1], @@ -401,15 +406,15 @@ describe('sql/sendLog', () => { it('deletes multiple recipients in a single transaction', async () => { const timestamp = Date.now(); - const recipientUuid1 = getGuid(); - const recipientUuid2 = getGuid(); + const recipientUuid1 = getUuid(); + const recipientUuid2 = getUuid(); const proto = { contentHint: 1, proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { - messageIds: [getGuid()], + messageIds: [getUuid()], recipients: { [recipientUuid1]: [1, 2], [recipientUuid2]: [1], @@ -446,8 +451,8 @@ describe('sql/sendLog', () => { it('returns matching payload', async () => { const timestamp = Date.now(); - const recipientUuid = getGuid(); - const messageIds = [getGuid(), getGuid()]; + const recipientUuid = getUuid(); + const messageIds = [getUuid(), getUuid()]; const proto = { contentHint: 1, proto: getRandomBytes(128), @@ -482,7 +487,7 @@ describe('sql/sendLog', () => { it('returns matching payload with no messageIds', async () => { const timestamp = Date.now(); - const recipientUuid = getGuid(); + const recipientUuid = getUuid(); const proto = { contentHint: 1, proto: getRandomBytes(128), @@ -517,14 +522,14 @@ describe('sql/sendLog', () => { it('returns nothing if payload does not have recipient', async () => { const timestamp = Date.now(); - const recipientUuid = getGuid(); + const recipientUuid = getUuid(); const proto = { contentHint: 1, proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { - messageIds: [getGuid()], + messageIds: [getUuid()], recipients: { [recipientUuid]: [1, 2], }, @@ -536,7 +541,7 @@ describe('sql/sendLog', () => { const actual = await getSentProtoByRecipient({ now: timestamp, timestamp, - recipientUuid: getGuid(), + recipientUuid: getUuid(), }); assert.isUndefined(actual); @@ -545,14 +550,14 @@ describe('sql/sendLog', () => { it('returns nothing if timestamp does not match', async () => { const timestamp = Date.now(); - const recipientUuid = getGuid(); + const recipientUuid = getUuid(); const proto = { contentHint: 1, proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { - messageIds: [getGuid()], + messageIds: [getUuid()], recipients: { [recipientUuid]: [1, 2], }, @@ -574,14 +579,14 @@ describe('sql/sendLog', () => { const TWO_DAYS = 2 * 24 * 60 * 60 * 1000; const timestamp = Date.now(); - const recipientUuid = getGuid(); + const recipientUuid = getUuid(); const proto = { contentHint: 1, proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { - messageIds: [getGuid()], + messageIds: [getUuid()], recipients: { [recipientUuid]: [1, 2], }, diff --git a/ts/test-electron/sql/stories_test.ts b/ts/test-electron/sql/stories_test.ts index 1c924be79..8120c54c6 100644 --- a/ts/test-electron/sql/stories_test.ts +++ b/ts/test-electron/sql/stories_test.ts @@ -28,6 +28,7 @@ describe('sql/stories', () => { const now = Date.now(); const conversationId = getUuid(); const sourceUuid = getUuid(); + const ourUuid = getUuid(); const story1: MessageAttributesType = { id: getUuid(), @@ -82,6 +83,7 @@ describe('sql/stories', () => { await saveMessages([story1, story2, story3, story4, story5], { forceSave: true, + ourUuid, }); assert.lengthOf(await _getAllMessages(), 5); @@ -149,6 +151,8 @@ describe('sql/stories', () => { const start = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); + const story1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -197,6 +201,7 @@ describe('sql/stories', () => { await saveMessages([story1, story2, story3, story4, story5], { forceSave: true, + ourUuid, }); assert.lengthOf(await _getAllMessages(), 5); diff --git a/ts/test-electron/sql/timelineFetches_test.ts b/ts/test-electron/sql/timelineFetches_test.ts index 767ee099b..665a5b9a5 100644 --- a/ts/test-electron/sql/timelineFetches_test.ts +++ b/ts/test-electron/sql/timelineFetches_test.ts @@ -35,6 +35,8 @@ describe('sql/timelineFetches', () => { const now = Date.now(); const conversationId = getUuid(); const storyId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -85,6 +87,7 @@ describe('sql/timelineFetches', () => { await saveMessages([message1, message2, message3, message4, message5], { forceSave: true, + ourUuid, }); assert.lengthOf(await _getAllMessages(), 5); @@ -105,6 +108,8 @@ describe('sql/timelineFetches', () => { const now = Date.now(); const conversationId = getUuid(); const storyId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { id: getUuid(), body: 'story', @@ -135,7 +140,10 @@ describe('sql/timelineFetches', () => { timestamp: now, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -152,6 +160,8 @@ describe('sql/timelineFetches', () => { const target = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -180,7 +190,10 @@ describe('sql/timelineFetches', () => { timestamp: target + 10, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -198,6 +211,8 @@ describe('sql/timelineFetches', () => { const target = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -226,7 +241,10 @@ describe('sql/timelineFetches', () => { timestamp: target, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -248,6 +266,8 @@ describe('sql/timelineFetches', () => { const target = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -276,7 +296,10 @@ describe('sql/timelineFetches', () => { timestamp: target, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -299,6 +322,8 @@ describe('sql/timelineFetches', () => { const now = Date.now(); const conversationId = getUuid(); const storyId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -349,6 +374,7 @@ describe('sql/timelineFetches', () => { await saveMessages([message1, message2, message3, message4, message5], { forceSave: true, + ourUuid, }); assert.lengthOf(await _getAllMessages(), 5); @@ -368,6 +394,8 @@ describe('sql/timelineFetches', () => { const now = Date.now(); const conversationId = getUuid(); const storyId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -398,7 +426,10 @@ describe('sql/timelineFetches', () => { timestamp: now + 20, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -416,6 +447,8 @@ describe('sql/timelineFetches', () => { const target = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -444,7 +477,10 @@ describe('sql/timelineFetches', () => { timestamp: target + 10, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -462,6 +498,8 @@ describe('sql/timelineFetches', () => { const target = Date.now(); const conversationId = getUuid(); + const ourUuid = getUuid(); + const message1: MessageAttributesType = { id: getUuid(), body: 'message 1', @@ -490,7 +528,10 @@ describe('sql/timelineFetches', () => { timestamp: target, }; - await saveMessages([message1, message2, message3], { forceSave: true }); + await saveMessages([message1, message2, message3], { + forceSave: true, + ourUuid, + }); assert.lengthOf(await _getAllMessages(), 3); @@ -514,6 +555,7 @@ describe('sql/timelineFetches', () => { const target = Date.now(); const conversationId = getUuid(); const storyId = getUuid(); + const ourUuid = getUuid(); const story: MessageAttributesType = { id: getUuid(), @@ -605,7 +647,7 @@ describe('sql/timelineFetches', () => { newestInStory, newest, ], - { forceSave: true } + { forceSave: true, ourUuid } ); assert.lengthOf(await _getAllMessages(), 8); diff --git a/ts/test-node/sql_migrations_test.ts b/ts/test-node/sql_migrations_test.ts index b9433f3cc..0df4e707c 100644 --- a/ts/test-node/sql_migrations_test.ts +++ b/ts/test-node/sql_migrations_test.ts @@ -910,35 +910,110 @@ describe('SQL migrations test', () => { }); }); - describe('updateToSchemaVersion46', () => { - it('creates new auto-generated isStory field', () => { - const STORY_ID_1 = generateGuid(); + describe('updateToSchemaVersion47', () => { + it('creates and pre-populates new isChangeCreatedByUs field', () => { + const OTHER_UUID = generateGuid(); const MESSAGE_ID_1 = generateGuid(); const MESSAGE_ID_2 = generateGuid(); const CONVERSATION_ID = generateGuid(); updateToVersion(46); + const uuidItem = JSON.stringify({ + value: `${OUR_UUID}.4`, + }); + const changeFromUs = JSON.stringify({ + groupV2Change: { + from: OUR_UUID, + details: [ + { + type: 'member-remove', + uuid: OTHER_UUID, + }, + ], + }, + }); + const changeFromOther = JSON.stringify({ + groupV2Change: { + from: OTHER_UUID, + details: [ + { + type: 'member-remove', + uuid: OUR_UUID, + }, + ], + }, + }); + db.exec( ` + INSERT INTO items (id, json) VALUES ('uuid_id', '${uuidItem}'); INSERT INTO messages - (id, storyId, conversationId, type, body) + (id, conversationId, type, json) VALUES - ('${MESSAGE_ID_1}', '${STORY_ID_1}', '${CONVERSATION_ID}', 'story', 'story 1'), - ('${MESSAGE_ID_2}', null, '${CONVERSATION_ID}', 'outgoing', 'reply to story 1'); + ('${MESSAGE_ID_1}', '${CONVERSATION_ID}', 'outgoing', '${changeFromUs}'), + ('${MESSAGE_ID_2}', '${CONVERSATION_ID}', 'outgoing', '${changeFromOther}'); ` ); + updateToVersion(47); + assert.strictEqual( db.prepare('SELECT COUNT(*) FROM messages;').pluck().get(), 2 ); + assert.strictEqual( + db + .prepare( + 'SELECT COUNT(*) FROM messages WHERE isChangeCreatedByUs IS 0;' + ) + .pluck() + .get(), + 1, + 'zero' + ); + assert.strictEqual( + db + .prepare( + 'SELECT COUNT(*) FROM messages WHERE isChangeCreatedByUs IS 1;' + ) + .pluck() + .get(), + 1, + 'one' + ); + }); + + it('creates new auto-generated isStory field', () => { + const STORY_ID_1 = generateGuid(); + const MESSAGE_ID_1 = generateGuid(); + const MESSAGE_ID_2 = generateGuid(); + const MESSAGE_ID_3 = generateGuid(); + const CONVERSATION_ID = generateGuid(); + + updateToVersion(47); + + db.exec( + ` + INSERT INTO messages + (id, storyId, conversationId, type, body) + VALUES + ('${MESSAGE_ID_1}', '${STORY_ID_1}', '${CONVERSATION_ID}', 'story', 'story 1'), + ('${MESSAGE_ID_2}', null, '${CONVERSATION_ID}', 'outgoing', 'reply to story 1'), + ('${MESSAGE_ID_3}', null, '${CONVERSATION_ID}', null, 'null type!'); + ` + ); + + assert.strictEqual( + db.prepare('SELECT COUNT(*) FROM messages;').pluck().get(), + 3 + ); assert.strictEqual( db .prepare('SELECT COUNT(*) FROM messages WHERE isStory IS 0;') .pluck() .get(), - 1 + 2 ); assert.strictEqual( db @@ -949,8 +1024,217 @@ describe('SQL migrations test', () => { ); }); + it('creates new auto-generated shouldAffectActivity/shouldAffectPreview/isUserInitiatedMessage fields', () => { + const MESSAGE_ID_1 = generateGuid(); + const MESSAGE_ID_2 = generateGuid(); + const MESSAGE_ID_3 = generateGuid(); + const MESSAGE_ID_4 = generateGuid(); + const CONVERSATION_ID = generateGuid(); + + updateToVersion(47); + + db.exec( + ` + INSERT INTO messages + (id, conversationId, type) + VALUES + ('${MESSAGE_ID_1}', '${CONVERSATION_ID}', 'story'), + ('${MESSAGE_ID_2}', '${CONVERSATION_ID}', 'keychange'), + ('${MESSAGE_ID_3}', '${CONVERSATION_ID}', 'outgoing'), + ('${MESSAGE_ID_4}', '${CONVERSATION_ID}', 'group-v2-change'); + ` + ); + + assert.strictEqual( + db.prepare('SELECT COUNT(*) FROM messages;').pluck().get(), + 4 + ); + assert.strictEqual( + db + .prepare( + 'SELECT COUNT(*) FROM messages WHERE shouldAffectPreview IS 1;' + ) + .pluck() + .get(), + 3 + ); + assert.strictEqual( + db + .prepare( + 'SELECT COUNT(*) FROM messages WHERE shouldAffectActivity IS 1;' + ) + .pluck() + .get(), + 2 + ); + assert.strictEqual( + db + .prepare( + 'SELECT COUNT(*) FROM messages WHERE isUserInitiatedMessage IS 1;' + ) + .pluck() + .get(), + 1 + ); + }); + + it('creates new auto-generated isTimerChangeFromSync fields', () => { + const MESSAGE_ID_1 = generateGuid(); + const MESSAGE_ID_2 = generateGuid(); + const MESSAGE_ID_3 = generateGuid(); + const CONVERSATION_ID = generateGuid(); + + updateToVersion(47); + + const timerUpdate = JSON.stringify({ + expirationTimerUpdate: { + expireTimer: 30, + fromSync: false, + }, + }); + const timerUpdateFromSync = JSON.stringify({ + expirationTimerUpdate: { + expireTimer: 30, + fromSync: true, + }, + }); + + db.exec( + ` + INSERT INTO messages + (id, conversationId, type, json) + VALUES + ('${MESSAGE_ID_1}', '${CONVERSATION_ID}', 'outgoing', '${timerUpdate}'), + ('${MESSAGE_ID_2}', '${CONVERSATION_ID}', 'outgoing', '${timerUpdateFromSync}'), + ('${MESSAGE_ID_3}', '${CONVERSATION_ID}', 'outgoing', '{}'); + ` + ); + + assert.strictEqual( + db.prepare('SELECT COUNT(*) FROM messages;').pluck().get(), + 3 + ); + assert.strictEqual( + db + .prepare( + 'SELECT COUNT(*) FROM messages WHERE isTimerChangeFromSync IS 1;' + ) + .pluck() + .get(), + 1 + ); + assert.strictEqual( + db + .prepare( + 'SELECT COUNT(*) FROM messages WHERE isTimerChangeFromSync IS 0;' + ) + .pluck() + .get(), + 2 + ); + }); + + it('creates new auto-generated isGroupLeaveEvent fields', () => { + const MESSAGE_ID_1 = generateGuid(); + const MESSAGE_ID_2 = generateGuid(); + const MESSAGE_ID_3 = generateGuid(); + const MESSAGE_ID_4 = generateGuid(); + const MESSAGE_ID_5 = generateGuid(); + const CONVERSATION_ID = generateGuid(); + const FIRST_UUID = generateGuid(); + const SECOND_UUID = generateGuid(); + const THIRD_UUID = generateGuid(); + + updateToVersion(47); + + const memberRemoveByOther = JSON.stringify({ + groupV2Change: { + from: FIRST_UUID, + details: [ + { + type: 'member-remove', + uuid: SECOND_UUID, + }, + ], + }, + }); + const memberLeave = JSON.stringify({ + groupV2Change: { + from: FIRST_UUID, + details: [ + { + type: 'member-remove', + uuid: FIRST_UUID, + }, + ], + }, + }); + const multipleRemoves = JSON.stringify({ + groupV2Change: { + from: FIRST_UUID, + details: [ + { + type: 'member-remove', + uuid: SECOND_UUID, + }, + { + type: 'member-remove', + uuid: THIRD_UUID, + }, + ], + }, + }); + const memberAdd = JSON.stringify({ + groupV2Change: { + from: FIRST_UUID, + details: [ + { + type: 'member-add', + uuid: FIRST_UUID, + }, + ], + }, + }); + + db.exec( + ` + INSERT INTO messages + (id, conversationId, type, json) + VALUES + ('${MESSAGE_ID_1}', '${CONVERSATION_ID}', 'outgoing', '${memberLeave}'), + ('${MESSAGE_ID_2}', '${CONVERSATION_ID}', 'group-v2-change', '${memberRemoveByOther}'), + ('${MESSAGE_ID_3}', '${CONVERSATION_ID}', 'group-v2-change', '${memberLeave}'), + ('${MESSAGE_ID_4}', '${CONVERSATION_ID}', 'group-v2-change', '${multipleRemoves}'), + ('${MESSAGE_ID_5}', '${CONVERSATION_ID}', 'group-v2-change', '${memberAdd}'); + ` + ); + + assert.strictEqual( + db.prepare('SELECT COUNT(*) FROM messages;').pluck().get(), + 5 + ); + assert.strictEqual( + db + .prepare( + 'SELECT COUNT(*) FROM messages WHERE isGroupLeaveEvent IS 1;' + ) + .pluck() + .get(), + 1 + ); + assert.strictEqual( + db + .prepare( + 'SELECT COUNT(*) FROM messages WHERE isGroupLeaveEvent IS 0;' + ) + .pluck() + .get(), + 4 + ); + }); + it('ensures that index is used for getOlderMessagesByConversation', () => { - updateToVersion(46); + updateToVersion(47); const { detail } = db .prepare( diff --git a/ts/util/messageBatcher.ts b/ts/util/messageBatcher.ts index 752d40d8f..af1a26df9 100644 --- a/ts/util/messageBatcher.ts +++ b/ts/util/messageBatcher.ts @@ -12,7 +12,9 @@ const updateMessageBatcher = createBatcher({ maxSize: 50, processBatch: async (messageAttrs: Array) => { log.info('updateMessageBatcher', messageAttrs.length); - await window.Signal.Data.saveMessages(messageAttrs); + await window.Signal.Data.saveMessages(messageAttrs, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); }, }); @@ -22,7 +24,9 @@ export function queueUpdateMessage(messageAttr: MessageAttributesType): void { if (shouldBatch) { updateMessageBatcher.add(messageAttr); } else { - window.Signal.Data.saveMessage(messageAttr); + window.Signal.Data.saveMessage(messageAttr, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); } } @@ -38,6 +42,7 @@ export const saveNewMessageBatcher = createWaitBatcher({ log.info('saveNewMessageBatcher', messageAttrs.length); await window.Signal.Data.saveMessages(messageAttrs, { forceSave: true, + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), }); }, }); diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index e01f5dfb3..1177b94d6 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -1475,6 +1475,7 @@ export class ConversationView extends window.Backbone.View { const DEFAULT_DOCUMENTS_FETCH_COUNT = 150; const conversationId = this.model.get('id'); + const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString(); const getProps = async () => { const rawMedia = @@ -1505,7 +1506,7 @@ export class ConversationView extends window.Backbone.View { // eslint-disable-next-line no-await-in-loop rawMedia[i] = await upgradeMessageSchema(message); // eslint-disable-next-line no-await-in-loop - await window.Signal.Data.saveMessage(rawMedia[i]); + await window.Signal.Data.saveMessage(rawMedia[i], { ourUuid }); } }