Retry outbound "normal" messages for up to a day
This commit is contained in:
parent
62cf51c060
commit
a85dd1be36
30 changed files with 1414 additions and 603 deletions
|
@ -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?
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isEmpty, isEqual, noop, omit, union } from 'lodash';
|
||||
import { isEmpty, isEqual, mapValues, noop, omit, union } from 'lodash';
|
||||
import {
|
||||
CustomError,
|
||||
GroupV1Update,
|
||||
|
@ -43,7 +43,6 @@ import {
|
|||
import * as Stickers from '../types/Stickers';
|
||||
import { AttachmentType, isImage, isVideo } from '../types/Attachment';
|
||||
import { IMAGE_WEBP, stringToMIMEType } from '../types/MIME';
|
||||
import { ourProfileKeyService } from '../services/ourProfileKey';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import {
|
||||
SendActionType,
|
||||
|
@ -112,6 +111,8 @@ import { ViewOnceOpenSyncs } from '../messageModifiers/ViewOnceOpenSyncs';
|
|||
import * as AttachmentDownloads from '../messageModifiers/AttachmentDownloads';
|
||||
import * as LinkPreview from '../types/LinkPreview';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue';
|
||||
import type { PreviewType as OutgoingPreviewType } from '../textsecure/SendMessage';
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable more/no-then */
|
||||
|
@ -134,10 +135,6 @@ const {
|
|||
} = window.Signal.Types;
|
||||
const {
|
||||
deleteExternalMessageFiles,
|
||||
loadAttachmentData,
|
||||
loadQuoteData,
|
||||
loadPreviewData,
|
||||
loadStickerData,
|
||||
upgradeMessageSchema,
|
||||
} = window.Signal.Migrations;
|
||||
const { getTextWithMentions, GoogleChrome } = window.Signal.Util;
|
||||
|
@ -190,6 +187,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
|
||||
syncPromise?: Promise<CallbackResultType | void>;
|
||||
|
||||
cachedOutgoingPreviewData?: Array<OutgoingPreviewType>;
|
||||
|
||||
cachedOutgoingQuoteData?: WhatIsThis;
|
||||
|
||||
cachedOutgoingStickerData?: WhatIsThis;
|
||||
|
||||
initialize(attributes: unknown): void {
|
||||
if (_.isObject(attributes)) {
|
||||
this.set(
|
||||
|
@ -1200,42 +1203,27 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return window.ConversationController.getOrCreate(source, 'private');
|
||||
}
|
||||
|
||||
// Send infrastructure
|
||||
// One caller today: event handler for the 'Retry Send' entry in triple-dot menu
|
||||
async retrySend(): Promise<string | null | void | Array<void>> {
|
||||
if (!window.textsecure.messaging) {
|
||||
window.log.error('retrySend: Cannot retry since we are offline!');
|
||||
return null;
|
||||
}
|
||||
|
||||
async retrySend(): Promise<void> {
|
||||
const retryOptions = this.get('retryOptions');
|
||||
|
||||
this.set({ errors: undefined, retryOptions: undefined });
|
||||
|
||||
if (retryOptions) {
|
||||
if (!window.textsecure.messaging) {
|
||||
window.log.error('retrySend: Cannot retry since we are offline!');
|
||||
return;
|
||||
}
|
||||
this.unset('errors');
|
||||
this.unset('retryOptions');
|
||||
return this.sendUtilityMessageWithRetry(retryOptions);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const conversation = this.getConversation()!;
|
||||
const currentRecipients = new Set<string>(
|
||||
conversation
|
||||
.getRecipients()
|
||||
.map(identifier =>
|
||||
window.ConversationController.getConversationId(identifier)
|
||||
)
|
||||
.filter(isNotNil)
|
||||
);
|
||||
|
||||
const profileKey = conversation.get('profileSharing')
|
||||
? await ourProfileKeyService.get()
|
||||
: undefined;
|
||||
const currentConversationRecipients = conversation.getRecipientConversationIds();
|
||||
|
||||
// Determine retry recipients and get their most up-to-date addressing information
|
||||
const oldSendStateByConversationId =
|
||||
this.get('sendStateByConversationId') || {};
|
||||
|
||||
const recipients: Array<string> = [];
|
||||
const newSendStateByConversationId = { ...oldSendStateByConversationId };
|
||||
for (const [conversationId, sendState] of Object.entries(
|
||||
oldSendStateByConversationId
|
||||
|
@ -1244,15 +1232,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
continue;
|
||||
}
|
||||
|
||||
const isStillInConversation = currentRecipients.has(conversationId);
|
||||
if (!isStillInConversation) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const recipient = window.ConversationController.get(
|
||||
conversationId
|
||||
)?.getSendTarget();
|
||||
if (!recipient) {
|
||||
const recipient = window.ConversationController.get(conversationId);
|
||||
if (
|
||||
!recipient ||
|
||||
(!currentConversationRecipients.has(conversationId) &&
|
||||
!isMe(recipient.attributes))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -1263,133 +1248,15 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
updatedAt: Date.now(),
|
||||
}
|
||||
);
|
||||
recipients.push(recipient);
|
||||
}
|
||||
|
||||
this.set('sendStateByConversationId', newSendStateByConversationId);
|
||||
|
||||
await window.Signal.Data.saveMessage(this.attributes);
|
||||
|
||||
if (!recipients.length) {
|
||||
window.log.warn('retrySend: Nobody to send to!');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const attachmentsWithData = await Promise.all(
|
||||
(this.get('attachments') || []).map(loadAttachmentData)
|
||||
);
|
||||
const {
|
||||
body,
|
||||
attachments,
|
||||
} = window.Whisper.Message.getLongMessageAttachment({
|
||||
body: this.get('body'),
|
||||
attachments: attachmentsWithData,
|
||||
now: this.get('sent_at'),
|
||||
});
|
||||
|
||||
const quoteWithData = await loadQuoteData(this.get('quote'));
|
||||
const previewWithData = await loadPreviewData(this.get('preview'));
|
||||
const stickerWithData = await loadStickerData(this.get('sticker'));
|
||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||
|
||||
// Special-case the self-send case - we send only a sync message
|
||||
if (
|
||||
recipients.length === 1 &&
|
||||
(recipients[0] === ourNumber || recipients[0] === this.OUR_UUID)
|
||||
) {
|
||||
const dataMessage = await window.textsecure.messaging.getDataMessage({
|
||||
attachments,
|
||||
body,
|
||||
deletedForEveryoneTimestamp: this.get('deletedForEveryoneTimestamp'),
|
||||
expireTimer: this.get('expireTimer'),
|
||||
// flags
|
||||
mentions: this.get('bodyRanges'),
|
||||
preview: previewWithData,
|
||||
profileKey,
|
||||
quote: quoteWithData,
|
||||
reaction: null,
|
||||
recipients,
|
||||
sticker: stickerWithData,
|
||||
timestamp: this.get('sent_at'),
|
||||
});
|
||||
|
||||
return this.sendSyncMessageOnly(dataMessage);
|
||||
}
|
||||
|
||||
let promise;
|
||||
const options = await getSendOptions(conversation.attributes);
|
||||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
if (isDirectConversation(conversation.attributes)) {
|
||||
const [identifier] = recipients;
|
||||
|
||||
promise = window.textsecure.messaging.sendMessageToIdentifier({
|
||||
identifier,
|
||||
messageText: body,
|
||||
attachments,
|
||||
quote: quoteWithData,
|
||||
preview: previewWithData,
|
||||
sticker: stickerWithData,
|
||||
reaction: null,
|
||||
deletedForEveryoneTimestamp: this.get('deletedForEveryoneTimestamp'),
|
||||
timestamp: this.get('sent_at'),
|
||||
expireTimer: this.get('expireTimer'),
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
groupId: undefined,
|
||||
profileKey,
|
||||
options,
|
||||
});
|
||||
} else {
|
||||
const initialGroupV2 = conversation.getGroupV2Info();
|
||||
const groupId = conversation.get('groupId');
|
||||
if (!groupId) {
|
||||
throw new Error("retrySend: Conversation didn't have groupId");
|
||||
await normalMessageSendJobQueue.add(
|
||||
{ messageId: this.id, conversationId: conversation.id },
|
||||
async jobToInsert => {
|
||||
await window.Signal.Data.saveMessage(this.attributes, { jobToInsert });
|
||||
}
|
||||
|
||||
const groupV2 = initialGroupV2
|
||||
? {
|
||||
...initialGroupV2,
|
||||
members: recipients,
|
||||
}
|
||||
: undefined;
|
||||
const groupV1 = groupV2
|
||||
? undefined
|
||||
: {
|
||||
id: groupId,
|
||||
members: recipients,
|
||||
};
|
||||
|
||||
promise = window.Signal.Util.sendToGroup({
|
||||
groupSendOptions: {
|
||||
messageText: body,
|
||||
timestamp: this.get('sent_at'),
|
||||
attachments,
|
||||
quote: quoteWithData,
|
||||
preview: previewWithData,
|
||||
sticker: stickerWithData,
|
||||
expireTimer: this.get('expireTimer'),
|
||||
mentions: this.get('bodyRanges'),
|
||||
profileKey,
|
||||
groupV2,
|
||||
groupV1,
|
||||
},
|
||||
conversation,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
// 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, {
|
||||
messageIds: [this.id],
|
||||
sendType: 'messageRetry',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1414,118 +1281,20 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return isEmpty(withoutMe) || someSendStatus(withoutMe, isSent);
|
||||
}
|
||||
|
||||
// Called when the user ran into an error with a specific user, wants to send to them
|
||||
// One caller today: ConversationView.forceSend()
|
||||
async resend(identifier: string): Promise<void | null | Array<void>> {
|
||||
const error = this.removeOutgoingErrors(identifier);
|
||||
if (!error) {
|
||||
window.log.warn(
|
||||
'resend: requested number was not present in errors. continuing.'
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isErased()) {
|
||||
window.log.warn('resend: message is erased; refusing to resend');
|
||||
return null;
|
||||
}
|
||||
|
||||
const profileKey = undefined;
|
||||
const attachmentsWithData = await Promise.all(
|
||||
(this.get('attachments') || []).map(loadAttachmentData)
|
||||
);
|
||||
const {
|
||||
body,
|
||||
attachments,
|
||||
} = window.Whisper.Message.getLongMessageAttachment({
|
||||
body: this.get('body'),
|
||||
attachments: attachmentsWithData,
|
||||
now: this.get('sent_at'),
|
||||
});
|
||||
|
||||
const quoteWithData = await loadQuoteData(this.get('quote'));
|
||||
const previewWithData = await loadPreviewData(this.get('preview'));
|
||||
const stickerWithData = await loadStickerData(this.get('sticker'));
|
||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||
|
||||
// Special-case the self-send case - we send only a sync message
|
||||
if (identifier === ourNumber || identifier === this.OUR_UUID) {
|
||||
const dataMessage = await window.textsecure.messaging.getDataMessage({
|
||||
attachments,
|
||||
body,
|
||||
deletedForEveryoneTimestamp: this.get('deletedForEveryoneTimestamp'),
|
||||
expireTimer: this.get('expireTimer'),
|
||||
mentions: this.get('bodyRanges'),
|
||||
preview: previewWithData,
|
||||
profileKey,
|
||||
quote: quoteWithData,
|
||||
reaction: null,
|
||||
recipients: [identifier],
|
||||
sticker: stickerWithData,
|
||||
timestamp: this.get('sent_at'),
|
||||
});
|
||||
|
||||
return this.sendSyncMessageOnly(dataMessage);
|
||||
}
|
||||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
const parentConversation = this.getConversation();
|
||||
const groupId = parentConversation?.get('groupId');
|
||||
|
||||
const recipientConversation = window.ConversationController.get(identifier);
|
||||
const sendOptions = recipientConversation
|
||||
? await getSendOptions(recipientConversation.attributes)
|
||||
: undefined;
|
||||
const group =
|
||||
groupId && isGroupV1(parentConversation?.attributes)
|
||||
? {
|
||||
id: groupId,
|
||||
type: Proto.GroupContext.Type.DELIVER,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const timestamp = this.get('sent_at');
|
||||
const contentMessage = await window.textsecure.messaging.getContentMessage({
|
||||
attachments,
|
||||
body,
|
||||
expireTimer: this.get('expireTimer'),
|
||||
group,
|
||||
groupV2: parentConversation?.getGroupV2Info(),
|
||||
preview: previewWithData,
|
||||
quote: quoteWithData,
|
||||
mentions: this.get('bodyRanges'),
|
||||
recipients: [identifier],
|
||||
sticker: stickerWithData,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
if (parentConversation) {
|
||||
const senderKeyInfo = parentConversation.get('senderKeyInfo');
|
||||
if (senderKeyInfo && senderKeyInfo.distributionId) {
|
||||
const senderKeyDistributionMessage = await window.textsecure.messaging.getSenderKeyDistributionMessage(
|
||||
senderKeyInfo.distributionId
|
||||
);
|
||||
|
||||
contentMessage.senderKeyDistributionMessage = senderKeyDistributionMessage.serialize();
|
||||
}
|
||||
}
|
||||
|
||||
const promise = window.textsecure.messaging.sendMessageProtoAndWait({
|
||||
timestamp,
|
||||
recipients: [identifier],
|
||||
proto: contentMessage,
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
groupId:
|
||||
groupId && isGroupV2(parentConversation?.attributes)
|
||||
? groupId
|
||||
: undefined,
|
||||
options: sendOptions,
|
||||
});
|
||||
|
||||
return this.send(
|
||||
handleMessageSend(promise, {
|
||||
messageIds: [this.id],
|
||||
sendType: 'messageRetry',
|
||||
})
|
||||
/**
|
||||
* Change any Pending send state to Failed. Note that this will not mark successful
|
||||
* sends failed.
|
||||
*/
|
||||
public markFailed(): void {
|
||||
const now = Date.now();
|
||||
this.set(
|
||||
'sendStateByConversationId',
|
||||
mapValues(this.get('sendStateByConversationId') || {}, sendState =>
|
||||
sendStateReducer(sendState, {
|
||||
type: SendActionType.Failed,
|
||||
updatedAt: now,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1552,7 +1321,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
|
||||
async send(
|
||||
promise: Promise<CallbackResultType | void | null>
|
||||
promise: Promise<CallbackResultType | void | null>,
|
||||
saveErrors?: (errors: Array<Error>) => void
|
||||
): Promise<void | Array<void>> {
|
||||
const updateLeftPane =
|
||||
this.getConversation()?.debouncedUpdateLastMessage || noop;
|
||||
|
@ -1655,7 +1425,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
window.ConversationController.get(error.identifier) ||
|
||||
window.ConversationController.get(error.number);
|
||||
|
||||
if (conversation) {
|
||||
if (conversation && !saveErrors) {
|
||||
const previousSendState = getOwn(
|
||||
sendStateByConversationId,
|
||||
conversation.id
|
||||
|
@ -1719,8 +1489,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
attributesToUpdate.errors = [];
|
||||
|
||||
this.set(attributesToUpdate);
|
||||
// We skip save because we'll save in the next step.
|
||||
this.saveErrors(errorsToSave, { skipSave: true });
|
||||
if (saveErrors) {
|
||||
saveErrors(errorsToSave);
|
||||
} else {
|
||||
// We skip save because we'll save in the next step.
|
||||
this.saveErrors(errorsToSave, { skipSave: true });
|
||||
}
|
||||
|
||||
if (!this.doNotSave) {
|
||||
await window.Signal.Data.saveMessage(this.attributes);
|
||||
|
@ -1734,6 +1508,14 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
|
||||
await Promise.all(promises);
|
||||
|
||||
const isTotalSuccess: boolean =
|
||||
result.success && !this.get('errors')?.length;
|
||||
if (isTotalSuccess) {
|
||||
delete this.cachedOutgoingPreviewData;
|
||||
delete this.cachedOutgoingQuoteData;
|
||||
delete this.cachedOutgoingStickerData;
|
||||
}
|
||||
|
||||
updateLeftPane();
|
||||
}
|
||||
|
||||
|
@ -1779,7 +1561,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
throw new Error(`Unsupported retriable type: ${options.type}`);
|
||||
}
|
||||
|
||||
async sendSyncMessageOnly(dataMessage: ArrayBuffer): Promise<void> {
|
||||
async sendSyncMessageOnly(
|
||||
dataMessage: ArrayBuffer,
|
||||
saveErrors?: (errors: Array<Error>) => void
|
||||
): Promise<void> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const conv = this.getConversation()!;
|
||||
this.set({ dataMessage });
|
||||
|
@ -1800,9 +1585,16 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
: undefined,
|
||||
});
|
||||
} catch (result) {
|
||||
const errors = (result && result.errors) || [new Error('Unknown error')];
|
||||
// We don't save because we're about to save below.
|
||||
this.saveErrors(errors, { skipSave: true });
|
||||
const resultErrors = result?.errors;
|
||||
const errors = Array.isArray(resultErrors)
|
||||
? resultErrors
|
||||
: [new Error('Unknown error')];
|
||||
if (saveErrors) {
|
||||
saveErrors(errors);
|
||||
} else {
|
||||
// We don't save because we're about to save below.
|
||||
this.saveErrors(errors, { skipSave: true });
|
||||
}
|
||||
} finally {
|
||||
await window.Signal.Data.saveMessage(this.attributes);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue