diff --git a/ts/messages/handleDataMessage.ts b/ts/messages/handleDataMessage.ts index 6455ba31c..594c62ddf 100644 --- a/ts/messages/handleDataMessage.ts +++ b/ts/messages/handleDataMessage.ts @@ -1,11 +1,10 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { isNumber, partition } from 'lodash'; +import { isNumber } from 'lodash'; import * as log from '../logging/log'; import * as Errors from '../types/errors'; -import * as MIME from '../types/MIME'; import * as LinkPreview from '../types/LinkPreview'; import { getAuthor, isStory, messageHasPaymentEvent } from './helpers'; @@ -531,22 +530,16 @@ export async function handleDataMessage( const ourPni = window.textsecure.storage.user.getCheckedPni(); const ourServiceIds: Set = new Set([ourAci, ourPni]); - const [longMessageAttachments, normalAttachments] = partition( - dataMessage.attachments ?? [], - attachment => MIME.isLongMessage(attachment.contentType) - ); - const bodyAttachment = longMessageAttachments[0]; - // eslint-disable-next-line no-param-reassign message = window.MessageCache.register(message); message.set({ id: messageId, - attachments: normalAttachments, - bodyAttachment, + attachments: dataMessage.attachments, + bodyAttachment: dataMessage.bodyAttachment, // We don't want to trim if we'll be downloading a body attachment; we might // drop bodyRanges which apply to the longer text we'll get in that download. - ...(bodyAttachment + ...(dataMessage.bodyAttachment ? { body: dataMessage.body, bodyRanges: dataMessage.bodyRanges, diff --git a/ts/test-both/processDataMessage_test.ts b/ts/test-both/processDataMessage_test.ts index fb1b7c729..eb298f7b3 100644 --- a/ts/test-both/processDataMessage_test.ts +++ b/ts/test-both/processDataMessage_test.ts @@ -11,7 +11,7 @@ import { } from '../textsecure/processDataMessage'; import type { ProcessedAttachment } from '../textsecure/Types.d'; import { SignalService as Proto } from '../protobuf'; -import { IMAGE_GIF, IMAGE_JPEG } from '../types/MIME'; +import { IMAGE_GIF, IMAGE_JPEG, LONG_MESSAGE } from '../types/MIME'; import { generateAci } from '../types/ServiceId'; import { uuidToBytes } from '../util/uuidToBytes'; @@ -86,6 +86,30 @@ describe('processDataMessage', () => { ]); }); + it('should move long text attachments to bodyAttachment', () => { + const out = check({ + attachments: [ + UNPROCESSED_ATTACHMENT, + { + ...UNPROCESSED_ATTACHMENT, + contentType: LONG_MESSAGE, + }, + ], + }); + + assert.deepStrictEqual(out.attachments, [ + { + ...PROCESSED_ATTACHMENT, + downloadPath: 'random-path', + }, + ]); + assert.deepStrictEqual(out.bodyAttachment, { + ...PROCESSED_ATTACHMENT, + downloadPath: 'random-path', + contentType: LONG_MESSAGE, + }); + }); + it('should process attachments with incrementalMac/chunkSize', () => { const out = check({ attachments: [ diff --git a/ts/test-node/util/queueAttachmentDownloads_test.ts b/ts/test-node/util/queueAttachmentDownloads_test.ts new file mode 100644 index 000000000..b8baf5752 --- /dev/null +++ b/ts/test-node/util/queueAttachmentDownloads_test.ts @@ -0,0 +1,162 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { assert } from 'chai'; +import type { MessageAttributesType } from '../../model-types'; +import type { AttachmentType } from '../../types/Attachment'; +import { IMAGE_JPEG, LONG_MESSAGE } from '../../types/MIME'; +import { generateMessageId } from '../../util/generateMessageId'; +import { ensureBodyAttachmentsAreSeparated } from '../../util/queueAttachmentDownloads'; +import * as logger from '../../logging/log'; + +export function composeMessage( + overrides?: Partial +): MessageAttributesType { + return { + ...generateMessageId(Date.now()), + sent_at: Date.now(), + timestamp: Date.now(), + type: 'incoming', + conversationId: 'conversationId', + ...overrides, + }; +} +export function composeAttachment( + overrides?: Partial +): AttachmentType { + return { + size: 100, + contentType: IMAGE_JPEG, + ...overrides, + }; +} + +describe('ensureBodyAttachmentsAreSeparated', () => { + it('separates first body attachment out, and drops any additional ones', () => { + const msg = composeMessage({ + attachments: [ + composeAttachment({ + clientUuid: 'normal attachment', + contentType: IMAGE_JPEG, + }), + composeAttachment({ + clientUuid: 'long message 1', + contentType: LONG_MESSAGE, + }), + composeAttachment({ + clientUuid: 'long message 2', + contentType: LONG_MESSAGE, + }), + ], + }); + const result = ensureBodyAttachmentsAreSeparated(msg, { + logId: 'test', + logger, + }); + assert.deepEqual(result.attachments, [msg.attachments?.[0]]); + assert.deepEqual(result.bodyAttachment, msg.attachments?.[1]); + }); + it('retains existing bodyAttachment', () => { + const msg = composeMessage({ + bodyAttachment: composeAttachment({ + clientUuid: 'existing body attachment', + contentType: LONG_MESSAGE, + }), + attachments: [ + composeAttachment({ + clientUuid: 'normal attachment', + contentType: IMAGE_JPEG, + }), + composeAttachment({ + clientUuid: 'long message 1', + contentType: LONG_MESSAGE, + }), + ], + }); + const result = ensureBodyAttachmentsAreSeparated(msg, { + logId: 'test', + logger, + }); + assert.deepEqual(result.attachments, [msg.attachments?.[0]]); + assert.deepEqual(result.bodyAttachment, msg.bodyAttachment); + }); + it('separates first body attachment out for all editHistory', () => { + const msg = composeMessage({ + attachments: [ + composeAttachment({ + clientUuid: 'normal attachment', + contentType: IMAGE_JPEG, + }), + composeAttachment({ + clientUuid: 'long message attachment 1', + contentType: LONG_MESSAGE, + }), + composeAttachment({ + clientUuid: 'long message attachment 2', + contentType: LONG_MESSAGE, + }), + ], + editHistory: [ + { + timestamp: Date.now(), + received_at: Date.now(), + attachments: [ + composeAttachment({ + clientUuid: 'edit attachment', + contentType: IMAGE_JPEG, + }), + composeAttachment({ + clientUuid: 'long message attachment 1', + contentType: LONG_MESSAGE, + }), + composeAttachment({ + clientUuid: 'long message attachment 2', + contentType: LONG_MESSAGE, + }), + ], + }, + { + timestamp: Date.now(), + received_at: Date.now(), + bodyAttachment: composeAttachment({ + clientUuid: 'long message attachment already as bodyattachment', + contentType: LONG_MESSAGE, + }), + attachments: [ + composeAttachment({ + clientUuid: 'edit attachment 1', + contentType: IMAGE_JPEG, + }), + composeAttachment({ + clientUuid: 'edit attachment 2', + contentType: IMAGE_JPEG, + }), + ], + }, + ], + }); + const result = ensureBodyAttachmentsAreSeparated(msg, { + logId: 'test', + logger, + }); + + assert.deepEqual(result.attachments, [msg.attachments?.[0]]); + assert.deepEqual(result.bodyAttachment, msg.attachments?.[1]); + assert.deepEqual(result.editHistory, [ + { + ...msg.editHistory![0], + attachments: [msg.editHistory![0].attachments![0]], + bodyAttachment: msg.editHistory![0].attachments![1], + }, + { + ...msg.editHistory![1], + attachments: [ + msg.editHistory![1].attachments![0], + msg.editHistory![1].attachments![1], + ], + bodyAttachment: msg.editHistory![1].bodyAttachment, + }, + ]); + }); +}); diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index f84b66d12..a8558b5a3 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -202,6 +202,7 @@ export type ProcessedGiftBadge = { export type ProcessedDataMessage = { body?: string; + bodyAttachment?: ProcessedAttachment; attachments: ReadonlyArray; groupV2?: ProcessedGroupV2Context; flags: number; diff --git a/ts/textsecure/processDataMessage.ts b/ts/textsecure/processDataMessage.ts index 5f44a4462..9a493e00e 100644 --- a/ts/textsecure/processDataMessage.ts +++ b/ts/textsecure/processDataMessage.ts @@ -33,6 +33,8 @@ import { isAciString } from '../util/isAciString'; import { normalizeAci } from '../util/normalizeAci'; import { bytesToUuid } from '../util/uuidToBytes'; import { createName } from '../util/attachmentPath'; +import { partitionBodyAndNormalAttachments } from '../types/Attachment'; +import { isNotNil } from '../util/isNotNil'; const FLAGS = Proto.DataMessage.Flags; export const ATTACHMENT_MAX = 32; @@ -316,14 +318,22 @@ export function processDataMessage( ); } + const processedAttachments = message.attachments + ?.map((attachment: Proto.IAttachmentPointer) => ({ + ...processAttachment(attachment), + downloadPath: doCreateName(), + })) + .filter(isNotNil); + + const { bodyAttachment, attachments } = partitionBodyAndNormalAttachments( + { attachments: processedAttachments ?? [] }, + { logId: `processDataMessage(${timestamp})` } + ); + const result: ProcessedDataMessage = { body: dropNull(message.body), - attachments: (message.attachments ?? []).map( - (attachment: Proto.IAttachmentPointer) => ({ - ...processAttachment(attachment), - downloadPath: doCreateName(), - }) - ), + bodyAttachment, + attachments, groupV2: processGroupV2Context(message.groupV2), flags: message.flags ?? 0, expireTimer: DurationInSeconds.fromSeconds(message.expireTimer ?? 0), diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index 7b0e084d4..928a574e7 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -9,11 +9,13 @@ import { isUndefined, isString, omit, + partition, } from 'lodash'; import { blobToArrayBuffer } from 'blob-util'; import type { LinkPreviewType } from './message/LinkPreviews'; import type { LoggerType } from './Logging'; +import * as logging from '../logging/log'; import * as MIME from './MIME'; import { toLogFormat } from './errors'; import { SignalService } from '../protobuf'; @@ -1301,3 +1303,48 @@ export function getAttachmentIdForLogging(attachment: AttachmentType): string { } return '[MissingDigest]'; } + +// We now partition out the bodyAttachment on receipt, but older +// messages may still have a bodyAttachment in the normal attachments field +export function partitionBodyAndNormalAttachments< + T extends Pick, +>( + { + attachments, + existingBodyAttachment, + }: { + attachments: ReadonlyArray; + existingBodyAttachment?: T; + }, + { logId, logger = logging }: { logId: string; logger?: LoggerType } +): { + bodyAttachment: T | undefined; + attachments: Array; +} { + const [bodyAttachments, normalAttachments] = partition( + attachments, + attachment => MIME.isLongMessage(attachment.contentType) + ); + + if (bodyAttachments.length > 1) { + logger.warn( + `${logId}: Received more than one long message attachment, ` + + `dropping ${bodyAttachments.length - 1}` + ); + } + + if (bodyAttachments.length > 0) { + if (existingBodyAttachment) { + logger.warn(`${logId}: there is already an existing body attachment`); + } else { + logger.info( + `${logId}: Moving a long message attachment to message.bodyAttachment` + ); + } + } + + return { + bodyAttachment: existingBodyAttachment ?? bodyAttachments[0], + attachments: normalAttachments, + }; +} diff --git a/ts/types/MIME.ts b/ts/types/MIME.ts index bf5068eb3..82f55d2ef 100644 --- a/ts/types/MIME.ts +++ b/ts/types/MIME.ts @@ -46,8 +46,8 @@ export const isVideo = (value: string): value is MIMEType => // recognize them as file attachments. export const isAudio = (value: string): value is MIMEType => Boolean(value) && value.startsWith('audio/') && !value.endsWith('aiff'); -export const isLongMessage = (value: unknown): value is MIMEType => +export const isLongMessage = (value: string): value is MIMEType => value === LONG_MESSAGE; -export const supportsIncrementalMac = (value: unknown): boolean => { +export const supportsIncrementalMac = (value: string): boolean => { return value === VIDEO_MP4; }; diff --git a/ts/util/attachmentDownloadQueue.ts b/ts/util/attachmentDownloadQueue.ts index f2f45c1c5..7d191770d 100644 --- a/ts/util/attachmentDownloadQueue.ts +++ b/ts/util/attachmentDownloadQueue.ts @@ -122,9 +122,11 @@ function hasRequiredAttachmentDownloads( ): boolean { const attachments: ReadonlyArray = message.attachments || []; - const hasLongMessageAttachments = attachments.some(attachment => { - return MIME.isLongMessage(attachment.contentType); - }); + const hasLongMessageAttachments = + Boolean(message.bodyAttachment) || + attachments.some(attachment => { + return MIME.isLongMessage(attachment.contentType); + }); if (hasLongMessageAttachments) { return true; diff --git a/ts/util/queueAttachmentDownloads.ts b/ts/util/queueAttachmentDownloads.ts index 48ea46867..df2011434 100644 --- a/ts/util/queueAttachmentDownloads.ts +++ b/ts/util/queueAttachmentDownloads.ts @@ -1,8 +1,7 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { partition } from 'lodash'; -import * as logger from '../logging/log'; +import * as defaultLogger from '../logging/log'; import { isLongMessage } from '../types/MIME'; import { getMessageIdForLogging } from './idForLogging'; import { @@ -24,6 +23,7 @@ import { getAttachmentSignatureSafe, isDownloading, isDownloaded, + partitionBodyAndNormalAttachments, } from '../types/Attachment'; import type { StickerType } from '../types/Stickers'; import type { LinkPreviewType } from '../types/message/LinkPreviews'; @@ -43,6 +43,7 @@ import { shouldUseAttachmentDownloadQueue, } from './attachmentDownloadQueue'; import { queueUpdateMessage } from './messageBatcher'; +import type { LoggerType } from '../types/Logging'; export type MessageAttachmentsDownloadedType = { bodyAttachment?: AttachmentType; @@ -56,7 +57,7 @@ export type MessageAttachmentsDownloadedType = { function getLogger(source: AttachmentDownloadSource) { const verbose = source !== AttachmentDownloadSource.BACKUP_IMPORT; - const log = verbose ? logger : { ...logger, info: () => null }; + const log = verbose ? defaultLogger : { ...defaultLogger, info: () => null }; return log; } @@ -64,7 +65,9 @@ export async function handleAttachmentDownloadsForNewMessage( message: MessageModel, conversation: ConversationModel ): Promise { - const idLog = `handleAttachmentDownloadsForNewMessage/${conversation.idForLogging()} ${getMessageIdForLogging(message.attributes)}`; + 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 @@ -79,7 +82,7 @@ export async function handleAttachmentDownloadsForNewMessage( if (shouldQueueForDownload) { if (shouldUseAttachmentDownloadQueue()) { - addToAttachmentDownloadQueue(idLog, message); + addToAttachmentDownloadQueue(logId, message); } else { await queueAttachmentDownloadsForMessage(message); } @@ -117,28 +120,21 @@ export async function queueAttachmentDownloads( attachmentDigestForImmediate?: string; } = {} ): Promise { - const attachmentsToQueue = message.get('attachments') || []; const messageId = message.id; const idForLogging = getMessageIdForLogging(message.attributes); let count = 0; - const idLog = `queueAttachmentDownloads(${idForLogging}})`; + const logId = `queueAttachmentDownloads(${idForLogging}})`; const log = getLogger(source); - const [longMessageAttachments, normalAttachments] = partition( - attachmentsToQueue, - attachment => isLongMessage(attachment.contentType) + message.set( + ensureBodyAttachmentsAreSeparated(message.attributes, { + logId, + logger: log, + }) ); - if (longMessageAttachments.length > 1) { - log.error(`${idLog}: Received more than one long message attachment`); - } - - if (longMessageAttachments.length > 0) { - message.set({ bodyAttachment: longMessageAttachments[0] }); - } - const bodyAttachmentsToDownload = [ message.get('bodyAttachment'), ...(message @@ -151,7 +147,7 @@ export async function queueAttachmentDownloads( if (bodyAttachmentsToDownload.length) { log.info( - `${idLog}: Queueing ${bodyAttachmentsToDownload.length} long message attachment download` + `${logId}: Queueing ${bodyAttachmentsToDownload.length} long message attachment download` ); await Promise.all( bodyAttachmentsToDownload.map(attachment => @@ -169,16 +165,11 @@ export async function queueAttachmentDownloads( count += bodyAttachmentsToDownload.length; } - if (normalAttachments.length > 0) { - log.info( - `${idLog}: Queueing ${normalAttachments.length} normal attachment downloads` - ); - } const { attachments, count: attachmentsCount } = await queueNormalAttachments( { - idLog, + logId, messageId, - attachments: normalAttachments, + attachments: message.get('attachments'), otherAttachments: message .get('editHistory') ?.flatMap(x => x.attachments ?? []), @@ -189,19 +180,23 @@ export async function queueAttachmentDownloads( attachmentDigestForImmediate, } ); + if (attachmentsCount > 0) { message.set({ attachments }); + log.info( + `${logId}: Queueing ${attachmentsCount} normal attachment downloads` + ); } count += attachmentsCount; const previewsToQueue = message.get('preview') || []; if (previewsToQueue.length > 0) { log.info( - `${idLog}: Queueing ${previewsToQueue.length} preview attachment downloads` + `${logId}: Queueing ${previewsToQueue.length} preview attachment downloads` ); } const { preview, count: previewCount } = await queuePreviews({ - idLog, + logId, messageId, previews: previewsToQueue, otherPreviews: message.get('editHistory')?.flatMap(x => x.preview ?? []), @@ -218,12 +213,12 @@ export async function queueAttachmentDownloads( const numQuoteAttachments = message.get('quote')?.attachments?.length ?? 0; if (numQuoteAttachments > 0) { log.info( - `${idLog}: Queueing ${numQuoteAttachments} ` + + `${logId}: Queueing ${numQuoteAttachments} ` + 'quote attachment downloads' ); } const { quote, count: thumbnailCount } = await queueQuoteAttachments({ - idLog, + logId, messageId, quote: message.get('quote'), otherQuotes: @@ -244,7 +239,7 @@ export async function queueAttachmentDownloads( const contactsToQueue = message.get('contact') || []; if (contactsToQueue.length > 0) { log.info( - `${idLog}: Queueing ${contactsToQueue.length} contact attachment downloads` + `${logId}: Queueing ${contactsToQueue.length} contact attachment downloads` ); } const contact = await Promise.all( @@ -254,7 +249,7 @@ export async function queueAttachmentDownloads( } // We've already downloaded this! if (item.avatar.avatar.path) { - log.info(`${idLog}: Contact attachment already downloaded`); + log.info(`${logId}: Contact attachment already downloaded`); return item; } @@ -280,9 +275,9 @@ export async function queueAttachmentDownloads( let sticker = message.get('sticker'); if (sticker && sticker.data && sticker.data.path) { - log.info(`${idLog}: Sticker attachment already downloaded`); + log.info(`${logId}: Sticker attachment already downloaded`); } else if (sticker) { - log.info(`${idLog}: Queueing sticker download`); + log.info(`${logId}: Queueing sticker download`); count += 1; const { packId, stickerId, packKey } = sticker; @@ -294,7 +289,7 @@ export async function queueAttachmentDownloads( data = await copyStickerToAttachments(packId, stickerId); } catch (error) { log.error( - `${idLog}: Problem copying sticker (${packId}, ${stickerId}) to attachments:`, + `${logId}: Problem copying sticker (${packId}, ${stickerId}) to attachments:`, Errors.toLogFormat(error) ); } @@ -311,7 +306,7 @@ export async function queueAttachmentDownloads( source, }); } else { - log.error(`${idLog}: Sticker data was missing`); + log.error(`${logId}: Sticker data was missing`); } } const stickerRef = { @@ -341,12 +336,12 @@ export async function queueAttachmentDownloads( let editHistory = message.get('editHistory'); if (editHistory) { - log.info(`${idLog}: Looping through ${editHistory.length} edits`); + log.info(`${logId}: Looping through ${editHistory.length} edits`); editHistory = await Promise.all( editHistory.map(async edit => { const { attachments: editAttachments, count: editAttachmentsCount } = await queueNormalAttachments({ - idLog, + logId, messageId, attachments: edit.attachments, otherAttachments: attachments, @@ -358,14 +353,14 @@ export async function queueAttachmentDownloads( count += editAttachmentsCount; if (editAttachmentsCount !== 0) { log.info( - `${idLog}: Queueing ${editAttachmentsCount} normal attachment ` + + `${logId}: Queueing ${editAttachmentsCount} normal attachment ` + `downloads (edited:${edit.timestamp})` ); } const { preview: editPreview, count: editPreviewCount } = await queuePreviews({ - idLog, + logId, messageId, previews: edit.preview, otherPreviews: preview, @@ -377,7 +372,7 @@ export async function queueAttachmentDownloads( count += editPreviewCount; if (editPreviewCount !== 0) { log.info( - `${idLog}: Queueing ${editPreviewCount} preview attachment ` + + `${logId}: Queueing ${editPreviewCount} preview attachment ` + `downloads (edited:${edit.timestamp})` ); } @@ -396,13 +391,13 @@ export async function queueAttachmentDownloads( return false; } - log.info(`${idLog}: Queued ${count} total attachment downloads`); + log.info(`${logId}: Queued ${count} total attachment downloads`); return true; } export async function queueNormalAttachments({ - idLog, + logId, messageId, attachments = [], otherAttachments, @@ -412,7 +407,7 @@ export async function queueNormalAttachments({ source, attachmentDigestForImmediate, }: { - idLog: string; + logId: string; messageId: string; attachments: MessageAttributesType['attachments']; otherAttachments: MessageAttributesType['attachments']; @@ -446,9 +441,16 @@ export async function queueNormalAttachments({ 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(`${idLog}: Normal attachment already downloaded`); + log.info(`${logId}: Normal attachment already downloaded`); return attachment; } @@ -463,7 +465,7 @@ export async function queueNormalAttachments({ (isDownloading(existingAttachment) || isDownloaded(existingAttachment)) ) { log.info( - `${idLog}: Normal attachment already downloaded in other attachments. Replacing` + `${logId}: Normal attachment already downloaded in other attachments. Replacing` ); // Incrementing count so that we update the message's fields downstream count += 1; @@ -511,7 +513,7 @@ function getLinkPreviewSignature(preview: LinkPreviewType): string | undefined { } async function queuePreviews({ - idLog, + logId, messageId, previews = [], otherPreviews, @@ -520,7 +522,7 @@ async function queuePreviews({ urgency, source, }: { - idLog: string; + logId: string; messageId: string; previews: MessageAttributesType['preview']; otherPreviews: MessageAttributesType['preview']; @@ -550,7 +552,7 @@ async function queuePreviews({ } // We've already downloaded this! if (isDownloaded(item.image)) { - log.info(`${idLog}: Preview attachment already downloaded`); + log.info(`${logId}: Preview attachment already downloaded`); return item; } const signature = getLinkPreviewSignature(item); @@ -564,7 +566,7 @@ async function queuePreviews({ (isDownloading(existingPreview.image) || isDownloaded(existingPreview.image)) ) { - log.info(`${idLog}: Preview already downloaded elsewhere. Replacing`); + log.info(`${logId}: Preview already downloaded elsewhere. Replacing`); // Incrementing count so that we update the message's fields downstream count += 1; return existingPreview; @@ -607,7 +609,7 @@ function getQuoteThumbnailSignature( } async function queueQuoteAttachments({ - idLog, + logId, messageId, quote, otherQuotes, @@ -616,7 +618,7 @@ async function queueQuoteAttachments({ urgency, source, }: { - idLog: string; + logId: string; messageId: string; quote: QuotedMessageType | undefined; otherQuotes: ReadonlyArray; @@ -663,7 +665,7 @@ async function queueQuoteAttachments({ } // We've already downloaded this! if (isDownloaded(item.thumbnail)) { - log.info(`${idLog}: Quote attachment already downloaded`); + log.info(`${logId}: Quote attachment already downloaded`); return item; } @@ -679,7 +681,7 @@ async function queueQuoteAttachments({ isDownloaded(existingThumbnail)) ) { log.info( - `${idLog}: Preview already downloaded elsewhere. Replacing` + `${logId}: Preview already downloaded elsewhere. Replacing` ); // Incrementing count so that we update the message's fields downstream count += 1; @@ -708,3 +710,42 @@ async function queueQuoteAttachments({ count, }; } + +export function ensureBodyAttachmentsAreSeparated( + messageAttributes: MessageAttributesType, + { logId, logger = defaultLogger }: { logId: string; logger?: LoggerType } +): { + bodyAttachment: AttachmentType | undefined; + attachments: Array; + editHistory: Array | 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, + }; +}