Message Send Log to enable comprehensive resend

This commit is contained in:
Scott Nonnenberg 2021-07-15 16:48:09 -07:00 committed by GitHub
parent 0fe68b57b1
commit a42c41ed01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 3154 additions and 1266 deletions

View file

@ -10,7 +10,6 @@ import {
MessageAttributesType,
MessageModelCollectionType,
QuotedMessageType,
ReactionModelType,
VerificationOptions,
WhatIsThis,
} from '../model-types.d';
@ -64,7 +63,6 @@ import {
isGroupV2,
isMe,
} from '../util/whatTypeOfConversation';
import { deprecated } from '../util/deprecated';
import { SignalService as Proto } from '../protobuf';
import {
hasErrors,
@ -73,7 +71,7 @@ import {
getMessagePropStatus,
} from '../state/selectors/message';
import { Deletes } from '../messageModifiers/Deletes';
import { Reactions } from '../messageModifiers/Reactions';
import { Reactions, ReactionModel } from '../messageModifiers/Reactions';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
@ -320,11 +318,6 @@ export class ConversationModel extends window.Backbone
}
}
isPrivate(): boolean {
deprecated('isPrivate()');
return isDirectConversation(this.attributes);
}
isMemberRequestingToJoin(conversationId: string): boolean {
if (!isGroupV2(this.attributes)) {
return false;
@ -1200,7 +1193,8 @@ export class ConversationModel extends window.Backbone
...sendOptions,
online: true,
},
})
}),
{ messageIds: [], sendType: 'typing' }
);
} else {
handleMessageSend(
@ -1208,11 +1202,14 @@ export class ConversationModel extends window.Backbone
contentHint: ContentHint.IMPLICIT,
contentMessage,
conversation: this,
messageId: undefined,
online: true,
recipients: groupMembers,
sendOptions,
sendType: 'typing',
timestamp,
})
}),
{ messageIds: [], sendType: 'typing' }
);
}
});
@ -1577,6 +1574,7 @@ export class ConversationModel extends window.Backbone
m => !hasErrors(m.attributes) && isIncoming(m.attributes)
);
const receiptSpecs = readMessages.map(m => ({
messageId: m.id,
senderE164: m.get('source'),
senderUuid: m.get('sourceUuid'),
senderId: window.ConversationController.ensureContactIds({
@ -1988,22 +1986,22 @@ export class ConversationModel extends window.Backbone
// server updates were successful.
await this.applyMessageRequestResponse(response);
const { ourNumber, ourUuid } = this;
const {
wrap,
sendOptions,
} = await window.ConversationController.prepareForSend(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ourNumber || ourUuid!,
{
syncMessage: true,
}
);
const ourConversation = window.ConversationController.getOurConversationOrThrow();
const sendOptions = await getSendOptions(ourConversation.attributes, {
syncMessage: true,
});
const groupId = this.getGroupIdBuffer();
if (window.ConversationController.areWePrimaryDevice()) {
window.log.warn(
'syncMessageRequestResponse: We are primary device; not sending message request sync'
);
return;
}
try {
await wrap(
await handleMessageSend(
window.textsecure.messaging.syncMessageRequestResponse(
{
threadE164: this.get('e164'),
@ -2012,7 +2010,8 @@ export class ConversationModel extends window.Backbone
type: response,
},
sendOptions
)
),
{ messageIds: [], sendType: 'otherSync' }
);
} catch (result) {
this.processSendResponse(result);
@ -2167,10 +2166,8 @@ export class ConversationModel extends window.Backbone
}
if (!options.viaSyncMessage) {
await this.sendVerifySyncMessage(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.get('e164')!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.get('uuid')!,
this.get('e164'),
this.get('uuid'),
verified
);
}
@ -2179,33 +2176,52 @@ export class ConversationModel extends window.Backbone
}
async sendVerifySyncMessage(
e164: string,
uuid: string,
e164: string | undefined,
uuid: string | undefined,
state: number
): Promise<WhatIsThis> {
): Promise<CallbackResultType | void> {
const identifier = uuid || e164;
if (!identifier) {
throw new Error(
'sendVerifySyncMessage: Neither e164 nor UUID were provided'
);
}
if (window.ConversationController.areWePrimaryDevice()) {
window.log.warn(
'sendVerifySyncMessage: We are primary device; not sending sync'
);
return;
}
// Because syncVerification sends a (null) message to the target of the verify and
// a sync message to our own devices, we need to send the accessKeys down for both
// contacts. So we merge their sendOptions.
const { sendOptions } = await window.ConversationController.prepareForSend(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.ourNumber || this.ourUuid!,
{ syncMessage: true }
);
const ourConversation = window.ConversationController.getOurConversationOrThrow();
const sendOptions = await getSendOptions(ourConversation.attributes, {
syncMessage: true,
});
const contactSendOptions = await getSendOptions(this.attributes);
const options = { ...sendOptions, ...contactSendOptions };
const promise = window.textsecure.storage.protocol.loadIdentityKey(e164);
return promise.then(key =>
handleMessageSend(
window.textsecure.messaging.syncVerification(
e164,
uuid,
state,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
key!,
options
)
)
const key = await window.textsecure.storage.protocol.loadIdentityKey(
identifier
);
if (!key) {
throw new Error(
`sendVerifySyncMessage: No identity key found for identifier ${identifier}`
);
}
await handleMessageSend(
window.textsecure.messaging.syncVerification(
e164,
uuid,
state,
key,
options
),
{ messageIds: [], sendType: 'verificationSync' }
);
}
@ -2214,13 +2230,12 @@ export class ConversationModel extends window.Backbone
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.get('verified') === this.verifiedEnum!.VERIFIED;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!this.contactCollection!.length) {
if (!this.contactCollection?.length) {
return false;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.contactCollection!.every(contact => {
return this.contactCollection?.every(contact => {
if (isMe(contact.attributes)) {
return true;
}
@ -2238,16 +2253,12 @@ export class ConversationModel extends window.Backbone
verified !== this.verifiedEnum!.DEFAULT
);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!this.contactCollection!.length) {
if (!this.contactCollection?.length) {
return true;
}
// Array.any does not exist. This is probably broken.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.contactCollection!.any(contact => {
return this.contactCollection?.some(contact => {
if (isMe(contact.attributes)) {
return false;
}
@ -2262,8 +2273,7 @@ export class ConversationModel extends window.Backbone
: new window.Backbone.Collection();
}
return new window.Backbone.Collection(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.contactCollection!.filter(contact => {
this.contactCollection?.filter(contact => {
if (isMe(contact.attributes)) {
return false;
}
@ -3158,7 +3168,11 @@ export class ConversationModel extends window.Backbone
window.reduxActions.stickers.useSticker(packId, stickerId);
}
async sendDeleteForEveryoneMessage(targetTimestamp: number): Promise<void> {
async sendDeleteForEveryoneMessage(options: {
id: string;
timestamp: number;
}): Promise<void> {
const { timestamp: targetTimestamp, id: messageId } = options;
const timestamp = Date.now();
if (timestamp - targetTimestamp > THREE_HOURS) {
@ -3224,7 +3238,7 @@ export class ConversationModel extends window.Backbone
deletedForEveryoneTimestamp: targetTimestamp,
timestamp,
expireTimer: undefined,
contentHint: ContentHint.DEFAULT,
contentHint: ContentHint.RESENDABLE,
groupId: undefined,
profileKey,
options: sendOptions,
@ -3240,8 +3254,10 @@ export class ConversationModel extends window.Backbone
profileKey,
},
conversation: this,
contentHint: ContentHint.DEFAULT,
contentHint: ContentHint.RESENDABLE,
messageId,
sendOptions,
sendType: 'deleteForEveryone',
});
})();
@ -3249,11 +3265,16 @@ export class ConversationModel extends window.Backbone
// anything to the database.
message.doNotSave = true;
const result = await message.send(handleMessageSend(promise));
const result = await message.send(
handleMessageSend(promise, {
messageIds: [messageId],
sendType: 'deleteForEveryone',
})
);
if (!message.hasSuccessfulDelivery()) {
// This is handled by `conversation_view` which displays a toast on
// send error.
// send error.
throw new Error('No successful delivery for delete for everyone');
}
Deletes.getSingleton().onDelete(deleteModel);
@ -3274,10 +3295,12 @@ export class ConversationModel extends window.Backbone
async sendReactionMessage(
reaction: { emoji: string; remove: boolean },
target: {
messageId: string;
targetAuthorUuid: string;
targetTimestamp: number;
}
): Promise<WhatIsThis> {
const { messageId } = target;
const timestamp = Date.now();
const outgoingReaction = { ...reaction, ...target };
@ -3373,7 +3396,7 @@ export class ConversationModel extends window.Backbone
deletedForEveryoneTimestamp: undefined,
timestamp,
expireTimer,
contentHint: ContentHint.DEFAULT,
contentHint: ContentHint.RESENDABLE,
groupId: undefined,
profileKey,
options,
@ -3392,12 +3415,19 @@ export class ConversationModel extends window.Backbone
profileKey,
},
conversation: this,
contentHint: ContentHint.DEFAULT,
contentHint: ContentHint.RESENDABLE,
messageId,
sendOptions: options,
sendType: 'reaction',
});
})();
const result = await message.send(handleMessageSend(promise));
const result = await message.send(
handleMessageSend(promise, {
messageIds: [messageId],
sendType: 'reaction',
})
);
if (!message.hasSuccessfulDelivery()) {
// This is handled by `conversation_view` which displays a toast on
@ -3407,7 +3437,7 @@ export class ConversationModel extends window.Backbone
return result;
}).catch(() => {
let reverseReaction: ReactionModelType;
let reverseReaction: ReactionModel;
if (oldReaction) {
// Either restore old reaction
reverseReaction = Reactions.getSingleton().add({
@ -3444,11 +3474,15 @@ export class ConversationModel extends window.Backbone
);
return;
}
await window.textsecure.messaging.sendProfileKeyUpdate(
profileKey,
recipients,
await getSendOptions(this.attributes),
this.get('groupId')
await handleMessageSend(
window.textsecure.messaging.sendProfileKeyUpdate(
profileKey,
recipients,
await getSendOptions(this.attributes),
this.get('groupId')
),
{ messageIds: [], sendType: 'profileKeyUpdate' }
);
}
@ -3537,6 +3571,7 @@ export class ConversationModel extends window.Backbone
await addStickerPackReference(model.id, sticker.packId);
}
const message = window.MessageController.register(model.id, model);
const messageId = message.id;
await window.Signal.Data.saveMessage(message.attributes, {
forceSave: true,
Message: window.Whisper.Message,
@ -3635,7 +3670,9 @@ export class ConversationModel extends window.Backbone
},
conversation: this,
contentHint: ContentHint.RESENDABLE,
messageId,
sendOptions: options,
sendType: 'message',
});
} else {
promise = window.textsecure.messaging.sendMessageToIdentifier({
@ -3656,7 +3693,12 @@ export class ConversationModel extends window.Backbone
});
}
return message.send(handleMessageSend(promise));
return message.send(
handleMessageSend(promise, {
messageIds: [messageId],
sendType: 'message',
})
);
});
}
@ -4099,7 +4141,12 @@ export class ConversationModel extends window.Backbone
);
}
await message.send(handleMessageSend(promise));
await message.send(
handleMessageSend(promise, {
messageIds: [],
sendType: 'expirationTimerUpdate',
})
);
return message;
}
@ -4220,7 +4267,8 @@ export class ConversationModel extends window.Backbone
groupId,
groupIdentifiers,
options
)
),
{ messageIds: [], sendType: 'legacyGroupChange' }
)
);
}

View file

@ -167,7 +167,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
isSelected?: boolean;
syncPromise?: Promise<unknown>;
syncPromise?: Promise<CallbackResultType | void>;
initialize(attributes: unknown): void {
if (_.isObject(attributes)) {
@ -774,8 +774,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
async cleanup(): Promise<void> {
const { messageDeleted } = window.reduxActions.conversations;
messageDeleted(this.id, this.get('conversationId'));
window.reduxActions?.conversations?.messageDeleted(
this.id,
this.get('conversationId')
);
this.getConversation()?.debouncedUpdateLastMessage?.();
@ -868,26 +870,26 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
const timestamp = this.get('sent_at');
const ourNumber = window.textsecure.storage.user.getNumber();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ourUuid = window.textsecure.storage.user.getUuid()!;
const {
wrap,
sendOptions,
} = await window.ConversationController.prepareForSend(
ourNumber || ourUuid,
{
syncMessage: true,
}
);
const ourConversation = window.ConversationController.getOurConversationOrThrow();
const sendOptions = await getSendOptions(ourConversation.attributes, {
syncMessage: true,
});
await wrap(
if (window.ConversationController.areWePrimaryDevice()) {
window.log.warn(
'markViewed: We are primary device; not sending view sync'
);
return;
}
await handleMessageSend(
window.textsecure.messaging.syncViewOnceOpen(
sender,
senderUuid,
timestamp,
sendOptions
)
),
{ messageIds: [this.id], sendType: 'viewOnceSync' }
);
}
}
@ -987,6 +989,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
Message: window.Whisper.Message,
});
}
await window.Signal.Data.deleteSentProtoByMessageId(this.id);
}
isEmpty(): boolean {
@ -1346,11 +1350,18 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// Important to ensure that we don't consider this recipient list to be the
// entire member list.
isPartialSend: true,
messageId: this.id,
sendOptions: options,
sendType: 'messageRetry',
});
}
return this.send(handleMessageSend(promise));
return this.send(
handleMessageSend(promise, {
messageIds: [this.id],
sendType: 'messageRetry',
})
);
}
// eslint-disable-next-line class-methods-use-this
@ -1429,10 +1440,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
const parentConversation = this.getConversation();
const groupId = parentConversation?.get('groupId');
const {
wrap,
sendOptions,
} = await window.ConversationController.prepareForSend(identifier);
const recipientConversation = window.ConversationController.get(identifier);
const sendOptions = recipientConversation
? await getSendOptions(recipientConversation.attributes)
: undefined;
const group =
groupId && isGroupV1(parentConversation?.attributes)
? {
@ -1479,7 +1491,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
options: sendOptions,
});
return this.send(wrap(promise));
return this.send(
handleMessageSend(promise, {
messageIds: [this.id],
sendType: 'messageRetry',
})
);
}
removeOutgoingErrors(incomingIdentifier: string): CustomError {
@ -1689,18 +1706,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// possible.
await this.send(
handleMessageSend(
// TODO: DESKTOP-724
// resetSession returns `Array<void>` which is incompatible with the
// expected promise return values. `[]` is truthy and handleMessageSend
// assumes it's a valid callback result type
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.textsecure.messaging.resetSession(
options.uuid,
options.e164,
options.now,
sendOptions
)
),
{ messageIds: [], sendType: 'resetSession' }
)
);
@ -1725,10 +1737,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
sent: true,
expirationStartTimestamp: Date.now(),
});
const result: typeof window.WhatIsThis = await this.sendSyncMessage();
const result = await this.sendSyncMessage();
this.set({
// We have to do this afterward, since we didn't have a previous send!
unidentifiedDeliveries: result ? result.unidentifiedDeliveries : null,
unidentifiedDeliveries:
result && result.unidentifiedDeliveries
? result.unidentifiedDeliveries
: undefined,
// These are unique to a Note to Self message - immediately read/delivered
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -1751,30 +1766,31 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
}
async sendSyncMessage(): Promise<WhatIsThis> {
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const {
wrap,
sendOptions,
} = await window.ConversationController.prepareForSend(
ourUuid || ourNumber,
{
syncMessage: true,
}
);
async sendSyncMessage(): Promise<CallbackResultType | void> {
const ourConversation = window.ConversationController.getOurConversationOrThrow();
const sendOptions = await getSendOptions(ourConversation.attributes, {
syncMessage: true,
});
if (window.ConversationController.areWePrimaryDevice()) {
window.log.warn(
'sendSyncMessage: We are primary device; not sending sync message'
);
this.set({ dataMessage: undefined });
return;
}
this.syncPromise = this.syncPromise || Promise.resolve();
const next = async () => {
const dataMessage = this.get('dataMessage');
if (!dataMessage) {
return Promise.resolve();
return;
}
const isUpdate = Boolean(this.get('synced'));
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const conv = this.getConversation()!;
return wrap(
return handleMessageSend(
window.textsecure.messaging.sendSyncMessage({
encodedDataMessage: dataMessage,
timestamp: this.get('sent_at'),
@ -1786,8 +1802,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
unidentifiedDeliveries: this.get('unidentifiedDeliveries') || [],
isUpdate,
options: sendOptions,
})
).then(async (result: unknown) => {
}),
{ messageIds: [this.id], sendType: 'sentSync' }
).then(async result => {
this.set({
synced: true,
dataMessage: null,
@ -2504,28 +2521,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
}
// Now check for decryption error placeholders
const { retryPlaceholders } = window.Signal.Services;
if (retryPlaceholders) {
const item = await retryPlaceholders.findByMessageAndRemove(
conversationId,
message.get('sent_at')
);
if (item && item.wasOpened) {
window.log.info(
`handleDataMessage: found retry placeholder for ${message.idForLogging()}, but conversation was opened. No updates made.`
);
} else if (item) {
window.log.info(
`handleDataMessage: found retry placeholder for ${message.idForLogging()}. Updating received_at/received_at_ms`
);
message.set({
received_at: item.receivedAtCounter,
received_at_ms: item.receivedAt,
});
}
}
// GroupV2
if (initialMessage.groupV2) {
@ -2640,6 +2635,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return;
}
const messageId = window.getGuid();
// Send delivery receipts, but only for incoming sealed sender messages
// and not for messages from unaccepted conversations
if (
@ -2653,6 +2650,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// The queue can be paused easily.
window.Whisper.deliveryReceiptQueue.add(() => {
window.Whisper.deliveryReceiptBatcher.add({
messageId,
source,
sourceUuid,
timestamp: this.get('sent_at'),
@ -2689,7 +2687,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
message.set({
id: window.getGuid(),
id: messageId,
attachments: dataMessage.attachments,
body: dataMessage.body,
bodyRanges: dataMessage.bodyRanges,
@ -3270,6 +3268,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
conversationId: this.get('conversationId'),
emoji: reaction.get('emoji'),
fromId: reaction.get('fromId'),
messageId: this.id,
messageReceivedAt: this.get('received_at'),
targetAuthorUuid: reaction.get('targetAuthorUuid'),
targetTimestamp: reaction.get('targetTimestamp'),