Convert js/modules/types/message to Typescript

This commit is contained in:
Scott Nonnenberg 2022-06-09 18:10:20 -07:00 committed by GitHub
parent 9975758fde
commit 924c271b13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 748 additions and 398 deletions

View file

@ -110,7 +110,7 @@ const searchSelectors = require('../../ts/state/selectors/search');
// Types // Types
const AttachmentType = require('../../ts/types/Attachment'); const AttachmentType = require('../../ts/types/Attachment');
const VisualAttachment = require('../../ts/types/VisualAttachment'); const VisualAttachment = require('../../ts/types/VisualAttachment');
const MessageType = require('./types/message'); const MessageType = require('../../ts/types/Message2');
const { UUID } = require('../../ts/types/UUID'); const { UUID } = require('../../ts/types/UUID');
const { Address } = require('../../ts/types/Address'); const { Address } = require('../../ts/types/Address');
const { QualifiedAddress } = require('../../ts/types/QualifiedAddress'); const { QualifiedAddress } = require('../../ts/types/QualifiedAddress');
@ -281,6 +281,8 @@ function initializeMigrations({
makeVideoScreenshot, makeVideoScreenshot,
logger, logger,
maxVersion, maxVersion,
getAbsoluteStickerPath,
writeNewStickerData,
}); });
}, },
writeMessageAttachments: MessageType.createAttachmentDataWriter({ writeMessageAttachments: MessageType.createAttachmentDataWriter({

View file

@ -1,4 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export const CURRENT_SCHEMA_VERSION: number;

View file

@ -62,7 +62,7 @@ import type {
GroupLogResponseType, GroupLogResponseType,
} from './textsecure/WebAPI'; } from './textsecure/WebAPI';
import type MessageSender from './textsecure/SendMessage'; import type MessageSender from './textsecure/SendMessage';
import { CURRENT_SCHEMA_VERSION as MAX_MESSAGE_SCHEMA } from '../js/modules/types/message'; import { CURRENT_SCHEMA_VERSION as MAX_MESSAGE_SCHEMA } from './types/Message2';
import type { ConversationModel } from './models/conversations'; import type { ConversationModel } from './models/conversations';
import { getGroupSizeHardLimit } from './groups/limits'; import { getGroupSizeHardLimit } from './groups/limits';
import { import {

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { isFunction, isNumber } from 'lodash'; import { isFunction, isNumber } from 'lodash';
import * as Message from '../../js/modules/types/message'; import { CURRENT_SCHEMA_VERSION } from '../types/Message2';
import type { MessageAttributesType } from '../model-types.d'; import type { MessageAttributesType } from '../model-types.d';
import type { UUIDStringType } from '../types/UUID'; import type { UUIDStringType } from '../types/UUID';
@ -14,7 +14,7 @@ export async function migrateMessageData({
upgradeMessageSchema, upgradeMessageSchema,
getMessagesNeedingUpgrade, getMessagesNeedingUpgrade,
saveMessage, saveMessage,
maxVersion = Message.CURRENT_SCHEMA_VERSION, maxVersion = CURRENT_SCHEMA_VERSION,
}: Readonly<{ }: Readonly<{
numMessagesPerBatch: number; numMessagesPerBatch: number;
upgradeMessageSchema: ( upgradeMessageSchema: (

17
ts/model-types.d.ts vendored
View file

@ -61,8 +61,14 @@ export type GroupMigrationType = {
droppedMemberIds: Array<string>; droppedMemberIds: Array<string>;
invitedMembers: Array<GroupV2PendingMemberType>; invitedMembers: Array<GroupV2PendingMemberType>;
}; };
export type PreviewType = {
domain: string;
image: AttachmentType;
title: string;
url: string;
};
export type PreviewMessageType = Array<WhatIsThis>; export type PreviewMessageType = Array<PreviewType>;
export type QuotedMessageType = { export type QuotedMessageType = {
attachments: Array<typeof window.WhatIsThis>; attachments: Array<typeof window.WhatIsThis>;
@ -90,6 +96,9 @@ export type StickerMessageType = {
stickerId: number; stickerId: number;
packKey: string; packKey: string;
data?: AttachmentType; data?: AttachmentType;
path?: string;
width?: number;
height?: number;
}; };
export type RetryOptions = Readonly<{ export type RetryOptions = Readonly<{
@ -129,9 +138,9 @@ export type MessageAttributesType = {
expireTimer?: number; expireTimer?: number;
groupMigration?: GroupMigrationType; groupMigration?: GroupMigrationType;
group_update?: GroupV1Update; group_update?: GroupV1Update;
hasAttachments?: boolean; hasAttachments?: boolean | 0 | 1;
hasFileAttachments?: boolean; hasFileAttachments?: boolean | 0 | 1;
hasVisualMediaAttachments?: boolean; hasVisualMediaAttachments?: boolean | 0 | 1;
isErased?: boolean; isErased?: boolean;
isTapToViewInvalid?: boolean; isTapToViewInvalid?: boolean;
isViewOnce?: boolean; isViewOnce?: boolean;

View file

@ -30,7 +30,7 @@ import { deleteExternalFiles } from '../types/Conversation';
import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion'; import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion';
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService'; import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message'; import { CURRENT_SCHEMA_VERSION } from '../types/Message2';
import { createBatcher } from '../util/batcher'; import { createBatcher } from '../util/batcher';
import { assert, strictAssert } from '../util/assert'; import { assert, strictAssert } from '../util/assert';
import { cleanDataForIpc } from './cleanDataForIpc'; import { cleanDataForIpc } from './cleanDataForIpc';

View file

@ -1491,7 +1491,7 @@ export function getPropsForAttachment(
url: path url: path
? window.Signal.Migrations.getAbsoluteAttachmentPath(path) ? window.Signal.Migrations.getAbsoluteAttachmentPath(path)
: undefined, : undefined,
screenshot: screenshot screenshot: screenshot?.path
? { ? {
...screenshot, ...screenshot,
url: window.Signal.Migrations.getAbsoluteAttachmentPath( url: window.Signal.Migrations.getAbsoluteAttachmentPath(
@ -1499,7 +1499,7 @@ export function getPropsForAttachment(
), ),
} }
: undefined, : undefined,
thumbnail: thumbnail thumbnail: thumbnail?.path
? { ? {
...thumbnail, ...thumbnail,
url: window.Signal.Migrations.getAbsoluteAttachmentPath( url: window.Signal.Migrations.getAbsoluteAttachmentPath(

View file

@ -270,7 +270,7 @@ describe('Contact', () => {
const result = await upgradeVersion(message.contact[0], { const result = await upgradeVersion(message.contact[0], {
message, message,
logger, logger,
regionCode: '1', getRegionCode: () => '1',
writeNewAttachmentData, writeNewAttachmentData,
}); });
assert.deepEqual(result, message.contact[0]); assert.deepEqual(result, message.contact[0]);
@ -311,7 +311,7 @@ describe('Contact', () => {
}; };
const result = await upgradeVersion(message.contact[0], { const result = await upgradeVersion(message.contact[0], {
message, message,
regionCode: 'US', getRegionCode: () => 'US',
logger, logger,
writeNewAttachmentData, writeNewAttachmentData,
}); });
@ -355,7 +355,7 @@ describe('Contact', () => {
], ],
}; };
const result = await upgradeVersion(message.contact[0], { const result = await upgradeVersion(message.contact[0], {
regionCode: '1', getRegionCode: () => '1',
writeNewAttachmentData, writeNewAttachmentData,
message, message,
logger, logger,
@ -440,7 +440,7 @@ describe('Contact', () => {
}; };
const result = await upgradeVersion(message.contact[0], { const result = await upgradeVersion(message.contact[0], {
regionCode: '1', getRegionCode: () => '1',
writeNewAttachmentData, writeNewAttachmentData,
message, message,
logger, logger,
@ -487,7 +487,7 @@ describe('Contact', () => {
], ],
}; };
const result = await upgradeVersion(message.contact[0], { const result = await upgradeVersion(message.contact[0], {
regionCode: '1', getRegionCode: () => '1',
writeNewAttachmentData, writeNewAttachmentData,
message, message,
logger, logger,
@ -534,7 +534,7 @@ describe('Contact', () => {
], ],
}; };
const result = await upgradeVersion(message.contact[0], { const result = await upgradeVersion(message.contact[0], {
regionCode: '1', getRegionCode: () => '1',
writeNewAttachmentData, writeNewAttachmentData,
message, message,
logger, logger,
@ -577,7 +577,7 @@ describe('Contact', () => {
}, },
}; };
const result = await upgradeVersion(message.contact[0], { const result = await upgradeVersion(message.contact[0], {
regionCode: '1', getRegionCode: () => '1',
writeNewAttachmentData, writeNewAttachmentData,
message, message,
logger, logger,
@ -606,7 +606,7 @@ describe('Contact', () => {
], ],
}; };
const result = await upgradeVersion(message.contact[0], { const result = await upgradeVersion(message.contact[0], {
regionCode: '1', getRegionCode: () => '1',
writeNewAttachmentData, writeNewAttachmentData,
message, message,
logger, logger,

View file

@ -1,30 +1,97 @@
// Copyright 2018-2020 Signal Messenger, LLC // Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
const { assert } = require('chai'); import { assert } from 'chai';
const sinon = require('sinon'); import * as sinon from 'sinon';
const Message = require('../../../js/modules/types/message'); import * as Message from '../../types/Message2';
const { SignalService } = require('../../../ts/protobuf'); import { SignalService } from '../../protobuf';
const Bytes = require('../../../ts/Bytes'); import * as Bytes from '../../Bytes';
import * as MIME from '../../types/MIME';
import type { EmbeddedContactType } from '../../types/EmbeddedContact';
import type {
MessageAttributesType,
StickerMessageType,
} from '../../model-types.d';
import type { AttachmentType } from '../../types/Attachment';
import type { LoggerType } from '../../types/Logging';
describe('Message', () => { describe('Message', () => {
const logger = { const logger: LoggerType = {
warn: () => null, warn: () => null,
error: () => null, error: () => null,
fatal: () => null,
info: () => null,
debug: () => null,
trace: () => null,
}; };
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,
};
}
function getDefaultContext(
props?: Partial<Message.ContextType>
): Message.ContextType {
return {
getAbsoluteAttachmentPath: (_path: string) =>
'fake-absolute-attachment-path',
getAbsoluteStickerPath: (_path: string) => 'fake-absolute-sticker-path',
getImageDimensions: async (_params: {
objectUrl: string;
logger: LoggerType;
}) => ({
width: 10,
height: 20,
}),
getRegionCode: () => 'region-code',
logger,
makeImageThumbnail: async (_params: {
size: number;
objectUrl: string;
contentType: MIME.MIMEType;
logger: LoggerType;
}) => new Blob(),
makeObjectUrl: (
_data: Uint8Array | ArrayBuffer,
_contentType: MIME.MIMEType
) => 'fake-object-url',
makeVideoScreenshot: async (_params: {
objectUrl: string;
contentType: MIME.MIMEType;
logger: LoggerType;
}) => new Blob(),
revokeObjectUrl: (_objectUrl: string) => undefined,
writeNewAttachmentData: async (_data: Uint8Array) =>
'fake-attachment-path',
writeNewStickerData: async (_sticker: StickerMessageType) =>
'fake-sticker-path',
...props,
};
}
const writeExistingAttachmentData = () => Promise.resolve();
describe('createAttachmentDataWriter', () => { describe('createAttachmentDataWriter', () => {
it('should ignore messages that didnt go through attachment migration', async () => { it('should ignore messages that didnt go through attachment migration', async () => {
const input = { const input = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 2, schemaVersion: 2,
}; });
const expected = { const expected = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 2, schemaVersion: 2,
}; });
const writeExistingAttachmentData = () => {};
const actual = await Message.createAttachmentDataWriter({ const actual = await Message.createAttachmentDataWriter({
writeExistingAttachmentData, writeExistingAttachmentData,
@ -34,17 +101,16 @@ describe('Message', () => {
}); });
it('should ignore messages without attachments', async () => { it('should ignore messages without attachments', async () => {
const input = { const input = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 4, schemaVersion: 4,
attachments: [], attachments: [],
}; });
const expected = { const expected = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 4, schemaVersion: 4,
attachments: [], attachments: [],
}; });
const writeExistingAttachmentData = () => {};
const actual = await Message.createAttachmentDataWriter({ const actual = await Message.createAttachmentDataWriter({
writeExistingAttachmentData, writeExistingAttachmentData,
@ -54,32 +120,39 @@ describe('Message', () => {
}); });
it('should write attachments to file system on original path', async () => { it('should write attachments to file system on original path', async () => {
const input = { const input = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 4, schemaVersion: 4,
attachments: [ attachments: [
{ {
contentType: MIME.IMAGE_GIF,
size: 3534,
path: 'ab/abcdefghi', path: 'ab/abcdefghi',
data: Bytes.fromString('Its easy if you try'), data: Bytes.fromString('Its easy if you try'),
}, },
], ],
}; });
const expected = { const expected = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 4, schemaVersion: 4,
attachments: [ attachments: [
{ {
contentType: MIME.IMAGE_GIF,
size: 3534,
path: 'ab/abcdefghi', path: 'ab/abcdefghi',
}, },
], ],
contact: [], contact: [],
preview: [], preview: [],
}; });
const writeExistingAttachmentData = attachment => { // eslint-disable-next-line @typescript-eslint/no-shadow
const writeExistingAttachmentData = async (
attachment: Pick<AttachmentType, 'data' | 'path'>
) => {
assert.equal(attachment.path, 'ab/abcdefghi'); assert.equal(attachment.path, 'ab/abcdefghi');
assert.strictEqual( assert.strictEqual(
Bytes.toString(attachment.data), Bytes.toString(attachment.data || new Uint8Array()),
'Its easy if you try' 'Its easy if you try'
); );
}; };
@ -92,11 +165,15 @@ describe('Message', () => {
}); });
it('should process quote attachment thumbnails', async () => { it('should process quote attachment thumbnails', async () => {
const input = { const input = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 4, schemaVersion: 4,
attachments: [], attachments: [],
quote: { quote: {
id: 3523,
isViewOnce: false,
messageId: 'some-message-id',
referencedMessageNotFound: false,
attachments: [ attachments: [
{ {
thumbnail: { thumbnail: {
@ -106,12 +183,16 @@ describe('Message', () => {
}, },
], ],
}, },
}; });
const expected = { const expected = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 4, schemaVersion: 4,
attachments: [], attachments: [],
quote: { quote: {
id: 3523,
isViewOnce: false,
messageId: 'some-message-id',
referencedMessageNotFound: false,
attachments: [ attachments: [
{ {
thumbnail: { thumbnail: {
@ -122,12 +203,15 @@ describe('Message', () => {
}, },
contact: [], contact: [],
preview: [], preview: [],
}; });
const writeExistingAttachmentData = attachment => { // eslint-disable-next-line @typescript-eslint/no-shadow
const writeExistingAttachmentData = async (
attachment: Pick<AttachmentType, 'data' | 'path'>
) => {
assert.equal(attachment.path, 'ab/abcdefghi'); assert.equal(attachment.path, 'ab/abcdefghi');
assert.strictEqual( assert.strictEqual(
Bytes.toString(attachment.data), Bytes.toString(attachment.data || new Uint8Array()),
'Its easy if you try' 'Its easy if you try'
); );
}; };
@ -140,45 +224,52 @@ describe('Message', () => {
}); });
it('should process contact avatars', async () => { it('should process contact avatars', async () => {
const input = { const input = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 4, schemaVersion: 4,
attachments: [], attachments: [],
contact: [ contact: [
{ {
name: 'john', name: { givenName: 'john' },
avatar: { avatar: {
isProfile: false, isProfile: false,
avatar: { avatar: {
contentType: MIME.IMAGE_PNG,
size: 47,
path: 'ab/abcdefghi', path: 'ab/abcdefghi',
data: Bytes.fromString('Its easy if you try'), data: Bytes.fromString('Its easy if you try'),
}, },
}, },
}, },
], ],
}; });
const expected = { const expected = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 4, schemaVersion: 4,
attachments: [], attachments: [],
contact: [ contact: [
{ {
name: 'john', name: { givenName: 'john' },
avatar: { avatar: {
isProfile: false, isProfile: false,
avatar: { avatar: {
contentType: MIME.IMAGE_PNG,
size: 47,
path: 'ab/abcdefghi', path: 'ab/abcdefghi',
}, },
}, },
}, },
], ],
preview: [], preview: [],
}; });
const writeExistingAttachmentData = attachment => { // eslint-disable-next-line @typescript-eslint/no-shadow
const writeExistingAttachmentData = async (
attachment: Pick<AttachmentType, 'data' | 'path'>
) => {
assert.equal(attachment.path, 'ab/abcdefghi'); assert.equal(attachment.path, 'ab/abcdefghi');
assert.strictEqual( assert.strictEqual(
Bytes.toString(attachment.data), Bytes.toString(attachment.data || new Uint8Array()),
'Its easy if you try' 'Its easy if you try'
); );
}; };
@ -193,14 +284,14 @@ describe('Message', () => {
describe('initializeSchemaVersion', () => { describe('initializeSchemaVersion', () => {
it('should ignore messages with previously inherited schema', () => { it('should ignore messages with previously inherited schema', () => {
const input = { const input = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 2, schemaVersion: 2,
}; });
const expected = { const expected = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
schemaVersion: 2, schemaVersion: 2,
}; });
const actual = Message.initializeSchemaVersion({ const actual = Message.initializeSchemaVersion({
message: input, message: input,
@ -211,15 +302,15 @@ describe('Message', () => {
context('for message without attachments', () => { context('for message without attachments', () => {
it('should initialize schema version to zero', () => { it('should initialize schema version to zero', () => {
const input = { const input = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
attachments: [], attachments: [],
}; });
const expected = { const expected = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
attachments: [], attachments: [],
schemaVersion: 0, schemaVersion: 0,
}; });
const actual = Message.initializeSchemaVersion({ const actual = Message.initializeSchemaVersion({
message: input, message: input,
@ -231,26 +322,28 @@ describe('Message', () => {
context('for message with attachments', () => { context('for message with attachments', () => {
it('should inherit existing attachment schema version', () => { it('should inherit existing attachment schema version', () => {
const input = { const input = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
attachments: [ attachments: [
{ {
contentType: 'image/jpeg', contentType: MIME.IMAGE_JPEG,
size: 45,
fileName: 'lennon.jpg', fileName: 'lennon.jpg',
schemaVersion: 7, schemaVersion: 7,
}, },
], ],
}; });
const expected = { const expected = getDefaultMessage({
body: 'Imagine there is no heaven…', body: 'Imagine there is no heaven…',
attachments: [ attachments: [
{ {
contentType: 'image/jpeg', contentType: MIME.IMAGE_JPEG,
size: 45,
fileName: 'lennon.jpg', fileName: 'lennon.jpg',
}, },
], ],
schemaVersion: 7, schemaVersion: 7,
}; });
const actual = Message.initializeSchemaVersion({ const actual = Message.initializeSchemaVersion({
message: input, message: input,
@ -263,10 +356,10 @@ describe('Message', () => {
describe('upgradeSchema', () => { describe('upgradeSchema', () => {
it('should upgrade an unversioned message to the latest version', async () => { it('should upgrade an unversioned message to the latest version', async () => {
const input = { const input = getDefaultMessage({
attachments: [ attachments: [
{ {
contentType: 'audio/aac', contentType: MIME.AUDIO_AAC,
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: Bytes.fromString('Its easy if you try'), data: Bytes.fromString('Its easy if you try'),
fileName: 'test\u202Dfig.exe', fileName: 'test\u202Dfig.exe',
@ -274,11 +367,11 @@ describe('Message', () => {
}, },
], ],
schemaVersion: 0, schemaVersion: 0,
}; });
const expected = { const expected = getDefaultMessage({
attachments: [ attachments: [
{ {
contentType: 'audio/aac', contentType: MIME.AUDIO_AAC,
flags: 1, flags: 1,
path: 'abc/abcdefg', path: 'abc/abcdefg',
fileName: 'test\uFFFDfig.exe', fileName: 'test\uFFFDfig.exe',
@ -290,10 +383,10 @@ describe('Message', () => {
hasFileAttachments: undefined, hasFileAttachments: undefined,
schemaVersion: Message.CURRENT_SCHEMA_VERSION, schemaVersion: Message.CURRENT_SCHEMA_VERSION,
contact: [], contact: [],
}; });
const expectedAttachmentData = 'Its easy if you try'; const expectedAttachmentData = 'Its easy if you try';
const context = { const context = getDefaultContext({
writeNewAttachmentData: async attachmentData => { writeNewAttachmentData: async attachmentData => {
assert.strictEqual( assert.strictEqual(
Bytes.toString(attachmentData), Bytes.toString(attachmentData),
@ -301,58 +394,46 @@ describe('Message', () => {
); );
return 'abc/abcdefg'; return 'abc/abcdefg';
}, },
getRegionCode: () => 'US', });
getAbsoluteAttachmentPath: () => 'some/path/on/disk',
makeObjectUrl: () => 'blob://FAKE',
revokeObjectUrl: () => null,
getImageDimensions: () => ({ height: 10, width: 15 }),
makeImageThumbnail: () => new Blob(),
makeVideoScreenshot: () => new Blob(),
logger: {
warn: () => null,
error: () => null,
},
};
const actual = await Message.upgradeSchema(input, context); const actual = await Message.upgradeSchema(input, context);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
context('with multiple upgrade steps', () => { context('with multiple upgrade steps', () => {
it('should return last valid message when any upgrade step fails', async () => { it('should return last valid message when any upgrade step fails', async () => {
const input = { const input = getDefaultMessage({
attachments: [ attachments: [
{ {
contentType: 'application/json', contentType: MIME.APPLICATION_JSON,
data: null,
fileName: 'test\u202Dfig.exe', fileName: 'test\u202Dfig.exe',
size: 1111, size: 1111,
}, },
], ],
body: 'start',
schemaVersion: 0, schemaVersion: 0,
}; });
const expected = { const expected = getDefaultMessage({
attachments: [ attachments: [
{ {
contentType: 'application/json', contentType: MIME.APPLICATION_JSON,
data: null,
fileName: 'test\u202Dfig.exe', fileName: 'test\u202Dfig.exe',
size: 1111, size: 1111,
}, },
], ],
hasUpgradedToVersion1: true, body: 'start +1',
schemaVersion: 1, schemaVersion: 1,
}; });
const v1 = async message => ({ const v1 = async (message: MessageAttributesType) => ({
...message, ...message,
hasUpgradedToVersion1: true, body: `${message.body} +1`,
}); });
const v2 = async () => { const v2 = async () => {
throw new Error('boom'); throw new Error('boom');
}; };
const v3 = async message => ({ const v3 = async (message: MessageAttributesType) => ({
...message, ...message,
hasUpgradedToVersion3: true, body: `${message.body} +3`,
}); });
const toVersion1 = Message._withSchemaVersion({ const toVersion1 = Message._withSchemaVersion({
@ -368,8 +449,8 @@ describe('Message', () => {
upgrade: v3, upgrade: v3,
}); });
const context = { logger }; const context = getDefaultContext({ logger });
const upgradeSchema = async message => const upgradeSchema = async (message: MessageAttributesType) =>
toVersion3( toVersion3(
await toVersion2(await toVersion1(message, context), context), await toVersion2(await toVersion1(message, context), context),
context context
@ -380,42 +461,40 @@ describe('Message', () => {
}); });
it('should skip out-of-order upgrade steps', async () => { it('should skip out-of-order upgrade steps', async () => {
const input = { const input = getDefaultMessage({
attachments: [ attachments: [
{ {
contentType: 'application/json', contentType: MIME.APPLICATION_JSON,
data: null,
fileName: 'test\u202Dfig.exe', fileName: 'test\u202Dfig.exe',
size: 1111, size: 1111,
}, },
], ],
body: 'start',
schemaVersion: 0, schemaVersion: 0,
}; });
const expected = { const expected = getDefaultMessage({
attachments: [ attachments: [
{ {
contentType: 'application/json', contentType: MIME.APPLICATION_JSON,
data: null,
fileName: 'test\u202Dfig.exe', fileName: 'test\u202Dfig.exe',
size: 1111, size: 1111,
}, },
], ],
body: 'start +1 +2',
schemaVersion: 2, schemaVersion: 2,
hasUpgradedToVersion1: true, });
hasUpgradedToVersion2: true,
};
const v1 = async attachment => ({ const v1 = async (message: MessageAttributesType) => ({
...attachment, ...message,
hasUpgradedToVersion1: true, body: `${message.body} +1`,
}); });
const v2 = async attachment => ({ const v2 = async (message: MessageAttributesType) => ({
...attachment, ...message,
hasUpgradedToVersion2: true, body: `${message.body} +2`,
}); });
const v3 = async attachment => ({ const v3 = async (message: MessageAttributesType) => ({
...attachment, ...message,
hasUpgradedToVersion3: true, body: `${message.body} +3`,
}); });
const toVersion1 = Message._withSchemaVersion({ const toVersion1 = Message._withSchemaVersion({
@ -431,15 +510,13 @@ describe('Message', () => {
upgrade: v3, upgrade: v3,
}); });
const context = { logger }; const context = getDefaultContext({ logger });
// NOTE: We upgrade to 3 before 2, i.e. the pipeline should abort: const atVersion1 = await toVersion1(input, context);
const upgradeSchema = async attachment =>
toVersion2(
await toVersion3(await toVersion1(attachment, context), context),
context
);
const actual = await upgradeSchema(input); // Note: this will fail to apply and log, since it's jumping two versions up
const atVersion3 = await toVersion3(atVersion1, context);
const actual = await toVersion2(atVersion3, context);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
}); });
@ -447,37 +524,49 @@ describe('Message', () => {
describe('_withSchemaVersion', () => { describe('_withSchemaVersion', () => {
it('should require a version number', () => { it('should require a version number', () => {
const toVersionX = () => {}; const toVersionX = () => null;
assert.throws( assert.throws(
() => () =>
Message._withSchemaVersion({ schemaVersion: toVersionX, upgrade: 2 }), Message._withSchemaVersion({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
schemaVersion: toVersionX as any,
upgrade: () => Promise.resolve(getDefaultMessage()),
}),
'_withSchemaVersion: schemaVersion is invalid' '_withSchemaVersion: schemaVersion is invalid'
); );
}); });
it('should require an upgrade function', () => { it('should require an upgrade function', () => {
assert.throws( assert.throws(
() => Message._withSchemaVersion({ schemaVersion: 2, upgrade: 3 }), () =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Message._withSchemaVersion({ schemaVersion: 2, upgrade: 3 as any }),
'_withSchemaVersion: upgrade must be a function' '_withSchemaVersion: upgrade must be a function'
); );
}); });
it('should skip upgrading if message has already been upgraded', async () => { it('should skip upgrading if message has already been upgraded', async () => {
const upgrade = async message => ({ ...message, foo: true }); const upgrade = async (message: MessageAttributesType) => ({
...message,
foo: true,
});
const upgradeWithVersion = Message._withSchemaVersion({ const upgradeWithVersion = Message._withSchemaVersion({
schemaVersion: 3, schemaVersion: 3,
upgrade, upgrade,
}); });
const input = { const input = getDefaultMessage({
id: 'guid-guid-guid-guid', id: 'guid-guid-guid-guid',
schemaVersion: 4, schemaVersion: 4,
}; });
const expected = { const expected = getDefaultMessage({
id: 'guid-guid-guid-guid', id: 'guid-guid-guid-guid',
schemaVersion: 4, schemaVersion: 4,
}; });
const actual = await upgradeWithVersion(input, { logger }); const actual = await upgradeWithVersion(
input,
getDefaultContext({ logger })
);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
@ -490,15 +579,18 @@ describe('Message', () => {
upgrade, upgrade,
}); });
const input = { const input = getDefaultMessage({
id: 'guid-guid-guid-guid', id: 'guid-guid-guid-guid',
schemaVersion: 0, schemaVersion: 0,
}; });
const expected = { const expected = getDefaultMessage({
id: 'guid-guid-guid-guid', id: 'guid-guid-guid-guid',
schemaVersion: 0, schemaVersion: 0,
}; });
const actual = await upgradeWithVersion(input, { logger }); const actual = await upgradeWithVersion(
input,
getDefaultContext({ logger })
);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
@ -506,18 +598,22 @@ describe('Message', () => {
const upgrade = async () => null; const upgrade = async () => null;
const upgradeWithVersion = Message._withSchemaVersion({ const upgradeWithVersion = Message._withSchemaVersion({
schemaVersion: 3, schemaVersion: 3,
upgrade, // eslint-disable-next-line @typescript-eslint/no-explicit-any
upgrade: upgrade as any,
}); });
const input = { const input = getDefaultMessage({
id: 'guid-guid-guid-guid', id: 'guid-guid-guid-guid',
schemaVersion: 0, schemaVersion: 0,
}; });
const expected = { const expected = getDefaultMessage({
id: 'guid-guid-guid-guid', id: 'guid-guid-guid-guid',
schemaVersion: 0, schemaVersion: 0,
}; });
const actual = await upgradeWithVersion(input, { logger }); const actual = await upgradeWithVersion(
input,
getDefaultContext({ logger })
);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
}); });
@ -529,10 +625,10 @@ describe('Message', () => {
.throws(new Error("Shouldn't be called")); .throws(new Error("Shouldn't be called"));
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = { const message = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
}; });
const result = await upgradeVersion(message); const result = await upgradeVersion(message, getDefaultContext());
assert.deepEqual(result, message); assert.deepEqual(result, message);
}); });
@ -542,20 +638,32 @@ describe('Message', () => {
.throws(new Error("Shouldn't be called")); .throws(new Error("Shouldn't be called"));
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = { const message = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
quote: { quote: {
text: 'hey!', text: 'hey!',
}, id: 34233,
}; isViewOnce: false,
const expected = { messageId: 'message-id',
referencedMessageNotFound: false,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
});
const expected = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
quote: { quote: {
text: 'hey!', text: 'hey!',
attachments: [], attachments: [],
id: 34233,
isViewOnce: false,
messageId: 'message-id',
referencedMessageNotFound: false,
}, },
}; });
const result = await upgradeVersion(message, { logger }); const result = await upgradeVersion(
message,
getDefaultContext({ logger })
);
assert.deepEqual(result, expected); assert.deepEqual(result, expected);
}); });
@ -565,14 +673,21 @@ describe('Message', () => {
.throws(new Error("Shouldn't be called")); .throws(new Error("Shouldn't be called"));
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = { const message = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
quote: { quote: {
text: 'hey!', text: 'hey!',
attachments: [], attachments: [],
id: 34233,
isViewOnce: false,
messageId: 'message-id',
referencedMessageNotFound: false,
}, },
}; });
const result = await upgradeVersion(message, { logger }); const result = await upgradeVersion(
message,
getDefaultContext({ logger })
);
assert.deepEqual(result, message); assert.deepEqual(result, message);
}); });
@ -582,7 +697,7 @@ describe('Message', () => {
.throws(new Error("Shouldn't be called")); .throws(new Error("Shouldn't be called"));
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = { const message = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
quote: { quote: {
text: 'hey!', text: 'hey!',
@ -592,9 +707,16 @@ describe('Message', () => {
contentType: 'text/plain', contentType: 'text/plain',
}, },
], ],
id: 34233,
isViewOnce: false,
messageId: 'message-id',
referencedMessageNotFound: false,
}, },
}; });
const result = await upgradeVersion(message, { logger }); const result = await upgradeVersion(
message,
getDefaultContext({ logger })
);
assert.deepEqual(result, message); assert.deepEqual(result, message);
}); });
@ -604,7 +726,7 @@ describe('Message', () => {
.returns({ fileName: 'processed!' }); .returns({ fileName: 'processed!' });
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = { const message = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
quote: { quote: {
text: 'hey!', text: 'hey!',
@ -617,9 +739,13 @@ describe('Message', () => {
}, },
}, },
], ],
id: 34233,
isViewOnce: false,
messageId: 'message-id',
referencedMessageNotFound: false,
}, },
}; });
const expected = { const expected = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
quote: { quote: {
text: 'hey!', text: 'hey!',
@ -632,9 +758,16 @@ describe('Message', () => {
}, },
}, },
], ],
id: 34233,
isViewOnce: false,
messageId: 'message-id',
referencedMessageNotFound: false,
}, },
}; });
const result = await upgradeVersion(message, { logger }); const result = await upgradeVersion(
message,
getDefaultContext({ logger })
);
assert.deepEqual(result, expected); assert.deepEqual(result, expected);
}); });
@ -644,7 +777,7 @@ describe('Message', () => {
}); });
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = { const message = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
quote: { quote: {
text: 'hey!', text: 'hey!',
@ -655,9 +788,13 @@ describe('Message', () => {
}, },
}, },
], ],
id: 34233,
isViewOnce: false,
messageId: 'message-id',
referencedMessageNotFound: false,
}, },
}; });
const expected = { const expected = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
quote: { quote: {
text: 'hey!', text: 'hey!',
@ -668,9 +805,16 @@ describe('Message', () => {
}, },
}, },
], ],
id: 34233,
isViewOnce: false,
messageId: 'message-id',
referencedMessageNotFound: false,
}, },
}; });
const result = await upgradeVersion(message, { logger }); const result = await upgradeVersion(
message,
getDefaultContext({ logger })
);
assert.deepEqual(result, expected); assert.deepEqual(result, expected);
}); });
}); });
@ -682,22 +826,23 @@ describe('Message', () => {
.throws(new Error("Shouldn't be called")); .throws(new Error("Shouldn't be called"));
const upgradeVersion = Message._mapContact(upgradeContact); const upgradeVersion = Message._mapContact(upgradeContact);
const message = { const message = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
}; });
const expected = { const expected = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
contact: [], contact: [],
}; });
const result = await upgradeVersion(message); const result = await upgradeVersion(message, getDefaultContext());
assert.deepEqual(result, expected); assert.deepEqual(result, expected);
}); });
it('handles one contact', async () => { it('handles one contact', async () => {
const upgradeContact = contact => Promise.resolve(contact); const upgradeContact = (contact: EmbeddedContactType) =>
Promise.resolve(contact);
const upgradeVersion = Message._mapContact(upgradeContact); const upgradeVersion = Message._mapContact(upgradeContact);
const message = { const message = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
contact: [ contact: [
{ {
@ -706,8 +851,8 @@ describe('Message', () => {
}, },
}, },
], ],
}; });
const expected = { const expected = getDefaultMessage({
body: 'hey there!', body: 'hey there!',
contact: [ contact: [
{ {
@ -716,8 +861,8 @@ describe('Message', () => {
}, },
}, },
], ],
}; });
const result = await upgradeVersion(message); const result = await upgradeVersion(message, getDefaultContext());
assert.deepEqual(result, expected); assert.deepEqual(result, expected);
}); });
}); });

View file

@ -4,15 +4,29 @@
import { assert } from 'chai'; import { assert } from 'chai';
import * as Message from '../../../types/message/initializeAttachmentMetadata'; import * as Message from '../../../types/message/initializeAttachmentMetadata';
import type { IncomingMessage } from '../../../types/Message';
import { SignalService } from '../../../protobuf'; import { SignalService } from '../../../protobuf';
import * as MIME from '../../../types/MIME'; import * as MIME from '../../../types/MIME';
import * as Bytes from '../../../Bytes'; 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('Message', () => {
describe('initializeAttachmentMetadata', () => { describe('initializeAttachmentMetadata', () => {
it('should classify visual media attachments', async () => { it('should classify visual media attachments', async () => {
const input: IncomingMessage = { const input = getDefaultMessage({
type: 'incoming', type: 'incoming',
conversationId: 'foo', conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111', id: '11111111-1111-1111-1111-111111111111',
@ -27,8 +41,8 @@ describe('Message', () => {
size: 1111, size: 1111,
}, },
], ],
}; });
const expected: IncomingMessage = { const expected = getDefaultMessage({
type: 'incoming', type: 'incoming',
conversationId: 'foo', conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111', id: '11111111-1111-1111-1111-111111111111',
@ -46,14 +60,14 @@ describe('Message', () => {
hasAttachments: 1, hasAttachments: 1,
hasVisualMediaAttachments: 1, hasVisualMediaAttachments: 1,
hasFileAttachments: undefined, hasFileAttachments: undefined,
}; });
const actual = await Message.initializeAttachmentMetadata(input); const actual = await Message.initializeAttachmentMetadata(input);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
it('should classify file attachments', async () => { it('should classify file attachments', async () => {
const input: IncomingMessage = { const input = getDefaultMessage({
type: 'incoming', type: 'incoming',
conversationId: 'foo', conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111', id: '11111111-1111-1111-1111-111111111111',
@ -68,8 +82,8 @@ describe('Message', () => {
size: 1111, size: 1111,
}, },
], ],
}; });
const expected: IncomingMessage = { const expected = getDefaultMessage({
type: 'incoming', type: 'incoming',
conversationId: 'foo', conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111', id: '11111111-1111-1111-1111-111111111111',
@ -87,14 +101,14 @@ describe('Message', () => {
hasAttachments: 1, hasAttachments: 1,
hasVisualMediaAttachments: undefined, hasVisualMediaAttachments: undefined,
hasFileAttachments: 1, hasFileAttachments: 1,
}; });
const actual = await Message.initializeAttachmentMetadata(input); const actual = await Message.initializeAttachmentMetadata(input);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
it('should classify voice message attachments', async () => { it('should classify voice message attachments', async () => {
const input: IncomingMessage = { const input = getDefaultMessage({
type: 'incoming', type: 'incoming',
conversationId: 'foo', conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111', id: '11111111-1111-1111-1111-111111111111',
@ -110,8 +124,8 @@ describe('Message', () => {
size: 1111, size: 1111,
}, },
], ],
}; });
const expected: IncomingMessage = { const expected = getDefaultMessage({
type: 'incoming', type: 'incoming',
conversationId: 'foo', conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111', id: '11111111-1111-1111-1111-111111111111',
@ -130,14 +144,14 @@ describe('Message', () => {
hasAttachments: 1, hasAttachments: 1,
hasVisualMediaAttachments: undefined, hasVisualMediaAttachments: undefined,
hasFileAttachments: undefined, hasFileAttachments: undefined,
}; });
const actual = await Message.initializeAttachmentMetadata(input); const actual = await Message.initializeAttachmentMetadata(input);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
it('does not include long message attachments', async () => { it('does not include long message attachments', async () => {
const input: IncomingMessage = { const input = getDefaultMessage({
type: 'incoming', type: 'incoming',
conversationId: 'foo', conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111', id: '11111111-1111-1111-1111-111111111111',
@ -152,8 +166,8 @@ describe('Message', () => {
size: 1111, size: 1111,
}, },
], ],
}; });
const expected: IncomingMessage = { const expected = getDefaultMessage({
type: 'incoming', type: 'incoming',
conversationId: 'foo', conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111', id: '11111111-1111-1111-1111-111111111111',
@ -171,14 +185,14 @@ describe('Message', () => {
hasAttachments: 0, hasAttachments: 0,
hasVisualMediaAttachments: undefined, hasVisualMediaAttachments: undefined,
hasFileAttachments: undefined, hasFileAttachments: undefined,
}; });
const actual = await Message.initializeAttachmentMetadata(input); const actual = await Message.initializeAttachmentMetadata(input);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
it('handles not attachments', async () => { it('handles not attachments', async () => {
const input: IncomingMessage = { const input = getDefaultMessage({
type: 'incoming', type: 'incoming',
conversationId: 'foo', conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111', id: '11111111-1111-1111-1111-111111111111',
@ -186,8 +200,8 @@ describe('Message', () => {
received_at: 1523317140899, received_at: 1523317140899,
sent_at: 1523317140800, sent_at: 1523317140800,
attachments: [], attachments: [],
}; });
const expected: IncomingMessage = { const expected = getDefaultMessage({
type: 'incoming', type: 'incoming',
conversationId: 'foo', conversationId: 'foo',
id: '11111111-1111-1111-1111-111111111111', id: '11111111-1111-1111-1111-111111111111',
@ -198,7 +212,7 @@ describe('Message', () => {
hasAttachments: 0, hasAttachments: 0,
hasVisualMediaAttachments: undefined, hasVisualMediaAttachments: undefined,
hasFileAttachments: undefined, hasFileAttachments: undefined,
}; });
const actual = await Message.initializeAttachmentMetadata(input); const actual = await Message.initializeAttachmentMetadata(input);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);

View file

@ -24,8 +24,8 @@ import {
} from '../util/GoogleChrome'; } from '../util/GoogleChrome';
import type { LocalizerType } from './Util'; import type { LocalizerType } from './Util';
import { ThemeType } from './Util'; import { ThemeType } from './Util';
import * as GoogleChrome from '../util/GoogleChrome';
import { scaleImageToLevel } from '../util/scaleImageToLevel'; import { scaleImageToLevel } from '../util/scaleImageToLevel';
import * as GoogleChrome from '../util/GoogleChrome';
import { parseIntOrThrow } from '../util/parseIntOrThrow'; import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { getValue } from '../RemoteConfig'; import { getValue } from '../RemoteConfig';
@ -58,6 +58,7 @@ export type AttachmentType = {
url?: string; url?: string;
contentType: MIME.MIMEType; contentType: MIME.MIMEType;
path: string; path: string;
data?: Uint8Array;
}; };
screenshotData?: Uint8Array; screenshotData?: Uint8Array;
screenshotPath?: string; screenshotPath?: string;
@ -74,6 +75,9 @@ export type AttachmentType = {
/** Legacy field. Used only for downloading old attachments */ /** Legacy field. Used only for downloading old attachments */
id?: number; id?: number;
/** Legacy field, used long ago for migrating attachments to disk. */
schemaVersion?: number;
/** Removed once we download the attachment */ /** Removed once we download the attachment */
digest?: string; digest?: string;
key?: string; key?: string;
@ -159,11 +163,12 @@ export type AttachmentDraftType =
}; };
export type ThumbnailType = { export type ThumbnailType = {
height: number; height?: number;
width: number; width?: number;
url?: string; url?: string;
contentType: MIME.MIMEType; contentType: MIME.MIMEType;
path: string; path?: string;
data?: Uint8Array;
// Only used when quote needed to make an in-memory thumbnail // Only used when quote needed to make an in-memory thumbnail
objectUrl?: string; objectUrl?: string;
}; };
@ -432,16 +437,19 @@ export async function captureDimensionsAndScreenshot(
attachment: AttachmentType, attachment: AttachmentType,
params: { params: {
writeNewAttachmentData: (data: Uint8Array) => Promise<string>; writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
getAbsoluteAttachmentPath: (path: string) => Promise<string>; getAbsoluteAttachmentPath: (path: string) => string;
makeObjectUrl: ( makeObjectUrl: (
data: Uint8Array | ArrayBuffer, data: Uint8Array | ArrayBuffer,
contentType: MIME.MIMEType contentType: MIME.MIMEType
) => string; ) => string;
revokeObjectUrl: (path: string) => void; revokeObjectUrl: (path: string) => void;
getImageDimensions: (params: { objectUrl: string; logger: LoggerType }) => { getImageDimensions: (params: {
objectUrl: string;
logger: LoggerType;
}) => Promise<{
width: number; width: number;
height: number; height: number;
}; }>;
makeImageThumbnail: (params: { makeImageThumbnail: (params: {
size: number; size: number;
objectUrl: string; objectUrl: string;
@ -481,7 +489,7 @@ export async function captureDimensionsAndScreenshot(
return attachment; return attachment;
} }
const absolutePath = await getAbsoluteAttachmentPath(attachment.path); const absolutePath = getAbsoluteAttachmentPath(attachment.path);
if (GoogleChrome.isImageTypeSupported(contentType)) { if (GoogleChrome.isImageTypeSupported(contentType)) {
try { try {

View file

@ -191,12 +191,12 @@ export function parseAndWriteAvatar(
contact: EmbeddedContactType, contact: EmbeddedContactType,
context: { context: {
message: MessageAttributesType; message: MessageAttributesType;
regionCode: string; getRegionCode: () => string | undefined;
logger: Pick<LoggerType, 'error'>; logger: Pick<LoggerType, 'error'>;
writeNewAttachmentData: (data: Uint8Array) => Promise<string>; writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
} }
): Promise<EmbeddedContactType> => { ): Promise<EmbeddedContactType> => {
const { message, regionCode, logger } = context; const { message, getRegionCode, logger } = context;
const { avatar } = contact; const { avatar } = contact;
const contactWithUpdatedAvatar = const contactWithUpdatedAvatar =
@ -212,7 +212,7 @@ export function parseAndWriteAvatar(
// eliminates empty numbers, emails, and addresses; adds type if not provided // eliminates empty numbers, emails, and addresses; adds type if not provided
const parsedContact = parseContact(contactWithUpdatedAvatar, { const parsedContact = parseContact(contactWithUpdatedAvatar, {
regionCode, regionCode: getRegionCode(),
}); });
const error = _validate(parsedContact, { const error = _validate(parsedContact, {
@ -231,7 +231,7 @@ export function parseAndWriteAvatar(
function parseContact( function parseContact(
contact: EmbeddedContactType, contact: EmbeddedContactType,
{ regionCode }: { regionCode: string } { regionCode }: { regionCode: string | undefined }
): EmbeddedContactType { ): EmbeddedContactType {
const boundParsePhone = (phoneNumber: Phone): Phone | undefined => const boundParsePhone = (phoneNumber: Phone): Phone | undefined =>
parsePhoneItem(phoneNumber, { regionCode }); parsePhoneItem(phoneNumber, { regionCode });
@ -294,7 +294,7 @@ export function _validate(
function parsePhoneItem( function parsePhoneItem(
item: Phone, item: Phone,
{ regionCode }: { regionCode: string } { regionCode }: { regionCode: string | undefined }
): Phone | undefined { ): Phone | undefined {
if (!item.value) { if (!item.value) {
return undefined; return undefined;

View file

@ -75,13 +75,13 @@ export type ProfileChangeNotificationMessage = Readonly<
ExpirationTimerUpdate ExpirationTimerUpdate
>; >;
type SharedMessageProperties = Readonly<{ export type SharedMessageProperties = Readonly<{
conversationId: string; conversationId: string;
sent_at: number; sent_at: number;
timestamp: number; timestamp: number;
}>; }>;
type ExpirationTimerUpdate = Partial< export type ExpirationTimerUpdate = Partial<
Readonly<{ Readonly<{
expirationTimerUpdate: Readonly<{ expirationTimerUpdate: Readonly<{
expireTimer: number; expireTimer: number;
@ -91,7 +91,7 @@ type ExpirationTimerUpdate = Partial<
}> }>
>; >;
type MessageSchemaVersion5 = Partial< export type MessageSchemaVersion5 = Partial<
Readonly<{ Readonly<{
hasAttachments: IndexableBoolean; hasAttachments: IndexableBoolean;
hasVisualMediaAttachments: IndexablePresence; hasVisualMediaAttachments: IndexablePresence;
@ -99,7 +99,7 @@ type MessageSchemaVersion5 = Partial<
}> }>
>; >;
type MessageSchemaVersion6 = Partial< export type MessageSchemaVersion6 = Partial<
Readonly<{ Readonly<{
contact: Array<EmbeddedContactType>; contact: Array<EmbeddedContactType>;
}> }>

View file

@ -1,19 +1,80 @@
// Copyright 2018-2021 Signal Messenger, LLC // Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
const { isFunction, isObject, isString, omit } = require('lodash'); import { isFunction, isObject, isString, omit } from 'lodash';
const Contact = require('../../../ts/types/EmbeddedContact'); import * as Contact from './EmbeddedContact';
const Attachment = require('../../../ts/types/Attachment'); import type { AttachmentType } from './Attachment';
const Errors = require('../../../ts/types/errors'); import {
const SchemaVersion = require('../../../ts/types/SchemaVersion'); autoOrientJPEG,
const { captureDimensionsAndScreenshot,
initializeAttachmentMetadata, hasData,
} = require('../../../ts/types/message/initializeAttachmentMetadata'); migrateDataToFileSystem,
const MessageTS = require('../../../ts/types/Message'); removeSchemaVersion,
replaceUnicodeOrderOverrides,
replaceUnicodeV2,
} from './Attachment';
import * as Errors from './errors';
import * as SchemaVersion from './SchemaVersion';
import { initializeAttachmentMetadata } from './message/initializeAttachmentMetadata';
const GROUP = 'group'; import type * as MIME from './MIME';
const PRIVATE = 'private'; import type { LoggerType } from './Logging';
import type { EmbeddedContactType } from './EmbeddedContact';
import type {
MessageAttributesType,
PreviewMessageType,
PreviewType,
QuotedMessageType,
StickerMessageType,
} from '../model-types.d';
export { hasExpiration } from './Message';
export const GROUP = 'group';
export const PRIVATE = 'private';
export type ContextType = {
getAbsoluteAttachmentPath: (path: string) => string;
getAbsoluteStickerPath: (path: string) => string;
getImageDimensions: (params: {
objectUrl: string;
logger: LoggerType;
}) => Promise<{
width: number;
height: number;
}>;
getRegionCode: () => string;
logger: LoggerType;
makeImageThumbnail: (params: {
size: number;
objectUrl: string;
contentType: MIME.MIMEType;
logger: LoggerType;
}) => Promise<Blob>;
makeObjectUrl: (
data: Uint8Array | ArrayBuffer,
contentType: MIME.MIMEType
) => string;
makeVideoScreenshot: (params: {
objectUrl: string;
contentType: MIME.MIMEType;
logger: LoggerType;
}) => Promise<Blob>;
maxVersion?: number;
revokeObjectUrl: (objectUrl: string) => void;
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
writeNewStickerData: (sticker: StickerMessageType) => Promise<string>;
};
type WriteExistingAttachmentDataType = (
attachment: Pick<AttachmentType, 'data' | 'path'>
) => Promise<void>;
export type ContextWithMessageType = ContextType & {
message: MessageAttributesType;
};
// Schema version history // Schema version history
// //
@ -55,32 +116,30 @@ const PRIVATE = 'private';
const INITIAL_SCHEMA_VERSION = 0; const INITIAL_SCHEMA_VERSION = 0;
// Public API
exports.GROUP = GROUP;
exports.PRIVATE = PRIVATE;
// Placeholder until we have stronger preconditions: // Placeholder until we have stronger preconditions:
exports.isValid = () => true; export const isValid = (_message: MessageAttributesType): boolean => true;
// Schema // Schema
exports.initializeSchemaVersion = ({ message, logger }) => { export const initializeSchemaVersion = ({
message,
logger,
}: {
message: MessageAttributesType;
logger: LoggerType;
}): MessageAttributesType => {
const isInitialized = const isInitialized =
SchemaVersion.isValid(message.schemaVersion) && message.schemaVersion >= 1; SchemaVersion.isValid(message.schemaVersion) && message.schemaVersion >= 1;
if (isInitialized) { if (isInitialized) {
return message; return message;
} }
const numAttachments = Array.isArray(message.attachments) const firstAttachment = message?.attachments?.[0];
? message.attachments.length if (!firstAttachment) {
: 0;
const hasAttachments = numAttachments > 0;
if (!hasAttachments) {
return { ...message, schemaVersion: INITIAL_SCHEMA_VERSION }; return { ...message, schemaVersion: INITIAL_SCHEMA_VERSION };
} }
// All attachments should have the same schema version, so we just pick // All attachments should have the same schema version, so we just pick
// the first one: // the first one:
const firstAttachment = message.attachments[0];
const inheritedSchemaVersion = SchemaVersion.isValid( const inheritedSchemaVersion = SchemaVersion.isValid(
firstAttachment.schemaVersion firstAttachment.schemaVersion
) )
@ -89,9 +148,10 @@ exports.initializeSchemaVersion = ({ message, logger }) => {
const messageWithInitialSchema = { const messageWithInitialSchema = {
...message, ...message,
schemaVersion: inheritedSchemaVersion, schemaVersion: inheritedSchemaVersion,
attachments: message.attachments.map(attachment => attachments:
Attachment.removeSchemaVersion({ attachment, logger }) message?.attachments?.map(attachment =>
), removeSchemaVersion({ attachment, logger })
) || [],
}; };
return messageWithInitialSchema; return messageWithInitialSchema;
@ -101,7 +161,19 @@ exports.initializeSchemaVersion = ({ message, logger }) => {
// type UpgradeStep = (Message, Context) -> Promise Message // type UpgradeStep = (Message, Context) -> Promise Message
// SchemaVersion -> UpgradeStep -> UpgradeStep // SchemaVersion -> UpgradeStep -> UpgradeStep
exports._withSchemaVersion = ({ schemaVersion, upgrade }) => { export const _withSchemaVersion = ({
schemaVersion,
upgrade,
}: {
schemaVersion: number;
upgrade: (
message: MessageAttributesType,
context: ContextType
) => Promise<MessageAttributesType>;
}): ((
message: MessageAttributesType,
context: ContextType
) => Promise<MessageAttributesType>) => {
if (!SchemaVersion.isValid(schemaVersion)) { if (!SchemaVersion.isValid(schemaVersion)) {
throw new TypeError('_withSchemaVersion: schemaVersion is invalid'); throw new TypeError('_withSchemaVersion: schemaVersion is invalid');
} }
@ -109,7 +181,7 @@ exports._withSchemaVersion = ({ schemaVersion, upgrade }) => {
throw new TypeError('_withSchemaVersion: upgrade must be a function'); throw new TypeError('_withSchemaVersion: upgrade must be a function');
} }
return async (message, context) => { return async (message: MessageAttributesType, context: ContextType) => {
if (!context || !isObject(context.logger)) { if (!context || !isObject(context.logger)) {
throw new TypeError( throw new TypeError(
'_withSchemaVersion: context must have logger object' '_withSchemaVersion: context must have logger object'
@ -117,7 +189,7 @@ exports._withSchemaVersion = ({ schemaVersion, upgrade }) => {
} }
const { logger } = context; const { logger } = context;
if (!exports.isValid(message)) { if (!isValid(message)) {
logger.error( logger.error(
'Message._withSchemaVersion: Invalid input message:', 'Message._withSchemaVersion: Invalid input message:',
message message
@ -125,7 +197,7 @@ exports._withSchemaVersion = ({ schemaVersion, upgrade }) => {
return message; return message;
} }
const isAlreadyUpgraded = message.schemaVersion >= schemaVersion; const isAlreadyUpgraded = (message.schemaVersion || 0) >= schemaVersion;
if (isAlreadyUpgraded) { if (isAlreadyUpgraded) {
return message; return message;
} }
@ -152,7 +224,7 @@ exports._withSchemaVersion = ({ schemaVersion, upgrade }) => {
return message; return message;
} }
if (!exports.isValid(upgradedMessage)) { if (!isValid(upgradedMessage)) {
logger.error( logger.error(
'Message._withSchemaVersion: Invalid upgraded message:', 'Message._withSchemaVersion: Invalid upgraded message:',
upgradedMessage upgradedMessage
@ -168,34 +240,59 @@ exports._withSchemaVersion = ({ schemaVersion, upgrade }) => {
// _mapAttachments :: (Attachment -> Promise Attachment) -> // _mapAttachments :: (Attachment -> Promise Attachment) ->
// (Message, Context) -> // (Message, Context) ->
// Promise Message // Promise Message
exports._mapAttachments = upgradeAttachment => async (message, context) => { export type UpgradeAttachmentType = (
const upgradeWithContext = attachment => attachment: AttachmentType,
upgradeAttachment(attachment, context, message); context: ContextType,
const attachments = await Promise.all( message: MessageAttributesType
(message.attachments || []).map(upgradeWithContext) ) => Promise<AttachmentType>;
);
return { ...message, attachments }; export const _mapAttachments =
}; (upgradeAttachment: UpgradeAttachmentType) =>
async (
message: MessageAttributesType,
context: ContextType
): Promise<MessageAttributesType> => {
const upgradeWithContext = (attachment: AttachmentType) =>
upgradeAttachment(attachment, context, message);
const attachments = await Promise.all(
(message.attachments || []).map(upgradeWithContext)
);
return { ...message, attachments };
};
// Public API // Public API
// _mapContact :: (Contact -> Promise Contact) -> // _mapContact :: (Contact -> Promise Contact) ->
// (Message, Context) -> // (Message, Context) ->
// Promise Message // Promise Message
exports._mapContact = upgradeContact => async (message, context) => {
const contextWithMessage = { ...context, message }; export type UpgradeContactType = (
const upgradeWithContext = contact => contact: EmbeddedContactType,
upgradeContact(contact, contextWithMessage); contextWithMessage: ContextWithMessageType
const contact = await Promise.all( ) => Promise<EmbeddedContactType>;
(message.contact || []).map(upgradeWithContext) export const _mapContact =
); (upgradeContact: UpgradeContactType) =>
return { ...message, contact }; async (
}; message: MessageAttributesType,
context: ContextType
): Promise<MessageAttributesType> => {
const contextWithMessage = { ...context, message };
const upgradeWithContext = (contact: EmbeddedContactType) =>
upgradeContact(contact, contextWithMessage);
const contact = await Promise.all(
(message.contact || []).map(upgradeWithContext)
);
return { ...message, contact };
};
// _mapQuotedAttachments :: (QuotedAttachment -> Promise QuotedAttachment) -> // _mapQuotedAttachments :: (QuotedAttachment -> Promise QuotedAttachment) ->
// (Message, Context) -> // (Message, Context) ->
// Promise Message // Promise Message
exports._mapQuotedAttachments = export const _mapQuotedAttachments =
upgradeAttachment => async (message, context) => { (upgradeAttachment: UpgradeAttachmentType) =>
async (
message: MessageAttributesType,
context: ContextType
): Promise<MessageAttributesType> => {
if (!message.quote) { if (!message.quote) {
return message; return message;
} }
@ -203,13 +300,19 @@ exports._mapQuotedAttachments =
throw new Error('_mapQuotedAttachments: context must have logger object'); throw new Error('_mapQuotedAttachments: context must have logger object');
} }
const upgradeWithContext = async attachment => { const upgradeWithContext = async (
attachment: AttachmentType
): Promise<AttachmentType> => {
const { thumbnail } = attachment; const { thumbnail } = attachment;
if (!thumbnail) { if (!thumbnail) {
return attachment; return attachment;
} }
const upgradedThumbnail = await upgradeAttachment(thumbnail, context); const upgradedThumbnail = await upgradeAttachment(
thumbnail as AttachmentType,
context,
message
);
return { ...attachment, thumbnail: upgradedThumbnail }; return { ...attachment, thumbnail: upgradedThumbnail };
}; };
@ -225,8 +328,12 @@ exports._mapQuotedAttachments =
// _mapPreviewAttachments :: (PreviewAttachment -> Promise PreviewAttachment) -> // _mapPreviewAttachments :: (PreviewAttachment -> Promise PreviewAttachment) ->
// (Message, Context) -> // (Message, Context) ->
// Promise Message // Promise Message
exports._mapPreviewAttachments = export const _mapPreviewAttachments =
upgradeAttachment => async (message, context) => { (upgradeAttachment: UpgradeAttachmentType) =>
async (
message: MessageAttributesType,
context: ContextType
): Promise<MessageAttributesType> => {
if (!message.preview) { if (!message.preview) {
return message; return message;
} }
@ -236,13 +343,13 @@ exports._mapPreviewAttachments =
); );
} }
const upgradeWithContext = async preview => { const upgradeWithContext = async (preview: PreviewType) => {
const { image } = preview; const { image } = preview;
if (!image) { if (!image) {
return preview; return preview;
} }
const upgradedImage = await upgradeAttachment(image, context); const upgradedImage = await upgradeAttachment(image, context, message);
return { ...preview, image: upgradedImage }; return { ...preview, image: upgradedImage };
}; };
@ -252,58 +359,59 @@ exports._mapPreviewAttachments =
return { ...message, preview }; return { ...message, preview };
}; };
const toVersion0 = async (message, context) => const toVersion0 = async (
exports.initializeSchemaVersion({ message, logger: context.logger }); message: MessageAttributesType,
const toVersion1 = exports._withSchemaVersion({ context: ContextType
) => initializeSchemaVersion({ message, logger: context.logger });
const toVersion1 = _withSchemaVersion({
schemaVersion: 1, schemaVersion: 1,
upgrade: exports._mapAttachments(Attachment.autoOrientJPEG), upgrade: _mapAttachments(autoOrientJPEG),
}); });
const toVersion2 = exports._withSchemaVersion({ const toVersion2 = _withSchemaVersion({
schemaVersion: 2, schemaVersion: 2,
upgrade: exports._mapAttachments(Attachment.replaceUnicodeOrderOverrides), upgrade: _mapAttachments(replaceUnicodeOrderOverrides),
}); });
const toVersion3 = exports._withSchemaVersion({ const toVersion3 = _withSchemaVersion({
schemaVersion: 3, schemaVersion: 3,
upgrade: exports._mapAttachments(Attachment.migrateDataToFileSystem), upgrade: _mapAttachments(migrateDataToFileSystem),
}); });
const toVersion4 = exports._withSchemaVersion({ const toVersion4 = _withSchemaVersion({
schemaVersion: 4, schemaVersion: 4,
upgrade: exports._mapQuotedAttachments(Attachment.migrateDataToFileSystem), upgrade: _mapQuotedAttachments(migrateDataToFileSystem),
}); });
const toVersion5 = exports._withSchemaVersion({ const toVersion5 = _withSchemaVersion({
schemaVersion: 5, schemaVersion: 5,
upgrade: initializeAttachmentMetadata, upgrade: initializeAttachmentMetadata,
}); });
const toVersion6 = exports._withSchemaVersion({ const toVersion6 = _withSchemaVersion({
schemaVersion: 6, schemaVersion: 6,
upgrade: exports._mapContact( upgrade: _mapContact(Contact.parseAndWriteAvatar(migrateDataToFileSystem)),
Contact.parseAndWriteAvatar(Attachment.migrateDataToFileSystem)
),
}); });
// IMPORTANT: Weve updated our definition of `initializeAttachmentMetadata`, so // IMPORTANT: Weve updated our definition of `initializeAttachmentMetadata`, so
// we need to run it again on existing items that have previously been incorrectly // we need to run it again on existing items that have previously been incorrectly
// classified: // classified:
const toVersion7 = exports._withSchemaVersion({ const toVersion7 = _withSchemaVersion({
schemaVersion: 7, schemaVersion: 7,
upgrade: initializeAttachmentMetadata, upgrade: initializeAttachmentMetadata,
}); });
const toVersion8 = exports._withSchemaVersion({ const toVersion8 = _withSchemaVersion({
schemaVersion: 8, schemaVersion: 8,
upgrade: exports._mapAttachments(Attachment.captureDimensionsAndScreenshot), upgrade: _mapAttachments(captureDimensionsAndScreenshot),
}); });
const toVersion9 = exports._withSchemaVersion({ const toVersion9 = _withSchemaVersion({
schemaVersion: 9, schemaVersion: 9,
upgrade: exports._mapAttachments(Attachment.replaceUnicodeV2), upgrade: _mapAttachments(replaceUnicodeV2),
}); });
const toVersion10 = exports._withSchemaVersion({ const toVersion10 = _withSchemaVersion({
schemaVersion: 10, schemaVersion: 10,
upgrade: async (message, context) => { upgrade: async (message, context) => {
const processPreviews = exports._mapPreviewAttachments( const processPreviews = _mapPreviewAttachments(migrateDataToFileSystem);
Attachment.migrateDataToFileSystem const processSticker = async (
); stickerMessage: MessageAttributesType,
const processSticker = async (stickerMessage, stickerContext) => { stickerContext: ContextType
): Promise<MessageAttributesType> => {
const { sticker } = stickerMessage; const { sticker } = stickerMessage;
if (!sticker || !sticker.data || !sticker.data.data) { if (!sticker || !sticker.data || !sticker.data.data) {
return stickerMessage; return stickerMessage;
@ -313,10 +421,7 @@ const toVersion10 = exports._withSchemaVersion({
...stickerMessage, ...stickerMessage,
sticker: { sticker: {
...sticker, ...sticker,
data: await Attachment.migrateDataToFileSystem( data: await migrateDataToFileSystem(sticker.data, stickerContext),
sticker.data,
stickerContext
),
}, },
}; };
}; };
@ -341,27 +446,29 @@ const VERSIONS = [
toVersion9, toVersion9,
toVersion10, toVersion10,
]; ];
exports.CURRENT_SCHEMA_VERSION = VERSIONS.length - 1; export const CURRENT_SCHEMA_VERSION = VERSIONS.length - 1;
// We need dimensions and screenshots for images for proper display // We need dimensions and screenshots for images for proper display
exports.VERSION_NEEDED_FOR_DISPLAY = 9; export const VERSION_NEEDED_FOR_DISPLAY = 9;
// UpgradeStep // UpgradeStep
exports.upgradeSchema = async ( export const upgradeSchema = async (
rawMessage, rawMessage: MessageAttributesType,
{ {
writeNewAttachmentData, writeNewAttachmentData,
getRegionCode, getRegionCode,
getAbsoluteAttachmentPath, getAbsoluteAttachmentPath,
getAbsoluteStickerPath,
makeObjectUrl, makeObjectUrl,
revokeObjectUrl, revokeObjectUrl,
getImageDimensions, getImageDimensions,
makeImageThumbnail, makeImageThumbnail,
makeVideoScreenshot, makeVideoScreenshot,
writeNewStickerData,
logger, logger,
maxVersion = exports.CURRENT_SCHEMA_VERSION, maxVersion = CURRENT_SCHEMA_VERSION,
} = {} }: ContextType
) => { ): Promise<MessageAttributesType> => {
if (!isFunction(writeNewAttachmentData)) { if (!isFunction(writeNewAttachmentData)) {
throw new TypeError('context.writeNewAttachmentData is required'); throw new TypeError('context.writeNewAttachmentData is required');
} }
@ -389,6 +496,12 @@ exports.upgradeSchema = async (
if (!isObject(logger)) { if (!isObject(logger)) {
throw new TypeError('context.logger is required'); throw new TypeError('context.logger is required');
} }
if (!isFunction(getAbsoluteStickerPath)) {
throw new TypeError('context.getAbsoluteStickerPath is required');
}
if (!isFunction(writeNewStickerData)) {
throw new TypeError('context.writeNewStickerData is required');
}
let message = rawMessage; let message = rawMessage;
for (let index = 0, max = VERSIONS.length; index < max; index += 1) { for (let index = 0, max = VERSIONS.length; index < max; index += 1) {
@ -402,7 +515,6 @@ exports.upgradeSchema = async (
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
message = await currentVersion(message, { message = await currentVersion(message, {
writeNewAttachmentData, writeNewAttachmentData,
regionCode: getRegionCode(),
getAbsoluteAttachmentPath, getAbsoluteAttachmentPath,
makeObjectUrl, makeObjectUrl,
revokeObjectUrl, revokeObjectUrl,
@ -410,6 +522,9 @@ exports.upgradeSchema = async (
makeImageThumbnail, makeImageThumbnail,
makeVideoScreenshot, makeVideoScreenshot,
logger, logger,
getAbsoluteStickerPath,
getRegionCode,
writeNewStickerData,
}); });
} }
@ -418,8 +533,8 @@ exports.upgradeSchema = async (
// Runs on attachments outside of the schema upgrade process, since attachments are // Runs on attachments outside of the schema upgrade process, since attachments are
// downloaded out of band. // downloaded out of band.
exports.processNewAttachment = async ( export const processNewAttachment = async (
attachment, attachment: AttachmentType,
{ {
writeNewAttachmentData, writeNewAttachmentData,
getAbsoluteAttachmentPath, getAbsoluteAttachmentPath,
@ -429,8 +544,8 @@ exports.processNewAttachment = async (
makeImageThumbnail, makeImageThumbnail,
makeVideoScreenshot, makeVideoScreenshot,
logger, logger,
} = {} }: ContextType
) => { ): Promise<AttachmentType> => {
if (!isFunction(writeNewAttachmentData)) { if (!isFunction(writeNewAttachmentData)) {
throw new TypeError('context.writeNewAttachmentData is required'); throw new TypeError('context.writeNewAttachmentData is required');
} }
@ -456,16 +571,13 @@ exports.processNewAttachment = async (
throw new TypeError('context.logger is required'); throw new TypeError('context.logger is required');
} }
const rotatedAttachment = await Attachment.autoOrientJPEG( const rotatedAttachment = await autoOrientJPEG(attachment, undefined, {
attachment, isIncoming: true,
undefined, });
{ isIncoming: true } const onDiskAttachment = await migrateDataToFileSystem(rotatedAttachment, {
); writeNewAttachmentData,
const onDiskAttachment = await Attachment.migrateDataToFileSystem( });
rotatedAttachment, const finalAttachment = await captureDimensionsAndScreenshot(
{ writeNewAttachmentData }
);
const finalAttachment = await Attachment.captureDimensionsAndScreenshot(
onDiskAttachment, onDiskAttachment,
{ {
writeNewAttachmentData, writeNewAttachmentData,
@ -482,15 +594,15 @@ exports.processNewAttachment = async (
return finalAttachment; return finalAttachment;
}; };
exports.processNewSticker = async ( export const processNewSticker = async (
stickerData, stickerData: StickerMessageType,
{ {
writeNewStickerData, writeNewStickerData,
getAbsoluteStickerPath, getAbsoluteStickerPath,
getImageDimensions, getImageDimensions,
logger, logger,
} = {} }: ContextType
) => { ): Promise<{ path: string; width: number; height: number }> => {
if (!isFunction(writeNewStickerData)) { if (!isFunction(writeNewStickerData)) {
throw new TypeError('context.writeNewStickerData is required'); throw new TypeError('context.writeNewStickerData is required');
} }
@ -519,25 +631,41 @@ exports.processNewSticker = async (
}; };
}; };
exports.createAttachmentLoader = loadAttachmentData => { type LoadAttachmentType = (
attachment: AttachmentType
) => Promise<AttachmentType>;
export const createAttachmentLoader = (
loadAttachmentData: LoadAttachmentType
): ((message: MessageAttributesType) => Promise<MessageAttributesType>) => {
if (!isFunction(loadAttachmentData)) { if (!isFunction(loadAttachmentData)) {
throw new TypeError( throw new TypeError(
'createAttachmentLoader: loadAttachmentData is required' 'createAttachmentLoader: loadAttachmentData is required'
); );
} }
return async message => ({ return async (
message: MessageAttributesType
): Promise<MessageAttributesType> => ({
...message, ...message,
attachments: await Promise.all(message.attachments.map(loadAttachmentData)), attachments: await Promise.all(
(message.attachments || []).map(loadAttachmentData)
),
}); });
}; };
exports.loadQuoteData = loadAttachmentData => { export const loadQuoteData = (
loadAttachmentData: LoadAttachmentType
): ((
quote: QuotedMessageType | undefined | null
) => Promise<QuotedMessageType | null>) => {
if (!isFunction(loadAttachmentData)) { if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadQuoteData: loadAttachmentData is required'); throw new TypeError('loadQuoteData: loadAttachmentData is required');
} }
return async quote => { return async (
quote: QuotedMessageType | undefined | null
): Promise<QuotedMessageType | null> => {
if (!quote) { if (!quote) {
return null; return null;
} }
@ -562,48 +690,58 @@ exports.loadQuoteData = loadAttachmentData => {
}; };
}; };
exports.loadContactData = loadAttachmentData => { export const loadContactData = (
loadAttachmentData: LoadAttachmentType
): ((
contact: Array<EmbeddedContactType> | undefined
) => Promise<Array<EmbeddedContactType> | null>) => {
if (!isFunction(loadAttachmentData)) { if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadContactData: loadAttachmentData is required'); throw new TypeError('loadContactData: loadAttachmentData is required');
} }
return async contact => { return async (
contact: Array<EmbeddedContactType> | undefined
): Promise<Array<EmbeddedContactType> | null> => {
if (!contact) { if (!contact) {
return null; return null;
} }
return Promise.all( return Promise.all(
contact.map(async item => { contact.map(
if ( async (item: EmbeddedContactType): Promise<EmbeddedContactType> => {
!item || if (
!item.avatar || !item ||
!item.avatar.avatar || !item.avatar ||
!item.avatar.avatar.path !item.avatar.avatar ||
) { !item.avatar.avatar.path
return item; ) {
} return item;
}
return { return {
...item, ...item,
avatar: {
...item.avatar,
avatar: { avatar: {
...item.avatar.avatar, ...item.avatar,
...(await loadAttachmentData(item.avatar.avatar)), avatar: {
...item.avatar.avatar,
...(await loadAttachmentData(item.avatar.avatar)),
},
}, },
}, };
}; }
}) )
); );
}; };
}; };
exports.loadPreviewData = loadAttachmentData => { export const loadPreviewData = (
loadAttachmentData: LoadAttachmentType
): ((preview: PreviewMessageType) => Promise<PreviewMessageType>) => {
if (!isFunction(loadAttachmentData)) { if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadPreviewData: loadAttachmentData is required'); throw new TypeError('loadPreviewData: loadAttachmentData is required');
} }
return async preview => { return async (preview: PreviewMessageType) => {
if (!preview || !preview.length) { if (!preview || !preview.length) {
return []; return [];
} }
@ -623,12 +761,14 @@ exports.loadPreviewData = loadAttachmentData => {
}; };
}; };
exports.loadStickerData = loadAttachmentData => { export const loadStickerData = (
loadAttachmentData: LoadAttachmentType
): ((sticker: StickerMessageType) => Promise<StickerMessageType | null>) => {
if (!isFunction(loadAttachmentData)) { if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadStickerData: loadAttachmentData is required'); throw new TypeError('loadStickerData: loadAttachmentData is required');
} }
return async sticker => { return async (sticker: StickerMessageType) => {
if (!sticker || !sticker.data) { if (!sticker || !sticker.data) {
return null; return null;
} }
@ -640,7 +780,13 @@ exports.loadStickerData = loadAttachmentData => {
}; };
}; };
exports.deleteAllExternalFiles = ({ deleteAttachmentData, deleteOnDisk }) => { export const deleteAllExternalFiles = ({
deleteAttachmentData,
deleteOnDisk,
}: {
deleteAttachmentData: (attachment: AttachmentType) => Promise<void>;
deleteOnDisk: (path: string) => Promise<void>;
}): ((message: MessageAttributesType) => Promise<void>) => {
if (!isFunction(deleteAttachmentData)) { if (!isFunction(deleteAttachmentData)) {
throw new TypeError( throw new TypeError(
'deleteAllExternalFiles: deleteAttachmentData must be a function' 'deleteAllExternalFiles: deleteAttachmentData must be a function'
@ -653,7 +799,7 @@ exports.deleteAllExternalFiles = ({ deleteAttachmentData, deleteOnDisk }) => {
); );
} }
return async message => { return async (message: MessageAttributesType) => {
const { attachments, quote, contact, preview, sticker } = message; const { attachments, quote, contact, preview, sticker } = message;
if (attachments && attachments.length) { if (attachments && attachments.length) {
@ -712,10 +858,13 @@ exports.deleteAllExternalFiles = ({ deleteAttachmentData, deleteOnDisk }) => {
// createAttachmentDataWriter :: (RelativePath -> IO Unit) // createAttachmentDataWriter :: (RelativePath -> IO Unit)
// Message -> // Message ->
// IO (Promise Message) // IO (Promise Message)
exports.createAttachmentDataWriter = ({ export const createAttachmentDataWriter = ({
writeExistingAttachmentData, writeExistingAttachmentData,
logger, logger,
}) => { }: {
writeExistingAttachmentData: WriteExistingAttachmentDataType;
logger: LoggerType;
}): ((message: MessageAttributesType) => Promise<MessageAttributesType>) => {
if (!isFunction(writeExistingAttachmentData)) { if (!isFunction(writeExistingAttachmentData)) {
throw new TypeError( throw new TypeError(
'createAttachmentDataWriter: writeExistingAttachmentData must be a function' 'createAttachmentDataWriter: writeExistingAttachmentData must be a function'
@ -725,12 +874,14 @@ exports.createAttachmentDataWriter = ({
throw new TypeError('createAttachmentDataWriter: logger must be an object'); throw new TypeError('createAttachmentDataWriter: logger must be an object');
} }
return async rawMessage => { return async (
if (!exports.isValid(rawMessage)) { rawMessage: MessageAttributesType
): Promise<MessageAttributesType> => {
if (!isValid(rawMessage)) {
throw new TypeError("'rawMessage' is not valid"); throw new TypeError("'rawMessage' is not valid");
} }
const message = exports.initializeSchemaVersion({ const message = initializeSchemaVersion({
message: rawMessage, message: rawMessage,
logger, logger,
}); });
@ -748,13 +899,13 @@ exports.createAttachmentDataWriter = ({
const lastVersionWithAttachmentDataInMemory = 2; const lastVersionWithAttachmentDataInMemory = 2;
const willAttachmentsGoToFileSystemOnUpgrade = const willAttachmentsGoToFileSystemOnUpgrade =
message.schemaVersion <= lastVersionWithAttachmentDataInMemory; (message.schemaVersion || 0) <= lastVersionWithAttachmentDataInMemory;
if (willAttachmentsGoToFileSystemOnUpgrade) { if (willAttachmentsGoToFileSystemOnUpgrade) {
return message; return message;
} }
(attachments || []).forEach(attachment => { (attachments || []).forEach(attachment => {
if (!Attachment.hasData(attachment)) { if (!hasData(attachment)) {
throw new TypeError( throw new TypeError(
"'attachment.data' is required during message import" "'attachment.data' is required during message import"
); );
@ -767,27 +918,41 @@ exports.createAttachmentDataWriter = ({
} }
}); });
const writeThumbnails = exports._mapQuotedAttachments(async thumbnail => { const writeQuoteAttachment = async (attachment: AttachmentType) => {
const { thumbnail } = attachment;
if (!thumbnail) {
return attachment;
}
const { data, path } = thumbnail; const { data, path } = thumbnail;
// we want to be bulletproof to thumbnails without data // we want to be bulletproof to attachments without data
if (!data || !path) { if (!data || !path) {
logger.warn( logger.warn(
'Thumbnail had neither data nor path.', 'quote attachment had neither data nor path.',
'id:', 'id:',
message.id, message.id,
'source:', 'source:',
message.source message.source
); );
return thumbnail; return attachment;
} }
await writeExistingAttachmentData(thumbnail); await writeExistingAttachmentData(thumbnail);
return omit(thumbnail, ['data']); return {
}); ...attachment,
thumbnail: omit(thumbnail, ['data']),
};
};
const writeContactAvatar = async messageContact => { const writeContactAvatar = async (
messageContact: EmbeddedContactType
): Promise<EmbeddedContactType> => {
const { avatar } = messageContact; const { avatar } = messageContact;
if (!avatar) {
return messageContact;
}
if (avatar && !avatar.avatar) { if (avatar && !avatar.avatar) {
return omit(messageContact, ['avatar']); return omit(messageContact, ['avatar']);
} }
@ -800,7 +965,9 @@ exports.createAttachmentDataWriter = ({
}; };
}; };
const writePreviewImage = async item => { const writePreviewImage = async (
item: PreviewType
): Promise<PreviewType> => {
const { image } = item; const { image } = item;
if (!image) { if (!image) {
return omit(item, ['image']); return omit(item, ['image']);
@ -812,7 +979,17 @@ exports.createAttachmentDataWriter = ({
}; };
const messageWithoutAttachmentData = { const messageWithoutAttachmentData = {
...(await writeThumbnails(message, { logger })), ...message,
...(quote
? {
quote: {
...quote,
attachments: await Promise.all(
(quote?.attachments || []).map(writeQuoteAttachment)
),
},
}
: undefined),
contact: await Promise.all((contact || []).map(writeContactAvatar)), contact: await Promise.all((contact || []).map(writeContactAvatar)),
preview: await Promise.all((preview || []).map(writePreviewImage)), preview: await Promise.all((preview || []).map(writePreviewImage)),
attachments: await Promise.all( attachments: await Promise.all(
@ -842,5 +1019,3 @@ exports.createAttachmentDataWriter = ({
return messageWithoutAttachmentData; return messageWithoutAttachmentData;
}; };
}; };
exports.hasExpiration = MessageTS.hasExpiration;

View file

@ -51,7 +51,7 @@ export const format = memoizee(_format, {
export function parse( export function parse(
phoneNumber: string, phoneNumber: string,
options: { options: {
regionCode: string; regionCode: string | undefined;
} }
): string { ): string {
const { regionCode } = options; const { regionCode } = options;

View file

@ -3,6 +3,6 @@
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
export const isValid = (value: unknown): boolean => { export const isValid = (value: unknown): value is number => {
return Boolean(isNumber(value) && value >= 0); return Boolean(isNumber(value) && value >= 0);
}; };

View file

@ -3,19 +3,20 @@
import * as Attachment from '../Attachment'; import * as Attachment from '../Attachment';
import * as IndexedDB from '../IndexedDB'; import * as IndexedDB from '../IndexedDB';
import type { Message, UserMessage } from '../Message';
import type { MessageAttributesType } from '../../model-types.d';
const hasAttachment = const hasAttachment =
(predicate: (value: Attachment.AttachmentType) => boolean) => (predicate: (value: Attachment.AttachmentType) => boolean) =>
(message: UserMessage): IndexedDB.IndexablePresence => (message: MessageAttributesType): IndexedDB.IndexablePresence =>
IndexedDB.toIndexablePresence(message.attachments.some(predicate)); IndexedDB.toIndexablePresence((message.attachments || []).some(predicate));
const hasFileAttachment = hasAttachment(Attachment.isFile); const hasFileAttachment = hasAttachment(Attachment.isFile);
const hasVisualMediaAttachment = hasAttachment(Attachment.isVisualMedia); const hasVisualMediaAttachment = hasAttachment(Attachment.isVisualMedia);
export const initializeAttachmentMetadata = async ( export const initializeAttachmentMetadata = async (
message: Message message: MessageAttributesType
): Promise<Message> => { ): Promise<MessageAttributesType> => {
if (message.type === 'verified-change') { if (message.type === 'verified-change') {
return message; return message;
} }
@ -26,7 +27,7 @@ export const initializeAttachmentMetadata = async (
return message; return message;
} }
const attachments = message.attachments.filter( const attachments = (message.attachments || []).filter(
(attachment: Attachment.AttachmentType) => (attachment: Attachment.AttachmentType) =>
attachment.contentType !== 'text/x-signal-plain' attachment.contentType !== 'text/x-signal-plain'
); );

View file

@ -1598,7 +1598,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
return { return {
path: attachment.path, path: attachment.path,
objectURL: getAbsoluteAttachmentPath(attachment.path), objectURL: getAbsoluteAttachmentPath(attachment.path),
thumbnailObjectUrl: thumbnail thumbnailObjectUrl: thumbnail?.path
? getAbsoluteAttachmentPath(thumbnail.path) ? getAbsoluteAttachmentPath(thumbnail.path)
: undefined, : undefined,
contentType: attachment.contentType, contentType: attachment.contentType,
@ -2566,7 +2566,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
return { return {
objectURL: getAbsoluteAttachmentPath(attachment.path || ''), objectURL: getAbsoluteAttachmentPath(attachment.path || ''),
thumbnailObjectUrl: thumbnail thumbnailObjectUrl: thumbnail?.path
? getAbsoluteAttachmentPath(thumbnail.path) ? getAbsoluteAttachmentPath(thumbnail.path)
: '', : '',
contentType: attachment.contentType, contentType: attachment.contentType,