diff --git a/js/background.js b/js/background.js index 8df8adade..fc0c6d215 100644 --- a/js/background.js +++ b/js/background.js @@ -1293,7 +1293,7 @@ unread: 1, }; - if (data.type === 'friend-request') { + if (data.friendRequest) { messageData = { ...messageData, type: 'friend-request', diff --git a/js/models/conversations.js b/js/models/conversations.js index c5ec5925b..66f773108 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -43,10 +43,12 @@ const FriendRequestStatusEnum = Object.freeze({ // New conversation, no messages sent or received none: 0, - // Friend request not complete yet, input blocked - pending: 1, + // Friend request sent, awaiting response + requestSent: 1, + // Friend request received, awaiting user input + requestReceived: 2, // We did it! - friends: 2, + friends: 3, }); const COLORS = [ @@ -264,9 +266,9 @@ // Get the pending friend requests that match the direction // If no direction is supplied then return all pending friend requests return messages.models.filter(m => { - if (m.attributes.friendStatus !== 'pending') + if (m.get('friendStatus') !== 'pending') return false; - return direction === null || m.attributes.direction === direction; + return direction === null || m.get('direction') === direction; }); }, getPropsForListItem() { @@ -430,8 +432,19 @@ return contact.isVerified(); }); }, + isNone() { + return this.get('friendRequestStatus') === FriendRequestStatusEnum.none; + }, isPending() { - return this.get('friendRequestStatus') === FriendRequestStatusEnum.pending; + const status = this.get('friendRequestStatus'); + return status === FriendRequestStatusEnum.requestSent || + status === FriendRequestStatusEnum.requestSent; + }, + hasSentFriendRequest() { + return this.get('friendRequestStatus') === FriendRequestStatusEnum.requestSent; + }, + hasReceivedFriendRequest() { + return this.get('friendRequestStatus') === FriendRequestStatusEnum.requestReceived; }, isFriend() { return this.get('friendRequestStatus') === FriendRequestStatusEnum.friends; @@ -442,7 +455,8 @@ this.trigger('disable:input', false); this.trigger('change:placeholder', 'friend-request'); return; - case FriendRequestStatusEnum.pending: + case FriendRequestStatusEnum.requestReceived: + case FriendRequestStatusEnum.requestSent: this.trigger('disable:input', true); this.trigger('change:placeholder', 'disabled'); return; @@ -455,11 +469,13 @@ } }, async setFriendRequestStatus(newStatus) { - this.set({ friendRequestStatus: newStatus }); - await window.Signal.Data.updateConversation(this.id, this.attributes, { - Conversation: Whisper.Conversation, - }); - this.updateTextInputState(); + if (this.get('friendRequestStatus') !== newStatus) { + this.set({ friendRequestStatus: newStatus }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + this.updateTextInputState(); + } }, async respondToAllPendingFriendRequests(options) { const { response, direction = null } = options; @@ -489,7 +505,7 @@ }, // We have accepted an incoming friend request async onAcceptFriendRequest() { - if (this.isPending()) { + if (this.hasReceivedFriendRequest()) { this.setFriendRequestStatus(FriendRequestStatusEnum.friends); await this.respondToAllPendingFriendRequests({ response: 'accepted', @@ -500,13 +516,11 @@ }, // Our outgoing friend request has been accepted async onFriendRequestAccepted() { - if (this.isPending()) { + if (this.hasSentFriendRequest()) { this.setFriendRequestStatus(FriendRequestStatusEnum.friends); await this.respondToAllPendingFriendRequests({ response: 'accepted', - direction: 'outgoing', }); - this.notifyFriendRequest(this.id, 'accepted') } }, async onFriendRequestTimeout() { @@ -532,10 +546,24 @@ await this.setFriendRequestStatus(FriendRequestStatusEnum.none); }, async onFriendRequestReceived() { - if (this.get('friendRequestStatus') === FriendRequestStatusEnum.none) { - this.setFriendRequestStatus(FriendRequestStatusEnum.pending); - this.notifyFriendRequest(this.id, 'requested'); + if (this.isNone()) { + this.setFriendRequestStatus(FriendRequestStatusEnum.requestReceived); + } else if (this.hasSentFriendRequest()) { + await Promise.all([ + this.setFriendRequestStatus(FriendRequestStatusEnum.friends), + // Accept all outgoing FR + this.respondToAllPendingFriendRequests({ + direction: 'outgoing', + response: 'accepted', + }), + ]); } + // Delete stale incoming friend requests + const incoming = await this.getPendingFriendRequests('incoming'); + await Promise.all( + incoming.map(request => this._removeMessage(request.id)) + ); + this.trigger('change'); }, async onFriendRequestSent() { // Check if we need to set the friend request expiry @@ -548,7 +576,7 @@ this.set({ unlockTimestamp: Date.now() + ms }); this.setFriendRequestExpiryTimeout(); } - await this.setFriendRequestStatus(FriendRequestStatusEnum.pending); + await this.setFriendRequestStatus(FriendRequestStatusEnum.requestSent); }, setFriendRequestExpiryTimeout() { const unlockTimestamp = this.get('unlockTimestamp'); @@ -1219,36 +1247,8 @@ }, }; }, + // eslint-disable-next-line no-unused-vars async onNewMessage(message) { - if (message.get('type') === 'friend-request' && message.get('direction') === 'incoming') { - // We need to make sure we remove any pending requests that we may have - // This is to ensure that one user cannot spam us with multiple friend requests. - const incoming = await this.getPendingFriendRequests('incoming'); - - // Delete the old messages if it's pending - await Promise.all( - incoming - .filter(i => i.id !== message.id) - .map(request => this._removeMessage(request.id)) - ); - - // If we have an outgoing friend request then - // we auto accept the incoming friend request - const outgoing = await this.getPendingFriendRequests('outgoing'); - if (outgoing.length > 0) { - const current = this.messageCollection.find(i => i.id === message.id); - if (current) { - await current.acceptFriendRequest(); - } else { - window.log.debug('onNewMessage: Failed to find incoming friend request'); - } - } - - // Trigger an update if we removed or updated messages - if (outgoing.length > 0 || incoming.length > 0) - this.trigger('change'); - } - return this.updateLastMessage(); }, async updateLastMessage() { @@ -1903,10 +1903,11 @@ }, notify(message) { - if (!message.isIncoming()) { - if (message.isFriendRequest()) - return this.notifyFriendRequest(message.get('source'), 'requested'); - return Promise.resolve(); + if (message.isOutgoing()) return Promise.resolve(); + if (message.isFriendRequest()) { + if (this.hasSentFriendRequest()) + return this.notifyFriendRequest(message.get('source'), 'accepted') + return this.notifyFriendRequest(message.get('source'), 'requested'); } const conversationId = this.id; diff --git a/js/models/messages.js b/js/models/messages.js index 96e270e6c..ec1a5e7ef 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1375,6 +1375,20 @@ } } + let autoAccept = false; + if (message.get('type') === 'friend-request') { + if (conversation.hasSentFriendRequest()) { + // Automatically accept incoming friend requests if we have send one already + autoAccept = true; + message.set({ friendStatus: 'accepted' }); + await conversation.onFriendRequestAccepted(); + window.libloki.sendFriendRequestAccepted(message.get('source')); + } else if (conversation.isNone()) { + await conversation.onFriendRequestReceived(); + } + } else { + await conversation.onFriendRequestAccepted(); + } const id = await window.Signal.Data.saveMessage(message.attributes, { Message: Whisper.Message, }); @@ -1422,7 +1436,11 @@ } if (message.get('unread')) { - await conversation.notify(message); + // Need to do this here because the conversation has already changed states + if (autoAccept) + await conversation.notifyFriendRequest(source, 'accepted'); + else + await conversation.notify(message); } confirm(); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 04a04e851..ea36300e6 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -904,10 +904,7 @@ MessageReceiver.prototype.extend({ }) ); }, - async handleFriendRequestMessage(envelope, msg) { - return this.handleDataMessage(envelope, msg, 'friend-request'); - }, - handleDataMessage(envelope, msg, type = 'data') { + handleDataMessage(envelope, msg) { window.log.info('data message from', this.getEnvelopeId(envelope)); let p = Promise.resolve(); // eslint-disable-next-line no-bitwise @@ -924,6 +921,8 @@ MessageReceiver.prototype.extend({ message.group && message.group.type === textsecure.protobuf.GroupContext.Type.QUIT ); + const friendRequest = + envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST; // Check if we need to update any profile names if (!isMe && conversation) { @@ -936,7 +935,7 @@ MessageReceiver.prototype.extend({ conversation.setProfile(profile); } - if (type === 'friend-request' && isMe) { + if (friendRequest && isMe) { window.log.info( 'refusing to add a friend request to ourselves' ); @@ -955,7 +954,7 @@ MessageReceiver.prototype.extend({ const ev = new Event('message'); ev.confirm = this.removeFromCache.bind(this, envelope); ev.data = { - type, + friendRequest, source: envelope.source, sourceDevice: envelope.sourceDevice, timestamp: envelope.timestamp.toNumber(), @@ -992,27 +991,11 @@ MessageReceiver.prototype.extend({ async innerHandleContentMessage(envelope, plaintext) { const content = textsecure.protobuf.Content.decode(plaintext); - let conversation; - try { - conversation = await window.ConversationController.getOrCreateAndWait(envelope.source, 'private'); - } catch (e) { - window.log.info('Error getting conversation: ', envelope.source); - } - if (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) { - conversation.onFriendRequestReceived(); - } else { - conversation.onFriendRequestAccepted(); - } - - if (content.preKeyBundleMessage) { - const preKeyBundleMessage = - this.decodePreKeyBundleMessage(content.preKeyBundleMessage); + if (content.preKeyBundleMessage) await this.savePreKeyBundleMessage( envelope.source, - preKeyBundleMessage + content.preKeyBundleMessage ); - return this.handleDataMessage(envelope, content.dataMessage, 'friend-request'); - } if (content.syncMessage) return this.handleSyncMessage(envelope, content.syncMessage); if (content.dataMessage) @@ -1024,6 +1007,12 @@ MessageReceiver.prototype.extend({ if (content.receiptMessage) return this.handleReceiptMessage(envelope, content.receiptMessage); + // Trigger conversation friend request event for empty message + const conversation = window.ConversationController.get(envelope.source); + if (conversation) { + conversation.onFriendRequestAccepted(); + conversation.notifyFriendRequest(envelope.source, 'accepted'); + } this.removeFromCache(envelope); return null; }, @@ -1227,7 +1216,7 @@ MessageReceiver.prototype.extend({ return this.removeFromCache(envelope); }, - decodePreKeyBundleMessage(preKeyBundleMessage) { + async savePreKeyBundleMessage(pubKey, preKeyBundleMessage) { const [identityKey, preKey, signedKey, signature] = [ preKeyBundleMessage.identityKey, preKeyBundleMessage.preKey, @@ -1235,24 +1224,9 @@ MessageReceiver.prototype.extend({ preKeyBundleMessage.signature, ].map(k => dcodeIO.ByteBuffer.wrap(k).toArrayBuffer()); - return { - ...preKeyBundleMessage, - identityKey, - preKey, - signedKey, - signature, - }; - }, - async savePreKeyBundleMessage(pubKey, preKeyBundleMessage) { - if (!preKeyBundleMessage) return null; - const { preKeyId, signedKeyId, - identityKey, - preKey, - signedKey, - signature, } = preKeyBundleMessage; if (pubKey !== StringView.arrayBufferToHex(identityKey)) { @@ -1261,7 +1235,7 @@ MessageReceiver.prototype.extend({ ); } - return libloki.savePreKeyBundleForNumber({ + await libloki.savePreKeyBundleForNumber({ pubKey, preKeyId, signedKeyId,