Message Requests

This commit is contained in:
Ken Powers 2020-05-27 17:37:06 -04:00 committed by Scott Nonnenberg
parent 4d4b7a26a5
commit 83574eb067
60 changed files with 2566 additions and 216 deletions

View file

@ -8,7 +8,8 @@
libsignal,
storage,
textsecure,
Whisper
Whisper,
Signal
*/
/* eslint-disable more/no-then */
@ -60,6 +61,8 @@
return {
unreadCount: 0,
verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT,
messageCount: 0,
sentMessageCount: 0,
};
},
@ -97,6 +100,8 @@
this.ourNumber = textsecure.storage.user.getNumber();
this.ourUuid = textsecure.storage.user.getUuid();
this.verifiedEnum = textsecure.storage.protocol.VerifiedStatus;
this.messageRequestEnum =
textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
// This may be overridden by ConversationController.getOrCreate, and signify
// our first save to the database. Or first fetch from the database.
@ -164,6 +169,52 @@
);
},
isBlocked() {
const uuid = this.get('uuid');
if (uuid) {
return window.storage.isUuidBlocked(uuid);
}
const e164 = this.get('e164');
if (e164) {
return window.storage.isBlocked(e164);
}
const groupId = this.get('groupId');
if (groupId) {
return window.storage.isGroupBlocked(groupId);
}
return false;
},
unblock() {
const uuid = this.get('uuid');
if (uuid) {
return window.storage.removeBlockedUuid(uuid);
}
const e164 = this.get('e164');
if (e164) {
return window.storage.removeBlockedNumber(e164);
}
const groupId = this.get('groupId');
if (groupId) {
return window.storage.removeBlockedGroup(groupId);
}
return false;
},
enableProfileSharing() {
this.set({ profileSharing: true });
},
disableProfileSharing() {
this.set({ profileSharing: false });
},
hasDraft() {
const draftAttachments = this.get('draftAttachments') || [];
return (
@ -320,6 +371,10 @@
this.messageCollection.remove(id);
existing.trigger('expired');
existing.cleanup();
// An expired message only counts as decrementing the message count, not
// the sent message count
this.decrementMessageCount();
};
// If a fetch is in progress, then we need to wait until that's complete to
@ -390,11 +445,15 @@
const shouldShowDraft =
this.hasDraft() && draftTimestamp && draftTimestamp >= timestamp;
const inboxPosition = this.get('inbox_position');
const messageRequestsEnabled = Signal.RemoteConfig.isEnabled(
'desktop.messageRequests'
);
const result = {
id: this.id,
isArchived: this.get('isArchived'),
isBlocked: this.isBlocked(),
activeAt: this.get('active_at'),
avatarPath: this.getAvatarPath(),
color,
@ -414,11 +473,17 @@
draftText,
phoneNumber: this.getNumber(),
membersCount: this.isPrivate()
? undefined
: (this.get('members') || []).length,
lastMessage: {
status: this.get('lastMessageStatus'),
text: this.get('lastMessage'),
deletedForEveryone: this.get('lastMessageDeletedForEveryone'),
},
acceptedMessageRequest: this.getAccepted(),
messageRequestsEnabled,
};
return result;
@ -449,6 +514,143 @@
}
},
incrementMessageCount() {
this.set({
messageCount: (this.get('messageCount') || 0) + 1,
});
window.Signal.Data.updateConversation(this.attributes);
},
decrementMessageCount() {
this.set({
messageCount: Math.max((this.get('messageCount') || 0) - 1, 0),
});
window.Signal.Data.updateConversation(this.attributes);
},
incrementSentMessageCount() {
this.set({
messageCount: (this.get('messageCount') || 0) + 1,
sentMessageCount: (this.get('sentMessageCount') || 0) + 1,
});
window.Signal.Data.updateConversation(this.attributes);
},
decrementSentMessageCount() {
this.set({
messageCount: Math.max((this.get('messageCount') || 0) - 1, 0),
sentMessageCount: Math.max((this.get('sentMessageCount') || 0) - 1, 0),
});
window.Signal.Data.updateConversation(this.attributes);
},
/**
* This function is called when a message request is accepted in order to
* handle sending read receipts and download any pending attachments.
*/
async handleReadAndDownloadAttachments() {
let messages;
do {
// eslint-disable-next-line no-await-in-loop
messages = await window.Signal.Data.getOlderMessagesByConversation(
this.get('id'),
{
MessageCollection: Whisper.MessageCollection,
limit: 100,
receivedAt: messages
? messages.first().get('received_at')
: undefined,
}
);
if (!messages.length) {
return;
}
const readMessages = messages.filter(
m => !m.hasErrors() && m.isIncoming()
);
const receiptSpecs = readMessages.map(m => ({
sender: m.get('source') || m.get('sourceUuid'),
timestamp: m.get('sent_at'),
hasErrors: m.hasErrors(),
}));
// eslint-disable-next-line no-await-in-loop
await this.sendReadReceiptsFor(receiptSpecs);
// eslint-disable-next-line no-await-in-loop
await Promise.all(readMessages.map(m => m.queueAttachmentDownloads()));
} while (messages.length > 0);
},
async applyMessageRequestResponse(response, { fromSync = false } = {}) {
// Apply message request response locally
this.set({
messageRequestResponseType: response,
});
window.Signal.Data.updateConversation(this.attributes);
if (response === this.messageRequestEnum.ACCEPT) {
this.unblock();
this.enableProfileSharing();
this.sendProfileKeyUpdate();
if (!fromSync) {
// Locally accepted
await this.handleReadAndDownloadAttachments();
}
} else if (response === this.messageRequestEnum.BLOCK) {
// Block locally, other devices should block upon receiving the sync message
window.storage.blockIdentifier(this.get('id'));
this.disableProfileSharing();
} else if (response === this.messageRequestEnum.DELETE) {
// Delete messages locally, other devices should delete upon receiving
// the sync message
this.destroyMessages();
this.disableProfileSharing();
this.updateLastMessage();
if (!fromSync) {
this.trigger('unload', 'deleted from message request');
}
} else if (response === this.messageRequestEnum.BLOCK_AND_DELETE) {
// Delete messages locally, other devices should delete upon receiving
// the sync message
this.destroyMessages();
this.disableProfileSharing();
this.updateLastMessage();
// Block locally, other devices should block upon receiving the sync message
window.storage.blockIdentifier(this.get('id'));
// Leave group if this was a local action
if (!fromSync) {
this.leaveGroup();
this.trigger('unload', 'blocked and deleted from message request');
}
}
},
async syncMessageRequestResponse(response) {
// Let this run, no await
this.applyMessageRequestResponse(response);
const { ourNumber, ourUuid } = this;
const { wrap, sendOptions } = ConversationController.prepareForSend(
ourNumber || ourUuid,
{
syncMessage: true,
}
);
await wrap(
textsecure.messaging.syncMessageRequestResponse(
{
threadE164: this.get('e164'),
threadUuid: this.get('uuid'),
groupId: this.get('groupId'),
type: response,
},
sendOptions
)
);
},
onMessageError() {
this.updateVerified();
},
@ -687,6 +889,52 @@
);
});
},
getSentMessageCount() {
return this.get('sentMessageCount') || 0;
},
getMessageRequestResponseType() {
return this.get('messageRequestResponseType') || 0;
},
/**
* Determine if this conversation should be considered "accepted" in terms
* of message requests
*/
getAccepted() {
const messageRequestsEnabled = Signal.RemoteConfig.isEnabled(
'desktop.messageRequests'
);
if (!messageRequestsEnabled) {
return true;
}
if (this.isMe()) {
return true;
}
if (
this.getMessageRequestResponseType() === this.messageRequestEnum.ACCEPT
) {
return true;
}
const fromContact = this.getIsAddedByContact();
const hasSentMessages = this.getSentMessageCount() > 0;
const hasMessagesBeforeMessageRequests =
(this.get('messageCountBeforeMessageRequests') || 0) > 0;
const hasNoMessages = (this.get('messageCount') || 0) === 0;
return (
fromContact ||
hasSentMessages ||
hasMessagesBeforeMessageRequests ||
hasNoMessages
);
},
onMemberVerifiedChange() {
// If the verified state of a member changes, our aggregate state changes.
// We trigger both events to replicate the behavior of Backbone.Model.set()
@ -1159,6 +1407,33 @@
});
},
async sendProfileKeyUpdate() {
const id = this.get('id');
const recipients = this.isPrivate()
? [this.get('uuid') || this.get('e164')]
: this.getRecipients();
if (!this.get('profileSharing')) {
window.log.error(
'Attempted to send profileKeyUpdate to conversation without profileSharing enabled',
id,
recipients
);
return;
}
window.log.info(
'Sending profileKeyUpdate to conversation',
id,
recipients
);
const profileKey = storage.get('profileKey');
await textsecure.messaging.sendProfileKeyUpdate(
profileKey,
recipients,
this.getSendOptions(),
this.get('groupId')
);
},
sendMessage(body, attachments, quote, preview, sticker) {
this.clearTypingTimers();
@ -1226,6 +1501,7 @@
draft: null,
draftTimestamp: null,
});
this.incrementSentMessageCount();
window.Signal.Data.updateConversation(this.attributes);
// We're offline!
@ -1487,6 +1763,32 @@
};
},
getIsContact() {
if (this.isPrivate()) {
return Boolean(this.get('name'));
}
return false;
},
getIsAddedByContact() {
if (this.isPrivate()) {
return this.getIsContact();
}
const addedBy = this.get('addedBy');
if (!addedBy) {
return false;
}
const conv = ConversationController.get(addedBy);
if (!conv) {
return false;
}
return conv.getIsContact();
},
async updateLastMessage() {
if (!this.id) {
return;
@ -1793,7 +2095,7 @@
async leaveGroup() {
const now = Date.now();
if (this.get('type') === 'group') {
const groupNumbers = this.getRecipients();
const groupIdentifiers = this.getRecipients();
this.set({ left: true });
window.Signal.Data.updateConversation(this.attributes);
@ -1816,7 +2118,7 @@
const options = this.getSendOptions();
message.send(
this.wrapSend(
textsecure.messaging.leaveGroup(this.id, groupNumbers, options)
textsecure.messaging.leaveGroup(this.id, groupIdentifiers, options)
)
);
}
@ -1845,11 +2147,10 @@
// Note that this will update the message in the database
await m.markRead(options.readAt);
const errors = m.get('errors');
return {
sender: m.get('source'),
sender: m.get('source') || m.get('sourceUuid'),
timestamp: m.get('sent_at'),
hasErrors: Boolean(errors && errors.length),
hasErrors: m.hasErrors(),
};
})
);
@ -1870,7 +2171,7 @@
read = read.filter(item => !item.hasErrors);
if (read.length && options.sendReadReceipts) {
window.log.info(`Sending ${read.length} read receipts`);
window.log.info(`Sending ${read.length} read syncs`);
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
// to a contact, we need accessKeys for both.
const { sendOptions } = ConversationController.prepareForSend(
@ -1880,25 +2181,31 @@
await this.wrapSend(
textsecure.messaging.syncReadMessages(read, sendOptions)
);
await this.sendReadReceiptsFor(read);
}
},
if (storage.get('read-receipt-setting')) {
const convoSendOptions = this.getSendOptions();
async sendReadReceiptsFor(items) {
// Only send read receipts for accepted conversations
if (storage.get('read-receipt-setting') && this.getAccepted()) {
window.log.info(`Sending ${items.length} read receipts`);
const convoSendOptions = this.getSendOptions();
const receiptsBySender = _.groupBy(items, 'sender');
await Promise.all(
_.map(_.groupBy(read, 'sender'), async (receipts, identifier) => {
const timestamps = _.map(receipts, 'timestamp');
const c = ConversationController.get(identifier);
await this.wrapSend(
textsecure.messaging.sendReadReceipts(
c.get('e164'),
c.get('uuid'),
timestamps,
convoSendOptions
)
);
})
);
}
await Promise.all(
_.map(receiptsBySender, async (receipts, identifier) => {
const timestamps = _.map(receipts, 'timestamp');
const c = ConversationController.get(identifier);
await this.wrapSend(
textsecure.messaging.sendReadReceipts(
c.get('e164'),
c.get('uuid'),
timestamps,
convoSendOptions
)
);
})
);
}
},