191 lines
5.6 KiB
TypeScript
191 lines
5.6 KiB
TypeScript
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import { PlaintextContent } from '@signalapp/libsignal-client';
|
|
|
|
import { handleMessageSend } from '../../util/handleMessageSend';
|
|
import { getSendOptions } from '../../util/getSendOptions';
|
|
import { isDirectConversation } from '../../util/whatTypeOfConversation';
|
|
import { SignalService as Proto } from '../../protobuf';
|
|
import {
|
|
handleMultipleSendErrors,
|
|
maybeExpandErrors,
|
|
} from './handleMultipleSendErrors';
|
|
|
|
import type { ConversationModel } from '../../models/conversations';
|
|
import type {
|
|
ConversationQueueJobBundle,
|
|
ResendRequestJobData,
|
|
} from '../conversationJobQueue';
|
|
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
|
import {
|
|
OutgoingIdentityKeyError,
|
|
UnregisteredUserError,
|
|
} from '../../textsecure/Errors';
|
|
import { drop } from '../../util/drop';
|
|
import { strictAssert } from '../../util/assert';
|
|
import type { DecryptionErrorEventData } from '../../textsecure/messageReceiverEvents';
|
|
import type { LoggerType } from '../../types/Logging';
|
|
import { startAutomaticSessionReset } from '../../util/handleRetry';
|
|
|
|
function failoverToLocalReset(
|
|
logger: LoggerType,
|
|
options: Pick<
|
|
DecryptionErrorEventData,
|
|
'senderAci' | 'senderDevice' | 'timestamp'
|
|
>
|
|
) {
|
|
logger.error('Failing over to local reset');
|
|
startAutomaticSessionReset(options);
|
|
}
|
|
|
|
export async function sendResendRequest(
|
|
conversation: ConversationModel,
|
|
{
|
|
isFinalAttempt,
|
|
messaging,
|
|
shouldContinue,
|
|
timeRemaining,
|
|
log,
|
|
}: ConversationQueueJobBundle,
|
|
{ senderAci, ...restOfData }: ResendRequestJobData
|
|
): Promise<void> {
|
|
const data = {
|
|
...restOfData,
|
|
senderAci,
|
|
};
|
|
|
|
const {
|
|
contentHint,
|
|
groupId,
|
|
plaintext: plaintextBase64,
|
|
receivedAtCounter,
|
|
receivedAtDate,
|
|
timestamp,
|
|
} = data;
|
|
|
|
if (!shouldContinue) {
|
|
log.info('Ran out of time. Giving up on sending resend request');
|
|
failoverToLocalReset(log, data);
|
|
return;
|
|
}
|
|
|
|
log.info(
|
|
`starting resend request send to ${conversation.idForLogging()} with timestamp ${timestamp}`
|
|
);
|
|
|
|
if (!isDirectConversation(conversation.attributes)) {
|
|
log.error('conversation is not direct, cancelling job.');
|
|
return;
|
|
}
|
|
|
|
if (isConversationUnregistered(conversation.attributes)) {
|
|
log.error('conversation is unregistered, cancelling job.');
|
|
failoverToLocalReset(log, data);
|
|
return;
|
|
}
|
|
|
|
// Note: we will send to blocked users, to those still in message request state, etc.
|
|
// Any needed blocking should still apply once the decryption error is fixed.
|
|
|
|
if (conversation.getAci() !== senderAci) {
|
|
log.error('conversation was missing a aci, cancelling job.');
|
|
failoverToLocalReset(log, data);
|
|
return;
|
|
}
|
|
|
|
const plaintext = PlaintextContent.deserialize(
|
|
Buffer.from(plaintextBase64, 'base64')
|
|
);
|
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
|
|
|
// We run this job on the queue for the individual sender we want the resend from, but
|
|
// the original message might have been sent in a group - and that's where we'll put
|
|
// the error or placeholder.
|
|
const groupConversationId = window.ConversationController.get(groupId)?.id;
|
|
const targetConversationId = groupConversationId ?? conversation.get('id');
|
|
|
|
try {
|
|
const options = await getSendOptions(conversation.attributes);
|
|
await handleMessageSend(
|
|
messaging.sendMessageProtoAndWait({
|
|
timestamp,
|
|
recipients: [senderAci],
|
|
proto: plaintext,
|
|
contentHint: ContentHint.DEFAULT,
|
|
groupId,
|
|
options,
|
|
urgent: false,
|
|
}),
|
|
{ messageIds: [], sendType: 'retryRequest' }
|
|
);
|
|
|
|
// Now that we've successfully sent, represent this to the user. Three options:
|
|
|
|
// 1. We believe that it could be successfully re-sent, so we'll add a placeholder.
|
|
if (contentHint === ContentHint.RESENDABLE) {
|
|
const { retryPlaceholders } = window.Signal.Services;
|
|
strictAssert(retryPlaceholders, 'sendResendRequest: adding placeholder');
|
|
|
|
log.info('contentHint is RESENDABLE, adding placeholder');
|
|
|
|
const state = window.reduxStore.getState();
|
|
const selectedId = state.conversations.selectedConversationId;
|
|
const wasOpened = selectedId === targetConversationId;
|
|
|
|
await retryPlaceholders.add({
|
|
conversationId: targetConversationId,
|
|
receivedAt: receivedAtDate,
|
|
receivedAtCounter,
|
|
sentAt: timestamp,
|
|
senderAci,
|
|
wasOpened,
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
// 2. This message cannot be resent. We'll show no error and trust the other side to
|
|
// reset their session.
|
|
if (contentHint === ContentHint.IMPLICIT) {
|
|
log.info('contentHint is IMPLICIT, adding no timeline item.');
|
|
return;
|
|
}
|
|
|
|
// 3. We don't know what kind of message this was, and add an eror
|
|
log.warn('No contentHint, adding error in conversation immediately');
|
|
drop(
|
|
conversation.queueJob('addDeliveryIssue', async () => {
|
|
await conversation.addDeliveryIssue({
|
|
receivedAt: receivedAtDate,
|
|
receivedAtCounter,
|
|
senderAci,
|
|
sentAt: timestamp,
|
|
});
|
|
})
|
|
);
|
|
} catch (error: unknown) {
|
|
if (
|
|
error instanceof OutgoingIdentityKeyError ||
|
|
error instanceof UnregisteredUserError
|
|
) {
|
|
log.info(
|
|
'Group send failures were all OutgoingIdentityKeyError or UnregisteredUserError. Cancelling job.'
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
if (isFinalAttempt) {
|
|
failoverToLocalReset(log, data);
|
|
}
|
|
|
|
await handleMultipleSendErrors({
|
|
errors: maybeExpandErrors(error),
|
|
isFinalAttempt,
|
|
log,
|
|
timeRemaining,
|
|
toThrow: error,
|
|
});
|
|
}
|
|
}
|