From d63f89c134db12a3efcc09113733e5e85fb126de Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:19:13 -0600 Subject: [PATCH] Minimize processing of forwarded attachments Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> --- ts/models/conversations.preload.ts | 6 ++- ts/util/maybeForwardMessages.preload.ts | 2 +- ts/util/uploadAttachment.preload.ts | 62 +++++++++++++++++++++---- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/ts/models/conversations.preload.ts b/ts/models/conversations.preload.ts index 62da6f376c..4879fc430a 100644 --- a/ts/models/conversations.preload.ts +++ b/ts/models/conversations.preload.ts @@ -4100,12 +4100,14 @@ export class ConversationModel { }, { dontClearDraft = false, + isForwarding = false, sendHQImages, storyId, timestamp, extraReduxActions, }: { dontClearDraft?: boolean; + isForwarding?: boolean; sendHQImages?: boolean; storyId?: string; timestamp?: number; @@ -4176,7 +4178,7 @@ export class ConversationModel { // any attachments as well. let attachmentsToSend = preview && preview.length ? [] : attachments; - if (preview && preview.length) { + if (preview && preview.length && !isForwarding) { attachments.forEach(attachment => { if (attachment.path) { void deleteAttachmentData(attachment.path); @@ -4197,7 +4199,7 @@ export class ConversationModel { * All draft attachments (with a path or just in-memory) will be written to disk for * real in `upgradeMessageSchema`. */ - if (!sendHQImages) { + if (!sendHQImages && !isForwarding) { attachmentsToSend = await Promise.all( attachmentsToSend.map(async attachment => { const downscaledAttachment = diff --git a/ts/util/maybeForwardMessages.preload.ts b/ts/util/maybeForwardMessages.preload.ts index 36a49d9ad6..6cfacee475 100644 --- a/ts/util/maybeForwardMessages.preload.ts +++ b/ts/util/maybeForwardMessages.preload.ts @@ -80,7 +80,7 @@ export async function maybeForwardMessages( return false; } - const sendMessageOptions = { dontClearDraft: true }; + const sendMessageOptions = { dontClearDraft: true, isForwarding: true }; const baseTimestamp = Date.now(); let timestampOffset = 0; diff --git a/ts/util/uploadAttachment.preload.ts b/ts/util/uploadAttachment.preload.ts index e9d09b051a..99cf41b5fa 100644 --- a/ts/util/uploadAttachment.preload.ts +++ b/ts/util/uploadAttachment.preload.ts @@ -6,6 +6,8 @@ import type { AttachmentWithHydratedData, UploadedAttachmentType, } from '../types/Attachment.std.js'; +import * as Bytes from '../Bytes.std.js'; +import { createLogger } from '../logging/log.std.js'; import { MIMETypeToString, supportsIncrementalMac } from '../types/MIME.std.js'; import { getRandomBytes } from '../Crypto.node.js'; import { backupsService } from '../services/backups/index.preload.js'; @@ -25,24 +27,66 @@ import { } from '../AttachmentCrypto.node.js'; import { missingCaseError } from './missingCaseError.std.js'; import { uuidToBytes } from './uuidToBytes.std.js'; +import { DAY } from './durations/index.std.js'; import { isVisualMedia } from './Attachment.std.js'; import { getAbsoluteAttachmentPath } from './migrations.preload.js'; +import { isMoreRecentThan } from './timestamp.std.js'; const CDNS_SUPPORTING_TUS = new Set([3]); +const log = createLogger('uploadAttachment'); + export async function uploadAttachment( attachment: AttachmentWithHydratedData ): Promise { - const keys = getRandomBytes(64); - const needIncrementalMac = supportsIncrementalMac(attachment.contentType); + let keys: Uint8Array; + let cdnKey: string; + let cdnNumber: number; + let encrypted: Pick< + EncryptedAttachmentV2, + 'digest' | 'plaintextHash' | 'incrementalMac' | 'chunkSize' + >; + let uploadTimestamp: number; - const uploadTimestamp = Date.now(); - const { cdnKey, cdnNumber, encrypted } = await encryptAndUploadAttachment({ - keys, - needIncrementalMac, - plaintext: { data: attachment.data }, - uploadType: 'standard', - }); + // Recently uploaded attachment + if ( + attachment.cdnKey && + attachment.cdnNumber && + attachment.key && + attachment.size != null && + attachment.digest && + attachment.contentType != null && + attachment.plaintextHash != null && + attachment.uploadTimestamp != null && + isMoreRecentThan(attachment.uploadTimestamp, 3 * DAY) + ) { + log.info('reusing attachment uploaded at', attachment.uploadTimestamp); + + ({ cdnKey, cdnNumber, uploadTimestamp } = attachment); + + keys = Bytes.fromBase64(attachment.key); + + encrypted = { + digest: Bytes.fromBase64(attachment.digest), + plaintextHash: attachment.plaintextHash, + incrementalMac: attachment.incrementalMac + ? Bytes.fromBase64(attachment.incrementalMac) + : undefined, + chunkSize: attachment.chunkSize, + }; + } else { + keys = getRandomBytes(64); + uploadTimestamp = Date.now(); + + const needIncrementalMac = supportsIncrementalMac(attachment.contentType); + + ({ cdnKey, cdnNumber, encrypted } = await encryptAndUploadAttachment({ + keys, + needIncrementalMac, + plaintext: { data: attachment.data }, + uploadType: 'standard', + })); + } const { blurHash, caption, clientUuid, flags, height, width } = attachment;