From 8f8e25bb3e789f905cf082ad45dba950c80b0435 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 8 Feb 2019 11:51:34 +1100 Subject: [PATCH 1/8] Added local link preview --- background.html | 1 + js/link_previews_helper.js | 151 +++++++++++++++++++++++++++++++++++++ js/models/messages.js | 70 +++++++++++++++-- 3 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 js/link_previews_helper.js diff --git a/background.html b/background.html index 7f85a30d1..6dfaf898b 100644 --- a/background.html +++ b/background.html @@ -730,6 +730,7 @@ + diff --git a/js/link_previews_helper.js b/js/link_previews_helper.js new file mode 100644 index 000000000..5a7e18268 --- /dev/null +++ b/js/link_previews_helper.js @@ -0,0 +1,151 @@ +/* global + Signal, + textsecure, + dcodeIO, +*/ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Signal = window.Signal || {}; + window.Signal.LinkPreviews = window.Signal.LinkPreviews || {}; + + const base64ImageCache = {}; + + function getBase64Image(preview) { + const { url, image } = preview; + if (!url || !image || !image.data) return null; + + // Return the cached value + if (base64ImageCache[url]) return base64ImageCache[url]; + + // Set the cache and return the value + const contentType = image.contentType || 'image/jpeg'; + const base64 = dcodeIO.ByteBuffer.wrap(image.data).toString('base64'); + + const data = `data:${contentType};base64, ${base64}`; + base64ImageCache[url] = data; + + return data; + } + + async function makeChunkedRequest(url) { + const PARALLELISM = 3; + const size = await textsecure.messaging.getProxiedSize(url); + const chunks = await Signal.LinkPreviews.getChunkPattern(size); + + let results = []; + const jobs = chunks.map(chunk => async () => { + const { start, end } = chunk; + + const result = await textsecure.messaging.makeProxiedRequest(url, { + start, + end, + returnArrayBuffer: true, + }); + + return { + ...chunk, + ...result, + }; + }); + + while (jobs.length > 0) { + const activeJobs = []; + for (let i = 0, max = PARALLELISM; i < max; i += 1) { + if (!jobs.length) { + break; + } + + const job = jobs.shift(); + activeJobs.push(job()); + } + + // eslint-disable-next-line no-await-in-loop + results = results.concat(await Promise.all(activeJobs)); + } + + if (!results.length) { + throw new Error('No responses received'); + } + + const { contentType } = results[0]; + const data = Signal.LinkPreviews.assembleChunks(results); + + return { + contentType, + data, + }; + } + + async function getPreview(url) { + let html; + try { + html = await textsecure.messaging.makeProxiedRequest(url); + } catch (error) { + if (error.code >= 300) { + return null; + } + } + + const title = window.Signal.LinkPreviews.getTitleMetaTag(html); + const imageUrl = window.Signal.LinkPreviews.getImageMetaTag(html); + + let image; + let objectUrl; + try { + if (imageUrl) { + if (!Signal.LinkPreviews.isMediaLinkInWhitelist(imageUrl)) { + const primaryDomain = Signal.LinkPreviews.getDomain(url); + const imageDomain = Signal.LinkPreviews.getDomain(imageUrl); + throw new Error( + `imageUrl for domain ${primaryDomain} did not match media whitelist. Domain: ${imageDomain}` + ); + } + + const data = await makeChunkedRequest(imageUrl); + + // Calculate dimensions + const file = new Blob([data.data], { + type: data.contentType, + }); + objectUrl = URL.createObjectURL(file); + + const dimensions = await Signal.Types.VisualAttachment.getImageDimensions( + { + objectUrl, + logger: window.log, + } + ); + + image = { + ...data, + ...dimensions, + contentType: file.type, + }; + } + } catch (error) { + // We still want to show the preview if we failed to get an image + window.log.error( + 'getPreview failed to get image for link preview:', + error.message + ); + } finally { + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + } + + return { + title, + url, + image, + }; + } + + window.Signal.LinkPreviews.helper = { + getPreview, + getBase64Image, + } +})(); diff --git a/js/models/messages.js b/js/models/messages.js index 796070e00..ee8d7e9d6 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -8,6 +8,7 @@ /* global Signal: false */ /* global textsecure: false */ /* global Whisper: false */ +/* global dcodeIO: false */ /* eslint-disable more/no-then */ @@ -84,6 +85,8 @@ this.on('unload', this.unload); this.on('expired', this.onExpired); this.setToExpire(); + + this.updatePreviews(); }, idForLogging() { return `${this.get('source')}.${this.get('sourceDevice')} ${this.get( @@ -109,6 +112,40 @@ // eslint-disable-next-line no-bitwise return !!(this.get('flags') & flag); }, + async updatePreviews() { + if (this.updatingPreview) return; + + // Only update the preview if we don't have any set + const preview = this.get('preview'); + if (!_.isEmpty(preview)) return; + + // Make sure we have links we can preview + const links = Signal.LinkPreviews.findLinks(this.get('body')); + const firstLink = links.find(link => Signal.LinkPreviews.isLinkInWhitelist(link)); + if (!firstLink) return; + + this.updatingPreview = true; + + try { + const result = await Signal.LinkPreviews.helper.getPreview(firstLink); + if (!result) { + this.updatingPreview = false; + return; + } + + if (!result.image && !result.title) { + // A link preview isn't worth showing unless we have either a title or an image + this.updatingPreview = false; + return; + } + + this.set({ preview: [result] }); + } catch (e) { + window.log.warn(`Failed to load previews for message: ${this.id}`); + } finally { + this.updatingPreview = false; + } + }, getEndSessionTranslationKey() { const sessionType = this.get('endSessionType'); if (sessionType === 'ongoing') { @@ -616,11 +653,34 @@ getPropsForPreview() { const previews = this.get('preview') || []; - return previews.map(preview => ({ - ...preview, - domain: window.Signal.LinkPreviews.getDomain(preview.url), - image: preview.image ? this.getPropsForAttachment(preview.image) : null, - })); + return previews.map(preview => { + let image = {}; + + // Try set the image from the attachment otherwise just pass in the object + if (preview.image) { + try { + const attachmentProps = this.getPropsForAttachment(preview.image); + if (attachmentProps.url) { + image = attachmentProps; + } + } catch (e) { + // Only set the image if we have a url to display + const url = Signal.LinkPreviews.helper.getBase64Image(preview); + if (preview.image.url || url) { + image = { + ...preview.image, + url: preview.image.url || url, + } + } + } + } + + return { + ...preview, + domain: window.Signal.LinkPreviews.getDomain(preview.url), + image, + }; + }); }, getPropsForQuote() { const quote = this.get('quote'); From ae695fe32f3607014c0292187b5a41d6ce2ef92e Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 8 Feb 2019 12:51:00 +1100 Subject: [PATCH 2/8] Remove sending attachments. --- js/link_previews_helper.js | 4 ++-- js/models/messages.js | 2 ++ libtextsecure/sendmessage.js | 7 +++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/js/link_previews_helper.js b/js/link_previews_helper.js index 5a7e18268..fe468690c 100644 --- a/js/link_previews_helper.js +++ b/js/link_previews_helper.js @@ -89,8 +89,8 @@ } } - const title = window.Signal.LinkPreviews.getTitleMetaTag(html); - const imageUrl = window.Signal.LinkPreviews.getImageMetaTag(html); + const title = Signal.LinkPreviews.getTitleMetaTag(html); + const imageUrl = Signal.LinkPreviews.getImageMetaTag(html); let image; let objectUrl; diff --git a/js/models/messages.js b/js/models/messages.js index ee8d7e9d6..4f917cc1f 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -139,6 +139,8 @@ return; } + // We don't want to save the base64 url in the message as it will increase the size of it + // Rather we fetch the base64 later this.set({ preview: [result] }); } catch (e) { window.log.warn(`Failed to load previews for message: ${this.id}`); diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 3cc6a8b85..b8e2ff991 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -307,6 +307,13 @@ MessageSender.prototype = { const message = new Message(attrs); const silent = false; + // Remove this when we add support for attachments + message.attachmentPointers = null; + message.preview = null; + if (message.quote) { + message.quote.attachments = null; + } + return Promise.all([ this.uploadAttachments(message), this.uploadThumbnails(message), From 27c60623516322952a36b3d25783f2aee8577bdc Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 8 Feb 2019 13:43:14 +1100 Subject: [PATCH 3/8] Fix incoming messages not showing previews. Linting. --- js/link_previews_helper.js | 16 ++++++++++------ js/models/messages.js | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/js/link_previews_helper.js b/js/link_previews_helper.js index fe468690c..bb73d386a 100644 --- a/js/link_previews_helper.js +++ b/js/link_previews_helper.js @@ -21,13 +21,17 @@ if (base64ImageCache[url]) return base64ImageCache[url]; // Set the cache and return the value - const contentType = image.contentType || 'image/jpeg'; - const base64 = dcodeIO.ByteBuffer.wrap(image.data).toString('base64'); + try { + const contentType = image.contentType || 'image/jpeg'; + const base64 = dcodeIO.ByteBuffer.wrap(image.data).toString('base64'); - const data = `data:${contentType};base64, ${base64}`; - base64ImageCache[url] = data; + const data = `data:${contentType};base64, ${base64}`; + base64ImageCache[url] = data; - return data; + return data; + } catch (e) { + return null; + } } async function makeChunkedRequest(url) { @@ -147,5 +151,5 @@ window.Signal.LinkPreviews.helper = { getPreview, getBase64Image, - } + }; })(); diff --git a/js/models/messages.js b/js/models/messages.js index 4f917cc1f..34b652021 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -8,7 +8,6 @@ /* global Signal: false */ /* global textsecure: false */ /* global Whisper: false */ -/* global dcodeIO: false */ /* eslint-disable more/no-then */ @@ -113,16 +112,29 @@ return !!(this.get('flags') & flag); }, async updatePreviews() { - if (this.updatingPreview) return; + // Don't generate link previews if user has turned them off + if (!storage.get('linkPreviews', false)) { + return; + } + + if (this.updatingPreview) { + return; + } // Only update the preview if we don't have any set const preview = this.get('preview'); - if (!_.isEmpty(preview)) return; + if (!_.isEmpty(preview)) { + return; + } // Make sure we have links we can preview const links = Signal.LinkPreviews.findLinks(this.get('body')); - const firstLink = links.find(link => Signal.LinkPreviews.isLinkInWhitelist(link)); - if (!firstLink) return; + const firstLink = links.find(link => + Signal.LinkPreviews.isLinkInWhitelist(link) + ); + if (!firstLink) { + return; + } this.updatingPreview = true; @@ -139,8 +151,8 @@ return; } - // We don't want to save the base64 url in the message as it will increase the size of it - // Rather we fetch the base64 later + // We don't want to save the base64 url in the message as + // it will increase the size of it, Rather we fetch the base64 later this.set({ preview: [result] }); } catch (e) { window.log.warn(`Failed to load previews for message: ${this.id}`); @@ -653,6 +665,11 @@ }); }, getPropsForPreview() { + // Don't generate link previews if user has turned them off + if (!storage.get('linkPreviews', false)) { + return null; + } + const previews = this.get('preview') || []; return previews.map(preview => { @@ -672,7 +689,7 @@ image = { ...preview.image, url: preview.image.url || url, - } + }; } } } @@ -1369,6 +1386,9 @@ schemaVersion: dataMessage.schemaVersion, }); + // Update the previews if we need to + message.updatePreviews(); + if (type === 'outgoing') { const receipts = Whisper.DeliveryReceipts.forMessage( conversation, From 725a41a8d72ea55a5eb8df59a38581c65f9c333c Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 8 Feb 2019 14:47:42 +1100 Subject: [PATCH 4/8] Added link preview setting. --- _locales/en/messages.json | 11 +++++++++++ js/background.js | 5 +++++ js/settings_start.js | 1 + js/views/settings_view.js | 6 ++++++ main.js | 3 +++ preload.js | 3 +++ settings.html | 4 ++++ settings_preload.js | 3 +++ 8 files changed, 36 insertions(+) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 9796f02e2..52f4889e7 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1102,6 +1102,17 @@ "message": "General", "description": "Header for general options on the settings screen" }, + "sendLinkPreviews": { + "message": "Send Link Previews", + "description": + "Option to control creation and send of link previews in setting screen" + }, + "linkPreviewsDescription": { + "message": + "Enable local link previews (Restart for changes to take effect)", + "description": + "Additional detail provided for Link Previews option in settings screen" + }, "spellCheckDescription": { "message": "Enable spell check of text entered in message composition box", "description": "Description of the media permission description" diff --git a/js/background.js b/js/background.js index 6623f98ae..46c52f36a 100644 --- a/js/background.js +++ b/js/background.js @@ -254,6 +254,11 @@ getReadReceiptSetting: () => storage.get('read-receipt-setting'), setReadReceiptSetting: value => storage.put('read-receipt-setting', value), + + getLinkPreviewSetting: () => storage.get('linkPreviews', false), + setLinkPreviewSetting: value => + storage.put('linkPreviews', value), + getNotificationSetting: () => storage.get('notification-setting', 'message'), setNotificationSetting: value => diff --git a/js/settings_start.js b/js/settings_start.js index f4ee85d24..607178f10 100644 --- a/js/settings_start.js +++ b/js/settings_start.js @@ -20,6 +20,7 @@ const getInitialData = async () => ({ messageTTL: await window.getMessageTTL(), readReceiptSetting: await window.getReadReceiptSetting(), + linkPreviewSetting: await window.getLinkPreviewSetting(), notificationSetting: await window.getNotificationSetting(), audioNotification: await window.getAudioNotification(), diff --git a/js/views/settings_view.js b/js/views/settings_view.js index 629660fb8..87e93fbb6 100644 --- a/js/views/settings_view.js +++ b/js/views/settings_view.js @@ -155,6 +155,12 @@ value: window.initialData.hideMenuBar, setFn: window.setHideMenuBar, }); + new CheckboxView({ + el: this.$('.link-preview-setting'), + name: 'link-preview-setting', + value: window.initialData.linkPreviewSetting, + setFn: window.setLinkPreviewSetting, + }); new MediaPermissionsSettingView({ el: this.$('.media-permissions'), value: window.initialData.mediaPermissions, diff --git a/main.js b/main.js index 16aaced00..7baf6c805 100644 --- a/main.js +++ b/main.js @@ -1091,6 +1091,9 @@ installSettingsSetter('notification-setting'); installSettingsGetter('audio-notification'); installSettingsSetter('audio-notification'); +installSettingsGetter('link-preview-setting'); +installSettingsSetter('link-preview-setting'); + installSettingsGetter('spell-check'); installSettingsSetter('spell-check'); diff --git a/preload.js b/preload.js index 14dd611bc..65a788d89 100644 --- a/preload.js +++ b/preload.js @@ -171,6 +171,9 @@ installSetter('notification-setting', 'setNotificationSetting'); installGetter('audio-notification', 'getAudioNotification'); installSetter('audio-notification', 'setAudioNotification'); +installGetter('link-preview-setting', 'getLinkPreviewSetting'); +installSetter('link-preview-setting', 'setLinkPreviewSetting'); + installGetter('spell-check', 'getSpellCheck'); installSetter('spell-check', 'setSpellCheck'); diff --git a/settings.html b/settings.html index e7e12e9ab..f7aef778d 100644 --- a/settings.html +++ b/settings.html @@ -98,6 +98,10 @@ +

{{ permissions }}

diff --git a/settings_preload.js b/settings_preload.js index 69704d53e..8fdb3c1ee 100644 --- a/settings_preload.js +++ b/settings_preload.js @@ -52,6 +52,9 @@ window.setNotificationSetting = makeSetter('notification-setting'); window.getAudioNotification = makeGetter('audio-notification'); window.setAudioNotification = makeSetter('audio-notification'); +window.getLinkPreviewSetting = makeGetter('link-preview-setting'); +window.setLinkPreviewSetting = makeSetter('link-preview-setting'); + window.getMediaPermissions = makeGetter('media-permissions'); window.setMediaPermissions = makeSetter('media-permissions'); From 7564e270f8d0409242951e7880d8056a5b12c133 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 8 Feb 2019 15:08:35 +1100 Subject: [PATCH 5/8] Lint and code cleanup --- js/background.js | 3 +-- js/models/messages.js | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/js/background.js b/js/background.js index 46c52f36a..64fabbd0b 100644 --- a/js/background.js +++ b/js/background.js @@ -256,8 +256,7 @@ storage.put('read-receipt-setting', value), getLinkPreviewSetting: () => storage.get('linkPreviews', false), - setLinkPreviewSetting: value => - storage.put('linkPreviews', value), + setLinkPreviewSetting: value => storage.put('linkPreviews', value), getNotificationSetting: () => storage.get('notification-setting', 'message'), diff --git a/js/models/messages.js b/js/models/messages.js index 34b652021..2c6cf7d45 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -140,13 +140,9 @@ try { const result = await Signal.LinkPreviews.helper.getPreview(firstLink); - if (!result) { - this.updatingPreview = false; - return; - } - if (!result.image && !result.title) { - // A link preview isn't worth showing unless we have either a title or an image + // A link preview isn't worth showing unless we have either a title or an image + if (!result || !(result.image || result.title)) { this.updatingPreview = false; return; } From a1e4b740f24924eae9765983503f0a30d1aa1dd0 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 8 Feb 2019 15:18:53 +1100 Subject: [PATCH 6/8] Fixed up settings view. Fix review issues. --- _locales/en/messages.json | 11 ++++++++--- js/views/settings_view.js | 3 ++- libtextsecure/sendmessage.js | 7 ++++--- settings.html | 13 +++++++++---- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 52f4889e7..43b522dd3 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1102,17 +1102,22 @@ "message": "General", "description": "Header for general options on the settings screen" }, - "sendLinkPreviews": { - "message": "Send Link Previews", + "linkPreviews": { + "message": "Link Previews", "description": "Option to control creation and send of link previews in setting screen" }, "linkPreviewsDescription": { "message": - "Enable local link previews (Restart for changes to take effect)", + "Previews are supported for Imgur, Instagram, Reddit, and YouTube links.", "description": "Additional detail provided for Link Previews option in settings screen" }, + "linkPreviewsSettingDescription": { + "message": + "Enable local link previews (Restart for changes to take effect).", + "description": "Description shown for the Link Preview option " + }, "spellCheckDescription": { "message": "Enable spell check of text entered in message composition box", "description": "Description of the media permission description" diff --git a/js/views/settings_view.js b/js/views/settings_view.js index 87e93fbb6..9cadfc4b3 100644 --- a/js/views/settings_view.js +++ b/js/views/settings_view.js @@ -218,8 +218,9 @@ spellCheckHeader: i18n('spellCheck'), spellCheckDescription: i18n('spellCheckDescription'), blockedHeader: 'Blocked Users', - sendLinkPreviews: i18n('sendLinkPreviews'), + linkPreviews: i18n('linkPreviews'), linkPreviewsDescription: i18n('linkPreviewsDescription'), + linkPreviewsSettingDescription: i18n('linkPreviewsSettingDescription'), }; }, onClose() { diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index b8e2ff991..98b6cb086 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -308,10 +308,11 @@ MessageSender.prototype = { const silent = false; // Remove this when we add support for attachments - message.attachmentPointers = null; - message.preview = null; + message.attachments = []; + message.attachmentPointers = []; + message.preview = []; if (message.quote) { - message.quote.attachments = null; + message.quote.attachments = []; } return Promise.all([ diff --git a/settings.html b/settings.html index f7aef778d..cca9fea8d 100644 --- a/settings.html +++ b/settings.html @@ -98,11 +98,16 @@
- +
+

{{ linkPreviews }}

+
{{ linkPreviewsDescription }}
+
+ +

{{ permissions }}

From a989a1a8186bb8a267f7d79f42523616a1059f63 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 11 Feb 2019 13:08:17 +1100 Subject: [PATCH 7/8] Save previews locally. --- js/link_previews_helper.js | 41 +++++++++------------ js/models/messages.js | 65 +++++++++++++++++++++------------- js/modules/signal.js | 2 ++ js/modules/types/attachment.js | 1 + 4 files changed, 59 insertions(+), 50 deletions(-) diff --git a/js/link_previews_helper.js b/js/link_previews_helper.js index bb73d386a..adaf8378b 100644 --- a/js/link_previews_helper.js +++ b/js/link_previews_helper.js @@ -1,9 +1,10 @@ /* global Signal, textsecure, - dcodeIO, */ +/* eslint-disable no-bitwise */ + // eslint-disable-next-line func-names (function() { 'use strict'; @@ -11,29 +12,6 @@ window.Signal = window.Signal || {}; window.Signal.LinkPreviews = window.Signal.LinkPreviews || {}; - const base64ImageCache = {}; - - function getBase64Image(preview) { - const { url, image } = preview; - if (!url || !image || !image.data) return null; - - // Return the cached value - if (base64ImageCache[url]) return base64ImageCache[url]; - - // Set the cache and return the value - try { - const contentType = image.contentType || 'image/jpeg'; - const base64 = dcodeIO.ByteBuffer.wrap(image.data).toString('base64'); - - const data = `data:${contentType};base64, ${base64}`; - base64ImageCache[url] = data; - - return data; - } catch (e) { - return null; - } - } - async function makeChunkedRequest(url) { const PARALLELISM = 3; const size = await textsecure.messaging.getProxiedSize(url); @@ -83,6 +61,19 @@ }; } + function hashCode(string) { + let hash = 0; + if (string.length === 0) { + return hash; + } + for (let i = 0; i < string.length; i += 1) { + const char = string.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash &= hash; // Convert to 32bit integer + } + return hash; + } + async function getPreview(url) { let html; try { @@ -145,11 +136,11 @@ title, url, image, + hash: hashCode(url), }; } window.Signal.LinkPreviews.helper = { getPreview, - getBase64Image, }; })(); diff --git a/js/models/messages.js b/js/models/messages.js index 2c6cf7d45..bc0e4b9b1 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -17,7 +17,12 @@ window.Whisper = window.Whisper || {}; - const { Message: TypedMessage, Contact, PhoneNumber } = Signal.Types; + const { + Message: TypedMessage, + Contact, + PhoneNumber, + Attachment, + } = Signal.Types; const { deleteAttachmentData, deleteExternalMessageFiles, @@ -26,6 +31,7 @@ loadQuoteData, loadPreviewData, writeNewAttachmentData, + writeAttachment, } = window.Signal.Migrations; window.AccountCache = Object.create(null); @@ -85,7 +91,7 @@ this.on('expired', this.onExpired); this.setToExpire(); - this.updatePreviews(); + this.updatePreview(); }, idForLogging() { return `${this.get('source')}.${this.get('sourceDevice')} ${this.get( @@ -111,7 +117,7 @@ // eslint-disable-next-line no-bitwise return !!(this.get('flags') & flag); }, - async updatePreviews() { + async updatePreview() { // Don't generate link previews if user has turned them off if (!storage.get('linkPreviews', false)) { return; @@ -141,15 +147,36 @@ try { const result = await Signal.LinkPreviews.helper.getPreview(firstLink); + const { image, title, hash } = result; + // A link preview isn't worth showing unless we have either a title or an image - if (!result || !(result.image || result.title)) { + if (!result || !(image || title)) { this.updatingPreview = false; return; } - // We don't want to save the base64 url in the message as - // it will increase the size of it, Rather we fetch the base64 later + // Save the image to disk + const { data } = image; + const extension = Attachment.getFileExtension(image); + if (data && extension) { + try { + const filePath = await writeAttachment({ + data, + path: `previews/${hash}.${extension}`, + }); + + // return the image without the data + result.image = _.omit({ ...image, path: filePath }, 'data'); + } catch (e) { + window.log.warn('Failed to write preview to disk', e); + } + } + + // Save it!! this.set({ preview: [result] }); + await window.Signal.Data.saveMessage(this.attributes, { + Message: Whisper.Message, + }); } catch (e) { window.log.warn(`Failed to load previews for message: ${this.id}`); } finally { @@ -669,25 +696,13 @@ const previews = this.get('preview') || []; return previews.map(preview => { - let image = {}; - - // Try set the image from the attachment otherwise just pass in the object - if (preview.image) { - try { - const attachmentProps = this.getPropsForAttachment(preview.image); - if (attachmentProps.url) { - image = attachmentProps; - } - } catch (e) { - // Only set the image if we have a url to display - const url = Signal.LinkPreviews.helper.getBase64Image(preview); - if (preview.image.url || url) { - image = { - ...preview.image, - url: preview.image.url || url, - }; - } + let image = null; + try { + if (preview.image) { + image = this.getPropsForAttachment(preview.image); } + } catch (e) { + window.log.info('Failed to show preview'); } return { @@ -1383,7 +1398,7 @@ }); // Update the previews if we need to - message.updatePreviews(); + message.updatePreview(); if (type === 'outgoing') { const receipts = Whisper.DeliveryReceipts.forMessage( diff --git a/js/modules/signal.js b/js/modules/signal.js index bde4104ab..f97a421a1 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -169,6 +169,8 @@ function initializeMigrations({ logger, }), writeNewAttachmentData: createWriterForNew(attachmentsPath), + writeAttachment: ({ data, path }) => + createWriterForExisting(attachmentsPath)({ data, path }), }; } diff --git a/js/modules/types/attachment.js b/js/modules/types/attachment.js index 272c0d439..0a6352350 100644 --- a/js/modules/types/attachment.js +++ b/js/modules/types/attachment.js @@ -208,6 +208,7 @@ exports.deleteData = deleteOnDisk => { exports.isVoiceMessage = AttachmentTS.isVoiceMessage; exports.save = AttachmentTS.save; +exports.getFileExtension = AttachmentTS.getFileExtension; const THUMBNAIL_SIZE = 150; const THUMBNAIL_CONTENT_TYPE = 'image/png'; From d30e950bf9688f6ecf2e98aadf9cad033ef84920 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 11 Feb 2019 15:00:30 +1100 Subject: [PATCH 8/8] Changed hashing algorithm. --- js/link_previews_helper.js | 20 ++++++++------------ js/models/messages.js | 3 ++- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/js/link_previews_helper.js b/js/link_previews_helper.js index adaf8378b..87b1899c5 100644 --- a/js/link_previews_helper.js +++ b/js/link_previews_helper.js @@ -1,6 +1,7 @@ /* global Signal, textsecure, + StringView */ /* eslint-disable no-bitwise */ @@ -61,17 +62,10 @@ }; } - function hashCode(string) { - let hash = 0; - if (string.length === 0) { - return hash; - } - for (let i = 0; i < string.length; i += 1) { - const char = string.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash &= hash; // Convert to 32bit integer - } - return hash; + async function sha256(string) { + const arraybuffer = new TextEncoder('utf-8').encode(string); + const digest = await window.crypto.subtle.digest('SHA-256', arraybuffer); + return StringView.arrayBufferToHex(digest); } async function getPreview(url) { @@ -132,11 +126,13 @@ } } + const hash = await sha256(url); + return { title, url, image, - hash: hashCode(url), + hash, }; } diff --git a/js/models/messages.js b/js/models/messages.js index bc0e4b9b1..2847af3bd 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -159,10 +159,11 @@ const { data } = image; const extension = Attachment.getFileExtension(image); if (data && extension) { + const hash32 = hash.substring(0, 32); try { const filePath = await writeAttachment({ data, - path: `previews/${hash}.${extension}`, + path: `previews/${hash32}.${extension}`, }); // return the image without the data