Retry outbound "normal" messages for up to a day

This commit is contained in:
Evan Hahn 2021-08-31 15:58:39 -05:00 committed by GitHub
parent 62cf51c060
commit a85dd1be36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1414 additions and 603 deletions

View file

@ -29,6 +29,7 @@ import {
CustomColorType,
} from '../types/Colors';
import { MessageModel } from './messages';
import { strictAssert } from '../util/assert';
import { isMuted } from '../util/isMuted';
import { isConversationSMSOnly } from '../util/isConversationSMSOnly';
import { isConversationUnregistered } from '../util/isConversationUnregistered';
@ -82,6 +83,7 @@ import {
isTapToView,
getMessagePropStatus,
} from '../state/selectors/message';
import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue';
import { Deletes } from '../messageModifiers/Deletes';
import { Reactions, ReactionModel } from '../messageModifiers/Reactions';
import { isAnnouncementGroupReady } from '../util/isAnnouncementGroupReady';
@ -119,11 +121,6 @@ const ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE = new Set([
'profileLastFetchedAt',
]);
type CustomError = Error & {
identifier?: string;
number?: string;
};
type CachedIdenticon = {
readonly url: string;
readonly content: string;
@ -3111,6 +3108,10 @@ export class ConversationModel extends window.Backbone
);
}
getRecipientConversationIds(): Set<string> {
return new Set(map(this.getMembers(), conversation => conversation.id));
}
async getQuoteAttachment(
attachments?: Array<WhatIsThis>,
preview?: Array<WhatIsThis>,
@ -3261,7 +3262,7 @@ export class ConversationModel extends window.Backbone
},
};
this.sendMessage(undefined, [], undefined, [], sticker);
this.enqueueMessageForSend(undefined, [], undefined, [], sticker);
window.reduxActions.stickers.useSticker(packId, stickerId);
}
@ -3577,7 +3578,7 @@ export class ConversationModel extends window.Backbone
);
}
sendMessage(
async enqueueMessageForSend(
body: string | undefined,
attachments: Array<AttachmentType>,
quote?: QuotedMessageType,
@ -3593,7 +3594,7 @@ export class ConversationModel extends window.Backbone
sendHQImages?: boolean;
timestamp?: number;
} = {}
): void {
): Promise<void> {
if (this.isGroupV1AndDisabled()) {
return;
}
@ -3614,223 +3615,134 @@ export class ConversationModel extends window.Backbone
const destination = this.getSendTarget()!;
const recipients = this.getRecipients();
if (timestamp) {
window.log.info(`sendMessage: Queueing send with timestamp ${timestamp}`);
}
this.queueJob('sendMessage', async () => {
const now = timestamp || Date.now();
const now = timestamp || Date.now();
await this.maybeApplyUniversalTimer(false);
await this.maybeApplyUniversalTimer(false);
const expireTimer = this.get('expireTimer');
const expireTimer = this.get('expireTimer');
window.log.info(
'Sending message to conversation',
this.idForLogging(),
'with timestamp',
now
);
window.log.info(
'Sending message to conversation',
this.idForLogging(),
'with timestamp',
now
);
const recipientMaybeConversations = map(recipients, identifier =>
window.ConversationController.get(identifier)
);
const recipientConversations = filter(
recipientMaybeConversations,
isNotNil
);
const recipientConversationIds = concat(
map(recipientConversations, c => c.id),
[window.ConversationController.getOurConversationIdOrThrow()]
);
const recipientMaybeConversations = map(recipients, identifier =>
window.ConversationController.get(identifier)
);
const recipientConversations = filter(
recipientMaybeConversations,
isNotNil
);
const recipientConversationIds = concat(
map(recipientConversations, c => c.id),
[window.ConversationController.getOurConversationIdOrThrow()]
);
// Here we move attachments to disk
const messageWithSchema = await upgradeMessageSchema({
timestamp: now,
type: 'outgoing',
body,
conversationId: this.id,
quote,
preview,
attachments,
sent_at: now,
received_at: window.Signal.Util.incrementMessageCounter(),
received_at_ms: now,
expireTimer,
recipients,
sticker,
bodyRanges: mentions,
sendHQImages,
sendStateByConversationId: zipObject(
recipientConversationIds,
repeat({
status: SendStatus.Pending,
updatedAt: now,
})
),
});
if (isDirectConversation(this.attributes)) {
messageWithSchema.destination = destination;
}
const attributes: MessageAttributesType = {
...messageWithSchema,
id: window.getGuid(),
};
const model = new window.Whisper.Message(attributes);
const message = window.MessageController.register(model.id, model);
const dbStart = Date.now();
await window.Signal.Data.saveMessage(message.attributes, {
forceSave: true,
});
const dbDuration = Date.now() - dbStart;
if (dbDuration > SEND_REPORTING_THRESHOLD_MS) {
window.log.info(
`ConversationModel(${this.idForLogging()}.sendMessage(${now}): ` +
`db save took ${dbDuration}ms`
);
}
const renderStart = Date.now();
this.addSingleMessage(model);
if (sticker) {
await addStickerPackReference(model.id, sticker.packId);
}
const messageId = message.id;
const draftProperties = dontClearDraft
? {}
: {
draft: null,
draftTimestamp: null,
lastMessage: model.getNotificationText(),
lastMessageStatus: 'sending' as const,
};
this.set({
...draftProperties,
active_at: now,
timestamp: now,
isArchived: false,
});
this.incrementSentMessageCount({ save: false });
const renderDuration = Date.now() - renderStart;
if (renderDuration > SEND_REPORTING_THRESHOLD_MS) {
window.log.info(
`ConversationModel(${this.idForLogging()}.sendMessage(${now}): ` +
`render save took ${renderDuration}ms`
);
}
window.Signal.Data.updateConversation(this.attributes);
// We're offline!
if (!window.textsecure.messaging) {
const errors = map(recipientConversationIds, conversationId => {
const error = new Error('Network is not available') as CustomError;
error.name = 'SendMessageNetworkError';
error.identifier = conversationId;
return error;
});
await message.saveErrors([...errors]);
return null;
}
const attachmentsWithData = await Promise.all(
messageWithSchema.attachments?.map(loadAttachmentData) ?? []
);
const {
body: messageBody,
attachments: finalAttachments,
} = window.Whisper.Message.getLongMessageAttachment({
body,
attachments: attachmentsWithData,
now,
});
let profileKey: ArrayBuffer | undefined;
if (this.get('profileSharing')) {
profileKey = await ourProfileKeyService.get();
}
// Special-case the self-send case - we send only a sync message
if (isMe(this.attributes)) {
const dataMessage = await window.textsecure.messaging.getDataMessage({
attachments: finalAttachments,
body: messageBody,
// deletedForEveryoneTimestamp
expireTimer,
preview,
profileKey,
quote,
// reaction
recipients: [destination],
sticker,
timestamp: now,
});
return message.sendSyncMessageOnly(dataMessage);
}
const conversationType = this.get('type');
const options = await getSendOptions(this.attributes);
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
let promise;
if (conversationType === Message.GROUP) {
promise = window.Signal.Util.sendToGroup({
groupSendOptions: {
attachments: finalAttachments,
expireTimer,
groupV1: this.getGroupV1Info(),
groupV2: this.getGroupV2Info(),
messageText: messageBody,
preview,
profileKey,
quote,
sticker,
timestamp: now,
mentions,
},
conversation: this,
contentHint: ContentHint.RESENDABLE,
messageId,
sendOptions: options,
sendType: 'message',
});
} else {
promise = window.textsecure.messaging.sendMessageToIdentifier({
identifier: destination,
messageText: messageBody,
attachments: finalAttachments,
quote,
preview,
sticker,
reaction: null,
deletedForEveryoneTimestamp: undefined,
timestamp: now,
expireTimer,
contentHint: ContentHint.RESENDABLE,
groupId: undefined,
profileKey,
options,
});
}
return message.send(
handleMessageSend(promise, {
messageIds: [messageId],
sendType: 'message',
// Here we move attachments to disk
const messageWithSchema = await upgradeMessageSchema({
timestamp: now,
type: 'outgoing',
body,
conversationId: this.id,
quote,
preview,
attachments,
sent_at: now,
received_at: window.Signal.Util.incrementMessageCounter(),
received_at_ms: now,
expireTimer,
recipients,
sticker,
bodyRanges: mentions,
sendHQImages,
sendStateByConversationId: zipObject(
recipientConversationIds,
repeat({
status: SendStatus.Pending,
updatedAt: now,
})
);
),
});
if (isDirectConversation(this.attributes)) {
messageWithSchema.destination = destination;
}
const attributes: MessageAttributesType = {
...messageWithSchema,
id: window.getGuid(),
};
const model = new window.Whisper.Message(attributes);
const message = window.MessageController.register(model.id, model);
message.cachedOutgoingPreviewData = preview;
message.cachedOutgoingQuoteData = quote;
message.cachedOutgoingStickerData = sticker;
const dbStart = Date.now();
strictAssert(
typeof message.attributes.timestamp === 'number',
'Expected a timestamp'
);
await normalMessageSendJobQueue.add(
{ messageId: message.id, conversationId: this.id },
async jobToInsert => {
window.log.info(
`enqueueMessageForSend: saving message ${message.id} and job ${jobToInsert.id}`
);
await window.Signal.Data.saveMessage(message.attributes, {
jobToInsert,
forceSave: true,
});
}
);
const dbDuration = Date.now() - dbStart;
if (dbDuration > SEND_REPORTING_THRESHOLD_MS) {
window.log.info(
`ConversationModel(${this.idForLogging()}.sendMessage(${now}): ` +
`db save took ${dbDuration}ms`
);
}
const renderStart = Date.now();
this.addSingleMessage(model);
if (sticker) {
await addStickerPackReference(model.id, sticker.packId);
}
const draftProperties = dontClearDraft
? {}
: {
draft: null,
draftTimestamp: null,
lastMessage: model.getNotificationText(),
lastMessageStatus: 'sending' as const,
};
this.set({
...draftProperties,
active_at: now,
timestamp: now,
isArchived: false,
});
this.incrementSentMessageCount({ save: false });
const renderDuration = Date.now() - renderStart;
if (renderDuration > SEND_REPORTING_THRESHOLD_MS) {
window.log.info(
`ConversationModel(${this.idForLogging()}.sendMessage(${now}): ` +
`render save took ${renderDuration}ms`
);
}
window.Signal.Data.updateConversation(this.attributes);
}
// Is this someone who is a contact, or are we sharing our profile with them?