Enable backfilling of attachments without message_attachment rows

This commit is contained in:
trevor-signal 2025-08-26 23:20:14 -04:00 committed by GitHub
commit 37ec000831
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 109 additions and 8 deletions

View file

@ -1238,6 +1238,8 @@ type WritableInterface = {
processGroupCallRingCancellation(ringId: bigint): void;
cleanExpiredGroupCallRingCancellations(): void;
_testOnlyRemoveMessageAttachments(timestamp: number): void;
};
// Adds a database argument

View file

@ -705,6 +705,8 @@ export const DataWriter: ServerWritableInterface = {
disableFSync,
enableFSyncAndCheckpoint,
_testOnlyRemoveMessageAttachments,
// Server-only
removeKnownStickers,
@ -2720,6 +2722,17 @@ function saveMessageAttachment({
).run(values);
}
function _testOnlyRemoveMessageAttachments(
db: WritableDB,
timestamp: number
): void {
const [query, params] = sql`
DELETE FROM message_attachments
WHERE sentAt = ${timestamp};`;
db.prepare(query).run(params);
}
function saveMessage(
db: WritableDB,
message: ReadonlyDeep<MessageType>,

View file

@ -23,13 +23,20 @@ import {
sql,
sqlJoin,
} from './util';
import type { AttachmentType } from '../types/Attachment';
import { IMAGE_JPEG, stringToMIMEType } from '../types/MIME';
import { type AttachmentType } from '../types/Attachment';
import {
APPLICATION_OCTET_STREAM,
IMAGE_JPEG,
IMAGE_PNG,
stringToMIMEType,
} from '../types/MIME';
import { strictAssert } from '../util/assert';
import type { MessageAttributesType } from '../model-types';
import { createLogger } from '../logging/log';
export const ROOT_MESSAGE_ATTACHMENT_EDIT_HISTORY_INDEX = -1;
const log = createLogger('hydrateMessage');
function toBoolean(value: number | null): boolean | undefined {
if (value == null) {
return undefined;
@ -48,9 +55,12 @@ export function hydrateMessages(
db: ReadableDB,
unhydratedMessages: Array<MessageTypeUnhydrated>
): Array<MessageType> {
const messagesWithColumnsHydrated = unhydratedMessages.map(
hydrateMessageTableColumns
);
const messagesWithColumnsHydrated = unhydratedMessages.map(msg => ({
...hydrateMessageTableColumns(msg),
hasAttachments: msg.hasAttachments === 1,
hasFileAttachments: msg.hasFileAttachments === 1,
hasVisualMediaAttachments: msg.hasVisualMediaAttachments === 1,
}));
return hydrateMessagesWithAttachments(db, messagesWithColumnsHydrated);
}
@ -142,7 +152,13 @@ export function getAttachmentReferencesForMessages(
function hydrateMessagesWithAttachments(
db: ReadableDB,
messagesWithoutAttachments: Array<MessageType>
messagesWithoutAttachments: Array<
MessageType & {
hasAttachments: boolean;
hasVisualMediaAttachments: boolean;
hasFileAttachments: boolean;
}
>
): Array<MessageType> {
const attachmentReferencesForAllMessages = getAttachmentReferencesForMessages(
db,
@ -153,12 +169,58 @@ function hydrateMessagesWithAttachments(
'messageId'
);
return messagesWithoutAttachments.map(msg => {
return messagesWithoutAttachments.map(msgWithExtraFields => {
const {
hasAttachments,
hasFileAttachments,
hasVisualMediaAttachments,
...msg
} = msgWithExtraFields;
const attachmentReferences = attachmentReferencesByMessage[msg.id] ?? [];
if (!attachmentReferences.length) {
if (msg.attachments?.length) {
// legacy message, attachments still on JSON
return msg;
}
if (msg.isErased || msg.deletedForEveryone || msg.isViewOnce) {
return msg;
}
if (msg.type !== 'incoming' && msg.type !== 'outgoing') {
return msg;
}
if (
!hasAttachments &&
!hasFileAttachments &&
!hasVisualMediaAttachments
) {
return msg;
}
log.warn(
`Retrieved message that should have attachments but missing message_attachment rows, timestamp: ${msg.timestamp}`
);
// Add an empty attachment to the message to enable backfilling in the UI
return {
...msg,
attachments: [
{
error: true,
size: 0,
width: hasVisualMediaAttachments ? 150 : undefined,
height: hasVisualMediaAttachments ? 150 : undefined,
contentType: hasVisualMediaAttachments
? IMAGE_PNG
: APPLICATION_OCTET_STREAM,
},
],
};
}
const attachmentsByEditHistoryIndex = groupBy(
attachmentReferences,
'editHistoryIndex'

View file

@ -688,4 +688,28 @@ describe('normalizes attachment references', () => {
omit(attachmentWithoutKey, 'randomKey')
);
});
it('adds a placeholder attachment when attachments had been deleted', async () => {
const message = composeMessage(Date.now(), {
attachments: [composeAttachment(), composeAttachment()],
});
await DataWriter.saveMessage(message, {
forceSave: true,
ourAci: generateAci(),
postSaveUpdates: () => Promise.resolve(),
});
await DataWriter._testOnlyRemoveMessageAttachments(message.timestamp);
const messageFromDB = await DataReader.getMessageById(message.id);
assert(messageFromDB, 'message was saved');
assert.deepEqual(messageFromDB.attachments?.[0], {
size: 0,
contentType: IMAGE_PNG,
width: 150,
height: 150,
error: true,
});
});
});