Signal-Desktop/ts/types/Message2.ts

1041 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isFunction, isObject, isString, omit } from 'lodash';
import * as Contact from './EmbeddedContact';
import type { AttachmentType, AttachmentWithHydratedData } from './Attachment';
import {
autoOrientJPEG,
captureDimensionsAndScreenshot,
hasData,
migrateDataToFileSystem,
removeSchemaVersion,
replaceUnicodeOrderOverrides,
replaceUnicodeV2,
} from './Attachment';
import * as Errors from './errors';
import * as SchemaVersion from './SchemaVersion';
import { initializeAttachmentMetadata } from './message/initializeAttachmentMetadata';
import type * as MIME from './MIME';
import type { LoggerType } from './Logging';
import type { EmbeddedContactType } from './EmbeddedContact';
import type {
MessageAttributesType,
QuotedMessageType,
} from '../model-types.d';
import type { LinkPreviewType } from './message/LinkPreviews';
import type { StickerType, StickerWithHydratedData } from './Stickers';
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 | undefined;
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: (data: Uint8Array) => Promise<string>;
};
type WriteExistingAttachmentDataType = (
attachment: Pick<AttachmentType, 'data' | 'path'>
) => Promise<string>;
export type ContextWithMessageType = ContextType & {
message: MessageAttributesType;
};
// Schema version history
//
// Version 0
// - Schema initialized
// Version 1
// - Attachments: Auto-orient JPEG attachments using EXIF `Orientation` data.
// N.B. The process of auto-orient for JPEGs strips (loses) all existing
// EXIF metadata improving privacy, e.g. geolocation, camera make, etc.
// Version 2
// - Attachments: Sanitize Unicode order override characters.
// Version 3
// - Attachments: Write attachment data to disk and store relative path to it.
// Version 4
// - Quotes: Write thumbnail data to disk and store relative path to it.
// Version 5 (deprecated)
// - Attachments: Track number and kind of attachments for media gallery
// - `hasAttachments?: 1 | 0`
// - `hasVisualMediaAttachments?: 1 | undefined` (for media gallery Media view)
// - `hasFileAttachments?: 1 | undefined` (for media gallery Documents view)
// - IMPORTANT: Version 7 changes the classification of visual media and files.
// Therefore version 5 is considered deprecated. For an easier implementation,
// new files have the same classification in version 5 as in version 7.
// Version 6
// - Contact: Write contact avatar to disk, ensure contact data is well-formed
// Version 7 (supersedes attachment classification in version 5)
// - Attachments: Update classification for:
// - `hasVisualMediaAttachments`: Include all images and video regardless of
// whether Chromium can render it or not.
// - `hasFileAttachments`: Exclude voice messages.
// Version 8
// - Attachments: Capture video/image dimensions and thumbnails, as well as a
// full-size screenshot for video.
// Version 9
// - Attachments: Expand the set of unicode characters we filter out of
// attachment filenames
// Version 10
// - Preview: A new type of attachment can be included in a message.
const INITIAL_SCHEMA_VERSION = 0;
// Placeholder until we have stronger preconditions:
export const isValid = (_message: MessageAttributesType): boolean => true;
// Schema
export const initializeSchemaVersion = ({
message,
logger,
}: {
message: MessageAttributesType;
logger: LoggerType;
}): MessageAttributesType => {
const isInitialized =
SchemaVersion.isValid(message.schemaVersion) && message.schemaVersion >= 1;
if (isInitialized) {
return message;
}
const firstAttachment = message?.attachments?.[0];
if (!firstAttachment) {
return { ...message, schemaVersion: INITIAL_SCHEMA_VERSION };
}
// All attachments should have the same schema version, so we just pick
// the first one:
const inheritedSchemaVersion = SchemaVersion.isValid(
firstAttachment.schemaVersion
)
? firstAttachment.schemaVersion
: INITIAL_SCHEMA_VERSION;
const messageWithInitialSchema = {
...message,
schemaVersion: inheritedSchemaVersion,
attachments:
message?.attachments?.map(attachment =>
removeSchemaVersion({ attachment, logger })
) || [],
};
return messageWithInitialSchema;
};
// Middleware
// type UpgradeStep = (Message, Context) -> Promise Message
// SchemaVersion -> UpgradeStep -> UpgradeStep
export const _withSchemaVersion = ({
schemaVersion,
upgrade,
}: {
schemaVersion: number;
upgrade: (
message: MessageAttributesType,
context: ContextType
) => Promise<MessageAttributesType>;
}): ((
message: MessageAttributesType,
context: ContextType
) => Promise<MessageAttributesType>) => {
if (!SchemaVersion.isValid(schemaVersion)) {
throw new TypeError('_withSchemaVersion: schemaVersion is invalid');
}
if (!isFunction(upgrade)) {
throw new TypeError('_withSchemaVersion: upgrade must be a function');
}
return async (message: MessageAttributesType, context: ContextType) => {
if (!context || !isObject(context.logger)) {
throw new TypeError(
'_withSchemaVersion: context must have logger object'
);
}
const { logger } = context;
if (!isValid(message)) {
logger.error(
'Message._withSchemaVersion: Invalid input message:',
message
);
return message;
}
const isAlreadyUpgraded = (message.schemaVersion || 0) >= schemaVersion;
if (isAlreadyUpgraded) {
return message;
}
const expectedVersion = schemaVersion - 1;
const hasExpectedVersion = message.schemaVersion === expectedVersion;
if (!hasExpectedVersion) {
logger.warn(
'WARNING: Message._withSchemaVersion: Unexpected version:',
`Expected message to have version ${expectedVersion},`,
`but got ${message.schemaVersion}.`
);
return message;
}
let upgradedMessage;
try {
upgradedMessage = await upgrade(message, context);
} catch (error) {
logger.error(
`Message._withSchemaVersion: error updating message ${message.id}:`,
Errors.toLogFormat(error)
);
return message;
}
if (!isValid(upgradedMessage)) {
logger.error(
'Message._withSchemaVersion: Invalid upgraded message:',
upgradedMessage
);
return message;
}
return { ...upgradedMessage, schemaVersion };
};
};
// Public API
// _mapAttachments :: (Attachment -> Promise Attachment) ->
// (Message, Context) ->
// Promise Message
export type UpgradeAttachmentType = (
attachment: AttachmentType,
context: ContextType,
message: MessageAttributesType
) => Promise<AttachmentType>;
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
// _mapContact :: (Contact -> Promise Contact) ->
// (Message, Context) ->
// Promise Message
export type UpgradeContactType = (
contact: EmbeddedContactType,
contextWithMessage: ContextWithMessageType
) => Promise<EmbeddedContactType>;
export const _mapContact =
(upgradeContact: UpgradeContactType) =>
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) ->
// (Message, Context) ->
// Promise Message
export const _mapQuotedAttachments =
(upgradeAttachment: UpgradeAttachmentType) =>
async (
message: MessageAttributesType,
context: ContextType
): Promise<MessageAttributesType> => {
if (!message.quote) {
return message;
}
if (!context || !isObject(context.logger)) {
throw new Error('_mapQuotedAttachments: context must have logger object');
}
const upgradeWithContext = async (
attachment: AttachmentType
): Promise<AttachmentType> => {
const { thumbnail } = attachment;
if (!thumbnail) {
return attachment;
}
const upgradedThumbnail = await upgradeAttachment(
thumbnail as AttachmentType,
context,
message
);
return { ...attachment, thumbnail: upgradedThumbnail };
};
const quotedAttachments =
(message.quote && message.quote.attachments) || [];
const attachments = await Promise.all(
quotedAttachments.map(upgradeWithContext)
);
return { ...message, quote: { ...message.quote, attachments } };
};
// _mapPreviewAttachments :: (PreviewAttachment -> Promise PreviewAttachment) ->
// (Message, Context) ->
// Promise Message
export const _mapPreviewAttachments =
(upgradeAttachment: UpgradeAttachmentType) =>
async (
message: MessageAttributesType,
context: ContextType
): Promise<MessageAttributesType> => {
if (!message.preview) {
return message;
}
if (!context || !isObject(context.logger)) {
throw new Error(
'_mapPreviewAttachments: context must have logger object'
);
}
const upgradeWithContext = async (preview: LinkPreviewType) => {
const { image } = preview;
if (!image) {
return preview;
}
const upgradedImage = await upgradeAttachment(image, context, message);
return { ...preview, image: upgradedImage };
};
const preview = await Promise.all(
(message.preview || []).map(upgradeWithContext)
);
return { ...message, preview };
};
const toVersion0 = async (
message: MessageAttributesType,
context: ContextType
) => initializeSchemaVersion({ message, logger: context.logger });
const toVersion1 = _withSchemaVersion({
schemaVersion: 1,
upgrade: _mapAttachments(autoOrientJPEG),
});
const toVersion2 = _withSchemaVersion({
schemaVersion: 2,
upgrade: _mapAttachments(replaceUnicodeOrderOverrides),
});
const toVersion3 = _withSchemaVersion({
schemaVersion: 3,
upgrade: _mapAttachments(migrateDataToFileSystem),
});
const toVersion4 = _withSchemaVersion({
schemaVersion: 4,
upgrade: _mapQuotedAttachments(migrateDataToFileSystem),
});
const toVersion5 = _withSchemaVersion({
schemaVersion: 5,
upgrade: initializeAttachmentMetadata,
});
const toVersion6 = _withSchemaVersion({
schemaVersion: 6,
upgrade: _mapContact(Contact.parseAndWriteAvatar(migrateDataToFileSystem)),
});
// IMPORTANT: Weve updated our definition of `initializeAttachmentMetadata`, so
// we need to run it again on existing items that have previously been incorrectly
// classified:
const toVersion7 = _withSchemaVersion({
schemaVersion: 7,
upgrade: initializeAttachmentMetadata,
});
const toVersion8 = _withSchemaVersion({
schemaVersion: 8,
upgrade: _mapAttachments(captureDimensionsAndScreenshot),
});
const toVersion9 = _withSchemaVersion({
schemaVersion: 9,
upgrade: _mapAttachments(replaceUnicodeV2),
});
const toVersion10 = _withSchemaVersion({
schemaVersion: 10,
upgrade: async (message, context) => {
const processPreviews = _mapPreviewAttachments(migrateDataToFileSystem);
const processSticker = async (
stickerMessage: MessageAttributesType,
stickerContext: ContextType
): Promise<MessageAttributesType> => {
const { sticker } = stickerMessage;
if (!sticker || !sticker.data || !sticker.data.data) {
return stickerMessage;
}
return {
...stickerMessage,
sticker: {
...sticker,
data: await migrateDataToFileSystem(sticker.data, stickerContext),
},
};
};
const previewProcessed = await processPreviews(message, context);
const stickerProcessed = await processSticker(previewProcessed, context);
return stickerProcessed;
},
});
const VERSIONS = [
toVersion0,
toVersion1,
toVersion2,
toVersion3,
toVersion4,
toVersion5,
toVersion6,
toVersion7,
toVersion8,
toVersion9,
toVersion10,
];
export const CURRENT_SCHEMA_VERSION = VERSIONS.length - 1;
// We need dimensions and screenshots for images for proper display
export const VERSION_NEEDED_FOR_DISPLAY = 9;
// UpgradeStep
export const upgradeSchema = async (
rawMessage: MessageAttributesType,
{
writeNewAttachmentData,
getRegionCode,
getAbsoluteAttachmentPath,
getAbsoluteStickerPath,
makeObjectUrl,
revokeObjectUrl,
getImageDimensions,
makeImageThumbnail,
makeVideoScreenshot,
writeNewStickerData,
logger,
maxVersion = CURRENT_SCHEMA_VERSION,
}: ContextType
): Promise<MessageAttributesType> => {
if (!isFunction(writeNewAttachmentData)) {
throw new TypeError('context.writeNewAttachmentData is required');
}
if (!isFunction(getRegionCode)) {
throw new TypeError('context.getRegionCode is required');
}
if (!isFunction(getAbsoluteAttachmentPath)) {
throw new TypeError('context.getAbsoluteAttachmentPath is required');
}
if (!isFunction(makeObjectUrl)) {
throw new TypeError('context.makeObjectUrl is required');
}
if (!isFunction(revokeObjectUrl)) {
throw new TypeError('context.revokeObjectUrl is required');
}
if (!isFunction(getImageDimensions)) {
throw new TypeError('context.getImageDimensions is required');
}
if (!isFunction(makeImageThumbnail)) {
throw new TypeError('context.makeImageThumbnail is required');
}
if (!isFunction(makeVideoScreenshot)) {
throw new TypeError('context.makeVideoScreenshot is required');
}
if (!isObject(logger)) {
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;
for (let index = 0, max = VERSIONS.length; index < max; index += 1) {
if (maxVersion < index) {
break;
}
const currentVersion = VERSIONS[index];
// We really do want this intra-loop await because this is a chained async action,
// each step dependent on the previous
// eslint-disable-next-line no-await-in-loop
message = await currentVersion(message, {
writeNewAttachmentData,
getAbsoluteAttachmentPath,
makeObjectUrl,
revokeObjectUrl,
getImageDimensions,
makeImageThumbnail,
makeVideoScreenshot,
logger,
getAbsoluteStickerPath,
getRegionCode,
writeNewStickerData,
});
}
return message;
};
// Runs on attachments outside of the schema upgrade process, since attachments are
// downloaded out of band.
export const processNewAttachment = async (
attachment: AttachmentType,
{
writeNewAttachmentData,
getAbsoluteAttachmentPath,
makeObjectUrl,
revokeObjectUrl,
getImageDimensions,
makeImageThumbnail,
makeVideoScreenshot,
logger,
}: Pick<
ContextType,
| 'writeNewAttachmentData'
| 'getAbsoluteAttachmentPath'
| 'makeObjectUrl'
| 'revokeObjectUrl'
| 'getImageDimensions'
| 'makeImageThumbnail'
| 'makeVideoScreenshot'
| 'logger'
>
): Promise<AttachmentType> => {
if (!isFunction(writeNewAttachmentData)) {
throw new TypeError('context.writeNewAttachmentData is required');
}
if (!isFunction(getAbsoluteAttachmentPath)) {
throw new TypeError('context.getAbsoluteAttachmentPath is required');
}
if (!isFunction(makeObjectUrl)) {
throw new TypeError('context.makeObjectUrl is required');
}
if (!isFunction(revokeObjectUrl)) {
throw new TypeError('context.revokeObjectUrl is required');
}
if (!isFunction(getImageDimensions)) {
throw new TypeError('context.getImageDimensions is required');
}
if (!isFunction(makeImageThumbnail)) {
throw new TypeError('context.makeImageThumbnail is required');
}
if (!isFunction(makeVideoScreenshot)) {
throw new TypeError('context.makeVideoScreenshot is required');
}
if (!isObject(logger)) {
throw new TypeError('context.logger is required');
}
const rotatedAttachment = await autoOrientJPEG(attachment, undefined, {
isIncoming: true,
});
const onDiskAttachment = await migrateDataToFileSystem(rotatedAttachment, {
writeNewAttachmentData,
});
const finalAttachment = await captureDimensionsAndScreenshot(
onDiskAttachment,
{
writeNewAttachmentData,
getAbsoluteAttachmentPath,
makeObjectUrl,
revokeObjectUrl,
getImageDimensions,
makeImageThumbnail,
makeVideoScreenshot,
logger,
}
);
return finalAttachment;
};
export const processNewSticker = async (
stickerData: Uint8Array,
{
writeNewStickerData,
getAbsoluteStickerPath,
getImageDimensions,
logger,
}: Pick<
ContextType,
| 'writeNewStickerData'
| 'getAbsoluteStickerPath'
| 'getImageDimensions'
| 'logger'
>
): Promise<{ path: string; width: number; height: number }> => {
if (!isFunction(writeNewStickerData)) {
throw new TypeError('context.writeNewStickerData is required');
}
if (!isFunction(getAbsoluteStickerPath)) {
throw new TypeError('context.getAbsoluteStickerPath is required');
}
if (!isFunction(getImageDimensions)) {
throw new TypeError('context.getImageDimensions is required');
}
if (!isObject(logger)) {
throw new TypeError('context.logger is required');
}
const path = await writeNewStickerData(stickerData);
const absolutePath = await getAbsoluteStickerPath(path);
const { width, height } = await getImageDimensions({
objectUrl: absolutePath,
logger,
});
return {
path,
width,
height,
};
};
type LoadAttachmentType = (
attachment: AttachmentType
) => Promise<AttachmentWithHydratedData>;
export const createAttachmentLoader = (
loadAttachmentData: LoadAttachmentType
): ((message: MessageAttributesType) => Promise<MessageAttributesType>) => {
if (!isFunction(loadAttachmentData)) {
throw new TypeError(
'createAttachmentLoader: loadAttachmentData is required'
);
}
return async (
message: MessageAttributesType
): Promise<MessageAttributesType> => ({
...message,
attachments: await Promise.all(
(message.attachments || []).map(loadAttachmentData)
),
});
};
export const loadQuoteData = (
loadAttachmentData: LoadAttachmentType
): ((
quote: QuotedMessageType | undefined | null
) => Promise<QuotedMessageType | null>) => {
if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadQuoteData: loadAttachmentData is required');
}
return async (
quote: QuotedMessageType | undefined | null
): Promise<QuotedMessageType | null> => {
if (!quote) {
return null;
}
return {
...quote,
attachments: await Promise.all(
(quote.attachments || []).map(async attachment => {
const { thumbnail } = attachment;
if (!thumbnail || !thumbnail.path) {
return attachment;
}
return {
...attachment,
thumbnail: await loadAttachmentData(thumbnail),
};
})
),
};
};
};
export const loadContactData = (
loadAttachmentData: LoadAttachmentType
): ((
contact: Array<EmbeddedContactType> | undefined
) => Promise<Array<EmbeddedContactType> | undefined>) => {
if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadContactData: loadAttachmentData is required');
}
return async (
contact: Array<EmbeddedContactType> | undefined
): Promise<Array<EmbeddedContactType> | undefined> => {
if (!contact) {
return undefined;
}
return Promise.all(
contact.map(
async (item: EmbeddedContactType): Promise<EmbeddedContactType> => {
if (
!item ||
!item.avatar ||
!item.avatar.avatar ||
!item.avatar.avatar.path
) {
return item;
}
return {
...item,
avatar: {
...item.avatar,
avatar: {
...item.avatar.avatar,
...(await loadAttachmentData(item.avatar.avatar)),
},
},
};
}
)
);
};
};
export const loadPreviewData = (
loadAttachmentData: LoadAttachmentType
): ((
preview: Array<LinkPreviewType> | undefined
) => Promise<Array<LinkPreviewType>>) => {
if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadPreviewData: loadAttachmentData is required');
}
return async (preview: Array<LinkPreviewType> | undefined) => {
if (!preview || !preview.length) {
return [];
}
return Promise.all(
preview.map(async item => {
if (!item.image) {
return item;
}
return {
...item,
image: await loadAttachmentData(item.image),
};
})
);
};
};
export const loadStickerData = (
loadAttachmentData: LoadAttachmentType
): ((
sticker: StickerType | undefined
) => Promise<StickerWithHydratedData | undefined>) => {
if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadStickerData: loadAttachmentData is required');
}
return async (sticker: StickerType | undefined) => {
if (!sticker || !sticker.data) {
return undefined;
}
return {
...sticker,
data: await loadAttachmentData(sticker.data),
};
};
};
export const deleteAllExternalFiles = ({
deleteAttachmentData,
deleteOnDisk,
}: {
deleteAttachmentData: (attachment: AttachmentType) => Promise<void>;
deleteOnDisk: (path: string) => Promise<void>;
}): ((message: MessageAttributesType) => Promise<void>) => {
if (!isFunction(deleteAttachmentData)) {
throw new TypeError(
'deleteAllExternalFiles: deleteAttachmentData must be a function'
);
}
if (!isFunction(deleteOnDisk)) {
throw new TypeError(
'deleteAllExternalFiles: deleteOnDisk must be a function'
);
}
return async (message: MessageAttributesType) => {
const { attachments, quote, contact, preview, sticker } = message;
if (attachments && attachments.length) {
await Promise.all(attachments.map(deleteAttachmentData));
}
if (quote && quote.attachments && quote.attachments.length) {
await Promise.all(
quote.attachments.map(async attachment => {
const { thumbnail } = attachment;
// To prevent spoofing, we copy the original image from the quoted message.
// If so, it will have a 'copied' field. We don't want to delete it if it has
// that field set to true.
if (thumbnail && thumbnail.path && !thumbnail.copied) {
await deleteOnDisk(thumbnail.path);
}
})
);
}
if (contact && contact.length) {
await Promise.all(
contact.map(async item => {
const { avatar } = item;
if (avatar && avatar.avatar && avatar.avatar.path) {
await deleteOnDisk(avatar.avatar.path);
}
})
);
}
if (preview && preview.length) {
await Promise.all(
preview.map(async item => {
const { image } = item;
if (image && image.path) {
await deleteOnDisk(image.path);
}
})
);
}
if (sticker && sticker.data && sticker.data.path) {
await deleteOnDisk(sticker.data.path);
if (sticker.data.thumbnail && sticker.data.thumbnail.path) {
await deleteOnDisk(sticker.data.thumbnail.path);
}
}
};
};
// createAttachmentDataWriter :: (RelativePath -> IO Unit)
// Message ->
// IO (Promise Message)
export const createAttachmentDataWriter = ({
writeExistingAttachmentData,
logger,
}: {
writeExistingAttachmentData: WriteExistingAttachmentDataType;
logger: LoggerType;
}): ((message: MessageAttributesType) => Promise<MessageAttributesType>) => {
if (!isFunction(writeExistingAttachmentData)) {
throw new TypeError(
'createAttachmentDataWriter: writeExistingAttachmentData must be a function'
);
}
if (!isObject(logger)) {
throw new TypeError('createAttachmentDataWriter: logger must be an object');
}
return async (
rawMessage: MessageAttributesType
): Promise<MessageAttributesType> => {
if (!isValid(rawMessage)) {
throw new TypeError("'rawMessage' is not valid");
}
const message = initializeSchemaVersion({
message: rawMessage,
logger,
});
const { attachments, quote, contact, preview } = message;
const hasFilesToWrite =
(quote && quote.attachments && quote.attachments.length > 0) ||
(attachments && attachments.length > 0) ||
(contact && contact.length > 0) ||
(preview && preview.length > 0);
if (!hasFilesToWrite) {
return message;
}
const lastVersionWithAttachmentDataInMemory = 2;
const willAttachmentsGoToFileSystemOnUpgrade =
(message.schemaVersion || 0) <= lastVersionWithAttachmentDataInMemory;
if (willAttachmentsGoToFileSystemOnUpgrade) {
return message;
}
(attachments || []).forEach(attachment => {
if (!hasData(attachment)) {
throw new TypeError(
"'attachment.data' is required during message import"
);
}
if (!isString(attachment.path)) {
throw new TypeError(
"'attachment.path' is required during message import"
);
}
});
const writeQuoteAttachment = async (attachment: AttachmentType) => {
const { thumbnail } = attachment;
if (!thumbnail) {
return attachment;
}
const { data, path } = thumbnail;
// we want to be bulletproof to attachments without data
if (!data || !path) {
logger.warn(
'quote attachment had neither data nor path.',
'id:',
message.id,
'source:',
message.source
);
return attachment;
}
await writeExistingAttachmentData(thumbnail);
return {
...attachment,
thumbnail: omit(thumbnail, ['data']),
};
};
const writeContactAvatar = async (
messageContact: EmbeddedContactType
): Promise<EmbeddedContactType> => {
const { avatar } = messageContact;
if (!avatar) {
return messageContact;
}
if (avatar && !avatar.avatar) {
return omit(messageContact, ['avatar']);
}
await writeExistingAttachmentData(avatar.avatar);
return {
...messageContact,
avatar: { ...avatar, avatar: omit(avatar.avatar, ['data']) },
};
};
const writePreviewImage = async (
item: LinkPreviewType
): Promise<LinkPreviewType> => {
const { image } = item;
if (!image) {
return omit(item, ['image']);
}
await writeExistingAttachmentData(image);
return { ...item, image: omit(image, ['data']) };
};
const messageWithoutAttachmentData = {
...message,
...(quote
? {
quote: {
...quote,
attachments: await Promise.all(
(quote?.attachments || []).map(writeQuoteAttachment)
),
},
}
: undefined),
contact: await Promise.all((contact || []).map(writeContactAvatar)),
preview: await Promise.all((preview || []).map(writePreviewImage)),
attachments: await Promise.all(
(attachments || []).map(async attachment => {
await writeExistingAttachmentData(attachment);
if (attachment.screenshot && attachment.screenshot.data) {
await writeExistingAttachmentData(attachment.screenshot);
}
if (attachment.thumbnail && attachment.thumbnail.data) {
await writeExistingAttachmentData(attachment.thumbnail);
}
return {
...omit(attachment, ['data']),
...(attachment.thumbnail
? { thumbnail: omit(attachment.thumbnail, ['data']) }
: null),
...(attachment.screenshot
? { screenshot: omit(attachment.screenshot, ['data']) }
: null),
};
})
),
};
return messageWithoutAttachmentData;
};
};