2022-02-11 21:38:52 +00:00
|
|
|
|
// Copyright 2018-2022 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
2018-04-09 23:29:38 +00:00
|
|
|
|
import is from '@sindresorhus/is';
|
2018-04-25 17:27:28 +00:00
|
|
|
|
import moment from 'moment';
|
2021-07-14 23:39:52 +00:00
|
|
|
|
import {
|
|
|
|
|
isNumber,
|
|
|
|
|
padStart,
|
2021-09-24 00:49:05 +00:00
|
|
|
|
isTypedArray,
|
2021-07-14 23:39:52 +00:00
|
|
|
|
isFunction,
|
|
|
|
|
isUndefined,
|
|
|
|
|
omit,
|
|
|
|
|
} from 'lodash';
|
2021-09-24 00:49:05 +00:00
|
|
|
|
import { blobToArrayBuffer } from 'blob-util';
|
2021-07-14 23:39:52 +00:00
|
|
|
|
|
2021-10-26 19:15:33 +00:00
|
|
|
|
import type { LoggerType } from './Logging';
|
2018-05-07 15:19:58 +00:00
|
|
|
|
import * as MIME from './MIME';
|
2022-04-13 17:47:39 +00:00
|
|
|
|
import * as log from '../logging/log';
|
2021-07-14 23:39:52 +00:00
|
|
|
|
import { toLogFormat } from './errors';
|
2018-05-07 18:15:06 +00:00
|
|
|
|
import { SignalService } from '../protobuf';
|
2019-01-14 21:49:58 +00:00
|
|
|
|
import {
|
|
|
|
|
isImageTypeSupported,
|
|
|
|
|
isVideoTypeSupported,
|
|
|
|
|
} from '../util/GoogleChrome';
|
2021-10-26 19:15:33 +00:00
|
|
|
|
import type { LocalizerType } from './Util';
|
|
|
|
|
import { ThemeType } from './Util';
|
2021-07-14 23:39:52 +00:00
|
|
|
|
import { scaleImageToLevel } from '../util/scaleImageToLevel';
|
2022-06-10 01:10:20 +00:00
|
|
|
|
import * as GoogleChrome from '../util/GoogleChrome';
|
2022-04-13 17:47:39 +00:00
|
|
|
|
import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
|
|
|
|
import { getValue } from '../RemoteConfig';
|
2019-01-14 21:49:58 +00:00
|
|
|
|
|
|
|
|
|
const MAX_WIDTH = 300;
|
|
|
|
|
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
|
|
|
|
const MIN_WIDTH = 200;
|
|
|
|
|
const MIN_HEIGHT = 50;
|
|
|
|
|
|
|
|
|
|
// Used for display
|
|
|
|
|
|
2021-01-14 18:07:05 +00:00
|
|
|
|
export type AttachmentType = {
|
2021-06-17 17:15:10 +00:00
|
|
|
|
error?: boolean;
|
2020-05-27 21:37:06 +00:00
|
|
|
|
blurHash?: string;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
caption?: string;
|
|
|
|
|
contentType: MIME.MIMEType;
|
2021-06-17 17:15:10 +00:00
|
|
|
|
fileName?: string;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
/** Not included in protobuf, needs to be pulled from flags */
|
|
|
|
|
isVoiceMessage?: boolean;
|
|
|
|
|
/** For messages not already on disk, this will be a data url */
|
2021-03-16 17:49:19 +00:00
|
|
|
|
url?: string;
|
2021-10-05 22:10:08 +00:00
|
|
|
|
size: number;
|
2021-06-24 19:05:27 +00:00
|
|
|
|
fileSize?: string;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
pending?: boolean;
|
|
|
|
|
width?: number;
|
|
|
|
|
height?: number;
|
2021-01-29 21:19:24 +00:00
|
|
|
|
path?: string;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
screenshot?: {
|
|
|
|
|
height: number;
|
|
|
|
|
width: number;
|
2021-07-14 23:39:52 +00:00
|
|
|
|
url?: string;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
contentType: MIME.MIMEType;
|
2021-01-29 21:19:24 +00:00
|
|
|
|
path: string;
|
2022-06-10 01:10:20 +00:00
|
|
|
|
data?: Uint8Array;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
};
|
2021-09-24 20:02:30 +00:00
|
|
|
|
screenshotData?: Uint8Array;
|
2021-06-25 16:08:16 +00:00
|
|
|
|
screenshotPath?: string;
|
2021-06-17 17:15:10 +00:00
|
|
|
|
flags?: number;
|
|
|
|
|
thumbnail?: ThumbnailType;
|
2021-03-22 18:51:53 +00:00
|
|
|
|
isCorrupted?: boolean;
|
2021-06-17 17:15:10 +00:00
|
|
|
|
downloadJobId?: string;
|
|
|
|
|
cdnNumber?: number;
|
|
|
|
|
cdnId?: string;
|
|
|
|
|
cdnKey?: string;
|
2021-09-24 00:49:05 +00:00
|
|
|
|
data?: Uint8Array;
|
2022-04-06 01:18:07 +00:00
|
|
|
|
textAttachment?: TextAttachmentType;
|
2021-07-09 19:36:10 +00:00
|
|
|
|
|
|
|
|
|
/** Legacy field. Used only for downloading old attachments */
|
|
|
|
|
id?: number;
|
2022-05-23 23:07:41 +00:00
|
|
|
|
|
2022-06-10 01:10:20 +00:00
|
|
|
|
/** Legacy field, used long ago for migrating attachments to disk. */
|
|
|
|
|
schemaVersion?: number;
|
|
|
|
|
|
2022-05-23 23:07:41 +00:00
|
|
|
|
/** Removed once we download the attachment */
|
|
|
|
|
digest?: string;
|
|
|
|
|
key?: string;
|
2021-06-17 17:15:10 +00:00
|
|
|
|
};
|
|
|
|
|
|
2022-06-13 21:39:35 +00:00
|
|
|
|
export type AttachmentWithHydratedData = AttachmentType & {
|
|
|
|
|
data: Uint8Array;
|
|
|
|
|
};
|
|
|
|
|
|
2022-04-06 01:18:07 +00:00
|
|
|
|
export enum TextAttachmentStyleType {
|
|
|
|
|
DEFAULT = 0,
|
|
|
|
|
REGULAR = 1,
|
|
|
|
|
BOLD = 2,
|
|
|
|
|
SERIF = 3,
|
|
|
|
|
SCRIPT = 4,
|
|
|
|
|
CONDENSED = 5,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type TextAttachmentType = {
|
|
|
|
|
text?: string | null;
|
|
|
|
|
textStyle?: number | null;
|
|
|
|
|
textForegroundColor?: number | null;
|
|
|
|
|
textBackgroundColor?: number | null;
|
|
|
|
|
preview?: {
|
|
|
|
|
url?: string | null;
|
|
|
|
|
title?: string | null;
|
|
|
|
|
} | null;
|
|
|
|
|
gradient?: {
|
|
|
|
|
startColor?: number | null;
|
|
|
|
|
endColor?: number | null;
|
|
|
|
|
angle?: number | null;
|
|
|
|
|
} | null;
|
|
|
|
|
color?: number | null;
|
|
|
|
|
};
|
|
|
|
|
|
2021-07-14 23:39:52 +00:00
|
|
|
|
export type DownloadedAttachmentType = AttachmentType & {
|
2021-09-24 00:49:05 +00:00
|
|
|
|
data: Uint8Array;
|
2021-07-14 23:39:52 +00:00
|
|
|
|
};
|
|
|
|
|
|
2021-08-30 21:32:56 +00:00
|
|
|
|
export type BaseAttachmentDraftType = {
|
2021-06-25 16:08:16 +00:00
|
|
|
|
blurHash?: string;
|
|
|
|
|
contentType: MIME.MIMEType;
|
|
|
|
|
screenshotContentType?: string;
|
|
|
|
|
screenshotSize?: number;
|
|
|
|
|
size: number;
|
2021-11-15 21:54:33 +00:00
|
|
|
|
flags?: number;
|
2021-06-25 16:08:16 +00:00
|
|
|
|
};
|
|
|
|
|
|
2021-11-15 21:54:33 +00:00
|
|
|
|
// An ephemeral attachment type, used between user's request to add the attachment as
|
|
|
|
|
// a draft and final save on disk and in conversation.draftAttachments.
|
2021-08-30 21:32:56 +00:00
|
|
|
|
export type InMemoryAttachmentDraftType =
|
|
|
|
|
| ({
|
2021-11-15 21:54:33 +00:00
|
|
|
|
data: Uint8Array;
|
2021-08-30 21:32:56 +00:00
|
|
|
|
pending: false;
|
2021-09-24 00:49:05 +00:00
|
|
|
|
screenshotData?: Uint8Array;
|
2021-11-15 21:54:33 +00:00
|
|
|
|
fileName?: string;
|
|
|
|
|
path?: string;
|
2021-08-30 21:32:56 +00:00
|
|
|
|
} & BaseAttachmentDraftType)
|
|
|
|
|
| {
|
|
|
|
|
contentType: MIME.MIMEType;
|
2021-11-15 21:54:33 +00:00
|
|
|
|
fileName?: string;
|
|
|
|
|
path?: string;
|
2021-08-30 21:32:56 +00:00
|
|
|
|
pending: true;
|
2021-10-05 22:10:08 +00:00
|
|
|
|
size: number;
|
2021-08-30 21:32:56 +00:00
|
|
|
|
};
|
2021-06-25 16:08:16 +00:00
|
|
|
|
|
2021-11-15 21:54:33 +00:00
|
|
|
|
// What's stored in conversation.draftAttachments
|
2021-08-30 21:32:56 +00:00
|
|
|
|
export type AttachmentDraftType =
|
|
|
|
|
| ({
|
2021-11-15 21:54:33 +00:00
|
|
|
|
url?: string;
|
2021-08-30 21:32:56 +00:00
|
|
|
|
screenshotPath?: string;
|
|
|
|
|
pending: false;
|
2021-11-10 00:25:29 +00:00
|
|
|
|
// Old draft attachments may have a caption, though they are no longer editable
|
|
|
|
|
// because we removed the caption editor.
|
|
|
|
|
caption?: string;
|
2021-11-15 21:54:33 +00:00
|
|
|
|
fileName?: string;
|
|
|
|
|
path: string;
|
2021-11-30 23:32:55 +00:00
|
|
|
|
width?: number;
|
|
|
|
|
height?: number;
|
2021-08-30 21:32:56 +00:00
|
|
|
|
} & BaseAttachmentDraftType)
|
|
|
|
|
| {
|
|
|
|
|
contentType: MIME.MIMEType;
|
2021-11-15 21:54:33 +00:00
|
|
|
|
fileName?: string;
|
|
|
|
|
path?: string;
|
2021-08-30 21:32:56 +00:00
|
|
|
|
pending: true;
|
2021-10-05 22:10:08 +00:00
|
|
|
|
size: number;
|
2021-08-30 21:32:56 +00:00
|
|
|
|
};
|
2021-06-25 16:08:16 +00:00
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
|
export type ThumbnailType = {
|
2022-06-10 01:10:20 +00:00
|
|
|
|
height?: number;
|
|
|
|
|
width?: number;
|
2021-07-14 23:39:52 +00:00
|
|
|
|
url?: string;
|
2021-06-17 17:15:10 +00:00
|
|
|
|
contentType: MIME.MIMEType;
|
2022-06-10 01:10:20 +00:00
|
|
|
|
path?: string;
|
|
|
|
|
data?: Uint8Array;
|
2021-06-17 17:15:10 +00:00
|
|
|
|
// Only used when quote needed to make an in-memory thumbnail
|
|
|
|
|
objectUrl?: string;
|
2021-01-14 18:07:05 +00:00
|
|
|
|
};
|
2019-01-14 21:49:58 +00:00
|
|
|
|
|
2021-07-14 23:39:52 +00:00
|
|
|
|
export async function migrateDataToFileSystem(
|
|
|
|
|
attachment: AttachmentType,
|
|
|
|
|
{
|
|
|
|
|
writeNewAttachmentData,
|
|
|
|
|
}: {
|
2021-09-24 00:49:05 +00:00
|
|
|
|
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
|
2021-07-14 23:39:52 +00:00
|
|
|
|
}
|
|
|
|
|
): Promise<AttachmentType> {
|
|
|
|
|
if (!isFunction(writeNewAttachmentData)) {
|
|
|
|
|
throw new TypeError("'writeNewAttachmentData' must be a function");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { data } = attachment;
|
|
|
|
|
const attachmentHasData = !isUndefined(data);
|
|
|
|
|
const shouldSkipSchemaUpgrade = !attachmentHasData;
|
|
|
|
|
if (shouldSkipSchemaUpgrade) {
|
|
|
|
|
return attachment;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
|
if (!isTypedArray(data)) {
|
2021-07-14 23:39:52 +00:00
|
|
|
|
throw new TypeError(
|
2021-09-24 00:49:05 +00:00
|
|
|
|
'Expected `attachment.data` to be a typed array;' +
|
2021-07-14 23:39:52 +00:00
|
|
|
|
` got: ${typeof attachment.data}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const path = await writeNewAttachmentData(data);
|
|
|
|
|
|
|
|
|
|
const attachmentWithoutData = omit({ ...attachment, path }, ['data']);
|
|
|
|
|
return attachmentWithoutData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// // Incoming message attachment fields
|
|
|
|
|
// {
|
|
|
|
|
// id: string
|
|
|
|
|
// contentType: MIMEType
|
2021-09-24 00:49:05 +00:00
|
|
|
|
// data: Uint8Array
|
|
|
|
|
// digest: Uint8Array
|
2021-07-14 23:39:52 +00:00
|
|
|
|
// fileName?: string
|
|
|
|
|
// flags: null
|
2021-09-24 00:49:05 +00:00
|
|
|
|
// key: Uint8Array
|
2021-07-14 23:39:52 +00:00
|
|
|
|
// size: integer
|
2021-09-24 00:49:05 +00:00
|
|
|
|
// thumbnail: Uint8Array
|
2021-07-14 23:39:52 +00:00
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // Outgoing message attachment fields
|
|
|
|
|
// {
|
|
|
|
|
// contentType: MIMEType
|
2021-09-24 00:49:05 +00:00
|
|
|
|
// data: Uint8Array
|
2021-07-14 23:39:52 +00:00
|
|
|
|
// fileName: string
|
|
|
|
|
// size: integer
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// Returns true if `rawAttachment` is a valid attachment based on our current schema.
|
|
|
|
|
// Over time, we can expand this definition to become more narrow, e.g. require certain
|
|
|
|
|
// fields, etc.
|
|
|
|
|
export function isValid(
|
|
|
|
|
rawAttachment?: AttachmentType
|
|
|
|
|
): rawAttachment is AttachmentType {
|
|
|
|
|
// NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is
|
|
|
|
|
// deserialized by protobuf:
|
|
|
|
|
if (!rawAttachment) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upgrade steps
|
|
|
|
|
// NOTE: This step strips all EXIF metadata from JPEG images as
|
|
|
|
|
// part of re-encoding the image:
|
|
|
|
|
export async function autoOrientJPEG(
|
|
|
|
|
attachment: AttachmentType,
|
|
|
|
|
_: unknown,
|
2021-07-20 19:31:23 +00:00
|
|
|
|
{
|
|
|
|
|
sendHQImages = false,
|
|
|
|
|
isIncoming = false,
|
|
|
|
|
}: {
|
|
|
|
|
sendHQImages?: boolean;
|
|
|
|
|
isIncoming?: boolean;
|
|
|
|
|
} = {}
|
2021-07-14 23:39:52 +00:00
|
|
|
|
): Promise<AttachmentType> {
|
2021-07-20 19:31:23 +00:00
|
|
|
|
if (isIncoming && !MIME.isJPEG(attachment.contentType)) {
|
|
|
|
|
return attachment;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-14 23:39:52 +00:00
|
|
|
|
if (!canBeTranscoded(attachment)) {
|
|
|
|
|
return attachment;
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-09 20:06:21 +00:00
|
|
|
|
// If we haven't downloaded the attachment yet, we won't have the data.
|
|
|
|
|
// All images go through handleImageAttachment before being sent and thus have
|
|
|
|
|
// already been scaled to level, oriented, stripped of exif data, and saved
|
|
|
|
|
// in high quality format. If we want to send the image in HQ we can return
|
2022-02-09 20:33:19 +00:00
|
|
|
|
// the attachment as-is. Otherwise we'll have to further scale it down.
|
2021-08-09 20:06:21 +00:00
|
|
|
|
if (!attachment.data || sendHQImages) {
|
2021-07-14 23:39:52 +00:00
|
|
|
|
return attachment;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
|
const dataBlob = new Blob([attachment.data], {
|
|
|
|
|
type: attachment.contentType,
|
|
|
|
|
});
|
2021-08-23 21:24:52 +00:00
|
|
|
|
const { blob: xcodedDataBlob } = await scaleImageToLevel(
|
|
|
|
|
dataBlob,
|
|
|
|
|
attachment.contentType,
|
|
|
|
|
isIncoming
|
|
|
|
|
);
|
2021-07-14 23:39:52 +00:00
|
|
|
|
const xcodedDataArrayBuffer = await blobToArrayBuffer(xcodedDataBlob);
|
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
|
// IMPORTANT: We overwrite the existing `data` `Uint8Array` losing the original
|
2021-07-14 23:39:52 +00:00
|
|
|
|
// image data. Ideally, we’d preserve the original image data for users who want to
|
|
|
|
|
// retain it but due to reports of data loss, we don’t want to overburden IndexedDB
|
|
|
|
|
// by potentially doubling stored image data.
|
|
|
|
|
// See: https://github.com/signalapp/Signal-Desktop/issues/1589
|
|
|
|
|
const xcodedAttachment = {
|
|
|
|
|
// `digest` is no longer valid for auto-oriented image data, so we discard it:
|
|
|
|
|
...omit(attachment, 'digest'),
|
2021-09-24 00:49:05 +00:00
|
|
|
|
data: new Uint8Array(xcodedDataArrayBuffer),
|
2021-07-14 23:39:52 +00:00
|
|
|
|
size: xcodedDataArrayBuffer.byteLength,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return xcodedAttachment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const UNICODE_LEFT_TO_RIGHT_OVERRIDE = '\u202D';
|
|
|
|
|
const UNICODE_RIGHT_TO_LEFT_OVERRIDE = '\u202E';
|
|
|
|
|
const UNICODE_REPLACEMENT_CHARACTER = '\uFFFD';
|
|
|
|
|
const INVALID_CHARACTERS_PATTERN = new RegExp(
|
|
|
|
|
`[${UNICODE_LEFT_TO_RIGHT_OVERRIDE}${UNICODE_RIGHT_TO_LEFT_OVERRIDE}]`,
|
|
|
|
|
'g'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// NOTE: Expose synchronous version to do property-based testing using `testcheck`,
|
|
|
|
|
// which currently doesn’t support async testing:
|
|
|
|
|
// https://github.com/leebyron/testcheck-js/issues/45
|
|
|
|
|
export function _replaceUnicodeOrderOverridesSync(
|
|
|
|
|
attachment: AttachmentType
|
|
|
|
|
): AttachmentType {
|
|
|
|
|
if (!is.string(attachment.fileName)) {
|
|
|
|
|
return attachment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalizedFilename = attachment.fileName.replace(
|
|
|
|
|
INVALID_CHARACTERS_PATTERN,
|
|
|
|
|
UNICODE_REPLACEMENT_CHARACTER
|
|
|
|
|
);
|
|
|
|
|
const newAttachment = { ...attachment, fileName: normalizedFilename };
|
|
|
|
|
|
|
|
|
|
return newAttachment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const replaceUnicodeOrderOverrides = async (
|
|
|
|
|
attachment: AttachmentType
|
|
|
|
|
): Promise<AttachmentType> => {
|
|
|
|
|
return _replaceUnicodeOrderOverridesSync(attachment);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// \u202A-\u202E is LRE, RLE, PDF, LRO, RLO
|
|
|
|
|
// \u2066-\u2069 is LRI, RLI, FSI, PDI
|
|
|
|
|
// \u200E is LRM
|
|
|
|
|
// \u200F is RLM
|
|
|
|
|
// \u061C is ALM
|
|
|
|
|
const V2_UNWANTED_UNICODE = /[\u202A-\u202E\u2066-\u2069\u200E\u200F\u061C]/g;
|
|
|
|
|
|
|
|
|
|
export async function replaceUnicodeV2(
|
|
|
|
|
attachment: AttachmentType
|
|
|
|
|
): Promise<AttachmentType> {
|
|
|
|
|
if (!is.string(attachment.fileName)) {
|
|
|
|
|
return attachment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fileName = attachment.fileName.replace(
|
|
|
|
|
V2_UNWANTED_UNICODE,
|
|
|
|
|
UNICODE_REPLACEMENT_CHARACTER
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...attachment,
|
|
|
|
|
fileName,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function removeSchemaVersion({
|
|
|
|
|
attachment,
|
|
|
|
|
logger,
|
|
|
|
|
}: {
|
|
|
|
|
attachment: AttachmentType;
|
|
|
|
|
logger: LoggerType;
|
|
|
|
|
}): AttachmentType {
|
2022-02-11 21:38:52 +00:00
|
|
|
|
if (!isValid(attachment)) {
|
2021-07-14 23:39:52 +00:00
|
|
|
|
logger.error(
|
|
|
|
|
'Attachment.removeSchemaVersion: Invalid input attachment:',
|
|
|
|
|
attachment
|
|
|
|
|
);
|
|
|
|
|
return attachment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return omit(attachment, 'schemaVersion');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function hasData(attachment: AttachmentType): boolean {
|
2021-09-24 00:49:05 +00:00
|
|
|
|
return attachment.data instanceof Uint8Array;
|
2021-07-14 23:39:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function loadData(
|
2021-09-24 00:49:05 +00:00
|
|
|
|
readAttachmentData: (path: string) => Promise<Uint8Array>
|
2022-06-13 21:39:35 +00:00
|
|
|
|
): (attachment: AttachmentType) => Promise<AttachmentWithHydratedData> {
|
2021-07-14 23:39:52 +00:00
|
|
|
|
if (!is.function_(readAttachmentData)) {
|
|
|
|
|
throw new TypeError("'readAttachmentData' must be a function");
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-13 21:39:35 +00:00
|
|
|
|
return async (
|
|
|
|
|
attachment: AttachmentType
|
|
|
|
|
): Promise<AttachmentWithHydratedData> => {
|
2021-07-14 23:39:52 +00:00
|
|
|
|
if (!isValid(attachment)) {
|
|
|
|
|
throw new TypeError("'attachment' is not valid");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isAlreadyLoaded = Boolean(attachment.data);
|
|
|
|
|
if (isAlreadyLoaded) {
|
2022-06-13 21:39:35 +00:00
|
|
|
|
return attachment as AttachmentWithHydratedData;
|
2021-07-14 23:39:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!is.string(attachment.path)) {
|
|
|
|
|
throw new TypeError("'attachment.path' is required");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await readAttachmentData(attachment.path);
|
|
|
|
|
return { ...attachment, data, size: data.byteLength };
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function deleteData(
|
|
|
|
|
deleteOnDisk: (path: string) => Promise<void>
|
|
|
|
|
): (attachment?: AttachmentType) => Promise<void> {
|
|
|
|
|
if (!is.function_(deleteOnDisk)) {
|
|
|
|
|
throw new TypeError('deleteData: deleteOnDisk must be a function');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return async (attachment?: AttachmentType): Promise<void> => {
|
|
|
|
|
if (!isValid(attachment)) {
|
|
|
|
|
throw new TypeError('deleteData: attachment is not valid');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { path, thumbnail, screenshot } = attachment;
|
|
|
|
|
if (is.string(path)) {
|
|
|
|
|
await deleteOnDisk(path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (thumbnail && is.string(thumbnail.path)) {
|
|
|
|
|
await deleteOnDisk(thumbnail.path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (screenshot && is.string(screenshot.path)) {
|
|
|
|
|
await deleteOnDisk(screenshot.path);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const THUMBNAIL_SIZE = 150;
|
|
|
|
|
const THUMBNAIL_CONTENT_TYPE = MIME.IMAGE_PNG;
|
|
|
|
|
|
|
|
|
|
export async function captureDimensionsAndScreenshot(
|
|
|
|
|
attachment: AttachmentType,
|
|
|
|
|
params: {
|
2021-09-24 00:49:05 +00:00
|
|
|
|
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
|
2022-06-10 01:10:20 +00:00
|
|
|
|
getAbsoluteAttachmentPath: (path: string) => string;
|
2021-09-24 00:49:05 +00:00
|
|
|
|
makeObjectUrl: (
|
|
|
|
|
data: Uint8Array | ArrayBuffer,
|
|
|
|
|
contentType: MIME.MIMEType
|
|
|
|
|
) => string;
|
2021-07-14 23:39:52 +00:00
|
|
|
|
revokeObjectUrl: (path: string) => void;
|
2022-06-10 01:10:20 +00:00
|
|
|
|
getImageDimensions: (params: {
|
|
|
|
|
objectUrl: string;
|
|
|
|
|
logger: LoggerType;
|
|
|
|
|
}) => Promise<{
|
2021-11-11 22:43:05 +00:00
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
2022-06-10 01:10:20 +00:00
|
|
|
|
}>;
|
2021-07-14 23:39:52 +00:00
|
|
|
|
makeImageThumbnail: (params: {
|
|
|
|
|
size: number;
|
|
|
|
|
objectUrl: string;
|
|
|
|
|
contentType: MIME.MIMEType;
|
|
|
|
|
logger: LoggerType;
|
|
|
|
|
}) => Promise<Blob>;
|
|
|
|
|
makeVideoScreenshot: (params: {
|
|
|
|
|
objectUrl: string;
|
|
|
|
|
contentType: MIME.MIMEType;
|
|
|
|
|
logger: LoggerType;
|
|
|
|
|
}) => Promise<Blob>;
|
|
|
|
|
logger: LoggerType;
|
|
|
|
|
}
|
|
|
|
|
): Promise<AttachmentType> {
|
|
|
|
|
const { contentType } = attachment;
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
writeNewAttachmentData,
|
|
|
|
|
getAbsoluteAttachmentPath,
|
|
|
|
|
makeObjectUrl,
|
|
|
|
|
revokeObjectUrl,
|
|
|
|
|
getImageDimensions: getImageDimensionsFromURL,
|
|
|
|
|
makeImageThumbnail,
|
|
|
|
|
makeVideoScreenshot,
|
|
|
|
|
logger,
|
|
|
|
|
} = params;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!GoogleChrome.isImageTypeSupported(contentType) &&
|
|
|
|
|
!GoogleChrome.isVideoTypeSupported(contentType)
|
|
|
|
|
) {
|
|
|
|
|
return attachment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the attachment hasn't been downloaded yet, we won't have a path
|
|
|
|
|
if (!attachment.path) {
|
|
|
|
|
return attachment;
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-10 01:10:20 +00:00
|
|
|
|
const absolutePath = getAbsoluteAttachmentPath(attachment.path);
|
2021-07-14 23:39:52 +00:00
|
|
|
|
|
|
|
|
|
if (GoogleChrome.isImageTypeSupported(contentType)) {
|
|
|
|
|
try {
|
|
|
|
|
const { width, height } = await getImageDimensionsFromURL({
|
|
|
|
|
objectUrl: absolutePath,
|
|
|
|
|
logger,
|
|
|
|
|
});
|
|
|
|
|
const thumbnailBuffer = await blobToArrayBuffer(
|
|
|
|
|
await makeImageThumbnail({
|
|
|
|
|
size: THUMBNAIL_SIZE,
|
|
|
|
|
objectUrl: absolutePath,
|
|
|
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
|
|
|
logger,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
|
const thumbnailPath = await writeNewAttachmentData(
|
|
|
|
|
new Uint8Array(thumbnailBuffer)
|
|
|
|
|
);
|
2021-07-14 23:39:52 +00:00
|
|
|
|
return {
|
|
|
|
|
...attachment,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
thumbnail: {
|
|
|
|
|
path: thumbnailPath,
|
|
|
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
|
|
|
width: THUMBNAIL_SIZE,
|
|
|
|
|
height: THUMBNAIL_SIZE,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(
|
|
|
|
|
'captureDimensionsAndScreenshot:',
|
|
|
|
|
'error processing image; skipping screenshot generation',
|
|
|
|
|
toLogFormat(error)
|
|
|
|
|
);
|
|
|
|
|
return attachment;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let screenshotObjectUrl: string | undefined;
|
|
|
|
|
try {
|
|
|
|
|
const screenshotBuffer = await blobToArrayBuffer(
|
|
|
|
|
await makeVideoScreenshot({
|
|
|
|
|
objectUrl: absolutePath,
|
|
|
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
|
|
|
logger,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
screenshotObjectUrl = makeObjectUrl(
|
|
|
|
|
screenshotBuffer,
|
|
|
|
|
THUMBNAIL_CONTENT_TYPE
|
|
|
|
|
);
|
|
|
|
|
const { width, height } = await getImageDimensionsFromURL({
|
|
|
|
|
objectUrl: screenshotObjectUrl,
|
|
|
|
|
logger,
|
|
|
|
|
});
|
2021-09-24 00:49:05 +00:00
|
|
|
|
const screenshotPath = await writeNewAttachmentData(
|
|
|
|
|
new Uint8Array(screenshotBuffer)
|
|
|
|
|
);
|
2021-07-14 23:39:52 +00:00
|
|
|
|
|
|
|
|
|
const thumbnailBuffer = await blobToArrayBuffer(
|
|
|
|
|
await makeImageThumbnail({
|
|
|
|
|
size: THUMBNAIL_SIZE,
|
|
|
|
|
objectUrl: screenshotObjectUrl,
|
|
|
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
|
|
|
logger,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
|
const thumbnailPath = await writeNewAttachmentData(
|
|
|
|
|
new Uint8Array(thumbnailBuffer)
|
|
|
|
|
);
|
2021-07-14 23:39:52 +00:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...attachment,
|
|
|
|
|
screenshot: {
|
|
|
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
|
|
|
path: screenshotPath,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
},
|
|
|
|
|
thumbnail: {
|
|
|
|
|
path: thumbnailPath,
|
|
|
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
|
|
|
width: THUMBNAIL_SIZE,
|
|
|
|
|
height: THUMBNAIL_SIZE,
|
|
|
|
|
},
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(
|
|
|
|
|
'captureDimensionsAndScreenshot: error processing video; skipping screenshot generation',
|
|
|
|
|
toLogFormat(error)
|
|
|
|
|
);
|
|
|
|
|
return attachment;
|
|
|
|
|
} finally {
|
|
|
|
|
if (screenshotObjectUrl !== undefined) {
|
|
|
|
|
revokeObjectUrl(screenshotObjectUrl);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
|
// UI-focused functions
|
|
|
|
|
|
|
|
|
|
export function getExtensionForDisplay({
|
|
|
|
|
fileName,
|
|
|
|
|
contentType,
|
|
|
|
|
}: {
|
2021-06-17 17:15:10 +00:00
|
|
|
|
fileName?: string;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
contentType: MIME.MIMEType;
|
|
|
|
|
}): string | undefined {
|
|
|
|
|
if (fileName && fileName.indexOf('.') >= 0) {
|
|
|
|
|
const lastPeriod = fileName.lastIndexOf('.');
|
|
|
|
|
const extension = fileName.slice(lastPeriod + 1);
|
|
|
|
|
if (extension.length) {
|
|
|
|
|
return extension;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!contentType) {
|
2020-09-14 21:56:35 +00:00
|
|
|
|
return undefined;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const slash = contentType.indexOf('/');
|
|
|
|
|
if (slash >= 0) {
|
|
|
|
|
return contentType.slice(slash + 1);
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-14 21:56:35 +00:00
|
|
|
|
return undefined;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-09-13 02:36:41 +00:00
|
|
|
|
export function isAudio(attachments?: ReadonlyArray<AttachmentType>): boolean {
|
2021-07-09 20:27:16 +00:00
|
|
|
|
return Boolean(
|
2019-01-14 21:49:58 +00:00
|
|
|
|
attachments &&
|
2021-07-09 20:27:16 +00:00
|
|
|
|
attachments[0] &&
|
|
|
|
|
attachments[0].contentType &&
|
|
|
|
|
!attachments[0].isCorrupted &&
|
|
|
|
|
MIME.isAudio(attachments[0].contentType)
|
2019-01-14 21:49:58 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-13 02:36:41 +00:00
|
|
|
|
export function canDisplayImage(
|
|
|
|
|
attachments?: ReadonlyArray<AttachmentType>
|
|
|
|
|
): boolean {
|
2019-01-14 21:49:58 +00:00
|
|
|
|
const { height, width } =
|
|
|
|
|
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
|
|
|
|
|
|
2021-07-09 20:27:16 +00:00
|
|
|
|
return Boolean(
|
2019-01-14 21:49:58 +00:00
|
|
|
|
height &&
|
2021-07-09 20:27:16 +00:00
|
|
|
|
height > 0 &&
|
|
|
|
|
height <= 4096 &&
|
|
|
|
|
width &&
|
|
|
|
|
width > 0 &&
|
|
|
|
|
width <= 4096
|
2019-01-14 21:49:58 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-16 17:49:19 +00:00
|
|
|
|
export function getThumbnailUrl(
|
|
|
|
|
attachment: AttachmentType
|
|
|
|
|
): string | undefined {
|
2019-01-14 21:49:58 +00:00
|
|
|
|
if (attachment.thumbnail) {
|
|
|
|
|
return attachment.thumbnail.url;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return getUrl(attachment);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-16 17:49:19 +00:00
|
|
|
|
export function getUrl(attachment: AttachmentType): string | undefined {
|
2019-01-14 21:49:58 +00:00
|
|
|
|
if (attachment.screenshot) {
|
|
|
|
|
return attachment.screenshot.url;
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-15 21:54:33 +00:00
|
|
|
|
if (isVideoAttachment(attachment)) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
|
return attachment.url;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-13 02:36:41 +00:00
|
|
|
|
export function isImage(attachments?: ReadonlyArray<AttachmentType>): boolean {
|
2021-07-09 20:27:16 +00:00
|
|
|
|
return Boolean(
|
2019-01-14 21:49:58 +00:00
|
|
|
|
attachments &&
|
2021-07-09 20:27:16 +00:00
|
|
|
|
attachments[0] &&
|
|
|
|
|
attachments[0].contentType &&
|
|
|
|
|
isImageTypeSupported(attachments[0].contentType)
|
2019-01-14 21:49:58 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-05 22:10:08 +00:00
|
|
|
|
export function isImageAttachment(
|
|
|
|
|
attachment?: Pick<AttachmentType, 'contentType'>
|
|
|
|
|
): boolean {
|
2021-01-08 19:39:32 +00:00
|
|
|
|
return Boolean(
|
2019-01-14 21:49:58 +00:00
|
|
|
|
attachment &&
|
2021-01-08 19:39:32 +00:00
|
|
|
|
attachment.contentType &&
|
|
|
|
|
isImageTypeSupported(attachment.contentType)
|
2019-01-14 21:49:58 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
2021-07-07 17:06:01 +00:00
|
|
|
|
|
|
|
|
|
export function canBeTranscoded(
|
2021-10-05 22:10:08 +00:00
|
|
|
|
attachment?: Pick<AttachmentType, 'contentType'>
|
|
|
|
|
): boolean {
|
2021-07-07 17:06:01 +00:00
|
|
|
|
return Boolean(
|
2021-09-02 19:35:23 +00:00
|
|
|
|
attachment &&
|
|
|
|
|
isImageAttachment(attachment) &&
|
|
|
|
|
!MIME.isGif(attachment.contentType)
|
2021-07-07 17:06:01 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-13 02:36:41 +00:00
|
|
|
|
export function hasImage(attachments?: ReadonlyArray<AttachmentType>): boolean {
|
2021-07-09 20:27:16 +00:00
|
|
|
|
return Boolean(
|
2019-01-14 21:49:58 +00:00
|
|
|
|
attachments &&
|
2021-07-09 20:27:16 +00:00
|
|
|
|
attachments[0] &&
|
|
|
|
|
(attachments[0].url || attachments[0].pending || attachments[0].blurHash)
|
2019-01-14 21:49:58 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-13 02:36:41 +00:00
|
|
|
|
export function isVideo(attachments?: ReadonlyArray<AttachmentType>): boolean {
|
2021-04-27 22:11:59 +00:00
|
|
|
|
if (!attachments || attachments.length === 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return isVideoAttachment(attachments[0]);
|
2019-01-14 21:49:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-04-27 22:11:59 +00:00
|
|
|
|
export function isVideoAttachment(attachment?: AttachmentType): boolean {
|
|
|
|
|
if (!attachment || !attachment.contentType) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return isVideoTypeSupported(attachment.contentType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isGIF(attachments?: ReadonlyArray<AttachmentType>): boolean {
|
|
|
|
|
if (!attachments || attachments.length !== 1) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [attachment] = attachments;
|
|
|
|
|
|
|
|
|
|
const flag = SignalService.AttachmentPointer.Flags.GIF;
|
|
|
|
|
const hasFlag =
|
|
|
|
|
// eslint-disable-next-line no-bitwise
|
|
|
|
|
!is.undefined(attachment.flags) && (attachment.flags & flag) === flag;
|
|
|
|
|
|
|
|
|
|
return hasFlag && isVideoAttachment(attachment);
|
2019-01-14 21:49:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-29 01:10:08 +00:00
|
|
|
|
export function isDownloaded(attachment?: AttachmentType): boolean {
|
2022-04-08 17:03:10 +00:00
|
|
|
|
return Boolean(attachment && (attachment.path || attachment.textAttachment));
|
2022-03-29 01:10:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function hasNotResolved(attachment?: AttachmentType): boolean {
|
2022-04-25 17:25:50 +00:00
|
|
|
|
return Boolean(attachment && !attachment.url && !attachment.textAttachment);
|
2021-01-29 22:58:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-29 01:10:08 +00:00
|
|
|
|
export function isDownloading(attachment?: AttachmentType): boolean {
|
|
|
|
|
return Boolean(attachment && attachment.downloadJobId && attachment.pending);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-29 22:58:28 +00:00
|
|
|
|
export function hasVideoBlurHash(attachments?: Array<AttachmentType>): boolean {
|
|
|
|
|
const firstAttachment = attachments ? attachments[0] : null;
|
|
|
|
|
|
|
|
|
|
return Boolean(firstAttachment && firstAttachment.blurHash);
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-14 21:56:35 +00:00
|
|
|
|
export function hasVideoScreenshot(
|
|
|
|
|
attachments?: Array<AttachmentType>
|
|
|
|
|
): string | null | undefined {
|
2019-01-14 21:49:58 +00:00
|
|
|
|
const firstAttachment = attachments ? attachments[0] : null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
firstAttachment &&
|
|
|
|
|
firstAttachment.screenshot &&
|
|
|
|
|
firstAttachment.screenshot.url
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type DimensionsType = {
|
|
|
|
|
height: number;
|
|
|
|
|
width: number;
|
|
|
|
|
};
|
|
|
|
|
|
2020-01-09 19:26:49 +00:00
|
|
|
|
export function getImageDimensions(
|
2021-07-14 23:39:52 +00:00
|
|
|
|
attachment: Pick<AttachmentType, 'width' | 'height'>,
|
2020-01-09 19:26:49 +00:00
|
|
|
|
forcedWidth?: number
|
|
|
|
|
): DimensionsType {
|
2019-01-14 21:49:58 +00:00
|
|
|
|
const { height, width } = attachment;
|
|
|
|
|
if (!height || !width) {
|
|
|
|
|
return {
|
|
|
|
|
height: MIN_HEIGHT,
|
|
|
|
|
width: MIN_WIDTH,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const aspectRatio = height / width;
|
2020-01-09 19:26:49 +00:00
|
|
|
|
const targetWidth =
|
|
|
|
|
forcedWidth || Math.max(Math.min(MAX_WIDTH, width), MIN_WIDTH);
|
2019-01-14 21:49:58 +00:00
|
|
|
|
const candidateHeight = Math.round(targetWidth * aspectRatio);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
width: targetWidth,
|
|
|
|
|
height: Math.max(Math.min(MAX_HEIGHT, candidateHeight), MIN_HEIGHT),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function areAllAttachmentsVisual(
|
2021-09-13 02:36:41 +00:00
|
|
|
|
attachments?: ReadonlyArray<AttachmentType>
|
2019-01-14 21:49:58 +00:00
|
|
|
|
): boolean {
|
|
|
|
|
if (!attachments) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const max = attachments.length;
|
|
|
|
|
for (let i = 0; i < max; i += 1) {
|
|
|
|
|
const attachment = attachments[i];
|
|
|
|
|
if (!isImageAttachment(attachment) && !isVideoAttachment(attachment)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getGridDimensions(
|
2021-09-13 02:36:41 +00:00
|
|
|
|
attachments?: ReadonlyArray<AttachmentType>
|
2019-01-14 21:49:58 +00:00
|
|
|
|
): null | DimensionsType {
|
|
|
|
|
if (!attachments || !attachments.length) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isImage(attachments) && !isVideo(attachments)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (attachments.length === 1) {
|
|
|
|
|
return getImageDimensions(attachments[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (attachments.length === 2) {
|
2021-06-24 21:00:11 +00:00
|
|
|
|
// A B
|
2019-01-14 21:49:58 +00:00
|
|
|
|
return {
|
|
|
|
|
height: 150,
|
|
|
|
|
width: 300,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-24 21:00:11 +00:00
|
|
|
|
if (attachments.length === 3) {
|
|
|
|
|
// A A B
|
|
|
|
|
// A A C
|
|
|
|
|
return {
|
|
|
|
|
height: 200,
|
|
|
|
|
width: 300,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
|
if (attachments.length === 4) {
|
2021-06-24 21:00:11 +00:00
|
|
|
|
// A B
|
|
|
|
|
// C D
|
2019-01-14 21:49:58 +00:00
|
|
|
|
return {
|
|
|
|
|
height: 300,
|
|
|
|
|
width: 300,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-24 21:00:11 +00:00
|
|
|
|
// A A A B B B
|
|
|
|
|
// A A A B B B
|
|
|
|
|
// A A A B B B
|
|
|
|
|
// C C D D E E
|
|
|
|
|
// C C D D E E
|
2019-01-14 21:49:58 +00:00
|
|
|
|
return {
|
2021-06-24 21:00:11 +00:00
|
|
|
|
height: 250,
|
2019-01-14 21:49:58 +00:00
|
|
|
|
width: 300,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getAlt(
|
|
|
|
|
attachment: AttachmentType,
|
|
|
|
|
i18n: LocalizerType
|
|
|
|
|
): string {
|
2021-04-27 22:11:59 +00:00
|
|
|
|
if (isVideoAttachment(attachment)) {
|
|
|
|
|
return i18n('videoAttachmentAlt');
|
|
|
|
|
}
|
|
|
|
|
return i18n('imageAttachmentAlt');
|
2019-01-14 21:49:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Migration-related attachment stuff
|
2018-04-09 23:28:54 +00:00
|
|
|
|
|
2021-07-14 23:39:52 +00:00
|
|
|
|
export const isVisualMedia = (attachment: AttachmentType): boolean => {
|
2018-04-09 23:29:38 +00:00
|
|
|
|
const { contentType } = attachment;
|
|
|
|
|
|
|
|
|
|
if (is.undefined(contentType)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-07 18:43:34 +00:00
|
|
|
|
if (isVoiceMessage(attachment)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return MIME.isImage(contentType) || MIME.isVideo(contentType);
|
2018-04-09 23:29:38 +00:00
|
|
|
|
};
|
2018-04-25 17:27:28 +00:00
|
|
|
|
|
2021-07-14 23:39:52 +00:00
|
|
|
|
export const isFile = (attachment: AttachmentType): boolean => {
|
2018-05-07 19:24:46 +00:00
|
|
|
|
const { contentType } = attachment;
|
|
|
|
|
|
|
|
|
|
if (is.undefined(contentType)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isVisualMedia(attachment)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isVoiceMessage(attachment)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
2021-07-14 23:39:52 +00:00
|
|
|
|
export const isVoiceMessage = (attachment: AttachmentType): boolean => {
|
2018-05-07 16:40:59 +00:00
|
|
|
|
const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
|
|
|
|
|
const hasFlag =
|
2020-09-14 21:56:35 +00:00
|
|
|
|
// eslint-disable-next-line no-bitwise
|
2018-05-07 16:40:59 +00:00
|
|
|
|
!is.undefined(attachment.flags) && (attachment.flags & flag) === flag;
|
|
|
|
|
if (hasFlag) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isLegacyAndroidVoiceMessage =
|
|
|
|
|
!is.undefined(attachment.contentType) &&
|
|
|
|
|
MIME.isAudio(attachment.contentType) &&
|
2019-01-14 21:49:58 +00:00
|
|
|
|
!attachment.fileName;
|
2018-05-07 16:40:59 +00:00
|
|
|
|
if (isLegacyAndroidVoiceMessage) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
2020-01-09 19:57:43 +00:00
|
|
|
|
export const save = async ({
|
2018-04-25 17:27:28 +00:00
|
|
|
|
attachment,
|
2019-03-08 20:27:45 +00:00
|
|
|
|
index,
|
2020-01-09 19:57:43 +00:00
|
|
|
|
readAttachmentData,
|
2020-01-15 22:23:02 +00:00
|
|
|
|
saveAttachmentToDisk,
|
2018-04-25 17:27:28 +00:00
|
|
|
|
timestamp,
|
|
|
|
|
}: {
|
2021-07-14 23:39:52 +00:00
|
|
|
|
attachment: AttachmentType;
|
|
|
|
|
index?: number;
|
2021-09-24 00:49:05 +00:00
|
|
|
|
readAttachmentData: (relativePath: string) => Promise<Uint8Array>;
|
2020-01-15 22:23:02 +00:00
|
|
|
|
saveAttachmentToDisk: (options: {
|
2021-09-24 00:49:05 +00:00
|
|
|
|
data: Uint8Array;
|
2020-01-09 19:57:43 +00:00
|
|
|
|
name: string;
|
2021-09-24 00:49:05 +00:00
|
|
|
|
}) => Promise<{ name: string; fullPath: string } | null>;
|
2018-04-25 17:27:28 +00:00
|
|
|
|
timestamp?: number;
|
2020-01-22 19:34:36 +00:00
|
|
|
|
}): Promise<string | null> => {
|
2021-09-24 00:49:05 +00:00
|
|
|
|
let data: Uint8Array;
|
2021-07-14 23:39:52 +00:00
|
|
|
|
if (attachment.path) {
|
|
|
|
|
data = await readAttachmentData(attachment.path);
|
|
|
|
|
} else if (attachment.data) {
|
|
|
|
|
data = attachment.data;
|
|
|
|
|
} else {
|
2020-01-09 19:57:43 +00:00
|
|
|
|
throw new Error('Attachment had neither path nor data');
|
2018-04-30 15:01:57 +00:00
|
|
|
|
}
|
2020-01-09 19:57:43 +00:00
|
|
|
|
|
|
|
|
|
const name = getSuggestedFilename({ attachment, timestamp, index });
|
|
|
|
|
|
2020-01-22 19:34:36 +00:00
|
|
|
|
const result = await saveAttachmentToDisk({
|
2020-01-09 19:57:43 +00:00
|
|
|
|
data,
|
|
|
|
|
name,
|
|
|
|
|
});
|
|
|
|
|
|
2020-01-22 19:34:36 +00:00
|
|
|
|
if (!result) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result.fullPath;
|
2018-04-25 17:27:28 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getSuggestedFilename = ({
|
|
|
|
|
attachment,
|
|
|
|
|
timestamp,
|
2019-03-08 20:27:45 +00:00
|
|
|
|
index,
|
2018-04-25 17:27:28 +00:00
|
|
|
|
}: {
|
2021-07-14 23:39:52 +00:00
|
|
|
|
attachment: AttachmentType;
|
2018-04-25 17:27:28 +00:00
|
|
|
|
timestamp?: number | Date;
|
2019-03-08 20:27:45 +00:00
|
|
|
|
index?: number;
|
2018-04-25 17:27:28 +00:00
|
|
|
|
}): string => {
|
2019-06-14 18:58:33 +00:00
|
|
|
|
if (!isNumber(index) && attachment.fileName) {
|
2018-04-25 17:27:28 +00:00
|
|
|
|
return attachment.fileName;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-10 21:48:41 +00:00
|
|
|
|
const prefix = 'signal';
|
2018-04-25 17:27:28 +00:00
|
|
|
|
const suffix = timestamp
|
|
|
|
|
? moment(timestamp).format('-YYYY-MM-DD-HHmmss')
|
|
|
|
|
: '';
|
|
|
|
|
const fileType = getFileExtension(attachment);
|
|
|
|
|
const extension = fileType ? `.${fileType}` : '';
|
2019-03-08 20:27:45 +00:00
|
|
|
|
const indexSuffix = index ? `_${padStart(index.toString(), 3, '0')}` : '';
|
2018-05-22 19:31:43 +00:00
|
|
|
|
|
2019-03-08 20:27:45 +00:00
|
|
|
|
return `${prefix}${suffix}${indexSuffix}${extension}`;
|
2018-04-25 17:27:28 +00:00
|
|
|
|
};
|
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
|
export const getFileExtension = (
|
2021-07-14 23:39:52 +00:00
|
|
|
|
attachment: AttachmentType
|
2019-01-14 21:49:58 +00:00
|
|
|
|
): string | undefined => {
|
2018-04-25 17:27:28 +00:00
|
|
|
|
if (!attachment.contentType) {
|
2020-09-14 21:56:35 +00:00
|
|
|
|
return undefined;
|
2018-04-25 17:27:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (attachment.contentType) {
|
|
|
|
|
case 'video/quicktime':
|
|
|
|
|
return 'mov';
|
|
|
|
|
default:
|
|
|
|
|
return attachment.contentType.split('/')[1];
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-02-09 20:11:07 +00:00
|
|
|
|
|
2022-04-13 17:47:39 +00:00
|
|
|
|
const MEBIBYTE = 1024 * 1024;
|
|
|
|
|
const DEFAULT_MAX = 100 * MEBIBYTE;
|
|
|
|
|
|
|
|
|
|
export const getMaximumAttachmentSize = (): number => {
|
|
|
|
|
try {
|
|
|
|
|
return parseIntOrThrow(
|
|
|
|
|
getValue('global.attachments.maxBytes'),
|
|
|
|
|
'preProcessAttachment/maxAttachmentSize'
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
log.warn(
|
|
|
|
|
'Failed to parse integer out of global.attachments.maxBytes feature flag'
|
|
|
|
|
);
|
|
|
|
|
return DEFAULT_MAX;
|
2021-02-09 20:11:07 +00:00
|
|
|
|
}
|
|
|
|
|
};
|
2021-04-27 22:11:59 +00:00
|
|
|
|
|
|
|
|
|
export const defaultBlurHash = (theme: ThemeType = ThemeType.light): string => {
|
|
|
|
|
if (theme === ThemeType.dark) {
|
|
|
|
|
return 'L05OQnoffQofoffQfQfQfQfQfQfQ';
|
|
|
|
|
}
|
|
|
|
|
return 'L1Q]+w-;fQ-;~qfQfQfQfQfQfQfQ';
|
|
|
|
|
};
|
2022-05-23 23:07:41 +00:00
|
|
|
|
|
|
|
|
|
export const canBeDownloaded = (
|
|
|
|
|
attachment: Pick<AttachmentType, 'key' | 'digest'>
|
|
|
|
|
): boolean => {
|
|
|
|
|
return Boolean(attachment.key && attachment.digest);
|
|
|
|
|
};
|