From 054d3887a15e97724f4359b62502b071e125ccbe Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 10 Apr 2018 15:09:29 -0700 Subject: [PATCH] Quotes: The full pipeline into the database 1. MessageReceiver always pulls down thumbnails included in quotes 2. Message.upgradeSchema has a new schema that puts all thumbnails on disk just like happens with full attachments. 3. handleDataMessage pipes quote from dataMessage into the final message destined for the database --- js/models/messages.js | 1 + js/modules/types/message.js | 44 ++++++++++++++--- libtextsecure/message_receiver.js | 12 +++++ test/modules/types/message_test.js | 78 ++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 6 deletions(-) diff --git a/js/models/messages.js b/js/models/messages.js index 624557994..60e0afe6b 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -447,6 +447,7 @@ body : dataMessage.body, conversationId : conversation.id, attachments : dataMessage.attachments, + quote : dataMessage.quote, decrypted_at : now, flags : dataMessage.flags, errors : [] diff --git a/js/modules/types/message.js b/js/modules/types/message.js index f17bcf88b..6a6cc64ca 100644 --- a/js/modules/types/message.js +++ b/js/modules/types/message.js @@ -26,7 +26,7 @@ const INITIAL_SCHEMA_VERSION = 0; // add more upgrade steps, we could design a pipeline that does this // incrementally, e.g. from version 0 / unknown -> 1, 1 --> 2, etc., similar to // how we do database migrations: -exports.CURRENT_SCHEMA_VERSION = 3; +exports.CURRENT_SCHEMA_VERSION = 4; // Public API @@ -149,6 +149,26 @@ exports._mapAttachments = upgradeAttachment => async (message, context) => { return Object.assign({}, message, { attachments }); }; +// _mapQuotedAttachments :: (QuotedAttachment -> Promise QuotedAttachment) -> +// (Message, Context) -> +// +exports._mapQuotedAttachments = upgradeAttachment => async (message, context) => { + if (!message.quote) { + return message; + } + + const upgradeWithContext = attachment => + upgradeAttachment(attachment, context); + const quotedAttachments = (message.quote && message.quote.attachments) || []; + + const attachments = await Promise.all(quotedAttachments.map(upgradeWithContext)); + return Object.assign({}, message, { + quote: Object.assign({}, message.quote, { + attachments, + }), + }); +}; + const toVersion0 = async message => exports.initializeSchemaVersion(message); @@ -164,17 +184,29 @@ const toVersion3 = exports._withSchemaVersion( 3, exports._mapAttachments(Attachment.migrateDataToFileSystem) ); +const toVersion4 = exports._withSchemaVersion( + 4, + exports._mapQuotedAttachments(Attachment.migrateDataToFileSystem) +); // UpgradeStep -exports.upgradeSchema = async (message, { writeNewAttachmentData } = {}) => { +exports.upgradeSchema = async (rawMessage, { writeNewAttachmentData } = {}) => { if (!isFunction(writeNewAttachmentData)) { throw new TypeError('`context.writeNewAttachmentData` is required'); } - return toVersion3( - await toVersion2(await toVersion1(await toVersion0(message))), - { writeNewAttachmentData } - ); + let message = rawMessage; + const versions = [toVersion0, toVersion1, toVersion2, toVersion3, toVersion4]; + + for (let i = 0, max = versions.length; i < max; i += 1) { + const currentVersion = versions[i]; + // We really do want this intra-loop await because this is a chained async action, + // each step dependent on the previous + // eslint-disable-next-line no-await-in-loop + message = await currentVersion(message, { writeNewAttachmentData }); + } + + return message; }; exports.createAttachmentLoader = (loadAttachmentData) => { diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index f5147b6d4..18434b535 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1006,6 +1006,18 @@ MessageReceiver.prototype.extend({ const attachment = decrypted.attachments[i]; promises.push(this.handleAttachment(attachment)); } + + if (decrypted.quote && decrypted.quote.attachments) { + const { attachments } = decrypted.quote; + + for (let i = 0, max = attachments.length; i < max; i += 1) { + const attachment = attachments[i]; + if (attachment.thumbnail) { + promises.push(this.handleAttachment(attachment.thumbnail)); + } + } + } + return Promise.all(promises).then(() => decrypted); /* eslint-enable no-bitwise, no-param-reassign */ }, diff --git a/test/modules/types/message_test.js b/test/modules/types/message_test.js index 7a8af0223..f0130fef3 100644 --- a/test/modules/types/message_test.js +++ b/test/modules/types/message_test.js @@ -1,4 +1,5 @@ const { assert } = require('chai'); +const sinon = require('sinon'); const Message = require('../../../js/modules/types/message'); const { stringToArrayBuffer } = require('../../../js/modules/string_to_array_buffer'); @@ -308,4 +309,81 @@ describe('Message', () => { assert.deepEqual(actual, expected); }); }); + + describe('_mapQuotedAttachments', () => { + it('handles message with no quote', async () => { + const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called")); + const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); + + const message = { + body: 'hey there!', + }; + const result = await upgradeVersion(message); + assert.deepEqual(result, message); + }); + + it('handles quote with no attachments', async () => { + const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called")); + const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); + + const message = { + body: 'hey there!', + quote: { + text: 'hey!', + }, + }; + const expected = { + body: 'hey there!', + quote: { + text: 'hey!', + attachments: [], + }, + }; + const result = await upgradeVersion(message); + assert.deepEqual(result, expected); + }); + + it('handles zero attachments', async () => { + const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called")); + const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); + + const message = { + body: 'hey there!', + quote: { + text: 'hey!', + attachments: [], + }, + }; + const result = await upgradeVersion(message); + assert.deepEqual(result, message); + }); + + it('calls provided async function for each quoted attachment', async () => { + const upgradeAttachment = sinon.stub().returns(Promise.resolve({ + path: '/new/path/on/disk', + })); + const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); + + const message = { + body: 'hey there!', + quote: { + text: 'hey!', + attachments: [{ + data: 'data is here', + }], + }, + }; + const expected = { + body: 'hey there!', + quote: { + text: 'hey!', + attachments: [{ + path: '/new/path/on/disk', + }], + }, + }; + const result = await upgradeVersion(message); + assert.deepEqual(result, expected); + }); + }); });