Normalize message attachments

This commit is contained in:
trevor-signal
2025-05-22 21:09:54 -04:00
committed by GitHub
parent 8d8e0329cf
commit d6e81eee11
39 changed files with 2540 additions and 807 deletions

View File

@@ -0,0 +1,89 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { sql, sqlJoin } from '../../sql/util';
import { createDB, explain, updateToVersion } from './helpers';
import type { WritableDB } from '../../sql/Interface';
import { DataWriter } from '../../sql/Server';
describe('SQL/updateToSchemaVersion1360', () => {
let db: WritableDB;
beforeEach(async () => {
db = createDB();
updateToVersion(db, 1360);
await DataWriter.removeAll(db);
});
afterEach(() => {
db.close();
});
describe('message attachments', () => {
it('uses covering index to delete based on messageId', async () => {
const details = explain(
db,
sql`DELETE from message_attachments WHERE messageId = ${'messageId'}`
);
assert.strictEqual(
details,
'SEARCH message_attachments USING COVERING INDEX message_attachments_messageId (messageId=?)'
);
});
it('uses index to select based on messageId', async () => {
const details = explain(
db,
sql`SELECT * from message_attachments WHERE messageId IN (${sqlJoin(['id1', 'id2'])});`
);
assert.strictEqual(
details,
'SEARCH message_attachments USING INDEX message_attachments_messageId (messageId=?)'
);
});
it('uses index find path with existing plaintextHash', async () => {
const details = explain(
db,
sql`
SELECT path, localKey
FROM message_attachments
WHERE plaintextHash = ${'plaintextHash'}
LIMIT 1;
`
);
assert.strictEqual(
details,
'SEARCH message_attachments USING INDEX message_attachments_plaintextHash (plaintextHash=?)'
);
});
it('uses all path indices to find if path is being referenced', async () => {
const path = 'path';
const details = explain(
db,
sql`
SELECT 1 FROM message_attachments
WHERE
path = ${path} OR
thumbnailPath = ${path} OR
screenshotPath = ${path} OR
backupThumbnailPath = ${path};
`
);
assert.deepStrictEqual(details.split('\n'), [
'MULTI-INDEX OR',
'INDEX 1',
'SEARCH message_attachments USING INDEX message_attachments_path (path=?)',
'INDEX 2',
'SEARCH message_attachments USING INDEX message_attachments_all_thumbnailPath (thumbnailPath=?)',
'INDEX 3',
'SEARCH message_attachments USING INDEX message_attachments_all_screenshotPath (screenshotPath=?)',
'INDEX 4',
'SEARCH message_attachments USING INDEX message_attachments_all_backupThumbnailPath (backupThumbnailPath=?)',
]);
});
});
});

View File

@@ -197,9 +197,6 @@ describe('Message', () => {
fileName: 'test\uFFFDfig.exe',
},
],
hasAttachments: 1,
hasVisualMediaAttachments: undefined,
hasFileAttachments: undefined,
schemaVersion: Message.CURRENT_SCHEMA_VERSION,
});
@@ -848,6 +845,7 @@ describe('Message', () => {
const result = await Message.upgradeSchema(message, {
...getDefaultContext(),
doesAttachmentExist: async () => false,
maxVersion: 14,
});
assert.deepEqual({ ...message, schemaVersion: 14 }, result);

View File

@@ -1,221 +0,0 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import * as Message from '../../../types/message/initializeAttachmentMetadata';
import { SignalService } from '../../../protobuf';
import * as MIME from '../../../types/MIME';
import * as Bytes from '../../../Bytes';
import type { MessageAttributesType } from '../../../model-types.d';
function getDefaultMessage(
props?: Partial<MessageAttributesType>
): MessageAttributesType {
return {
id: 'some-id',
type: 'incoming',
sent_at: 45,
received_at: 45,
timestamp: 45,
conversationId: 'some-conversation-id',
...props,
};
}
describe('Message', () => {
describe('initializeAttachmentMetadata', () => {
it('should classify visual media attachments', async () => {
const input = getDefaultMessage({
type: 'incoming',
conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111',
timestamp: 1523317140899,
received_at: 1523317140899,
sent_at: 1523317140800,
attachments: [
{
contentType: MIME.IMAGE_JPEG,
data: Bytes.fromString('foo'),
fileName: 'foo.jpg',
size: 1111,
},
],
});
const expected = getDefaultMessage({
type: 'incoming',
conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111',
timestamp: 1523317140899,
received_at: 1523317140899,
sent_at: 1523317140800,
attachments: [
{
contentType: MIME.IMAGE_JPEG,
data: Bytes.fromString('foo'),
fileName: 'foo.jpg',
size: 1111,
},
],
hasAttachments: 1,
hasVisualMediaAttachments: 1,
hasFileAttachments: undefined,
});
const actual = await Message.initializeAttachmentMetadata(input);
assert.deepEqual(actual, expected);
});
it('should classify file attachments', async () => {
const input = getDefaultMessage({
type: 'incoming',
conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111',
timestamp: 1523317140899,
received_at: 1523317140899,
sent_at: 1523317140800,
attachments: [
{
contentType: MIME.APPLICATION_OCTET_STREAM,
data: Bytes.fromString('foo'),
fileName: 'foo.bin',
size: 1111,
},
],
});
const expected = getDefaultMessage({
type: 'incoming',
conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111',
timestamp: 1523317140899,
received_at: 1523317140899,
sent_at: 1523317140800,
attachments: [
{
contentType: MIME.APPLICATION_OCTET_STREAM,
data: Bytes.fromString('foo'),
fileName: 'foo.bin',
size: 1111,
},
],
hasAttachments: 1,
hasVisualMediaAttachments: undefined,
hasFileAttachments: 1,
});
const actual = await Message.initializeAttachmentMetadata(input);
assert.deepEqual(actual, expected);
});
it('should classify voice message attachments', async () => {
const input = getDefaultMessage({
type: 'incoming',
conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111',
timestamp: 1523317140899,
received_at: 1523317140899,
sent_at: 1523317140800,
attachments: [
{
contentType: MIME.AUDIO_AAC,
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: Bytes.fromString('foo'),
fileName: 'Voice Message.aac',
size: 1111,
},
],
});
const expected = getDefaultMessage({
type: 'incoming',
conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111',
timestamp: 1523317140899,
received_at: 1523317140899,
sent_at: 1523317140800,
attachments: [
{
contentType: MIME.AUDIO_AAC,
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: Bytes.fromString('foo'),
fileName: 'Voice Message.aac',
size: 1111,
},
],
hasAttachments: 1,
hasVisualMediaAttachments: undefined,
hasFileAttachments: undefined,
});
const actual = await Message.initializeAttachmentMetadata(input);
assert.deepEqual(actual, expected);
});
it('does not include long message attachments', async () => {
const input = getDefaultMessage({
type: 'incoming',
conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111',
timestamp: 1523317140899,
received_at: 1523317140899,
sent_at: 1523317140800,
attachments: [
{
contentType: MIME.LONG_MESSAGE,
data: Bytes.fromString('foo'),
fileName: 'message.txt',
size: 1111,
},
],
});
const expected = getDefaultMessage({
type: 'incoming',
conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111',
timestamp: 1523317140899,
received_at: 1523317140899,
sent_at: 1523317140800,
attachments: [
{
contentType: MIME.LONG_MESSAGE,
data: Bytes.fromString('foo'),
fileName: 'message.txt',
size: 1111,
},
],
hasAttachments: 0,
hasVisualMediaAttachments: undefined,
hasFileAttachments: undefined,
});
const actual = await Message.initializeAttachmentMetadata(input);
assert.deepEqual(actual, expected);
});
it('handles not attachments', async () => {
const input = getDefaultMessage({
type: 'incoming',
conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111',
timestamp: 1523317140899,
received_at: 1523317140899,
sent_at: 1523317140800,
attachments: [],
});
const expected = getDefaultMessage({
type: 'incoming',
conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111',
timestamp: 1523317140899,
received_at: 1523317140899,
sent_at: 1523317140800,
attachments: [],
hasAttachments: 0,
hasVisualMediaAttachments: undefined,
hasFileAttachments: undefined,
});
const actual = await Message.initializeAttachmentMetadata(input);
assert.deepEqual(actual, expected);
});
});
});