diff --git a/DATABASE_SCHEMA.md b/DATABASE_SCHEMA.md index 519a877e4..7e639723a 100644 --- a/DATABASE_SCHEMA.md +++ b/DATABASE_SCHEMA.md @@ -215,11 +215,20 @@ CREATE TABLE attachment_downloads_backup_stats ( ```sql CREATE TABLE attachments_protected_from_deletion ( path TEXT NOT NULL, - messageId TEXT NOT NULL, - PRIMARY KEY (path, messageId) + reuseToken TEXT NOT NULL, + PRIMARY KEY (path, reuseToken) ) STRICT ``` +
+Index: attachments_protected_from_deletion → attachments_protected_from_deletion_reuseToken + +```sql +CREATE INDEX attachments_protected_from_deletion_reuseToken ON attachments_protected_from_deletion (reuseToken) +``` + +
+
Index: attachments_protected_from_deletion → sqlite_autoindex_attachments_protected_from_deletion_1 @@ -1169,53 +1178,6 @@ CREATE INDEX message_attachments_thumbnailPath ON message_attachments (thumbnail
-
-Trigger: message_attachments → stop_protecting_attachments_after_insert - -```sql -CREATE TRIGGER stop_protecting_attachments_after_insert AFTER INSERT ON message_attachments BEGIN -DELETE FROM attachments_protected_from_deletion -WHERE - messageId IS NEW.messageId - AND path IN ( - NEW.path, - NEW.thumbnailPath, - NEW.screenshotPath, - NEW.backupThumbnailPath - ); - -END -``` - -
- -
-Trigger: message_attachments → stop_protecting_attachments_after_update - -```sql -CREATE TRIGGER stop_protecting_attachments_after_update AFTER -UPDATE OF path, -thumbnailPath, -screenshotPath, -backupThumbnailPath ON message_attachments WHEN OLD.path IS NOT NEW.path -OR OLD.thumbnailPath IS NOT NEW.thumbnailPath -OR OLD.screenshotPath IS NOT NEW.screenshotPath -OR OLD.backupThumbnailPath IS NOT NEW.backupThumbnailPath BEGIN -DELETE FROM attachments_protected_from_deletion -WHERE - messageId IS NEW.messageId - AND path IN ( - NEW.path, - NEW.thumbnailPath, - NEW.screenshotPath, - NEW.backupThumbnailPath - ); - -END -``` - -
- --- diff --git a/ts/jobs/AttachmentDownloadManager.preload.ts b/ts/jobs/AttachmentDownloadManager.preload.ts index 5e6b622ad..d82029eb0 100644 --- a/ts/jobs/AttachmentDownloadManager.preload.ts +++ b/ts/jobs/AttachmentDownloadManager.preload.ts @@ -893,7 +893,6 @@ export async function runDownloadAttachmentJobInner({ const existingAttachmentData = await getExistingAttachmentDataForReuse({ plaintextHash: downloadedAttachment.plaintextHash, contentType: attachment.contentType, - messageId, logId, }); diff --git a/ts/sql/Interface.std.ts b/ts/sql/Interface.std.ts index 9777bed99..b2bef44f5 100644 --- a/ts/sql/Interface.std.ts +++ b/ts/sql/Interface.std.ts @@ -1484,20 +1484,12 @@ type WritableInterface = { plaintextHash, version, contentType, - messageId, }: { plaintextHash: string; version: number; contentType: MIMEType; - messageId: string; - }) => ExistingAttachmentData | undefined; - _protectAttachmentPathFromDeletion: ({ - path, - messageId, - }: { - path: string; - messageId: string; - }) => void; + }) => (ExistingAttachmentData & { reuseToken: string }) | undefined; + _protectAttachmentPathFromDeletion: ({ path }: { path: string }) => string; resetProtectedAttachmentPaths: () => void; removeAll: () => void; diff --git a/ts/sql/Server.node.ts b/ts/sql/Server.node.ts index 07c29983e..8efa53e41 100644 --- a/ts/sql/Server.node.ts +++ b/ts/sql/Server.node.ts @@ -302,7 +302,10 @@ import type { } from '../types/Colors.std.ts'; import { sqlLogger } from './sqlLogger.node.ts'; import { permissiveMessageAttachmentSchema } from './server/messageAttachments.std.ts'; -import { getFilePathsReferencedByMessage } from '../util/messageFilePaths.std.ts'; +import { + getFilePathsReferencedByAttachment, + getFilePathsReferencedByMessage, +} from '../util/messageFilePaths.std.ts'; import { createMessagesOnInsertTrigger } from './migrations/1500-search-polls.std.ts'; import { isValidPlaintextHash } from '../types/Crypto.std.ts'; import { Emoji } from '../axo/emoji.std.ts'; @@ -2916,6 +2919,10 @@ function saveMessageAttachment({ logger.info('Recovered from invalid message_attachment save'); } + + if (attachment.reuseToken != null) { + releaseAttachmentPathProtections(db, attachment); + } } function getAndProtectExistingAttachmentPath( @@ -2924,14 +2931,12 @@ function getAndProtectExistingAttachmentPath( plaintextHash, version, contentType, - messageId, }: { plaintextHash: string; version: number; contentType: string; - messageId: string; } -): ExistingAttachmentData | undefined { +): (ExistingAttachmentData & { reuseToken: string }) | undefined { if (!isValidPlaintextHash(plaintextHash)) { logger.error('getAndProtectExistingAttachmentPath: Invalid plaintextHash'); return; @@ -2971,40 +2976,71 @@ function getAndProtectExistingAttachmentPath( LIMIT 1; `; - const existingData = db.prepare(query).get(params); + return db.transaction(() => { + const existingData = db.prepare(query).get(params); - if (!existingData) { - return undefined; - } + if (!existingData) { + return undefined; + } - const [protectQuery, protectParams] = sql` + const reuseToken = randomBytes(16).toString('hex'); + + const [protectQuery, protectParams] = sql` WITH existingMessageAttachmentPaths(path) AS ( VALUES (${existingData.path}), (${existingData.thumbnailPath}), (${existingData.screenshotPath}) ) - INSERT OR REPLACE INTO attachments_protected_from_deletion(path, messageId) - SELECT path, ${messageId} + INSERT OR REPLACE INTO attachments_protected_from_deletion(path, reuseToken) + SELECT path, ${reuseToken} FROM existingMessageAttachmentPaths WHERE path IS NOT NULL; `; - db.prepare(protectQuery).run(protectParams); + db.prepare(protectQuery).run(protectParams); - return existingData; + return { ...existingData, reuseToken }; + })(); } function _protectAttachmentPathFromDeletion( db: WritableDB, - { path, messageId }: { path: string; messageId: string } -): void { + { path }: { path: string } +): string { + const reuseToken = randomBytes(16).toString('hex'); const [protectQuery, protectParams] = sql` INSERT OR REPLACE INTO attachments_protected_from_deletion - (path, messageId) + (path, reuseToken) VALUES - (${path}, ${messageId}); + (${path}, ${reuseToken}); `; db.prepare(protectQuery).run(protectParams); + return reuseToken; +} + +function releaseAttachmentPathProtections( + db: WritableDB, + attachment: AttachmentType +): void { + const { reuseToken } = attachment; + + if (!reuseToken) { + return; + } + + const { externalAttachments } = + getFilePathsReferencedByAttachment(attachment); + + if (!externalAttachments.size) { + return; + } + + const [query, params] = sql` + DELETE FROM attachments_protected_from_deletion + WHERE reuseToken = ${reuseToken} + AND path IN (${sqlJoin([...externalAttachments])}); + `; + db.prepare(query).run(params); } function resetProtectedAttachmentPaths(db: WritableDB): void { @@ -3013,7 +3049,7 @@ function resetProtectedAttachmentPaths(db: WritableDB): void { function getAllProtectedAttachmentPaths(db: ReadableDB): Array { return db - .prepare('SELECT path FROM attachments_protected_from_deletion', { + .prepare('SELECT DISTINCT path FROM attachments_protected_from_deletion', { pluck: true, }) .all(); diff --git a/ts/sql/migrations/1730-protected-attachments-dedupe-token.std.ts b/ts/sql/migrations/1730-protected-attachments-dedupe-token.std.ts new file mode 100644 index 000000000..92a1099a0 --- /dev/null +++ b/ts/sql/migrations/1730-protected-attachments-dedupe-token.std.ts @@ -0,0 +1,24 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +import type { WritableDB } from '../Interface.std.ts'; + +export default function updateToSchemaVersion1730(db: WritableDB): void { + // Each row is an outstanding reuse claim on a path, keyed by an ephemeral token + // returned to the caller. saveMessageAttachments releases the claim in the same + // transaction that writes the message_attachments row referencing the path. + db.exec(` + DROP TABLE attachments_protected_from_deletion; + + CREATE TABLE attachments_protected_from_deletion ( + path TEXT NOT NULL, + reuseToken TEXT NOT NULL, + PRIMARY KEY (path, reuseToken) + ) STRICT; + + CREATE INDEX attachments_protected_from_deletion_reuseToken + ON attachments_protected_from_deletion (reuseToken); + + DROP TRIGGER stop_protecting_attachments_after_update; + DROP TRIGGER stop_protecting_attachments_after_insert; + `); +} diff --git a/ts/sql/migrations/index.node.ts b/ts/sql/migrations/index.node.ts index f282527ba..412900f01 100644 --- a/ts/sql/migrations/index.node.ts +++ b/ts/sql/migrations/index.node.ts @@ -149,6 +149,7 @@ import updateToSchemaVersion1690 from './1690-poll-terminate-notification-timest import updateToSchemaVersion1700 from './1700-trim-profile-names.std.ts'; import updateToSchemaVersion1710 from './1710-emoji-skin-tone-default.std.ts'; import updateToSchemaVersion1720 from './1720-update-recent-emoji.std.ts'; +import updateToSchemaVersion1730 from './1730-protected-attachments-dedupe-token.std.ts'; import { DataWriter } from '../Server.node.ts'; import { strictAssert } from '../../util/assert.std.ts'; @@ -1660,6 +1661,7 @@ export const SCHEMA_VERSIONS: ReadonlyArray = [ { version: 1700, update: updateToSchemaVersion1700 }, { version: 1710, update: updateToSchemaVersion1710 }, { version: 1720, update: updateToSchemaVersion1720 }, + { version: 1730, update: updateToSchemaVersion1730 }, ]; class DBVersionFromFutureError extends Error { diff --git a/ts/test-electron/cleanupOrphanedAttachments_test.preload.ts b/ts/test-electron/cleanupOrphanedAttachments_test.preload.ts index 44d914c12..6b191367c 100644 --- a/ts/test-electron/cleanupOrphanedAttachments_test.preload.ts +++ b/ts/test-electron/cleanupOrphanedAttachments_test.preload.ts @@ -277,7 +277,6 @@ describe('cleanupOrphanedAttachments', () => { await DataWriter._protectAttachmentPathFromDeletion({ path: `attachment${attachmentIndex + 1}`, - messageId: 'messageId', }); await DataWriter.cleanupOrphanedAttachments({ _block: true }); diff --git a/ts/test-electron/deleteMessageAttachments_test.preload.ts b/ts/test-electron/deleteMessageAttachments_test.preload.ts index 4dd62e02b..05e3200a9 100644 --- a/ts/test-electron/deleteMessageAttachments_test.preload.ts +++ b/ts/test-electron/deleteMessageAttachments_test.preload.ts @@ -304,7 +304,6 @@ describe('deleteMessageAttachments', () => { it('is not safe to delete if the file is protected, even if no references', async () => { await DataWriter._protectAttachmentPathFromDeletion({ path: 'attachment0', - messageId: 'messageId', }); assert.isFalse(await DataReader.isAttachmentSafeToDelete('attachment0')); @@ -333,9 +332,6 @@ describe('deleteMessageAttachments', () => { conversationId: 'convoId', attachments: [attachment1], }; - const message2 = { ...message1, id: generateUuid() }; - const message3 = { ...message1, id: generateUuid() }; - assert.isTrue(await DataReader.isAttachmentSafeToDelete('attachment1')); await DataWriter.saveMessage(message1, { @@ -345,20 +341,43 @@ describe('deleteMessageAttachments', () => { }); assert.isFalse(await DataReader.isAttachmentSafeToDelete('attachment1')); - // Protect it twice - await DataWriter.getAndProtectExistingAttachmentPath({ - plaintextHash: attachment1.plaintextHash, - version: 2, - contentType: IMAGE_JPEG, - messageId: message2.id, - }); + // Protect it twice, once for each message that will reuse the file + const existingDataForMessage2 = + await DataWriter.getAndProtectExistingAttachmentPath({ + plaintextHash: attachment1.plaintextHash, + version: 2, + contentType: IMAGE_JPEG, + }); + strictAssert(existingDataForMessage2, 'existing attachment data exists'); - await DataWriter.getAndProtectExistingAttachmentPath({ - plaintextHash: attachment1.plaintextHash, - version: 2, - contentType: IMAGE_JPEG, - messageId: message3.id, - }); + const existingDataForMessage3 = + await DataWriter.getAndProtectExistingAttachmentPath({ + plaintextHash: attachment1.plaintextHash, + version: 2, + contentType: IMAGE_JPEG, + }); + strictAssert(existingDataForMessage3, 'existing attachment data exists'); + + const message2: MessageAttributesType = { + ...message1, + id: generateUuid(), + attachments: [ + { + ...attachment1, + ...existingDataForMessage2, + }, + ], + }; + const message3: MessageAttributesType = { + ...message1, + id: generateUuid(), + attachments: [ + { + ...attachment1, + ...existingDataForMessage3, + }, + ], + }; // Delete the original message await DataWriter.removeMessageById(message1.id, { @@ -366,7 +385,7 @@ describe('deleteMessageAttachments', () => { }); assert.isFalse(await DataReader.isAttachmentSafeToDelete('attachment1')); - // Save message2 + // Save message2; this releases message2's dedupe protection await DataWriter.saveMessage(message2, { forceSave: true, ourAci: generateAci(), @@ -380,7 +399,7 @@ describe('deleteMessageAttachments', () => { assert.isFalse(await DataReader.isAttachmentSafeToDelete('attachment1')); - // Save message3 + // Save message3; this releases message3's dedupe protection await DataWriter.saveMessage(message3, { forceSave: true, ourAci: generateAci(), @@ -494,7 +513,6 @@ describe('deleteMessageAttachments', () => { plaintextHash: attachment1.plaintextHash, version: attachment1.version, contentType: attachment1.contentType, - messageId: 'newmessage', }); assert.strictEqual(existingAttachment?.path, attachment1.path); @@ -604,7 +622,6 @@ describe('deleteMessageAttachments', () => { await DataWriter._protectAttachmentPathFromDeletion({ path: 'attachment0', - messageId: 'messageId', }); await cleanupAttachmentFiles(attachment); assert.sameDeepMembers(listFiles('attachment'), [ diff --git a/ts/test-node/types/Attachment_test.std.ts b/ts/test-node/types/Attachment_test.std.ts index 9afd63610..a209e8c73 100644 --- a/ts/test-node/types/Attachment_test.std.ts +++ b/ts/test-node/types/Attachment_test.std.ts @@ -453,16 +453,12 @@ describe('Attachment', () => { return FAKE_LOCAL_ATTACHMENT; }; - const actual = await migrateDataToFileSystem( - input, - { - writeNewAttachmentData, - getExistingAttachmentDataForReuse: async () => null, - getPlaintextHashForInMemoryAttachment: () => 'fakeplaintextHash', - logger, - }, - { id: 'messageId' } - ); + const actual = await migrateDataToFileSystem(input, { + writeNewAttachmentData, + getExistingAttachmentDataForReuse: async () => null, + getPlaintextHashForInMemoryAttachment: () => 'fakeplaintextHash', + logger, + }); assert.deepEqual(actual, expected); }); @@ -481,16 +477,12 @@ describe('Attachment', () => { const writeNewAttachmentData = async () => FAKE_LOCAL_ATTACHMENT; - const actual = await migrateDataToFileSystem( - input, - { - writeNewAttachmentData, - getExistingAttachmentDataForReuse: async () => null, - getPlaintextHashForInMemoryAttachment: () => 'fakeplaintextHash', - logger, - }, - { id: 'messageId' } - ); + const actual = await migrateDataToFileSystem(input, { + writeNewAttachmentData, + getExistingAttachmentDataForReuse: async () => null, + getPlaintextHashForInMemoryAttachment: () => 'fakeplaintextHash', + logger, + }); assert.deepEqual(actual, expected); }); @@ -505,16 +497,12 @@ describe('Attachment', () => { const writeNewAttachmentData = async () => FAKE_LOCAL_ATTACHMENT; - const actual = await migrateDataToFileSystem( - input, - { - writeNewAttachmentData, - getExistingAttachmentDataForReuse: async () => null, - getPlaintextHashForInMemoryAttachment: () => 'fakeplaintextHash', - logger, - }, - { id: 'messageId' } - ); + const actual = await migrateDataToFileSystem(input, { + writeNewAttachmentData, + getExistingAttachmentDataForReuse: async () => null, + getPlaintextHashForInMemoryAttachment: () => 'fakeplaintextHash', + logger, + }); assert.isUndefined(actual.data); }); @@ -528,27 +516,24 @@ describe('Attachment', () => { const writeNewAttachmentData = sandbox.stub(); - const actual = await migrateDataToFileSystem( - input, - { - writeNewAttachmentData, - getExistingAttachmentDataForReuse: async ({ - plaintextHash, - contentType, - }) => { - assert.strictEqual(plaintextHash, 'somePlaintextHash'); - assert.strictEqual(contentType, MIME.IMAGE_JPEG); - return { - path: 'new-path', - version: 2, - localKey: 'new-local-key', - }; - }, - getPlaintextHashForInMemoryAttachment: () => 'somePlaintextHash', - logger, + const actual = await migrateDataToFileSystem(input, { + writeNewAttachmentData, + getExistingAttachmentDataForReuse: async ({ + plaintextHash, + contentType, + }) => { + assert.strictEqual(plaintextHash, 'somePlaintextHash'); + assert.strictEqual(contentType, MIME.IMAGE_JPEG); + return { + path: 'new-path', + version: 2, + localKey: 'new-local-key', + reuseToken: 'reuse-token', + }; }, - { id: 'messageId' } - ); + getPlaintextHashForInMemoryAttachment: () => 'somePlaintextHash', + logger, + }); assert.strictEqual(writeNewAttachmentData.callCount, 0); assert.deepEqual(actual, { version: 2, @@ -558,6 +543,7 @@ describe('Attachment', () => { path: 'new-path', localKey: 'new-local-key', fileName: 'foo.jpg', + reuseToken: 'reuse-token', }); }); }); diff --git a/ts/types/Attachment.std.ts b/ts/types/Attachment.std.ts index d8884d2e9..a553f23a8 100644 --- a/ts/types/Attachment.std.ts +++ b/ts/types/Attachment.std.ts @@ -27,6 +27,12 @@ export type BackupThumbnailType = WithOptionalProperties; export type EphemeralAttachmentFields = { totalDownloaded?: number; data?: Uint8Array; + /** + * Identifies this attachment's claim on reused paths in + * attachments_protected_from_deletion; saveMessageAttachments releases the claim when + * the attachment is saved. + */ + reuseToken?: string; /** Not included in protobuf, needs to be pulled from flags */ isVoiceMessage?: boolean; /** For messages not already on disk, this will be a data url */ diff --git a/ts/types/EmbeddedContact.std.ts b/ts/types/EmbeddedContact.std.ts index 1885f17cf..8e0c03af4 100644 --- a/ts/types/EmbeddedContact.std.ts +++ b/ts/types/EmbeddedContact.std.ts @@ -249,7 +249,7 @@ export function parseAndWriteAvatar( ...contact, avatar: { ...avatar, - avatar: await upgradeAttachment(avatar.avatar, context, message), + avatar: await upgradeAttachment(avatar.avatar, context), }, } : omit(contact, ['avatar']); diff --git a/ts/types/Message2.preload.ts b/ts/types/Message2.preload.ts index 184bb24f8..78f1d9f02 100644 --- a/ts/types/Message2.preload.ts +++ b/ts/types/Message2.preload.ts @@ -529,11 +529,7 @@ const toVersion10 = _withSchemaVersion({ ...stickerMessage, sticker: { ...sticker, - data: await migrateDataToFileSystem( - sticker.data, - stickerContext, - stickerMessage - ), + data: await migrateDataToFileSystem(sticker.data, stickerContext), }, }; }; diff --git a/ts/types/Stickers.preload.ts b/ts/types/Stickers.preload.ts index 38d27c614..d2f285545 100644 --- a/ts/types/Stickers.preload.ts +++ b/ts/types/Stickers.preload.ts @@ -986,8 +986,7 @@ async function resolveReferences(packId: string): Promise { try { attachments = await pMap( messageIds, - messageId => - copyStickerToAttachments({ packId, stickerId, messageId }), + () => copyStickerToAttachments({ packId, stickerId }), { concurrency: 3 } ); } catch (error) { @@ -1076,11 +1075,9 @@ export function getSticker( export async function copyStickerToAttachments({ packId, stickerId, - messageId, }: { packId: string; stickerId: number; - messageId: string; }): Promise { const sticker = getSticker(packId, stickerId); if (!sticker) { @@ -1116,7 +1113,6 @@ export async function copyStickerToAttachments({ const existingAttachmentData = await getExistingAttachmentDataForReuse({ plaintextHash, contentType, - messageId, logId: 'copyStickerToAttachments', }); diff --git a/ts/util/attachments/deduplicateAttachment.preload.ts b/ts/util/attachments/deduplicateAttachment.preload.ts index eb77e4d13..3c5c4afc6 100644 --- a/ts/util/attachments/deduplicateAttachment.preload.ts +++ b/ts/util/attachments/deduplicateAttachment.preload.ts @@ -23,19 +23,18 @@ type AttachmentDataToBeReused = WithRequiredProperties< | 'screenshot' | 'width' | 'height' + | 'reuseToken' >, - 'path' | 'localKey' | 'version' + 'path' | 'localKey' | 'version' | 'reuseToken' >; export async function getExistingAttachmentDataForReuse({ plaintextHash, contentType, - messageId, logId, }: { plaintextHash: string; contentType: MIMEType; - messageId: string; logId?: string; }): Promise { const existingAttachmentData = @@ -43,7 +42,6 @@ export async function getExistingAttachmentDataForReuse({ plaintextHash, version: CURRENT_ATTACHMENT_VERSION, contentType, - messageId, }); if (!existingAttachmentData) { @@ -72,6 +70,7 @@ export async function getExistingAttachmentDataForReuse({ version: existingAttachmentData.version, width: existingAttachmentData.width ?? undefined, height: existingAttachmentData.height ?? undefined, + reuseToken: existingAttachmentData.reuseToken, }; const { thumbnailPath, thumbnailSize, thumbnailContentType } = existingAttachmentData; diff --git a/ts/util/attachments/migrateDataToFilesystem.std.ts b/ts/util/attachments/migrateDataToFilesystem.std.ts index 347329ed9..486673a97 100644 --- a/ts/util/attachments/migrateDataToFilesystem.std.ts +++ b/ts/util/attachments/migrateDataToFilesystem.std.ts @@ -4,7 +4,6 @@ import lodash from 'lodash'; import type { AttachmentType } from '../../types/Attachment.std.ts'; import type { ContextType } from '../../types/Message2.preload.ts'; -import type { MessageAttributesType } from '../../model-types.d.ts'; const { isFunction, isTypedArray, isUndefined, omit } = lodash; @@ -21,8 +20,7 @@ export async function migrateDataToFileSystem( | 'getExistingAttachmentDataForReuse' | 'getPlaintextHashForInMemoryAttachment' | 'logger' - >, - message: Pick + > ): Promise { if (!isFunction(writeNewAttachmentData)) { throw new TypeError("'writeNewAttachmentData' must be a function"); @@ -48,7 +46,6 @@ export async function migrateDataToFileSystem( const existingData = await getExistingAttachmentDataForReuse({ plaintextHash, - messageId: message.id, contentType: attachment.contentType, logId: 'migrateDataToFileSystem', }); diff --git a/ts/util/maybeForwardMessages.preload.ts b/ts/util/maybeForwardMessages.preload.ts index 0b3124a8d..a2bf720c8 100644 --- a/ts/util/maybeForwardMessages.preload.ts +++ b/ts/util/maybeForwardMessages.preload.ts @@ -127,6 +127,7 @@ export async function maybeForwardMessages( data: { ...stickerWithData.data, path: undefined, + reuseToken: undefined, }, } : undefined; @@ -152,6 +153,7 @@ export async function maybeForwardMessages( thumbnail: undefined, thumbnailFromBackup: undefined, screenshot: undefined, + reuseToken: undefined, })) ); const attachmentsToSend = attachmentsWithData.filter( diff --git a/ts/util/queueAttachmentDownloads.preload.ts b/ts/util/queueAttachmentDownloads.preload.ts index 9149e0158..e58369dd2 100644 --- a/ts/util/queueAttachmentDownloads.preload.ts +++ b/ts/util/queueAttachmentDownloads.preload.ts @@ -353,7 +353,6 @@ export async function queueAttachmentDownloads( const data = await copyStickerToAttachments({ packId, stickerId, - messageId, }); // Refresh sticker attachment since we had to await above const freshSticker = message.get('sticker');