From c589f4a4af399d7dc80ebe9848323f702f7bae5f Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 29 Jun 2020 14:17:05 +1000 Subject: [PATCH] Finish hooking up attachments --- js/models/conversations.js | 203 +++++++++++++------------ js/models/messages.js | 259 ++++++++++++++++++-------------- ts/session/utils/Attachments.ts | 12 +- 3 files changed, 256 insertions(+), 218 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 6e3dbd861..778117b57 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1191,6 +1191,18 @@ }; }, + getOpenGroup() { + if (!this.isPublic()) { + return undefined; + } + + return new libsession.Types.OpenGroup({ + server: this.get('server'), + channel: this.get('channelId'), + conversationId: this.id, + }); + }, + async sendMessage( body, attachments, @@ -1291,122 +1303,113 @@ return null; } - const attachmentsWithData = await Promise.all( - messageWithSchema.attachments.map(loadAttachmentData) - ); - - const { - body: messageBody, - attachments: finalAttachments, - } = Whisper.Message.getLongMessageAttachment({ - body, - attachments: attachmentsWithData, - now, - }); - - // FIXME audric add back profileKey - const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ - body: messageBody, - timestamp: Date.now(), - attachments: finalAttachments, - expireTimer, - preview, - quote, - }); - // Start handle ChatMessages (attachments/quote/preview/body) - // FIXME AUDRIC handle attachments, quote, preview, profileKey + try { + const uploads = await message.uploadData(); - if (this.isMe()) { - await message.markMessageSyncOnly(); - // sending is done in the 'private' case below - } - const options = {}; - - options.messageType = message.get('type'); - options.isPublic = this.isPublic(); - if (this.isPublic()) { - // FIXME audric add back attachments, quote, preview - const openGroup = { - server: this.get('server'), - channel: this.get('channelId'), - conversationId: this.id, - }; - const openGroupParams = { - body, + // FIXME audric add back profileKey + const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ + body: uploads.body, timestamp: Date.now(), - group: openGroup, - }; - const openGroupMessage = new libsession.Messages.Outgoing.OpenGroupMessage( - openGroupParams - ); - await libsession.getMessageQueue().sendToGroup(openGroupMessage); - - return null; - } + attachments: uploads.attachments, + expireTimer, + preview: uploads.preview, + quote: uploads.quote, + }); - options.sessionRestoration = sessionRestoration; - const destinationPubkey = new libsession.Types.PubKey(destination); - // Handle Group Invitation Message - if (groupInvitation) { - if (conversationType !== Message.PRIVATE) { - window.console.warning('Cannot send groupInvite to group chat'); + if (this.isMe()) { + await message.markMessageSyncOnly(); + // sending is done in the 'private' case below + } + const options = {}; + + options.messageType = message.get('type'); + options.isPublic = this.isPublic(); + if (this.isPublic()) { + const openGroup = this.getOpenGroup(); + + const openGroupParams = { + body, + timestamp: Date.now(), + group: openGroup, + }; + const openGroupMessage = new libsession.Messages.Outgoing.OpenGroupMessage( + openGroupParams + ); + await libsession.getMessageQueue().sendToGroup(openGroupMessage); return null; } - const groupInvitMessage = new libsession.Messages.Outgoing.GroupInvitationMessage( - { - serverName: groupInvitation.name, - channelId: groupInvitation.channelId, - serverAddress: groupInvitation.address, - } - ); - - return libsession - .getMessageQueue() - .sendUsingMultiDevice(destinationPubkey, groupInvitMessage); - } + options.sessionRestoration = sessionRestoration; + const destinationPubkey = new libsession.Types.PubKey(destination); + // Handle Group Invitation Message + if (groupInvitation) { + if (conversationType !== Message.PRIVATE) { + window.console.warning('Cannot send groupInvite to group chat'); - if (conversationType === Message.PRIVATE) { - return libsession - .getMessageQueue() - .sendUsingMultiDevice(destinationPubkey, chatMessage); - } + return null; + } - if (conversationType === Message.GROUP) { - if (this.isMediumGroup()) { - const mediumGroupChatMessage = new libsession.Messages.Outgoing.MediumGroupChatMessage( + const groupInvitMessage = new libsession.Messages.Outgoing.GroupInvitationMessage( { - chatMessage, - groupId: destination, + serverName: groupInvitation.name, + channelId: groupInvitation.channelId, + serverAddress: groupInvitation.address, } ); - const members = this.get('members'); - await Promise.all( - members.map(async m => { - const memberPubKey = new libsession.Types.PubKey(m); - await libsession - .getMessageQueue() - .sendUsingMultiDevice(memberPubKey, mediumGroupChatMessage); - }) - ); + + return libsession + .getMessageQueue() + .sendUsingMultiDevice(destinationPubkey, groupInvitMessage); + } + + if (conversationType === Message.PRIVATE) { + return libsession + .getMessageQueue() + .sendUsingMultiDevice(destinationPubkey, chatMessage); + } + + if (conversationType === Message.GROUP) { + if (this.isMediumGroup()) { + const mediumGroupChatMessage = new libsession.Messages.Outgoing.MediumGroupChatMessage( + { + chatMessage, + groupId: destination, + } + ); + const members = this.get('members'); + await Promise.all( + members.map(async m => { + const memberPubKey = new libsession.Types.PubKey(m); + await libsession + .getMessageQueue() + .sendUsingMultiDevice(memberPubKey, mediumGroupChatMessage); + }) + ); + } else { + const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage( + { + chatMessage, + groupId: destination, + } + ); + await libsession + .getMessageQueue() + .sendToGroup(closedGroupChatMessage); + } } else { - const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage( - { - chatMessage, - groupId: destination, - } + throw new TypeError( + `Invalid conversation type: '${conversationType}'` ); - await libsession - .getMessageQueue() - .sendToGroup(closedGroupChatMessage); } - } else { - throw new TypeError( - `Invalid conversation type: '${conversationType}'` - ); + + return true; + } catch (e) { + log.warn('Failed to send message: ', e); + await message.saveErrors(e); + + return null; } - return true; }); }, wrapSend(promise) { diff --git a/js/models/messages.js b/js/models/messages.js index 36787bd7f..8215a4a2b 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -28,8 +28,8 @@ deleteExternalMessageFiles, getAbsoluteAttachmentPath, loadAttachmentData, - // loadQuoteData, - // loadPreviewData, + loadQuoteData, + loadPreviewData, } = window.Signal.Migrations; const { bytesFromString } = window.Signal.Crypto; @@ -991,6 +991,49 @@ }); }, + /** + * Uploads attachments, previews and quotes. + * If body is too long then it is also converted to an attachment. + * + * @returns The uploaded data which includes: body, attachments, preview and quote. + */ + async uploadData() { + // TODO: In the future it might be best if we cache the upload results if possible. + // This way we don't upload duplicated data. + + const attachmentsWithData = await Promise.all( + (this.get('attachments') || []).map(loadAttachmentData) + ); + const { + body, + attachments: finalAttachments, + } = Whisper.Message.getLongMessageAttachment({ + body: this.get('body'), + attachments: attachmentsWithData, + now: this.get('sent_at'), + }); + + const quoteWithData = await loadQuoteData(this.get('quote')); + const previewWithData = await loadPreviewData(this.get('preview')); + + const conversation = this.getConversation(); + const openGroup = conversation && conversation.getOpenGroup(); + + const { AttachmentUtils } = libsession.Utils; + const [attachments, preview, quote] = await Promise.all([ + AttachmentUtils.uploadAttachments(finalAttachments, openGroup), + AttachmentUtils.uploadLinkPreviews(previewWithData, openGroup), + AttachmentUtils.uploadQuoteThumbnails(quoteWithData, openGroup), + ]); + + return { + body, + attachments, + preview, + quote, + }; + }, + // One caller today: event handler for the 'Retry Send' entry in triple-dot menu async retrySend() { if (!textsecure.messaging) { @@ -1000,85 +1043,80 @@ this.set({ errors: null }); - const conversation = this.getConversation(); - const intendedRecipients = this.get('recipients') || []; - const successfulRecipients = this.get('sent_to') || []; - const currentRecipients = conversation.getRecipients(); + try { + const conversation = this.getConversation(); + const intendedRecipients = this.get('recipients') || []; + const successfulRecipients = this.get('sent_to') || []; + const currentRecipients = conversation.getRecipients(); - // const profileKey = conversation.get('profileSharing') - // ? storage.get('profileKey') - // : null; + // const profileKey = conversation.get('profileSharing') + // ? storage.get('profileKey') + // : null; - let recipients = _.intersection(intendedRecipients, currentRecipients); - recipients = _.without(recipients, successfulRecipients); + let recipients = _.intersection(intendedRecipients, currentRecipients); + recipients = _.without(recipients, successfulRecipients); - if (!recipients.length) { - window.log.warn('retrySend: Nobody to send to!'); - - return window.Signal.Data.saveMessage(this.attributes, { - Message: Whisper.Message, - }); - } + if (!recipients.length) { + window.log.warn('retrySend: Nobody to send to!'); - const attachmentsWithData = await Promise.all( - (this.get('attachments') || []).map(loadAttachmentData) - ); - const { body } = Whisper.Message.getLongMessageAttachment({ - body: this.get('body'), - attachments: attachmentsWithData, - now: this.get('sent_at'), - }); - // TODO add logic for attachments, quote and preview here - // don't blindly reuse the one from loadQuoteData loadPreviewData and getLongMessageAttachment. - // they have similar data structure to the ones we need - // but the main difference is that they haven't been uploaded - // so no url exists in them - // so passing it to chat message is incorrect - - // const quoteWithData = await loadQuoteData(this.get('quote')); - // const previewWithData = await loadPreviewData(this.get('preview')); - const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ - body, - timestamp: this.get('sent_at'), - expireTimer: this.get('expireTimer'), - }); - // Special-case the self-send case - we send only a sync message - if (recipients.length === 1 && recipients[0] === this.OUR_NUMBER) { - this.trigger('pending'); - // FIXME audric add back profileKey - await this.markMessageSyncOnly(); - // sending is done in the private case below - } + return window.Signal.Data.saveMessage(this.attributes, { + Message: Whisper.Message, + }); + } - if (conversation.isPrivate()) { - const [number] = recipients; - const recipientPubKey = new libsession.Types.PubKey(number); - this.trigger('pending'); + const { body, attachments, preview, quote } = await this.uploadData(); - return libsession - .getMessageQueue() - .sendUsingMultiDevice(recipientPubKey, chatMessage); - } + const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ + body, + timestamp: this.get('sent_at'), + expireTimer: this.get('expireTimer'), + attachments, + preview, + quote, + }); - this.trigger('pending'); - // TODO should we handle open groups message here too? and mediumgroups - // Not sure there is the concept of retrySend for those - const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage( - { - chatMessage, - groupId: this.get('conversationId'), + // Special-case the self-send case - we send only a sync message + if (recipients.length === 1 && recipients[0] === this.OUR_NUMBER) { + this.trigger('pending'); + // FIXME audric add back profileKey + await this.markMessageSyncOnly(); + // sending is done in the private case below } - ); - // Because this is a partial group send, we send the message with the groupId field set, but individually - // to each recipient listed - return Promise.all( - recipients.map(async r => { - const recipientPubKey = new libsession.Types.PubKey(r); + + if (conversation.isPrivate()) { + const [number] = recipients; + const recipientPubKey = new libsession.Types.PubKey(number); + this.trigger('pending'); + return libsession .getMessageQueue() - .sendUsingMultiDevice(recipientPubKey, closedGroupChatMessage); - }) - ); + .sendUsingMultiDevice(recipientPubKey, chatMessage); + } + + this.trigger('pending'); + // TODO should we handle open groups message here too? and mediumgroups + // Not sure there is the concept of retrySend for those + const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage( + { + chatMessage, + groupId: this.get('conversationId'), + } + ); + // Because this is a partial group send, we send the message with the groupId field set, but individually + // to each recipient listed + return Promise.all( + recipients.map(async r => { + const recipientPubKey = new libsession.Types.PubKey(r); + return libsession + .getMessageQueue() + .sendUsingMultiDevice(recipientPubKey, closedGroupChatMessage); + }) + ); + } catch (e) { + window.log.warn('Failed message retry send: ', e); + await this.saveErrors(e); + return null; + } }, isReplayableError(e) { return ( @@ -1102,55 +1140,48 @@ return null; } - const attachmentsWithData = await Promise.all( - (this.get('attachments') || []).map(loadAttachmentData) - ); - const { body } = Whisper.Message.getLongMessageAttachment({ - body: this.get('body'), - attachments: attachmentsWithData, - now: this.get('sent_at'), - }); - // TODO add logic for attachments, quote and preview here - // don't blindly reuse the one from loadQuoteData loadPreviewData and getLongMessageAttachment. - // they have similar data structure to the ones we need - // but the main difference is that they haven't been uploaded - // so no url exists in them - // so passing it to chat message is incorrect - // const quoteWithData = await loadQuoteData(this.get('quote')); - // const previewWithData = await loadPreviewData(this.get('preview')); - const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ - body, - timestamp: this.get('sent_at'), - expireTimer: this.get('expireTimer'), - }); + try { + const { body, attachments, preview, quote } = await this.uploadData(); - // Special-case the self-send case - we send only a sync message - if (number === this.OUR_NUMBER) { - this.trigger('pending'); - await this.markMessageSyncOnly(); - // sending is done in the private case below - } - const conversation = this.getConversation(); - const recipientPubKey = new libsession.Types.PubKey(number); + const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ + body, + timestamp: this.get('sent_at'), + expireTimer: this.get('expireTimer'), + attachments, + preview, + quote, + }); - if (conversation.isPrivate()) { this.trigger('pending'); + + // Special-case the self-send case - we send only a sync message + if (number === this.OUR_NUMBER) { + await this.markMessageSyncOnly(); + // sending is done in the private case below + } + const conversation = this.getConversation(); + const recipientPubKey = new libsession.Types.PubKey(number); + + if (conversation.isPrivate()) { + return libsession + .getMessageQueue() + .sendUsingMultiDevice(recipientPubKey, chatMessage); + } + + const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage( + { + chatMessage, + groupId: this.get('conversationId'), + } + ); + // resend tries to send the message to that specific user only in the context of a closed group return libsession .getMessageQueue() - .sendUsingMultiDevice(recipientPubKey, chatMessage); + .sendUsingMultiDevice(recipientPubKey, closedGroupChatMessage); + } catch (e) { + await this.saveErrors(e); + return null; } - - const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage( - { - chatMessage, - groupId: this.get('conversationId'), - } - ); - // resend tries to send the message to that specific user only in the context of a closed group - this.trigger('pending'); - return libsession - .getMessageQueue() - .sendUsingMultiDevice(recipientPubKey, closedGroupChatMessage); }, removeOutgoingErrors(number) { const errors = _.partition( diff --git a/ts/session/utils/Attachments.ts b/ts/session/utils/Attachments.ts index 37b8aca43..7199d535e 100644 --- a/ts/session/utils/Attachments.ts +++ b/ts/session/utils/Attachments.ts @@ -108,7 +108,7 @@ export class AttachmentUtils { attachments: Array, openGroup?: OpenGroup ): Promise> { - const promises = attachments.map(async attachment => + const promises = (attachments || []).map(async attachment => this.upload({ attachment, openGroup, @@ -122,7 +122,7 @@ export class AttachmentUtils { previews: Array, openGroup?: OpenGroup ): Promise> { - const promises = previews.map(async item => ({ + const promises = (previews || []).map(async item => ({ ...item, image: await this.upload({ attachment: item.image, @@ -133,9 +133,13 @@ export class AttachmentUtils { } public static async uploadQuoteThumbnails( - quote: RawQuote, + quote?: RawQuote, openGroup?: OpenGroup - ): Promise { + ): Promise { + if (!quote) { + return undefined; + } + const promises = (quote.attachments ?? []).map(async attachment => { let thumbnail: AttachmentPointer | undefined; if (attachment.thumbnail) {