Receive support for DOE messages

This commit is contained in:
Ken Powers 2020-04-29 17:24:12 -04:00 committed by Scott Nonnenberg
parent b8a674bbb6
commit ba5e2ff6e5
17 changed files with 291 additions and 42 deletions

View file

@ -2241,7 +2241,7 @@
// Note: We do very little in this function, since everything in handleDataMessage is
// inside a conversation-specific queue(). Any code here might run before an earlier
// message is processed in handleDataMessage().
async function onMessageReceived(event) {
function onMessageReceived(event) {
const { data, confirm } = event;
const messageDescriptor = getDescriptorForReceived(data);
@ -2250,17 +2250,16 @@
// eslint-disable-next-line no-bitwise
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
if (isProfileUpdate) {
await handleMessageReceivedProfileUpdate({
return handleMessageReceivedProfileUpdate({
data,
confirm,
messageDescriptor,
});
return;
}
const message = await initIncomingMessage(data);
const message = initIncomingMessage(data);
const result = await ConversationController.getOrCreateAndWait(
const result = ConversationController.getOrCreate(
messageDescriptor.id,
messageDescriptor.type
);
@ -2286,11 +2285,26 @@
// Note: We do not wait for completion here
Whisper.Reactions.onReaction(reactionModel);
confirm();
return;
return Promise.resolve();
}
if (data.message.delete) {
const { delete: del } = data.message;
const deleteModel = Whisper.Deletes.add({
targetSentTimestamp: del.targetSentTimestamp,
serverTimestamp: data.serverTimestamp,
fromId: data.source || data.sourceUuid,
});
// Note: We do not wait for completion here
Whisper.Deletes.onDelete(deleteModel);
confirm();
return Promise.resolve();
}
// Don't wait for handleDataMessage, as it has its own per-conversation queueing
message.handleDataMessage(data.message, event.confirm);
return Promise.resolve();
}
async function handleMessageSentProfileUpdate({
@ -2341,6 +2355,7 @@
sourceUuid: textsecure.storage.user.getUuid(),
sourceDevice: data.device,
sent_at: data.timestamp,
serverTimestamp: data.serverTimestamp,
sent_to: sentTo,
received_at: now,
conversationId: data.destination,
@ -2357,7 +2372,7 @@
// Note: We do very little in this function, since everything in handleDataMessage is
// inside a conversation-specific queue(). Any code here might run before an earlier
// message is processed in handleDataMessage().
async function onSentMessage(event) {
function onSentMessage(event) {
const { data, confirm } = event;
const messageDescriptor = getDescriptorForSent(data);
@ -2366,20 +2381,20 @@
// eslint-disable-next-line no-bitwise
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
if (isProfileUpdate) {
await handleMessageSentProfileUpdate({
return handleMessageSentProfileUpdate({
data,
confirm,
messageDescriptor,
});
return;
}
const message = await createSentMessage(data);
const message = createSentMessage(data);
const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
if (data.message.reaction) {
const { reaction } = data.message;
const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const reactionModel = Whisper.Reactions.add({
emoji: reaction.emoji,
remove: reaction.remove,
@ -2394,10 +2409,23 @@
Whisper.Reactions.onReaction(reactionModel);
event.confirm();
return;
return Promise.resolve();
}
await ConversationController.getOrCreateAndWait(
if (data.message.delete) {
const { delete: del } = data.message;
const deleteModel = Whisper.Deletes.add({
targetSentTimestamp: del.targetSentTimestamp,
serverTimestamp: del.serverTimestamp,
fromId: ourNumber || ourUuid,
});
// Note: We do not wait for completion here
Whisper.Deletes.onDelete(deleteModel);
confirm();
return Promise.resolve();
}
ConversationController.getOrCreate(
messageDescriptor.id,
messageDescriptor.type
);
@ -2406,9 +2434,11 @@
message.handleDataMessage(data.message, event.confirm, {
data,
});
return Promise.resolve();
}
async function initIncomingMessage(data) {
function initIncomingMessage(data) {
const targetId = data.source || data.sourceUuid;
const conversation = ConversationController.get(targetId);
const conversationId = conversation ? conversation.id : targetId;
@ -2418,6 +2448,7 @@
sourceUuid: data.sourceUuid,
sourceDevice: data.sourceDevice,
sent_at: data.timestamp,
serverTimestamp: data.serverTimestamp,
received_at: Date.now(),
conversationId,
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
@ -2485,7 +2516,7 @@
}
}
async function onError(ev) {
function onError(ev) {
const { error } = ev;
window.log.error('background onError:', Errors.toLogFormat(error));
@ -2494,8 +2525,7 @@
error.name === 'HTTPError' &&
(error.code === 401 || error.code === 403)
) {
await unlinkAndDisconnect();
return;
return unlinkAndDisconnect();
}
if (
@ -2510,7 +2540,7 @@
Whisper.events.trigger('reconnectTimer');
}
return;
return Promise.resolve();
}
if (ev.proto) {
@ -2520,13 +2550,13 @@
}
// Ignore this message. It is likely a duplicate delivery
// because the server lost our ack the first time.
return;
return Promise.resolve();
}
const envelope = ev.proto;
const message = await initIncomingMessage(envelope);
const message = initIncomingMessage(envelope);
const conversationId = message.get('conversationId');
const conversation = await ConversationController.getOrCreateAndWait(
const conversation = ConversationController.getOrCreate(
conversationId,
'private'
);

108
js/deletes.js Normal file
View file

@ -0,0 +1,108 @@
/* global
Backbone,
Whisper,
MessageController,
ConversationController
*/
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() {
'use strict';
const ONE_DAY = 24 * 60 * 60 * 1000;
window.Whisper = window.Whisper || {};
Whisper.Deletes = new (Backbone.Collection.extend({
forMessage(message) {
const matchingDeletes = this.filter({
targetSentTimestamp: message.get('sent_at'),
fromId: message.getContact().get('id'),
});
if (matchingDeletes.length > 0) {
window.log.info('Found early DOE for message');
this.remove(matchingDeletes);
return matchingDeletes;
}
return [];
},
async onDelete(del) {
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
del.get('targetSentTimestamp'),
{
MessageCollection: Whisper.MessageCollection,
}
);
// The contact the delete message came from
const fromContact = ConversationController.get(del.get('fromId'));
if (!fromContact) {
window.log.info(
'No contact for DOE',
del.get('fromId'),
del.get('targetSentTimestamp')
);
return;
}
const targetMessage = messages.find(m => {
const messageContact = m.getContact();
if (!messageContact) {
return false;
}
// Find messages which are from the same contact who sent the DOE
return messageContact.get('id') === fromContact.get('id');
});
if (!targetMessage) {
window.log.info(
'No message for DOE',
del.get('fromId'),
del.get('targetSentTimestamp')
);
return;
}
// Make sure the server timestamps for the DOE and the matching message
// are less than one day apart
const delta = Math.abs(
del.get('serverTimestamp') - targetMessage.get('serverTimestamp')
);
if (delta > ONE_DAY) {
window.log.info('Received late DOE. Dropping.', {
fromId: del.get('fromId'),
targetSentTimestamp: del.get('targetSentTimestamp'),
messageServerTimestamp: message.get('serverTimestamp'),
deleteServerTimestamp: del.get('serverTimestamp'),
});
this.remove(del);
return;
}
const message = MessageController.register(
targetMessage.id,
targetMessage
);
await message.handleDeleteForEveryone(del);
this.remove(del);
} catch (error) {
window.log.error(
'Deletes.onDelete error:',
error && error.stack ? error.stack : error
);
}
},
}))();
})();

View file

@ -414,6 +414,7 @@
lastMessage: {
status: this.get('lastMessageStatus'),
text: this.get('lastMessage'),
deletedForEveryone: this.get('lastMessageDeletedForEveryone'),
},
};

View file

@ -600,6 +600,8 @@
isTapToViewExpired: isTapToView && this.get('isErased'),
isTapToViewError:
isTapToView && this.isIncoming() && this.get('isTapToViewInvalid'),
deletedForEveryone: this.get('deletedForEveryone') || false,
};
},
@ -1105,7 +1107,7 @@
isErased() {
return Boolean(this.get('isErased'));
},
async eraseContents() {
async eraseContents(additionalProperties = {}) {
if (this.get('isErased')) {
return;
}
@ -1129,6 +1131,7 @@
contact: [],
sticker: null,
preview: [],
...additionalProperties,
});
this.trigger('content-changed');
@ -1442,12 +1445,17 @@
const isOutgoing = this.get('type') === 'outgoing';
const numDelivered = this.get('delivered');
// Case 1: We can reply if this is outgoing and delievered to at least one recipient
// Case 1: We cannot reply if this message is deleted for everyone
if (this.get('deletedForEveryone')) {
return false;
}
// Case 2: We can reply if this is outgoing and delievered to at least one recipient
if (isOutgoing && numDelivered > 0) {
return true;
}
// Case 2: We can reply if there are no errors
// Case 3: We can reply if there are no errors
if (!errors || (errors && errors.length === 0)) {
return true;
}
@ -2457,6 +2465,11 @@
message.handleReaction(reaction);
});
// Does this message have any pending, previously-received associated
// delete for everyone messages?
const deletes = Whisper.Deletes.forMessage(message);
deletes.forEach(del => Whisper.Deletes.onDelete(del));
Whisper.events.trigger('incrementProgress');
confirm();
} catch (error) {
@ -2473,6 +2486,10 @@
},
async handleReaction(reaction) {
if (this.get('deletedForEveryone')) {
return;
}
const reactions = this.get('reactions') || [];
const messageId = this.idForLogging();
const count = reactions.length;
@ -2512,6 +2529,27 @@
Message: Whisper.Message,
});
},
async handleDeleteForEveryone(del) {
window.log.info('Handling DOE.', {
fromId: del.get('fromId'),
targetSentTimestamp: del.get('targetSentTimestamp'),
messageServerTimestamp: this.get('serverTimestamp'),
deleteServerTimestamp: del.get('serverTimestamp'),
});
// Remove any notifications for this message
const notificationForMessage = Whisper.Notifications.findWhere({
messageId: this.get('id'),
});
Whisper.Notifications.remove(notificationForMessage);
// Erase the contents of this message
await this.eraseContents({ deletedForEveryone: true, reactions: [] });
// Update the conversation's last message in case this was the last message
this.getConversation().updateLastMessage();
},
});
// Receive will be enabled before we enable send