807 lines
23 KiB
TypeScript
807 lines
23 KiB
TypeScript
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import { createLogger } from '../logging/log';
|
|
import { isAudio, isImage, isLongMessage, isVideo } from '../types/MIME';
|
|
import { getMessageIdForLogging } from './idForLogging';
|
|
import {
|
|
copyStickerToAttachments,
|
|
savePackMetadata,
|
|
getStickerPackStatus,
|
|
} from '../types/Stickers';
|
|
import { DataWriter } from '../sql/Client';
|
|
|
|
import type { AttachmentType, ThumbnailType } from '../types/Attachment';
|
|
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
|
import type {
|
|
EditHistoryType,
|
|
MessageAttributesType,
|
|
QuotedMessageType,
|
|
} from '../model-types.d';
|
|
import * as Errors from '../types/errors';
|
|
import {
|
|
isDownloading,
|
|
isDownloaded,
|
|
isVoiceMessage,
|
|
partitionBodyAndNormalAttachments,
|
|
getCachedAttachmentBySignature,
|
|
cacheAttachmentBySignature,
|
|
getUndownloadedAttachmentSignature,
|
|
} from '../types/Attachment';
|
|
import { AttachmentDownloadUrgency } from '../types/AttachmentDownload';
|
|
import type { StickerType } from '../types/Stickers';
|
|
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
|
import { strictAssert } from './assert';
|
|
import { isNotNil } from './isNotNil';
|
|
import { AttachmentDownloadManager } from '../jobs/AttachmentDownloadManager';
|
|
import { AttachmentDownloadSource } from '../sql/Interface';
|
|
import type { MessageModel } from '../models/messages';
|
|
import type { ConversationModel } from '../models/conversations';
|
|
import { isOutgoing, isStory } from '../messages/helpers';
|
|
import { shouldDownloadStory } from './shouldDownloadStory';
|
|
import { hasAttachmentDownloads } from './hasAttachmentDownloads';
|
|
import {
|
|
addToAttachmentDownloadQueue,
|
|
shouldUseAttachmentDownloadQueue,
|
|
} from './attachmentDownloadQueue';
|
|
import { queueUpdateMessage } from './messageBatcher';
|
|
import type { LoggerType } from '../types/Logging';
|
|
import { DEFAULT_AUTO_DOWNLOAD_ATTACHMENT } from '../textsecure/Storage';
|
|
|
|
const defaultLogger = createLogger('queueAttachmentDownloads');
|
|
|
|
export type MessageAttachmentsDownloadedType = {
|
|
bodyAttachment?: AttachmentType;
|
|
attachments: ReadonlyArray<AttachmentType>;
|
|
editHistory?: ReadonlyArray<EditHistoryType>;
|
|
preview: ReadonlyArray<LinkPreviewType>;
|
|
contact: ReadonlyArray<EmbeddedContactType>;
|
|
quote?: QuotedMessageType;
|
|
sticker?: StickerType;
|
|
};
|
|
|
|
function getLogger(source: AttachmentDownloadSource) {
|
|
const verbose = source !== AttachmentDownloadSource.BACKUP_IMPORT;
|
|
const log = verbose ? defaultLogger : { ...defaultLogger, info: () => null };
|
|
return log;
|
|
}
|
|
|
|
export async function handleAttachmentDownloadsForNewMessage(
|
|
message: MessageModel,
|
|
conversation: ConversationModel
|
|
): Promise<void> {
|
|
const logId =
|
|
`handleAttachmentDownloadsForNewMessage/${conversation.idForLogging()} ` +
|
|
`${getMessageIdForLogging(message.attributes)}`;
|
|
|
|
// Only queue attachments for downloads if this is a story (with additional logic), or
|
|
// if it's either an outgoing message or we've accepted the conversation
|
|
let shouldQueueForDownload = false;
|
|
if (isStory(message.attributes)) {
|
|
shouldQueueForDownload = await shouldDownloadStory(conversation.attributes);
|
|
} else {
|
|
shouldQueueForDownload =
|
|
hasAttachmentDownloads(message.attributes) &&
|
|
(conversation.getAccepted() || isOutgoing(message.attributes));
|
|
}
|
|
|
|
if (shouldQueueForDownload) {
|
|
if (shouldUseAttachmentDownloadQueue()) {
|
|
addToAttachmentDownloadQueue(logId, message);
|
|
} else {
|
|
await queueAttachmentDownloadsForMessage(message, {
|
|
isManualDownload: false,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function queueAttachmentDownloadsForMessage(
|
|
message: MessageModel,
|
|
options: {
|
|
urgency?: AttachmentDownloadUrgency;
|
|
source?: AttachmentDownloadSource;
|
|
isManualDownload: boolean;
|
|
}
|
|
): Promise<boolean> {
|
|
const updated = await queueAttachmentDownloads(message, options);
|
|
if (!updated) {
|
|
return false;
|
|
}
|
|
|
|
queueUpdateMessage(message.attributes);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Receive logic
|
|
// NOTE: If you're changing any logic in this function that deals with the
|
|
// count then you'll also have to modify ./hasAttachmentsDownloads
|
|
export async function queueAttachmentDownloads(
|
|
message: MessageModel,
|
|
{
|
|
attachmentSignatureForImmediate,
|
|
isManualDownload,
|
|
source = AttachmentDownloadSource.STANDARD,
|
|
urgency = AttachmentDownloadUrgency.STANDARD,
|
|
}: {
|
|
attachmentSignatureForImmediate?: string;
|
|
isManualDownload: boolean;
|
|
source?: AttachmentDownloadSource;
|
|
urgency?: AttachmentDownloadUrgency;
|
|
}
|
|
): Promise<boolean> {
|
|
const autoDownloadAttachment = window.storage.get(
|
|
'auto-download-attachment',
|
|
DEFAULT_AUTO_DOWNLOAD_ATTACHMENT
|
|
);
|
|
|
|
const messageId = message.id;
|
|
const idForLogging = getMessageIdForLogging(message.attributes);
|
|
|
|
let count = 0;
|
|
|
|
const logId = `queueAttachmentDownloads(${idForLogging}})`;
|
|
const log = getLogger(source);
|
|
|
|
message.set(
|
|
ensureBodyAttachmentsAreSeparated(message.attributes, {
|
|
logId,
|
|
logger: log,
|
|
})
|
|
);
|
|
|
|
const bodyAttachmentsToDownload = [
|
|
message.get('bodyAttachment'),
|
|
...(message
|
|
.get('editHistory')
|
|
?.slice(1) // first entry is the same as the root level message!
|
|
.map(editHistory => editHistory.bodyAttachment) ?? []),
|
|
]
|
|
.filter(isNotNil)
|
|
.filter(attachment => !isDownloaded(attachment));
|
|
|
|
if (bodyAttachmentsToDownload.length) {
|
|
log.info(
|
|
`${logId}: Queueing ${bodyAttachmentsToDownload.length} long message attachment download`
|
|
);
|
|
await Promise.all(
|
|
bodyAttachmentsToDownload.map(attachment =>
|
|
AttachmentDownloadManager.addJob({
|
|
attachment,
|
|
attachmentType: 'long-message',
|
|
isManualDownload,
|
|
messageId,
|
|
receivedAt: message.get('received_at'),
|
|
sentAt: message.get('sent_at'),
|
|
source,
|
|
urgency,
|
|
})
|
|
)
|
|
);
|
|
count += bodyAttachmentsToDownload.length;
|
|
}
|
|
|
|
const startingAttachments = message.get('attachments') || [];
|
|
const { attachments, count: attachmentsCount } = await queueNormalAttachments(
|
|
{
|
|
attachmentSignatureForImmediate,
|
|
attachments: startingAttachments,
|
|
isManualDownload,
|
|
logId,
|
|
messageId,
|
|
otherAttachments: message
|
|
.get('editHistory')
|
|
?.flatMap(x => x.attachments ?? []),
|
|
receivedAt: message.get('received_at'),
|
|
sentAt: message.get('sent_at'),
|
|
source,
|
|
urgency,
|
|
}
|
|
);
|
|
|
|
if (attachmentsCount > 0) {
|
|
message.set({ attachments });
|
|
}
|
|
if (startingAttachments.length > 0) {
|
|
log.info(
|
|
`${logId}: Queued ${attachmentsCount} (of ${startingAttachments.length}) normal attachment downloads`
|
|
);
|
|
}
|
|
count += attachmentsCount;
|
|
|
|
const previewsToQueue = message.get('preview') || [];
|
|
const { preview, count: previewCount } = await queuePreviews({
|
|
logId,
|
|
isManualDownload,
|
|
messageId,
|
|
previews: previewsToQueue,
|
|
otherPreviews: message.get('editHistory')?.flatMap(x => x.preview ?? []),
|
|
receivedAt: message.get('received_at'),
|
|
sentAt: message.get('sent_at'),
|
|
urgency,
|
|
source,
|
|
});
|
|
if (previewCount > 0) {
|
|
message.set({ preview });
|
|
}
|
|
if (previewsToQueue.length > 0) {
|
|
log.info(
|
|
`${logId}: Queued ${previewCount} (of ${previewsToQueue.length}) preview attachment downloads`
|
|
);
|
|
}
|
|
count += previewCount;
|
|
|
|
const numQuoteAttachments = message.get('quote')?.attachments?.length ?? 0;
|
|
const { quote, count: thumbnailCount } = await queueQuoteAttachments({
|
|
logId,
|
|
isManualDownload,
|
|
messageId,
|
|
otherQuotes:
|
|
message
|
|
.get('editHistory')
|
|
?.map(x => x.quote)
|
|
.filter(isNotNil) ?? [],
|
|
quote: message.get('quote'),
|
|
receivedAt: message.get('received_at'),
|
|
sentAt: message.get('sent_at'),
|
|
source,
|
|
urgency,
|
|
});
|
|
if (thumbnailCount > 0) {
|
|
message.set({ quote });
|
|
}
|
|
if (numQuoteAttachments > 0) {
|
|
log.info(
|
|
`${logId}: Queued ${thumbnailCount} (of ${numQuoteAttachments}) quote attachment downloads`
|
|
);
|
|
}
|
|
count += thumbnailCount;
|
|
|
|
const contactsToQueue = message.get('contact') || [];
|
|
let avatarCount = 0;
|
|
const contact = await Promise.all(
|
|
contactsToQueue.map(async item => {
|
|
if (!item.avatar || !item.avatar.avatar) {
|
|
return item;
|
|
}
|
|
// We've already downloaded this!
|
|
if (item.avatar.avatar.path) {
|
|
log.info(`${logId}: Contact attachment already downloaded`);
|
|
return item;
|
|
}
|
|
|
|
if (!isManualDownload) {
|
|
if (autoDownloadAttachment.photos === false) {
|
|
return item;
|
|
}
|
|
}
|
|
|
|
avatarCount += 1;
|
|
return {
|
|
...item,
|
|
avatar: {
|
|
...item.avatar,
|
|
avatar: await AttachmentDownloadManager.addJob({
|
|
attachment: item.avatar.avatar,
|
|
attachmentType: 'contact',
|
|
isManualDownload,
|
|
messageId,
|
|
receivedAt: message.get('received_at'),
|
|
sentAt: message.get('sent_at'),
|
|
source,
|
|
urgency,
|
|
}),
|
|
},
|
|
};
|
|
})
|
|
);
|
|
if (avatarCount > 0) {
|
|
message.set({ contact });
|
|
}
|
|
if (contactsToQueue.length > 0) {
|
|
log.info(
|
|
`${logId}: Queued ${avatarCount} (of ${contactsToQueue.length}) contact attachment downloads`
|
|
);
|
|
}
|
|
count += avatarCount;
|
|
|
|
let sticker = message.get('sticker');
|
|
let copiedSticker = false;
|
|
if (sticker && sticker.data && sticker.data.path) {
|
|
log.info(`${logId}: Sticker attachment already downloaded`);
|
|
} else if (sticker) {
|
|
count += 1;
|
|
const { packId, stickerId, packKey } = sticker;
|
|
|
|
const status = getStickerPackStatus(packId);
|
|
|
|
if (status && (status === 'downloaded' || status === 'installed')) {
|
|
try {
|
|
log.info(`${logId}: Copying sticker from installed pack`);
|
|
copiedSticker = true;
|
|
const data = await copyStickerToAttachments(packId, stickerId);
|
|
|
|
// Refresh sticker attachment since we had to await above
|
|
const freshSticker = message.get('sticker');
|
|
strictAssert(freshSticker != null, 'Sticker is gone while copying');
|
|
sticker = {
|
|
...freshSticker,
|
|
data,
|
|
};
|
|
message.set({
|
|
sticker,
|
|
});
|
|
} catch (error) {
|
|
log.error(
|
|
`${logId}: Problem copying sticker (${packId}, ${stickerId}) to attachments:`,
|
|
Errors.toLogFormat(error)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!copiedSticker) {
|
|
if (sticker.data) {
|
|
log.info(`${logId}: Queueing sticker download`);
|
|
await AttachmentDownloadManager.addJob({
|
|
attachment: sticker.data,
|
|
attachmentType: 'sticker',
|
|
isManualDownload,
|
|
messageId,
|
|
receivedAt: message.get('received_at'),
|
|
sentAt: message.get('sent_at'),
|
|
source,
|
|
urgency,
|
|
});
|
|
} else {
|
|
log.error(`${logId}: Sticker data was missing`);
|
|
}
|
|
}
|
|
const stickerRef = {
|
|
messageId,
|
|
packId,
|
|
stickerId,
|
|
isUnresolved: sticker.data?.error === true,
|
|
};
|
|
if (!status) {
|
|
// Save the packId/packKey for future download/install
|
|
await savePackMetadata(packId, packKey, stickerRef);
|
|
} else {
|
|
await DataWriter.addStickerPackReference(stickerRef);
|
|
}
|
|
}
|
|
|
|
let editHistory = message.get('editHistory');
|
|
|
|
let allEditsAttachmentCount = 0;
|
|
if (editHistory) {
|
|
log.info(`${logId}: Looping through ${editHistory.length} edits`);
|
|
editHistory = await Promise.all(
|
|
editHistory.map(async edit => {
|
|
const { attachments: editAttachments, count: editAttachmentsCount } =
|
|
await queueNormalAttachments({
|
|
attachments: edit.attachments,
|
|
isManualDownload,
|
|
logId,
|
|
messageId,
|
|
otherAttachments: attachments,
|
|
receivedAt: message.get('received_at'),
|
|
sentAt: message.get('sent_at'),
|
|
source,
|
|
urgency,
|
|
});
|
|
count += editAttachmentsCount;
|
|
allEditsAttachmentCount += editAttachmentsCount;
|
|
if (editAttachments.length !== 0) {
|
|
log.info(
|
|
`${logId}: Queued ${editAttachmentsCount} (of ${edit.attachments?.length ?? 0}) ` +
|
|
`normal attachment downloads (edited:${edit.timestamp})`
|
|
);
|
|
}
|
|
|
|
const { preview: editPreview, count: editPreviewCount } =
|
|
await queuePreviews({
|
|
logId,
|
|
isManualDownload,
|
|
messageId,
|
|
previews: edit.preview,
|
|
otherPreviews: preview,
|
|
receivedAt: message.get('received_at'),
|
|
sentAt: message.get('sent_at'),
|
|
urgency,
|
|
source,
|
|
});
|
|
count += editPreviewCount;
|
|
allEditsAttachmentCount += editPreviewCount;
|
|
if (editPreview.length !== 0) {
|
|
log.info(
|
|
`${logId}: Queued ${editPreviewCount} (of ${edit.preview?.length ?? 0}) ` +
|
|
`preview attachment downloads (edited:${edit.timestamp})`
|
|
);
|
|
}
|
|
|
|
return {
|
|
...edit,
|
|
attachments: editAttachments,
|
|
preview: editPreview,
|
|
};
|
|
})
|
|
);
|
|
}
|
|
if (allEditsAttachmentCount > 0) {
|
|
message.set({ editHistory });
|
|
}
|
|
|
|
if (count <= 0) {
|
|
return false;
|
|
}
|
|
|
|
log.info(`${logId}: Queued ${count} total attachment downloads`);
|
|
|
|
return true;
|
|
}
|
|
|
|
export async function queueNormalAttachments({
|
|
attachmentSignatureForImmediate,
|
|
attachments = [],
|
|
isManualDownload,
|
|
logId,
|
|
messageId,
|
|
otherAttachments,
|
|
receivedAt,
|
|
sentAt,
|
|
source,
|
|
urgency,
|
|
}: {
|
|
attachmentSignatureForImmediate?: string;
|
|
attachments: MessageAttributesType['attachments'];
|
|
isManualDownload: boolean;
|
|
logId: string;
|
|
messageId: string;
|
|
otherAttachments: MessageAttributesType['attachments'];
|
|
receivedAt: number;
|
|
sentAt: number;
|
|
source: AttachmentDownloadSource;
|
|
urgency: AttachmentDownloadUrgency;
|
|
}): Promise<{
|
|
attachments: Array<AttachmentType>;
|
|
count: number;
|
|
}> {
|
|
const log = getLogger(source);
|
|
// Look through "otherAttachments" which can either be attachments in the
|
|
// edit history or the message's attachments and see if any of the attachments
|
|
// are the same. If they are let's replace it so that we don't download more
|
|
// than once.
|
|
// We don't also register the signatures for "attachments" because they would
|
|
// then not be added to the AttachmentDownloads job.
|
|
const attachmentSignatures: Map<string, AttachmentType> = new Map();
|
|
otherAttachments?.forEach(attachment => {
|
|
cacheAttachmentBySignature(attachmentSignatures, attachment);
|
|
});
|
|
|
|
let count = 0;
|
|
const nextAttachments = await Promise.all(
|
|
attachments.map(attachment => {
|
|
if (!attachment) {
|
|
return attachment;
|
|
}
|
|
|
|
if (isLongMessage(attachment.contentType)) {
|
|
throw new Error(
|
|
`${logId}: queueNormalAttachments passed long-message attachment`
|
|
);
|
|
}
|
|
|
|
// We've already downloaded this!
|
|
if (isDownloaded(attachment)) {
|
|
log.info(`${logId}: Normal attachment already downloaded`);
|
|
return attachment;
|
|
}
|
|
|
|
const existingAttachment = getCachedAttachmentBySignature(
|
|
attachmentSignatures,
|
|
attachment
|
|
);
|
|
|
|
// We've already downloaded this elsewhere!
|
|
if (
|
|
existingAttachment &&
|
|
(isDownloading(existingAttachment) || isDownloaded(existingAttachment))
|
|
) {
|
|
log.info(
|
|
`${logId}: Normal attachment already downloaded in other attachments. Replacing`
|
|
);
|
|
// Incrementing count so that we update the message's fields downstream
|
|
count += 1;
|
|
return existingAttachment;
|
|
}
|
|
|
|
const { contentType } = attachment;
|
|
if (!isManualDownload) {
|
|
const autoDownloadAttachment = window.storage.get(
|
|
'auto-download-attachment',
|
|
DEFAULT_AUTO_DOWNLOAD_ATTACHMENT
|
|
);
|
|
|
|
if (isVideo(contentType)) {
|
|
if (autoDownloadAttachment.videos === false) {
|
|
return attachment;
|
|
}
|
|
} else if (isImage(contentType)) {
|
|
if (autoDownloadAttachment.photos === false) {
|
|
return attachment;
|
|
}
|
|
} else if (isAudio(contentType)) {
|
|
if (
|
|
autoDownloadAttachment.audio === false &&
|
|
!isVoiceMessage(attachment)
|
|
) {
|
|
return attachment;
|
|
}
|
|
} else if (autoDownloadAttachment.documents === false) {
|
|
return attachment;
|
|
}
|
|
}
|
|
|
|
count += 1;
|
|
|
|
const urgencyForAttachment =
|
|
attachmentSignatureForImmediate &&
|
|
attachmentSignatureForImmediate ===
|
|
getUndownloadedAttachmentSignature(attachment)
|
|
? AttachmentDownloadUrgency.IMMEDIATE
|
|
: urgency;
|
|
return AttachmentDownloadManager.addJob({
|
|
attachment,
|
|
attachmentType: 'attachment',
|
|
isManualDownload,
|
|
messageId,
|
|
receivedAt,
|
|
sentAt,
|
|
source,
|
|
urgency: urgencyForAttachment,
|
|
});
|
|
})
|
|
);
|
|
|
|
return {
|
|
attachments: nextAttachments,
|
|
count,
|
|
};
|
|
}
|
|
|
|
async function queuePreviews({
|
|
isManualDownload,
|
|
logId,
|
|
messageId,
|
|
otherPreviews,
|
|
previews = [],
|
|
receivedAt,
|
|
sentAt,
|
|
source,
|
|
urgency,
|
|
}: {
|
|
isManualDownload: boolean;
|
|
logId: string;
|
|
messageId: string;
|
|
otherPreviews: MessageAttributesType['preview'];
|
|
previews: MessageAttributesType['preview'];
|
|
receivedAt: number;
|
|
sentAt: number;
|
|
source: AttachmentDownloadSource;
|
|
urgency: AttachmentDownloadUrgency;
|
|
}): Promise<{ preview: Array<LinkPreviewType>; count: number }> {
|
|
const log = getLogger(source);
|
|
const previewSignatures: Map<string, AttachmentType> = new Map();
|
|
otherPreviews?.forEach(preview => {
|
|
if (preview.image) {
|
|
cacheAttachmentBySignature(previewSignatures, preview.image);
|
|
}
|
|
});
|
|
|
|
let count = 0;
|
|
|
|
const preview = await Promise.all(
|
|
previews.map(async item => {
|
|
if (!item.image) {
|
|
return item;
|
|
}
|
|
// We've already downloaded this!
|
|
if (isDownloaded(item.image)) {
|
|
log.info(`${logId}: Preview attachment already downloaded`);
|
|
return item;
|
|
}
|
|
|
|
const existingPreviewImage = getCachedAttachmentBySignature(
|
|
previewSignatures,
|
|
item.image
|
|
);
|
|
|
|
// We've already downloaded this elsewhere!
|
|
if (
|
|
existingPreviewImage &&
|
|
(isDownloading(existingPreviewImage) ||
|
|
isDownloaded(existingPreviewImage))
|
|
) {
|
|
log.info(`${logId}: Preview already downloaded elsewhere. Replacing`);
|
|
// Incrementing count so that we update the message's fields downstream
|
|
count += 1;
|
|
return { ...item, image: existingPreviewImage };
|
|
}
|
|
|
|
if (!isManualDownload) {
|
|
const autoDownloadAttachment = window.storage.get(
|
|
'auto-download-attachment',
|
|
DEFAULT_AUTO_DOWNLOAD_ATTACHMENT
|
|
);
|
|
|
|
if (autoDownloadAttachment.photos === false) {
|
|
return item;
|
|
}
|
|
}
|
|
|
|
count += 1;
|
|
return {
|
|
...item,
|
|
image: await AttachmentDownloadManager.addJob({
|
|
attachment: item.image,
|
|
attachmentType: 'preview',
|
|
isManualDownload,
|
|
messageId,
|
|
receivedAt,
|
|
sentAt,
|
|
source,
|
|
urgency,
|
|
}),
|
|
};
|
|
})
|
|
);
|
|
|
|
return {
|
|
preview,
|
|
count,
|
|
};
|
|
}
|
|
|
|
async function queueQuoteAttachments({
|
|
isManualDownload,
|
|
logId,
|
|
messageId,
|
|
otherQuotes,
|
|
quote,
|
|
receivedAt,
|
|
sentAt,
|
|
source,
|
|
urgency,
|
|
}: {
|
|
logId: string;
|
|
isManualDownload: boolean;
|
|
messageId: string;
|
|
otherQuotes: ReadonlyArray<QuotedMessageType>;
|
|
quote: QuotedMessageType | undefined;
|
|
receivedAt: number;
|
|
sentAt: number;
|
|
source: AttachmentDownloadSource;
|
|
urgency: AttachmentDownloadUrgency;
|
|
}): Promise<{ quote?: QuotedMessageType; count: number }> {
|
|
const log = getLogger(source);
|
|
let count = 0;
|
|
if (!quote) {
|
|
return { quote, count };
|
|
}
|
|
|
|
const quoteAttachmentsToQueue =
|
|
quote && quote.attachments ? quote.attachments : [];
|
|
if (quoteAttachmentsToQueue.length === 0) {
|
|
return { quote, count };
|
|
}
|
|
|
|
// Similar to queueNormalAttachments' logic for detecting same attachments
|
|
// except here we also pick by quote sent timestamp.
|
|
const thumbnailSignatures: Map<string, ThumbnailType> = new Map();
|
|
otherQuotes.forEach(otherQuote => {
|
|
for (const attachment of otherQuote.attachments) {
|
|
if (attachment.thumbnail) {
|
|
cacheAttachmentBySignature(thumbnailSignatures, attachment.thumbnail);
|
|
}
|
|
}
|
|
});
|
|
|
|
return {
|
|
quote: {
|
|
...quote,
|
|
attachments: await Promise.all(
|
|
quote.attachments.map(async item => {
|
|
if (!item.thumbnail) {
|
|
return item;
|
|
}
|
|
// We've already downloaded this!
|
|
if (isDownloaded(item.thumbnail)) {
|
|
log.info(`${logId}: Quote attachment already downloaded`);
|
|
return item;
|
|
}
|
|
|
|
const existingThumbnail = getCachedAttachmentBySignature(
|
|
thumbnailSignatures,
|
|
item.thumbnail
|
|
);
|
|
|
|
// We've already downloaded this elsewhere!
|
|
if (
|
|
existingThumbnail &&
|
|
(isDownloading(existingThumbnail) ||
|
|
isDownloaded(existingThumbnail))
|
|
) {
|
|
log.info(
|
|
`${logId}: Preview already downloaded elsewhere. Replacing`
|
|
);
|
|
// Incrementing count so that we update the message's fields downstream
|
|
count += 1;
|
|
return {
|
|
...item,
|
|
thumbnail: existingThumbnail,
|
|
};
|
|
}
|
|
|
|
// Note: we always download quote attachments
|
|
|
|
count += 1;
|
|
return {
|
|
...item,
|
|
thumbnail: await AttachmentDownloadManager.addJob({
|
|
attachment: item.thumbnail,
|
|
attachmentType: 'quote',
|
|
isManualDownload,
|
|
messageId,
|
|
receivedAt,
|
|
sentAt,
|
|
source,
|
|
urgency,
|
|
}),
|
|
};
|
|
})
|
|
),
|
|
},
|
|
count,
|
|
};
|
|
}
|
|
|
|
export function ensureBodyAttachmentsAreSeparated(
|
|
messageAttributes: MessageAttributesType,
|
|
{ logId, logger = defaultLogger }: { logId: string; logger?: LoggerType }
|
|
): {
|
|
bodyAttachment: AttachmentType | undefined;
|
|
attachments: Array<AttachmentType>;
|
|
editHistory: Array<EditHistoryType> | undefined;
|
|
} {
|
|
const { bodyAttachment, attachments } = partitionBodyAndNormalAttachments(
|
|
{
|
|
attachments: messageAttributes.attachments ?? [],
|
|
existingBodyAttachment: messageAttributes.bodyAttachment,
|
|
},
|
|
{ logId, logger }
|
|
);
|
|
|
|
const updatedEditHistory = messageAttributes.editHistory?.map(edit => {
|
|
return {
|
|
...edit,
|
|
...partitionBodyAndNormalAttachments(
|
|
{
|
|
attachments: edit.attachments ?? [],
|
|
existingBodyAttachment: edit.bodyAttachment,
|
|
},
|
|
{
|
|
logId: `${logId}/editHistory(${edit.timestamp})`,
|
|
logger,
|
|
}
|
|
),
|
|
};
|
|
});
|
|
|
|
return {
|
|
bodyAttachment: bodyAttachment ?? messageAttributes.bodyAttachment,
|
|
attachments,
|
|
editHistory: updatedEditHistory,
|
|
};
|
|
}
|