162 lines
4.2 KiB
TypeScript
162 lines
4.2 KiB
TypeScript
// Copyright 2024 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import { mapValues, pick } from 'lodash';
|
|
|
|
import type { CustomError } from '../../textsecure/Types';
|
|
|
|
import type { MessageAttributesType } from '../../model-types';
|
|
import { createLogger } from '../../logging/log';
|
|
import * as Errors from '../../types/errors';
|
|
import {
|
|
getChangesForPropAtTimestamp,
|
|
getPropForTimestamp,
|
|
} from '../../util/editHelpers';
|
|
import {
|
|
isSent,
|
|
SendActionType,
|
|
sendStateReducer,
|
|
someRecipientSendStatus,
|
|
} from '../../messages/MessageSendState';
|
|
import { isStory } from '../../messages/helpers';
|
|
import {
|
|
notificationService,
|
|
NotificationType,
|
|
} from '../../services/notifications';
|
|
import type { MessageModel } from '../../models/messages';
|
|
|
|
const log = createLogger('messageFailures');
|
|
|
|
export async function saveErrorsOnMessage(
|
|
message: MessageModel,
|
|
providedErrors: Error | Array<Error>,
|
|
options: { skipSave?: boolean } = {}
|
|
): Promise<void> {
|
|
const { skipSave } = options;
|
|
|
|
let errors: Array<CustomError>;
|
|
|
|
if (!(providedErrors instanceof Array)) {
|
|
errors = [providedErrors];
|
|
} else {
|
|
errors = providedErrors;
|
|
}
|
|
|
|
errors.forEach(e => {
|
|
log.error('Message.saveErrors:', Errors.toLogFormat(e));
|
|
});
|
|
errors = errors.map(e => {
|
|
// Note: in our environment, instanceof can be scary, so we have a backup check
|
|
// (Node.js vs Browser context).
|
|
// We check instanceof second because typescript believes that anything that comes
|
|
// through here must be an instance of Error, so e is 'never' after that check.
|
|
if ((e.message && e.stack) || e instanceof Error) {
|
|
return pick(
|
|
e,
|
|
'name',
|
|
'message',
|
|
'code',
|
|
'number',
|
|
'identifier',
|
|
'retryAfter',
|
|
'data',
|
|
'reason'
|
|
) as Required<Error>;
|
|
}
|
|
return e;
|
|
});
|
|
|
|
message.set({
|
|
errors: errors.concat(message.get('errors') || []),
|
|
});
|
|
|
|
if (!skipSave) {
|
|
await window.MessageCache.saveMessage(message);
|
|
}
|
|
}
|
|
|
|
export function isReplayableError(e: Error): boolean {
|
|
return (
|
|
e.name === 'MessageError' ||
|
|
e.name === 'OutgoingMessageError' ||
|
|
e.name === 'SendMessageNetworkError' ||
|
|
e.name === 'SendMessageChallengeError' ||
|
|
e.name === 'OutgoingIdentityKeyError'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Change any Pending send state to Failed. Note that this will not mark successful
|
|
* sends failed.
|
|
*/
|
|
export function markFailed(
|
|
message: MessageModel,
|
|
editMessageTimestamp?: number
|
|
): void {
|
|
const now = Date.now();
|
|
|
|
const targetTimestamp = editMessageTimestamp || message.get('timestamp');
|
|
const sendStateByConversationId = getPropForTimestamp({
|
|
log,
|
|
message: message.attributes,
|
|
prop: 'sendStateByConversationId',
|
|
targetTimestamp,
|
|
});
|
|
|
|
const newSendStateByConversationId = mapValues(
|
|
sendStateByConversationId || {},
|
|
sendState =>
|
|
sendStateReducer(sendState, {
|
|
type: SendActionType.Failed,
|
|
updatedAt: now,
|
|
})
|
|
);
|
|
|
|
const updates = getChangesForPropAtTimestamp({
|
|
log,
|
|
message: message.attributes,
|
|
prop: 'sendStateByConversationId',
|
|
targetTimestamp,
|
|
value: newSendStateByConversationId,
|
|
});
|
|
if (updates) {
|
|
message.set(updates);
|
|
}
|
|
|
|
notifyStorySendFailed(message);
|
|
}
|
|
|
|
export function notifyStorySendFailed(message: MessageModel): void {
|
|
if (!isStory(message.attributes)) {
|
|
return;
|
|
}
|
|
|
|
const { conversationId, id, timestamp } = message.attributes;
|
|
const conversation = window.ConversationController.get(conversationId);
|
|
|
|
notificationService.add({
|
|
conversationId,
|
|
storyId: id,
|
|
messageId: id,
|
|
senderTitle: conversation?.getTitle() ?? window.i18n('icu:Stories__mine'),
|
|
message: hasSuccessfulDelivery(message.attributes)
|
|
? window.i18n('icu:Stories__failed-send--partial')
|
|
: window.i18n('icu:Stories__failed-send--full'),
|
|
isExpiringMessage: false,
|
|
sentAt: timestamp,
|
|
type: NotificationType.Message,
|
|
});
|
|
}
|
|
|
|
function hasSuccessfulDelivery(message: MessageAttributesType): boolean {
|
|
const { sendStateByConversationId } = message;
|
|
const ourConversationId =
|
|
window.ConversationController.getOurConversationIdOrThrow();
|
|
|
|
return someRecipientSendStatus(
|
|
sendStateByConversationId ?? {},
|
|
ourConversationId,
|
|
isSent
|
|
);
|
|
}
|