diff --git a/js/models/conversations.js b/js/models/conversations.js index ba3c21834..be10cd40f 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -723,15 +723,13 @@ if (!this.contactCollection.length) { return false; } - // console.log('this.contactCollection', this.contactCollection); - // FIXME AUDRIC - return true; - // return this.contactCollection.every(contact => { - // if (contact.isMe()) { - // return true; - // } - // return contact.isVerified(); - // }); + + return this.contactCollection.every(contact => { + if (contact.isMe()) { + return true; + } + return contact.isVerified(); + }); }, async getPrimaryConversation() { if (!this.isSecondaryDevice()) { @@ -1217,11 +1215,6 @@ const expireTimer = this.get('expireTimer'); const recipients = this.getRecipients(); - // let profileKey; - // if (this.get('profileSharing')) { - // profileKey = storage.get('profileKey'); - // } - this.queueJob(async () => { const now = Date.now(); @@ -1306,14 +1299,15 @@ try { const uploads = await message.uploadData(); - // FIXME audric add back profileKey const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ body: uploads.body, + identifier: id, timestamp: Date.now(), attachments: uploads.attachments, expireTimer, preview: uploads.preview, quote: uploads.quote, + lokiProfile: this.getOurProfile(), }); if (this.isMe()) { @@ -1330,6 +1324,9 @@ body, timestamp: Date.now(), group: openGroup, + attachments: uploads.attachments, + preview: uploads.preview, + quote: uploads.quote, }; const openGroupMessage = new libsession.Messages.Outgoing.OpenGroupMessage( openGroupParams @@ -1351,6 +1348,8 @@ const groupInvitMessage = new libsession.Messages.Outgoing.GroupInvitationMessage( { + identifier: id, + serverName: groupInvitation.name, channelId: groupInvitation.channelId, serverAddress: groupInvitation.address, @@ -1641,6 +1640,7 @@ } const expireUpdate = { + identifier: id, timestamp: message.get('sent_at'), expireTimer, profileKey, @@ -1823,6 +1823,7 @@ const createParams = { timestamp: Date.now(), groupId: id, + identifier: messageId, groupSecretKey: secretKey, members: members.map(pkHex => StringView.hexToArrayBuffer(pkHex)), groupName: name, @@ -1834,10 +1835,10 @@ const mediumGroupCreateMessage = new libsession.Messages.Outgoing.MediumGroupCreateMessage( createParams ); - message.trigger('pending'); - members.forEach(member => { + members.forEach(async member => { const memberPubKey = new libsession.Types.PubKey(member); + await ConversationController.getOrCreateAndWait(member, 'private'); libsession .getMessageQueue() .sendUsingMultiDevice(memberPubKey, mediumGroupCreateMessage); @@ -1847,6 +1848,7 @@ } const updateParams = { + // if we do set an identifier here, be sure to not sync the message two times in msg.handleMessageSentSuccess() timestamp: Date.now(), groupId: this.id, name: this.get('name'), @@ -1921,6 +1923,7 @@ const quitGroup = { timestamp: Date.now(), groupId: this.id, + // if we do set an identifier here, be sure to not sync it a second time in handleMessageSentSuccess() }; const quitGroupMessage = new libsession.Messages.Outgoing.ClosedGroupLeaveMessage( quitGroup @@ -2589,6 +2592,30 @@ return this.getNumber(); }, + /** + * Returns + * displayName: string; + * avatarPointer: string; + * profileKey: Uint8Array; + */ + getOurProfile() { + try { + // Secondary devices have their profile stored + // in their primary device's conversation + const ourNumber = window.storage.get('primaryDevicePubKey'); + const ourConversation = window.ConversationController.get(ourNumber); + let profileKey = null; + if (this.get('profileSharing')) { + profileKey = storage.get('profileKey'); + } + const avatarPointer = ourConversation.get('avatarPointer'); + const { displayName } = ourConversation.getLokiProfile(); + return { displayName, avatarPointer, profileKey }; + } catch (e) { + window.log.error(`Failed to get our profile: ${e}`); + return null; + } + }, getNumber() { if (!this.isPrivate()) { diff --git a/js/models/messages.js b/js/models/messages.js index 7d94c7089..e1ff2bfde 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -5,7 +5,6 @@ filesize, ConversationController, MessageController, - getAccountManager, i18n, Signal, textsecure, @@ -1042,19 +1041,42 @@ } this.set({ errors: null }); - + await window.Signal.Data.saveMessage(this.attributes, { + Message: Whisper.Message, + }); 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; + if (conversation.isPublic()) { + const openGroup = { + server: conversation.get('server'), + channel: conversation.get('channelId'), + conversationId: conversation.id, + }; + const { body, attachments, preview, quote } = await this.uploadData(); + + const openGroupParams = { + identifier: this.id, + body, + timestamp: Date.now(), + group: openGroup, + attachments, + preview, + quote, + }; + const openGroupMessage = new libsession.Messages.Outgoing.OpenGroupMessage( + openGroupParams + ); + return libsession.getMessageQueue().sendToGroup(openGroupMessage); + } let recipients = _.intersection(intendedRecipients, currentRecipients); - recipients = _.without(recipients, successfulRecipients); + recipients = recipients.filter( + key => !successfulRecipients.includes(key) + ); if (!recipients.length) { window.log.warn('retrySend: Nobody to send to!'); @@ -1067,36 +1089,35 @@ const { body, attachments, preview, quote } = await this.uploadData(); const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ + identifier: this.id, body, timestamp: this.get('sent_at'), expireTimer: this.get('expireTimer'), attachments, preview, quote, + lokiProfile: this.getOurProfile(), }); // 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 return this.sendSyncMessageOnly(chatMessage); } if (conversation.isPrivate()) { const [number] = recipients; const recipientPubKey = new libsession.Types.PubKey(number); - this.trigger('pending'); return libsession .getMessageQueue() .sendUsingMultiDevice(recipientPubKey, chatMessage); } - this.trigger('pending'); - // TODO should we handle open groups message here too? and mediumgroups + // TODO should we handle medium groups message here too? // Not sure there is the concept of retrySend for those const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage( { + identifier: this.id, chatMessage, groupId: this.get('conversationId'), } @@ -1142,6 +1163,7 @@ const { body, attachments, preview, quote } = await this.uploadData(); const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ + identifier: this.id, body, timestamp: this.get('sent_at'), expireTimer: this.get('expireTimer'), @@ -1150,8 +1172,6 @@ quote, }); - this.trigger('pending'); - // Special-case the self-send case - we send only a sync message if (number === this.OUR_NUMBER) { return this.sendSyncMessageOnly(chatMessage); @@ -1196,6 +1216,74 @@ return errors[0][0]; }, + async handleMessageSentSuccess(sentMessage) { + const sentTo = this.get('sent_to') || []; + + const isOurDevice = await window.libsession.Protocols.MultiDeviceProtocol.isOurDevice( + sentMessage.device + ); + + // Handle the sync logic here + if (!isOurDevice && !this.get('synced') && !this.get('sentSync')) { + const contentDecoded = textsecure.protobuf.Content.decode( + sentMessage.plainTextBuffer + ); + const { dataMessage } = contentDecoded; + if (dataMessage) { + this.sendSyncMessage(dataMessage); + } + } else if (isOurDevice && this.get('sentSync')) { + this.set({ synced: true }); + } + const primaryPubKey = await libsession.Protocols.MultiDeviceProtocol.getPrimaryDevice( + sentMessage.device + ); + this.set({ + sent_to: _.union(sentTo, [primaryPubKey.key]), + sent: true, + expirationStartTimestamp: Date.now(), + // unidentifiedDeliveries: result.unidentifiedDeliveries, + }); + + await window.Signal.Data.saveMessage(this.attributes, { + Message: Whisper.Message, + }); + this.getConversation().updateLastMessage(); + + this.trigger('sent', this); + }, + + async handleMessageSentFailure(sentMessage, error) { + if (error instanceof Error) { + this.saveErrors(error); + if (error.name === 'SignedPreKeyRotationError') { + await window.getAccountManager().rotateSignedPreKey(); + } else if (error.name === 'OutgoingIdentityKeyError') { + const c = ConversationController.get(sentMessage.device); + await c.getProfiles(); + } + } + const isOurDevice = await window.libsession.Protocols.MultiDeviceProtocol.isOurDevice( + sentMessage.device + ); + const expirationStartTimestamp = Date.now(); + if (isOurDevice && !this.get('sync')) { + this.set({ sentSync: false }); + } + this.set({ + sent: true, + expirationStartTimestamp, + // unidentifiedDeliveries: result.unidentifiedDeliveries, + }); + await window.Signal.Data.saveMessage(this.attributes, { + Message: Whisper.Message, + }); + this.trigger('change', this); + + this.getConversation().updateLastMessage(); + this.trigger('done'); + }, + getConversation() { // This needs to be an unsafe call, because this method is called during // initial module setup. We may be in the middle of the initial fetch to @@ -1341,8 +1429,7 @@ }, async sendSyncMessage(dataMessage) { - // TODO: Return here if we've already sent a sync message - if (this.get('synced')) { + if (this.get('synced') || this.get('sentSync')) { return; } @@ -1357,93 +1444,11 @@ }); await libsession.getMessageQueue().sendSyncMessage(syncMessage); - }, - send(promise) { - this.trigger('pending'); - return promise - .then(async result => { - this.trigger('done'); - - // This is used by sendSyncMessage, then set to null - if (!this.get('synced') && result.dataMessage) { - this.set({ dataMessage: result.dataMessage }); - } - - const sentTo = this.get('sent_to') || []; - this.set({ - sent_to: _.union(sentTo, result.successfulNumbers), - sent: true, - expirationStartTimestamp: Date.now(), - unidentifiedDeliveries: result.unidentifiedDeliveries, - }); - - await window.Signal.Data.saveMessage(this.attributes, { - Message: Whisper.Message, - }); - - this.trigger('sent', this); - }) - .catch(result => { - this.trigger('done'); - - if (result.dataMessage) { - this.set({ dataMessage: result.dataMessage }); - } - - let promises = []; - - if (result instanceof Error) { - this.saveErrors(result); - if (result.name === 'SignedPreKeyRotationError') { - promises.push(getAccountManager().rotateSignedPreKey()); - } else if (result.name === 'OutgoingIdentityKeyError') { - const c = ConversationController.get(result.number); - promises.push(c.getProfiles()); - } - } else { - if (result.successfulNumbers.length > 0) { - const sentTo = this.get('sent_to') || []; - - // In groups, we don't treat unregistered users as a user-visible - // error. The message will look successful, but the details - // screen will show that we didn't send to these unregistered users. - const filteredErrors = _.reject( - result.errors, - error => error.name === 'UnregisteredUserError' - ); - - // We don't start the expiration timer if there are real errors - // left after filtering out all of the unregistered user errors. - const expirationStartTimestamp = filteredErrors.length - ? null - : Date.now(); - - this.saveErrors(filteredErrors); - - this.set({ - sent_to: _.union(sentTo, result.successfulNumbers), - sent: true, - expirationStartTimestamp, - unidentifiedDeliveries: result.unidentifiedDeliveries, - }); - } else { - this.saveErrors(result.errors); - } - promises = promises.concat( - _.map(result.errors, error => { - if (error.name === 'OutgoingIdentityKeyError') { - const c = ConversationController.get(error.number); - promises.push(c.getProfiles()); - } - }) - ); - } - - this.trigger('send-error', this.get('errors')); - - return Promise.all(promises); - }); + this.set({ sentSync: true }); + await window.Signal.Data.saveMessage(this.attributes, { + Message: Whisper.Message, + }); }, someRecipientsFailed() { diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index d57b6a824..0e4c89681 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -9,7 +9,7 @@ i18n, Whisper, textsecure, - Signal + Signal, */ // eslint-disable-next-line func-names @@ -243,6 +243,11 @@ // } // } // }); + this.fetchHandleMessageSentData = this.fetchHandleMessageSentData.bind( + this + ); + this.handleMessageSentFailure = this.handleMessageSentFailure.bind(this); + this.handleMessageSentSuccess = this.handleMessageSentSuccess.bind(this); this.listenTo(convoCollection, 'remove', conversation => { const { id } = conversation || {}; @@ -258,12 +263,63 @@ }); this.listenTo(convoCollection, 'reset', removeAllConversations); + window.libsession + .getMessageQueue() + .events.addListener('success', this.handleMessageSentSuccess); + + window.libsession + .getMessageQueue() + .events.addListener('fail', this.handleMessageSentFailure); + Whisper.events.on('messageExpired', messageExpired); Whisper.events.on('userChanged', userChanged); // Finally, add it to the DOM this.$('.left-pane-placeholder').append(this.leftPaneView.el); }, + + async fetchHandleMessageSentData(m) { + // nobody is listening to this freshly fetched message .trigger calls + const tmpMsg = await window.Signal.Data.getMessageById(m.identifier, { + Message: Whisper.Message, + }); + + if (!tmpMsg) { + return null; + } + + // find the corresponding conversation of this message + const conv = window.ConversationController.get( + tmpMsg.get('conversationId') + ); + + // then, find in this conversation the very same message + const msg = conv.messageCollection.models.find( + convMsg => convMsg.id === tmpMsg.id + ); + return { msg }; + }, + + async handleMessageSentSuccess(sentMessage) { + const fetchedData = await this.fetchHandleMessageSentData(sentMessage); + if (!fetchedData) { + return; + } + const { msg } = fetchedData; + + msg.handleMessageSentSuccess(sentMessage); + }, + + async handleMessageSentFailure(sentMessage, error) { + const fetchedData = await this.fetchHandleMessageSentData(sentMessage); + if (!fetchedData) { + return; + } + const { msg } = fetchedData; + + await msg.handleMessageSentFailure(sentMessage, error); + }, + startConnectionListener() { this.interval = setInterval(() => { const status = window.getSocketStatus(); diff --git a/libloki/api.js b/libloki/api.js index 679055724..d187cfb45 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -1,4 +1,4 @@ -/* global window, textsecure, libsession */ +/* global window, textsecure, libsession, ConversationController */ /* eslint-disable no-bitwise */ // eslint-disable-next-line func-names @@ -108,6 +108,7 @@ const ourPubKey = textsecure.storage.user.getNumber(); if (memberStr !== ourPubKey) { const memberPubkey = new libsession.Types.PubKey(memberStr); + await ConversationController.getOrCreateAndWait(memberStr, 'private'); await libsession.Protocols.SessionProtocol.sendSessionRequestIfNeeded( memberPubkey ); diff --git a/libtextsecure/errors.js b/libtextsecure/errors.js index 70ec36d5b..fca487f0a 100644 --- a/libtextsecure/errors.js +++ b/libtextsecure/errors.js @@ -108,25 +108,6 @@ } inherit(ReplayableError, MessageError); - function UnregisteredUserError(number, httpError) { - this.message = httpError.message; - this.name = 'UnregisteredUserError'; - - Error.call(this, this.message); - - // Maintains proper stack trace, where our error was thrown (only available on V8) - // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - if (Error.captureStackTrace) { - Error.captureStackTrace(this); - } - - this.number = number; - this.code = httpError.code; - - appendStack(this, httpError); - } - inherit(Error, UnregisteredUserError); - function EmptySwarmError(number, message) { // eslint-disable-next-line prefer-destructuring this.number = number.split('.')[0]; @@ -276,7 +257,6 @@ } } - window.textsecure.UnregisteredUserError = UnregisteredUserError; window.textsecure.SendMessageNetworkError = SendMessageNetworkError; window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError; window.textsecure.OutgoingIdentityKeyError = OutgoingIdentityKeyError; diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 68e2466b0..3ef1c3799 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -281,14 +281,7 @@ OutgoingMessage.prototype = { }).then(results => results.every(value => value === true)); }) .catch(e => { - if (e.name === 'HTTPError' && e.code === 404) { - if (device !== 1) { - return this.removeDeviceIdsForNumber(number, [device]); - } - throw new textsecure.UnregisteredUserError(number, e); - } else { - throw e; - } + throw e; }) ); }); @@ -312,12 +305,6 @@ OutgoingMessage.prototype = { await lokiMessageAPI.sendMessage(pubKey, data, timestamp, ttl, options); } catch (e) { if (e.name === 'HTTPError' && e.code !== 409 && e.code !== 410) { - // 409 and 410 should bubble and be handled by doSendMessage - // 404 should throw UnregisteredUserError - // all other network errors can be retried later. - if (e.code === 404) { - throw new textsecure.UnregisteredUserError(number, e); - } throw new textsecure.SendMessageNetworkError(number, '', e, timestamp); } else if (e.name === 'TimedOutError') { throw new textsecure.PoWError(number, e); diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 69be6de73..0a5300c5d 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -142,7 +142,6 @@ message DataMessage { PROFILE_KEY_UPDATE = 4; SESSION_RESTORE = 64; UNPAIRING_REQUEST = 128; - SESSION_REQUEST = 256; } message Quote { diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 98b43c557..59f57566f 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -266,6 +266,14 @@ export class Message extends React.PureComponent { const isShowingImage = this.isShowingImage(); const withImageNoCaption = Boolean(!text && isShowingImage); const showError = status === 'error' && direction === 'outgoing'; + const showSentNoErrors = + !textPending && + direction === 'outgoing' && + status !== 'error' && + status !== 'sending'; + + const showSending = + !textPending && direction === 'outgoing' && status === 'sending'; return (
{
) : null} - {!textPending && direction === 'outgoing' && status !== 'error' ? ( + {showSending ? ( +
+ ) : null} + {showSentNoErrors ? (
{ - // window.log.error( - // `Failed to send delivery receipt to ${source} for message ${timestamp}:`, - // error && error.stack ? error.stack : error - // ); +// FIXME audric + // const receiptMessage = new DeliveryReceiptMessage({ + // timestamp: Date.now(), + // timestamps: [timestamp], // }); + // const device = new PubKey(source); + // await getMessageQueue().sendUsingMultiDevice(device, receiptMessage); } // tslint:disable:cyclomatic-complexity max-func-body-length */ @@ -554,7 +544,6 @@ export async function handleMessageEvent(event: any): Promise { const { PROFILE_KEY_UPDATE, - SESSION_REQUEST, SESSION_RESTORE, } = SignalService.DataMessage.Flags; diff --git a/ts/session/messages/outgoing/content/data/ChatMessage.ts b/ts/session/messages/outgoing/content/data/ChatMessage.ts index 47e9f8eaa..0ad70faa8 100644 --- a/ts/session/messages/outgoing/content/data/ChatMessage.ts +++ b/ts/session/messages/outgoing/content/data/ChatMessage.ts @@ -2,6 +2,7 @@ import { DataMessage } from './DataMessage'; import { SignalService } from '../../../../../protobuf'; import { MessageParams } from '../../Message'; import { LokiProfile } from '../../../../../types/Message'; +import ByteBuffer from 'bytebuffer'; export interface AttachmentPointer { id?: number; @@ -62,7 +63,12 @@ export class ChatMessage extends DataMessage { this.body = params.body; this.quote = params.quote; this.expireTimer = params.expireTimer; - this.profileKey = params.lokiProfile && params.lokiProfile.profileKey; + if (params.lokiProfile && params.lokiProfile.profileKey) { + this.profileKey = new Uint8Array( + ByteBuffer.wrap(params.lokiProfile.profileKey).toArrayBuffer() + ); + } + this.displayName = params.lokiProfile && params.lokiProfile.displayName; this.avatarPointer = params.lokiProfile && params.lokiProfile.avatarPointer; this.preview = params.preview; @@ -85,10 +91,6 @@ export class ChatMessage extends DataMessage { dataMessage.expireTimer = this.expireTimer; } - if (this.profileKey) { - dataMessage.profileKey = this.profileKey; - } - if (this.preview) { dataMessage.preview = this.preview; }