Show lightbox for GIFs
This commit is contained in:
parent
62ab66c1c8
commit
c3bdf3d411
30 changed files with 790 additions and 815 deletions
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import { app, dialog, clipboard } from 'electron';
|
import { app, dialog, clipboard } from 'electron';
|
||||||
|
|
||||||
import * as Errors from '../js/modules/types/errors';
|
import * as Errors from '../ts/types/errors';
|
||||||
import { redactAll } from '../ts/util/privacy';
|
import { redactAll } from '../ts/util/privacy';
|
||||||
import { LocaleMessagesType } from '../ts/types/I18N';
|
import { LocaleMessagesType } from '../ts/types/I18N';
|
||||||
import { reallyJsonStringify } from '../ts/util/reallyJsonStringify';
|
import { reallyJsonStringify } from '../ts/util/reallyJsonStringify';
|
||||||
|
|
|
@ -131,11 +131,11 @@ const conversationsSelectors = require('../../ts/state/selectors/conversations')
|
||||||
const searchSelectors = require('../../ts/state/selectors/search');
|
const searchSelectors = require('../../ts/state/selectors/search');
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
const AttachmentType = require('./types/attachment');
|
const AttachmentType = require('../../ts/types/Attachment');
|
||||||
const VisualAttachment = require('./types/visual_attachment');
|
const VisualAttachment = require('./types/visual_attachment');
|
||||||
const Contact = require('../../ts/types/Contact');
|
const Contact = require('../../ts/types/Contact');
|
||||||
const Conversation = require('./types/conversation');
|
const Conversation = require('./types/conversation');
|
||||||
const Errors = require('./types/errors');
|
const Errors = require('../../ts/types/errors');
|
||||||
const MediaGalleryMessage = require('../../ts/components/conversation/media-gallery/types/Message');
|
const MediaGalleryMessage = require('../../ts/components/conversation/media-gallery/types/Message');
|
||||||
const MessageType = require('./types/message');
|
const MessageType = require('./types/message');
|
||||||
const MIME = require('../../ts/types/MIME');
|
const MIME = require('../../ts/types/MIME');
|
||||||
|
|
|
@ -1,347 +0,0 @@
|
||||||
// Copyright 2018-2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
const is = require('@sindresorhus/is');
|
|
||||||
|
|
||||||
const { arrayBufferToBlob, blobToArrayBuffer } = require('blob-util');
|
|
||||||
const AttachmentTS = require('../../../ts/types/Attachment');
|
|
||||||
const GoogleChrome = require('../../../ts/util/GoogleChrome');
|
|
||||||
const { toLogFormat } = require('./errors');
|
|
||||||
const { scaleImageToLevel } = require('../../../ts/util/scaleImageToLevel');
|
|
||||||
const {
|
|
||||||
migrateDataToFileSystem,
|
|
||||||
} = require('./attachment/migrate_data_to_file_system');
|
|
||||||
|
|
||||||
// // Incoming message attachment fields
|
|
||||||
// {
|
|
||||||
// id: string
|
|
||||||
// contentType: MIMEType
|
|
||||||
// data: ArrayBuffer
|
|
||||||
// digest: ArrayBuffer
|
|
||||||
// fileName?: string
|
|
||||||
// flags: null
|
|
||||||
// key: ArrayBuffer
|
|
||||||
// size: integer
|
|
||||||
// thumbnail: ArrayBuffer
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Outgoing message attachment fields
|
|
||||||
// {
|
|
||||||
// contentType: MIMEType
|
|
||||||
// data: ArrayBuffer
|
|
||||||
// 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.
|
|
||||||
exports.isValid = rawAttachment => {
|
|
||||||
// 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:
|
|
||||||
exports.autoOrientJPEG = async (attachment, _, message) => {
|
|
||||||
if (!AttachmentTS.canBeTranscoded(attachment)) {
|
|
||||||
return attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we haven't downloaded the attachment yet, we won't have the data
|
|
||||||
if (!attachment.data) {
|
|
||||||
return attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataBlob = await arrayBufferToBlob(
|
|
||||||
attachment.data,
|
|
||||||
attachment.contentType
|
|
||||||
);
|
|
||||||
const xcodedDataBlob = await scaleImageToLevel(
|
|
||||||
dataBlob,
|
|
||||||
message ? message.sendHQImages : false
|
|
||||||
);
|
|
||||||
const xcodedDataArrayBuffer = await blobToArrayBuffer(xcodedDataBlob);
|
|
||||||
|
|
||||||
// IMPORTANT: We overwrite the existing `data` `ArrayBuffer` losing the original
|
|
||||||
// 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 = {
|
|
||||||
...attachment,
|
|
||||||
data: xcodedDataArrayBuffer,
|
|
||||||
size: xcodedDataArrayBuffer.byteLength,
|
|
||||||
};
|
|
||||||
|
|
||||||
// `digest` is no longer valid for auto-oriented image data, so we discard it:
|
|
||||||
delete xcodedAttachment.digest;
|
|
||||||
|
|
||||||
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
|
|
||||||
exports._replaceUnicodeOrderOverridesSync = attachment => {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.replaceUnicodeOrderOverrides = async attachment =>
|
|
||||||
exports._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;
|
|
||||||
|
|
||||||
exports.replaceUnicodeV2 = async attachment => {
|
|
||||||
if (!is.string(attachment.fileName)) {
|
|
||||||
return attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = attachment.fileName.replace(
|
|
||||||
V2_UNWANTED_UNICODE,
|
|
||||||
UNICODE_REPLACEMENT_CHARACTER
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...attachment,
|
|
||||||
fileName,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.removeSchemaVersion = ({ attachment, logger }) => {
|
|
||||||
if (!exports.isValid(attachment)) {
|
|
||||||
logger.error(
|
|
||||||
'Attachment.removeSchemaVersion: Invalid input attachment:',
|
|
||||||
attachment
|
|
||||||
);
|
|
||||||
return attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachmentWithoutSchemaVersion = { ...attachment };
|
|
||||||
delete attachmentWithoutSchemaVersion.schemaVersion;
|
|
||||||
return attachmentWithoutSchemaVersion;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.migrateDataToFileSystem = migrateDataToFileSystem;
|
|
||||||
|
|
||||||
// hasData :: Attachment -> Boolean
|
|
||||||
exports.hasData = attachment =>
|
|
||||||
attachment.data instanceof ArrayBuffer || ArrayBuffer.isView(attachment.data);
|
|
||||||
|
|
||||||
// loadData :: (RelativePath -> IO (Promise ArrayBuffer))
|
|
||||||
// Attachment ->
|
|
||||||
// IO (Promise Attachment)
|
|
||||||
exports.loadData = readAttachmentData => {
|
|
||||||
if (!is.function(readAttachmentData)) {
|
|
||||||
throw new TypeError("'readAttachmentData' must be a function");
|
|
||||||
}
|
|
||||||
|
|
||||||
return async attachment => {
|
|
||||||
if (!exports.isValid(attachment)) {
|
|
||||||
throw new TypeError("'attachment' is not valid");
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAlreadyLoaded = exports.hasData(attachment);
|
|
||||||
if (isAlreadyLoaded) {
|
|
||||||
return attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!is.string(attachment.path)) {
|
|
||||||
throw new TypeError("'attachment.path' is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await readAttachmentData(attachment.path);
|
|
||||||
return { ...attachment, data, size: data.byteLength };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// deleteData :: (RelativePath -> IO Unit)
|
|
||||||
// Attachment ->
|
|
||||||
// IO Unit
|
|
||||||
exports.deleteData = deleteOnDisk => {
|
|
||||||
if (!is.function(deleteOnDisk)) {
|
|
||||||
throw new TypeError('deleteData: deleteOnDisk must be a function');
|
|
||||||
}
|
|
||||||
|
|
||||||
return async attachment => {
|
|
||||||
if (!exports.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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.isImage = AttachmentTS.isImage;
|
|
||||||
exports.isVideo = AttachmentTS.isVideo;
|
|
||||||
exports.isGIF = AttachmentTS.isGIF;
|
|
||||||
exports.isAudio = AttachmentTS.isAudio;
|
|
||||||
exports.isVoiceMessage = AttachmentTS.isVoiceMessage;
|
|
||||||
exports.getUploadSizeLimitKb = AttachmentTS.getUploadSizeLimitKb;
|
|
||||||
exports.save = AttachmentTS.save;
|
|
||||||
|
|
||||||
const THUMBNAIL_SIZE = 150;
|
|
||||||
const THUMBNAIL_CONTENT_TYPE = 'image/png';
|
|
||||||
|
|
||||||
exports.captureDimensionsAndScreenshot = async (
|
|
||||||
attachment,
|
|
||||||
{
|
|
||||||
writeNewAttachmentData,
|
|
||||||
getAbsoluteAttachmentPath,
|
|
||||||
makeObjectUrl,
|
|
||||||
revokeObjectUrl,
|
|
||||||
getImageDimensions,
|
|
||||||
makeImageThumbnail,
|
|
||||||
makeVideoScreenshot,
|
|
||||||
logger,
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const { contentType } = attachment;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const absolutePath = await getAbsoluteAttachmentPath(attachment.path);
|
|
||||||
|
|
||||||
if (GoogleChrome.isImageTypeSupported(contentType)) {
|
|
||||||
try {
|
|
||||||
const { width, height } = await getImageDimensions({
|
|
||||||
objectUrl: absolutePath,
|
|
||||||
logger,
|
|
||||||
});
|
|
||||||
const thumbnailBuffer = await blobToArrayBuffer(
|
|
||||||
await makeImageThumbnail({
|
|
||||||
size: THUMBNAIL_SIZE,
|
|
||||||
objectUrl: absolutePath,
|
|
||||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
||||||
logger,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
|
|
||||||
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;
|
|
||||||
try {
|
|
||||||
const screenshotBuffer = await blobToArrayBuffer(
|
|
||||||
await makeVideoScreenshot({
|
|
||||||
objectUrl: absolutePath,
|
|
||||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
||||||
logger,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
screenshotObjectUrl = makeObjectUrl(
|
|
||||||
screenshotBuffer,
|
|
||||||
THUMBNAIL_CONTENT_TYPE
|
|
||||||
);
|
|
||||||
const { width, height } = await getImageDimensions({
|
|
||||||
objectUrl: screenshotObjectUrl,
|
|
||||||
logger,
|
|
||||||
});
|
|
||||||
const screenshotPath = await writeNewAttachmentData(screenshotBuffer);
|
|
||||||
|
|
||||||
const thumbnailBuffer = await blobToArrayBuffer(
|
|
||||||
await makeImageThumbnail({
|
|
||||||
size: THUMBNAIL_SIZE,
|
|
||||||
objectUrl: screenshotObjectUrl,
|
|
||||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
||||||
logger,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
|
|
||||||
|
|
||||||
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 {
|
|
||||||
revokeObjectUrl(screenshotObjectUrl);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,40 +0,0 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
const { isArrayBuffer, isFunction, isUndefined, omit } = require('lodash');
|
|
||||||
|
|
||||||
// type Context :: {
|
|
||||||
// writeNewAttachmentData :: ArrayBuffer -> Promise (IO Path)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// migrateDataToFileSystem :: Attachment ->
|
|
||||||
// Context ->
|
|
||||||
// Promise Attachment
|
|
||||||
exports.migrateDataToFileSystem = async (
|
|
||||||
attachment,
|
|
||||||
{ writeNewAttachmentData } = {}
|
|
||||||
) => {
|
|
||||||
if (!isFunction(writeNewAttachmentData)) {
|
|
||||||
throw new TypeError("'writeNewAttachmentData' must be a function");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = attachment;
|
|
||||||
const hasData = !isUndefined(data);
|
|
||||||
const shouldSkipSchemaUpgrade = !hasData;
|
|
||||||
if (shouldSkipSchemaUpgrade) {
|
|
||||||
return attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValidData = isArrayBuffer(data);
|
|
||||||
if (!isValidData) {
|
|
||||||
throw new TypeError(
|
|
||||||
'Expected `attachment.data` to be an array buffer;' +
|
|
||||||
` got: ${typeof attachment.data}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = await writeNewAttachmentData(data);
|
|
||||||
|
|
||||||
const attachmentWithoutData = omit({ ...attachment, path }, ['data']);
|
|
||||||
return attachmentWithoutData;
|
|
||||||
};
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
const { omit, compact, map } = require('lodash');
|
const { omit, compact, map } = require('lodash');
|
||||||
|
|
||||||
const { toLogFormat } = require('./errors');
|
const { toLogFormat } = require('../../../ts/types/errors');
|
||||||
const { SignalService } = require('../../../ts/protobuf');
|
const { SignalService } = require('../../../ts/protobuf');
|
||||||
const { parse: parsePhoneNumber } = require('../../../ts/types/PhoneNumber');
|
const { parse: parsePhoneNumber } = require('../../../ts/types/PhoneNumber');
|
||||||
|
|
||||||
|
|
4
js/modules/types/errors.d.ts
vendored
4
js/modules/types/errors.d.ts
vendored
|
@ -1,4 +0,0 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
export function toLogFormat(error: any): string;
|
|
|
@ -1,15 +0,0 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
// toLogFormat :: Error -> String
|
|
||||||
exports.toLogFormat = error => {
|
|
||||||
if (!error) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error && error.stack) {
|
|
||||||
return error.stack;
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.toString();
|
|
||||||
};
|
|
|
@ -4,8 +4,8 @@
|
||||||
const { isFunction, isObject, isString, omit } = require('lodash');
|
const { isFunction, isObject, isString, omit } = require('lodash');
|
||||||
|
|
||||||
const Contact = require('./contact');
|
const Contact = require('./contact');
|
||||||
const Attachment = require('./attachment');
|
const Attachment = require('../../../ts/types/Attachment');
|
||||||
const Errors = require('./errors');
|
const Errors = require('../../../ts/types/errors');
|
||||||
const SchemaVersion = require('./schema_version');
|
const SchemaVersion = require('./schema_version');
|
||||||
const {
|
const {
|
||||||
initializeAttachmentMetadata,
|
initializeAttachmentMetadata,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
const loadImage = require('blueimp-load-image');
|
const loadImage = require('blueimp-load-image');
|
||||||
const { blobToArrayBuffer } = require('blob-util');
|
const { blobToArrayBuffer } = require('blob-util');
|
||||||
const { toLogFormat } = require('./errors');
|
const { toLogFormat } = require('../../../ts/types/errors');
|
||||||
const {
|
const {
|
||||||
arrayBufferToObjectURL,
|
arrayBufferToObjectURL,
|
||||||
} = require('../../../ts/util/arrayBufferToObjectURL');
|
} = require('../../../ts/util/arrayBufferToObjectURL');
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
"axe-core": "4.1.4",
|
"axe-core": "4.1.4",
|
||||||
"backbone": "1.4.0",
|
"backbone": "1.4.0",
|
||||||
"better-sqlite3": "https://github.com/signalapp/better-sqlite3#2fa02d2484e9f9a10df5ac7ea4617fb2dff30006",
|
"better-sqlite3": "https://github.com/signalapp/better-sqlite3#2fa02d2484e9f9a10df5ac7ea4617fb2dff30006",
|
||||||
"blob-util": "1.3.0",
|
"blob-util": "2.0.2",
|
||||||
"blueimp-load-image": "5.14.0",
|
"blueimp-load-image": "5.14.0",
|
||||||
"blurhash": "1.1.3",
|
"blurhash": "1.1.3",
|
||||||
"classnames": "2.2.5",
|
"classnames": "2.2.5",
|
||||||
|
|
|
@ -4084,6 +4084,10 @@ button.module-conversation-details__action-button {
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only if it's a sticker do we put the outline inside it
|
// Only if it's a sticker do we put the outline inside it
|
||||||
|
|
|
@ -1,234 +0,0 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
require('mocha-testcheck').install();
|
|
||||||
|
|
||||||
const { assert } = require('chai');
|
|
||||||
|
|
||||||
const Attachment = require('../../../js/modules/types/attachment');
|
|
||||||
const {
|
|
||||||
stringToArrayBuffer,
|
|
||||||
} = require('../../../js/modules/string_to_array_buffer');
|
|
||||||
|
|
||||||
describe('Attachment', () => {
|
|
||||||
describe('replaceUnicodeOrderOverrides', () => {
|
|
||||||
it('should sanitize left-to-right order override character', async () => {
|
|
||||||
const input = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
fileName: 'test\u202Dfig.exe',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
const expected = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
fileName: 'test\uFFFDfig.exe',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
|
||||||
assert.deepEqual(actual, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should sanitize right-to-left order override character', async () => {
|
|
||||||
const input = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
fileName: 'test\u202Efig.exe',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
const expected = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
fileName: 'test\uFFFDfig.exe',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
|
||||||
assert.deepEqual(actual, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should sanitize multiple override characters', async () => {
|
|
||||||
const input = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
fileName: 'test\u202e\u202dlol\u202efig.exe',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
const expected = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
fileName: 'test\uFFFD\uFFFDlol\uFFFDfig.exe',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
|
||||||
assert.deepEqual(actual, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasNoUnicodeOrderOverrides = value =>
|
|
||||||
!value.includes('\u202D') && !value.includes('\u202E');
|
|
||||||
|
|
||||||
check.it(
|
|
||||||
'should ignore non-order-override characters',
|
|
||||||
gen.string.suchThat(hasNoUnicodeOrderOverrides),
|
|
||||||
fileName => {
|
|
||||||
const input = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
fileName,
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const actual = Attachment._replaceUnicodeOrderOverridesSync(input);
|
|
||||||
assert.deepEqual(actual, input);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('replaceUnicodeV2', () => {
|
|
||||||
it('should remove all bad characters', async () => {
|
|
||||||
const input = {
|
|
||||||
size: 1111,
|
|
||||||
fileName:
|
|
||||||
'file\u202A\u202B\u202C\u202D\u202E\u2066\u2067\u2068\u2069\u200E\u200F\u061C.jpeg',
|
|
||||||
};
|
|
||||||
const expected = {
|
|
||||||
fileName:
|
|
||||||
'file\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD.jpeg',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const actual = await Attachment.replaceUnicodeV2(input);
|
|
||||||
assert.deepEqual(actual, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should should leave normal filename alone', async () => {
|
|
||||||
const input = {
|
|
||||||
fileName: 'normal.jpeg',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
const expected = {
|
|
||||||
fileName: 'normal.jpeg',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const actual = await Attachment.replaceUnicodeV2(input);
|
|
||||||
assert.deepEqual(actual, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle missing fileName', async () => {
|
|
||||||
const input = {
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
const expected = {
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const actual = await Attachment.replaceUnicodeV2(input);
|
|
||||||
assert.deepEqual(actual, expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('removeSchemaVersion', () => {
|
|
||||||
it('should remove existing schema version', () => {
|
|
||||||
const input = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
fileName: 'foo.jpg',
|
|
||||||
size: 1111,
|
|
||||||
schemaVersion: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
fileName: 'foo.jpg',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const actual = Attachment.removeSchemaVersion({
|
|
||||||
attachment: input,
|
|
||||||
logger: {
|
|
||||||
error: () => null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.deepEqual(actual, expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('migrateDataToFileSystem', () => {
|
|
||||||
it('should write data to disk and store relative path to it', async () => {
|
|
||||||
const input = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
data: stringToArrayBuffer('Above us only sky'),
|
|
||||||
fileName: 'foo.jpg',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
path: 'abc/abcdefgh123456789',
|
|
||||||
fileName: 'foo.jpg',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expectedAttachmentData = stringToArrayBuffer('Above us only sky');
|
|
||||||
const writeNewAttachmentData = async attachmentData => {
|
|
||||||
assert.deepEqual(attachmentData, expectedAttachmentData);
|
|
||||||
return 'abc/abcdefgh123456789';
|
|
||||||
};
|
|
||||||
|
|
||||||
const actual = await Attachment.migrateDataToFileSystem(input, {
|
|
||||||
writeNewAttachmentData,
|
|
||||||
logger: {
|
|
||||||
warn: () => null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.deepEqual(actual, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip over (invalid) attachments without data', async () => {
|
|
||||||
const input = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
fileName: 'foo.jpg',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
fileName: 'foo.jpg',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
|
||||||
|
|
||||||
const actual = await Attachment.migrateDataToFileSystem(input, {
|
|
||||||
writeNewAttachmentData,
|
|
||||||
logger: {
|
|
||||||
warn: () => null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.deepEqual(actual, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error if data is not valid', async () => {
|
|
||||||
const input = {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
data: 42,
|
|
||||||
fileName: 'foo.jpg',
|
|
||||||
size: 1111,
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Attachment.migrateDataToFileSystem(input, {
|
|
||||||
writeNewAttachmentData,
|
|
||||||
logger: {
|
|
||||||
warn: () => null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
assert.strictEqual(
|
|
||||||
error.message,
|
|
||||||
'Expected `attachment.data` to be an array buffer; got: number'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.fail('Unreachable');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -31,6 +31,7 @@ export type Props = {
|
||||||
objectURL: string;
|
objectURL: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
isViewOnce: boolean;
|
isViewOnce: boolean;
|
||||||
|
loop?: boolean;
|
||||||
onNext?: () => void;
|
onNext?: () => void;
|
||||||
onPrevious?: () => void;
|
onPrevious?: () => void;
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
|
@ -300,6 +301,7 @@ export class Lightbox extends React.Component<Props, State> {
|
||||||
contentType,
|
contentType,
|
||||||
i18n,
|
i18n,
|
||||||
isViewOnce,
|
isViewOnce,
|
||||||
|
loop = false,
|
||||||
objectURL,
|
objectURL,
|
||||||
onNext,
|
onNext,
|
||||||
onPrevious,
|
onPrevious,
|
||||||
|
@ -320,7 +322,13 @@ export class Lightbox extends React.Component<Props, State> {
|
||||||
<div style={styles.controlsOffsetPlaceholder} />
|
<div style={styles.controlsOffsetPlaceholder} />
|
||||||
<div style={styles.objectContainer}>
|
<div style={styles.objectContainer}>
|
||||||
{!is.undefined(contentType)
|
{!is.undefined(contentType)
|
||||||
? this.renderObject({ objectURL, contentType, i18n, isViewOnce })
|
? this.renderObject({
|
||||||
|
objectURL,
|
||||||
|
contentType,
|
||||||
|
i18n,
|
||||||
|
isViewOnce,
|
||||||
|
loop,
|
||||||
|
})
|
||||||
: null}
|
: null}
|
||||||
{caption ? <div style={styles.caption}>{caption}</div> : null}
|
{caption ? <div style={styles.caption}>{caption}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
|
@ -363,11 +371,13 @@ export class Lightbox extends React.Component<Props, State> {
|
||||||
contentType,
|
contentType,
|
||||||
i18n,
|
i18n,
|
||||||
isViewOnce,
|
isViewOnce,
|
||||||
|
loop,
|
||||||
}: {
|
}: {
|
||||||
objectURL: string;
|
objectURL: string;
|
||||||
contentType: MIME.MIMEType;
|
contentType: MIME.MIMEType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isViewOnce: boolean;
|
isViewOnce: boolean;
|
||||||
|
loop: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
|
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
|
||||||
if (isImageTypeSupported) {
|
if (isImageTypeSupported) {
|
||||||
|
@ -392,8 +402,8 @@ export class Lightbox extends React.Component<Props, State> {
|
||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
ref={this.videoRef}
|
ref={this.videoRef}
|
||||||
loop={isViewOnce}
|
loop={loop || isViewOnce}
|
||||||
controls={!isViewOnce}
|
controls={!loop && !isViewOnce}
|
||||||
style={styles.object}
|
style={styles.object}
|
||||||
key={objectURL}
|
key={objectURL}
|
||||||
>
|
>
|
||||||
|
|
|
@ -29,6 +29,7 @@ export type Props = {
|
||||||
readonly reducedMotion?: boolean;
|
readonly reducedMotion?: boolean;
|
||||||
|
|
||||||
onError(): void;
|
onError(): void;
|
||||||
|
showVisualAttachment(): void;
|
||||||
kickOffAttachmentDownload(): void;
|
kickOffAttachmentDownload(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,6 +49,7 @@ export const GIF: React.FC<Props> = props => {
|
||||||
),
|
),
|
||||||
|
|
||||||
onError,
|
onError,
|
||||||
|
showVisualAttachment,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -191,6 +193,12 @@ export const GIF: React.FC<Props> = props => {
|
||||||
onTimeUpdate={onTimeUpdate}
|
onTimeUpdate={onTimeUpdate}
|
||||||
onEnded={onEnded}
|
onEnded={onEnded}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
|
onClick={(event: React.MouseEvent): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
showVisualAttachment();
|
||||||
|
}}
|
||||||
className="module-image--gif__video"
|
className="module-image--gif__video"
|
||||||
autoPlay
|
autoPlay
|
||||||
playsInline
|
playsInline
|
||||||
|
|
|
@ -716,6 +716,12 @@ export class Message extends React.Component<Props, State> {
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
reducedMotion={reducedMotion}
|
reducedMotion={reducedMotion}
|
||||||
onError={this.handleImageError}
|
onError={this.handleImageError}
|
||||||
|
showVisualAttachment={() => {
|
||||||
|
showVisualAttachment({
|
||||||
|
attachment: firstAttachment,
|
||||||
|
messageId: id,
|
||||||
|
});
|
||||||
|
}}
|
||||||
kickOffAttachmentDownload={() => {
|
kickOffAttachmentDownload={() => {
|
||||||
kickOffAttachmentDownload({
|
kickOffAttachmentDownload({
|
||||||
attachment: firstAttachment,
|
attachment: firstAttachment,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
// 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
|
||||||
|
|
||||||
import { Attachment } from '../../../../types/Attachment';
|
import { AttachmentType } from '../../../../types/Attachment';
|
||||||
|
|
||||||
export type Message = {
|
export type Message = {
|
||||||
id: string;
|
id: string;
|
||||||
attachments: Array<Attachment>;
|
attachments: Array<AttachmentType>;
|
||||||
// Assuming this is for the API
|
// Assuming this is for the API
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
received_at: number;
|
received_at: number;
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const Path = require('path');
|
import * as Path from 'path';
|
||||||
|
import { assert } from 'chai';
|
||||||
const { assert } = require('chai');
|
import * as Errors from '../../types/errors';
|
||||||
|
|
||||||
const Errors = require('../../../js/modules/types/errors');
|
|
||||||
|
|
||||||
const APP_ROOT_PATH = Path.join(__dirname, '..', '..', '..');
|
const APP_ROOT_PATH = Path.join(__dirname, '..', '..', '..');
|
||||||
|
|
||||||
|
@ -26,9 +24,9 @@ describe('Errors', () => {
|
||||||
|
|
||||||
it('should return error string representation if stack is missing', () => {
|
it('should return error string representation if stack is missing', () => {
|
||||||
const error = new Error('boom');
|
const error = new Error('boom');
|
||||||
error.stack = null;
|
error.stack = undefined;
|
||||||
assert.typeOf(error, 'Error');
|
assert.typeOf(error, 'Error');
|
||||||
assert.isNull(error.stack);
|
assert.isUndefined(error.stack);
|
||||||
|
|
||||||
const formattedError = Errors.toLogFormat(error);
|
const formattedError = Errors.toLogFormat(error);
|
||||||
assert.strictEqual(formattedError, 'Error: boom');
|
assert.strictEqual(formattedError, 'Error: boom');
|
||||||
|
@ -37,7 +35,7 @@ describe('Errors', () => {
|
||||||
[0, false, null, undefined].forEach(value => {
|
[0, false, null, undefined].forEach(value => {
|
||||||
it(`should return \`${value}\` argument`, () => {
|
it(`should return \`${value}\` argument`, () => {
|
||||||
const formattedNonError = Errors.toLogFormat(value);
|
const formattedNonError = Errors.toLogFormat(value);
|
||||||
assert.strictEqual(formattedNonError, value);
|
assert.strictEqual(formattedNonError, String(value));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -7,6 +7,7 @@ import * as Attachment from '../../types/Attachment';
|
||||||
import * as MIME from '../../types/MIME';
|
import * as MIME from '../../types/MIME';
|
||||||
import { SignalService } from '../../protobuf';
|
import { SignalService } from '../../protobuf';
|
||||||
import { stringToArrayBuffer } from '../../../js/modules/string_to_array_buffer';
|
import { stringToArrayBuffer } from '../../../js/modules/string_to_array_buffer';
|
||||||
|
import * as logger from '../../logging/log';
|
||||||
|
|
||||||
describe('Attachment', () => {
|
describe('Attachment', () => {
|
||||||
describe('getUploadSizeLimitKb', () => {
|
describe('getUploadSizeLimitKb', () => {
|
||||||
|
@ -36,7 +37,7 @@ describe('Attachment', () => {
|
||||||
|
|
||||||
describe('getFileExtension', () => {
|
describe('getFileExtension', () => {
|
||||||
it('should return file extension from content type', () => {
|
it('should return file extension from content type', () => {
|
||||||
const input: Attachment.Attachment = {
|
const input: Attachment.AttachmentType = {
|
||||||
data: stringToArrayBuffer('foo'),
|
data: stringToArrayBuffer('foo'),
|
||||||
contentType: MIME.IMAGE_GIF,
|
contentType: MIME.IMAGE_GIF,
|
||||||
};
|
};
|
||||||
|
@ -44,7 +45,7 @@ describe('Attachment', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return file extension for QuickTime videos', () => {
|
it('should return file extension for QuickTime videos', () => {
|
||||||
const input: Attachment.Attachment = {
|
const input: Attachment.AttachmentType = {
|
||||||
data: stringToArrayBuffer('foo'),
|
data: stringToArrayBuffer('foo'),
|
||||||
contentType: MIME.VIDEO_QUICKTIME,
|
contentType: MIME.VIDEO_QUICKTIME,
|
||||||
};
|
};
|
||||||
|
@ -55,7 +56,7 @@ describe('Attachment', () => {
|
||||||
describe('getSuggestedFilename', () => {
|
describe('getSuggestedFilename', () => {
|
||||||
context('for attachment with filename', () => {
|
context('for attachment with filename', () => {
|
||||||
it('should return existing filename if present', () => {
|
it('should return existing filename if present', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
fileName: 'funny-cat.mov',
|
fileName: 'funny-cat.mov',
|
||||||
data: stringToArrayBuffer('foo'),
|
data: stringToArrayBuffer('foo'),
|
||||||
contentType: MIME.VIDEO_QUICKTIME,
|
contentType: MIME.VIDEO_QUICKTIME,
|
||||||
|
@ -67,7 +68,7 @@ describe('Attachment', () => {
|
||||||
});
|
});
|
||||||
context('for attachment without filename', () => {
|
context('for attachment without filename', () => {
|
||||||
it('should generate a filename based on timestamp', () => {
|
it('should generate a filename based on timestamp', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
data: stringToArrayBuffer('foo'),
|
data: stringToArrayBuffer('foo'),
|
||||||
contentType: MIME.VIDEO_QUICKTIME,
|
contentType: MIME.VIDEO_QUICKTIME,
|
||||||
};
|
};
|
||||||
|
@ -82,7 +83,7 @@ describe('Attachment', () => {
|
||||||
});
|
});
|
||||||
context('for attachment with index', () => {
|
context('for attachment with index', () => {
|
||||||
it('should generate a filename based on timestamp', () => {
|
it('should generate a filename based on timestamp', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
data: stringToArrayBuffer('foo'),
|
data: stringToArrayBuffer('foo'),
|
||||||
contentType: MIME.VIDEO_QUICKTIME,
|
contentType: MIME.VIDEO_QUICKTIME,
|
||||||
};
|
};
|
||||||
|
@ -100,7 +101,7 @@ describe('Attachment', () => {
|
||||||
|
|
||||||
describe('isVisualMedia', () => {
|
describe('isVisualMedia', () => {
|
||||||
it('should return true for images', () => {
|
it('should return true for images', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
fileName: 'meme.gif',
|
fileName: 'meme.gif',
|
||||||
data: stringToArrayBuffer('gif'),
|
data: stringToArrayBuffer('gif'),
|
||||||
contentType: MIME.IMAGE_GIF,
|
contentType: MIME.IMAGE_GIF,
|
||||||
|
@ -109,7 +110,7 @@ describe('Attachment', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for videos', () => {
|
it('should return true for videos', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
fileName: 'meme.mp4',
|
fileName: 'meme.mp4',
|
||||||
data: stringToArrayBuffer('mp4'),
|
data: stringToArrayBuffer('mp4'),
|
||||||
contentType: MIME.VIDEO_MP4,
|
contentType: MIME.VIDEO_MP4,
|
||||||
|
@ -118,7 +119,7 @@ describe('Attachment', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for voice message attachment', () => {
|
it('should return false for voice message attachment', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
fileName: 'Voice Message.aac',
|
fileName: 'Voice Message.aac',
|
||||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||||
data: stringToArrayBuffer('voice message'),
|
data: stringToArrayBuffer('voice message'),
|
||||||
|
@ -128,7 +129,7 @@ describe('Attachment', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for other attachments', () => {
|
it('should return false for other attachments', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
fileName: 'foo.json',
|
fileName: 'foo.json',
|
||||||
data: stringToArrayBuffer('{"foo": "bar"}'),
|
data: stringToArrayBuffer('{"foo": "bar"}'),
|
||||||
contentType: MIME.APPLICATION_JSON,
|
contentType: MIME.APPLICATION_JSON,
|
||||||
|
@ -139,7 +140,7 @@ describe('Attachment', () => {
|
||||||
|
|
||||||
describe('isFile', () => {
|
describe('isFile', () => {
|
||||||
it('should return true for JSON', () => {
|
it('should return true for JSON', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
fileName: 'foo.json',
|
fileName: 'foo.json',
|
||||||
data: stringToArrayBuffer('{"foo": "bar"}'),
|
data: stringToArrayBuffer('{"foo": "bar"}'),
|
||||||
contentType: MIME.APPLICATION_JSON,
|
contentType: MIME.APPLICATION_JSON,
|
||||||
|
@ -148,7 +149,7 @@ describe('Attachment', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for images', () => {
|
it('should return false for images', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
fileName: 'meme.gif',
|
fileName: 'meme.gif',
|
||||||
data: stringToArrayBuffer('gif'),
|
data: stringToArrayBuffer('gif'),
|
||||||
contentType: MIME.IMAGE_GIF,
|
contentType: MIME.IMAGE_GIF,
|
||||||
|
@ -157,7 +158,7 @@ describe('Attachment', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for videos', () => {
|
it('should return false for videos', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
fileName: 'meme.mp4',
|
fileName: 'meme.mp4',
|
||||||
data: stringToArrayBuffer('mp4'),
|
data: stringToArrayBuffer('mp4'),
|
||||||
contentType: MIME.VIDEO_MP4,
|
contentType: MIME.VIDEO_MP4,
|
||||||
|
@ -166,7 +167,7 @@ describe('Attachment', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for voice message attachment', () => {
|
it('should return false for voice message attachment', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
fileName: 'Voice Message.aac',
|
fileName: 'Voice Message.aac',
|
||||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||||
data: stringToArrayBuffer('voice message'),
|
data: stringToArrayBuffer('voice message'),
|
||||||
|
@ -178,7 +179,7 @@ describe('Attachment', () => {
|
||||||
|
|
||||||
describe('isVoiceMessage', () => {
|
describe('isVoiceMessage', () => {
|
||||||
it('should return true for voice message attachment', () => {
|
it('should return true for voice message attachment', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
fileName: 'Voice Message.aac',
|
fileName: 'Voice Message.aac',
|
||||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||||
data: stringToArrayBuffer('voice message'),
|
data: stringToArrayBuffer('voice message'),
|
||||||
|
@ -188,7 +189,7 @@ describe('Attachment', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for legacy Android voice message attachment', () => {
|
it('should return true for legacy Android voice message attachment', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
data: stringToArrayBuffer('voice message'),
|
data: stringToArrayBuffer('voice message'),
|
||||||
contentType: MIME.AUDIO_MP3,
|
contentType: MIME.AUDIO_MP3,
|
||||||
};
|
};
|
||||||
|
@ -196,7 +197,7 @@ describe('Attachment', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for other attachments', () => {
|
it('should return false for other attachments', () => {
|
||||||
const attachment: Attachment.Attachment = {
|
const attachment: Attachment.AttachmentType = {
|
||||||
fileName: 'foo.gif',
|
fileName: 'foo.gif',
|
||||||
data: stringToArrayBuffer('foo'),
|
data: stringToArrayBuffer('foo'),
|
||||||
contentType: MIME.IMAGE_GIF,
|
contentType: MIME.IMAGE_GIF,
|
||||||
|
@ -204,4 +205,222 @@ describe('Attachment', () => {
|
||||||
assert.isFalse(Attachment.isVoiceMessage(attachment));
|
assert.isFalse(Attachment.isVoiceMessage(attachment));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('replaceUnicodeOrderOverrides', () => {
|
||||||
|
it('should sanitize left-to-right order override character', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'test\u202Dfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'test\uFFFDfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sanitize right-to-left order override character', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'test\u202Efig.exe',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'test\uFFFDfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sanitize multiple override characters', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'test\u202e\u202dlol\u202efig.exe',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'test\uFFFD\uFFFDlol\uFFFDfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore non-order-override characters', () => {
|
||||||
|
const input = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'abc',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = Attachment._replaceUnicodeOrderOverridesSync(input);
|
||||||
|
assert.deepEqual(actual, input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace order-override characters', () => {
|
||||||
|
const input = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'abc\u202D\u202E',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = Attachment._replaceUnicodeOrderOverridesSync(input);
|
||||||
|
assert.deepEqual(actual, {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'abc\uFFFD\uFFFD',
|
||||||
|
size: 1111,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('replaceUnicodeV2', () => {
|
||||||
|
it('should remove all bad characters', async () => {
|
||||||
|
const input = {
|
||||||
|
size: 1111,
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName:
|
||||||
|
'file\u202A\u202B\u202C\u202D\u202E\u2066\u2067\u2068\u2069\u200E\u200F\u061C.jpeg',
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
fileName:
|
||||||
|
'file\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD.jpeg',
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Attachment.replaceUnicodeV2(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should should leave normal filename alone', async () => {
|
||||||
|
const input = {
|
||||||
|
fileName: 'normal.jpeg',
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
fileName: 'normal.jpeg',
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Attachment.replaceUnicodeV2(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing fileName', async () => {
|
||||||
|
const input = {
|
||||||
|
size: 1111,
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
size: 1111,
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Attachment.replaceUnicodeV2(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeSchemaVersion', () => {
|
||||||
|
it('should remove existing schema version', () => {
|
||||||
|
const input = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'foo.jpg',
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'foo.jpg',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = Attachment.removeSchemaVersion({
|
||||||
|
attachment: input,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('migrateDataToFileSystem', () => {
|
||||||
|
it('should write data to disk and store relative path to it', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
data: stringToArrayBuffer('Above us only sky'),
|
||||||
|
fileName: 'foo.jpg',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
path: 'abc/abcdefgh123456789',
|
||||||
|
fileName: 'foo.jpg',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedAttachmentData = stringToArrayBuffer('Above us only sky');
|
||||||
|
const writeNewAttachmentData = async (attachmentData: ArrayBuffer) => {
|
||||||
|
assert.deepEqual(attachmentData, expectedAttachmentData);
|
||||||
|
return 'abc/abcdefgh123456789';
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Attachment.migrateDataToFileSystem(input, {
|
||||||
|
writeNewAttachmentData,
|
||||||
|
});
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip over (invalid) attachments without data', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'foo.jpg',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
fileName: 'foo.jpg',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
||||||
|
|
||||||
|
const actual = await Attachment.migrateDataToFileSystem(input, {
|
||||||
|
writeNewAttachmentData,
|
||||||
|
});
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if data is not valid', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: MIME.IMAGE_JPEG,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
data: 123 as any,
|
||||||
|
fileName: 'foo.jpg',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
||||||
|
|
||||||
|
await assert.isRejected(
|
||||||
|
Attachment.migrateDataToFileSystem(input, {
|
||||||
|
writeNewAttachmentData,
|
||||||
|
}),
|
||||||
|
'Expected `attachment.data` to be an array buffer; got: number'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
7
ts/textsecure.d.ts
vendored
7
ts/textsecure.d.ts
vendored
|
@ -37,13 +37,6 @@ export type UnprocessedType = {
|
||||||
|
|
||||||
export { StorageServiceCallOptionsType, StorageServiceCredentials };
|
export { StorageServiceCallOptionsType, StorageServiceCredentials };
|
||||||
|
|
||||||
export type DownloadAttachmentType = Omit<
|
|
||||||
ProcessedAttachment,
|
|
||||||
'digest' | 'key'
|
|
||||||
> & {
|
|
||||||
data: ArrayBuffer;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TextSecureType = {
|
export type TextSecureType = {
|
||||||
createTaskWithTimeout: (
|
createTaskWithTimeout: (
|
||||||
task: () => Promise<any> | any,
|
task: () => Promise<any> | any,
|
||||||
|
|
|
@ -62,11 +62,13 @@ import * as Bytes from '../Bytes';
|
||||||
import Crypto from './Crypto';
|
import Crypto from './Crypto';
|
||||||
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
|
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
|
||||||
import { ContactBuffer, GroupBuffer } from './ContactsParser';
|
import { ContactBuffer, GroupBuffer } from './ContactsParser';
|
||||||
|
import { DownloadedAttachmentType } from '../types/Attachment';
|
||||||
|
import * as MIME from '../types/MIME';
|
||||||
import { SocketStatus } from '../types/SocketStatus';
|
import { SocketStatus } from '../types/SocketStatus';
|
||||||
|
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
|
||||||
import { DownloadAttachmentType, UnprocessedType } from '../textsecure.d';
|
import { UnprocessedType } from '../textsecure.d';
|
||||||
import {
|
import {
|
||||||
ProcessedAttachment,
|
ProcessedAttachment,
|
||||||
ProcessedDataMessage,
|
ProcessedDataMessage,
|
||||||
|
@ -2441,7 +2443,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
|
|
||||||
async downloadAttachment(
|
async downloadAttachment(
|
||||||
attachment: ProcessedAttachment
|
attachment: ProcessedAttachment
|
||||||
): Promise<DownloadAttachmentType> {
|
): Promise<DownloadedAttachmentType> {
|
||||||
const cdnId = attachment.cdnId || attachment.cdnKey;
|
const cdnId = attachment.cdnId || attachment.cdnKey;
|
||||||
const { cdnNumber } = attachment;
|
const { cdnNumber } = attachment;
|
||||||
|
|
||||||
|
@ -2454,7 +2456,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
cdnId,
|
cdnId,
|
||||||
dropNull(cdnNumber)
|
dropNull(cdnNumber)
|
||||||
);
|
);
|
||||||
const { key, digest, size } = attachment;
|
const { key, digest, size, contentType } = attachment;
|
||||||
|
|
||||||
if (!digest) {
|
if (!digest) {
|
||||||
throw new Error('Failure: Ask sender to update Signal and resend.');
|
throw new Error('Failure: Ask sender to update Signal and resend.');
|
||||||
|
@ -2479,13 +2481,17 @@ class MessageReceiverInner extends EventTarget {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...omit(attachment, 'digest', 'key'),
|
...omit(attachment, 'digest', 'key'),
|
||||||
|
|
||||||
|
contentType: contentType
|
||||||
|
? MIME.fromString(contentType)
|
||||||
|
: MIME.APPLICATION_OCTET_STREAM,
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAttachment(
|
async handleAttachment(
|
||||||
attachment: Proto.IAttachmentPointer
|
attachment: Proto.IAttachmentPointer
|
||||||
): Promise<DownloadAttachmentType> {
|
): Promise<DownloadedAttachmentType> {
|
||||||
const cleaned = processAttachment(attachment);
|
const cleaned = processAttachment(attachment);
|
||||||
return this.downloadAttachment(cleaned);
|
return this.downloadAttachment(cleaned);
|
||||||
}
|
}
|
||||||
|
@ -2661,7 +2667,7 @@ export default class MessageReceiver {
|
||||||
|
|
||||||
downloadAttachment: (
|
downloadAttachment: (
|
||||||
attachment: ProcessedAttachment
|
attachment: ProcessedAttachment
|
||||||
) => Promise<DownloadAttachmentType>;
|
) => Promise<DownloadedAttachmentType>;
|
||||||
|
|
||||||
getStatus: () => SocketStatus;
|
getStatus: () => SocketStatus;
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,27 @@
|
||||||
|
|
||||||
import is from '@sindresorhus/is';
|
import is from '@sindresorhus/is';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { isNumber, padStart } from 'lodash';
|
import {
|
||||||
|
isNumber,
|
||||||
|
padStart,
|
||||||
|
isArrayBuffer,
|
||||||
|
isFunction,
|
||||||
|
isUndefined,
|
||||||
|
omit,
|
||||||
|
} from 'lodash';
|
||||||
|
import { arrayBufferToBlob, blobToArrayBuffer } from 'blob-util';
|
||||||
|
|
||||||
|
import { LoggerType } from './Logging';
|
||||||
import * as MIME from './MIME';
|
import * as MIME from './MIME';
|
||||||
|
import { toLogFormat } from './errors';
|
||||||
import { SignalService } from '../protobuf';
|
import { SignalService } from '../protobuf';
|
||||||
import {
|
import {
|
||||||
isImageTypeSupported,
|
isImageTypeSupported,
|
||||||
isVideoTypeSupported,
|
isVideoTypeSupported,
|
||||||
} from '../util/GoogleChrome';
|
} from '../util/GoogleChrome';
|
||||||
import { LocalizerType, ThemeType } from './Util';
|
import { LocalizerType, ThemeType } from './Util';
|
||||||
|
import * as GoogleChrome from '../util/GoogleChrome';
|
||||||
|
import { scaleImageToLevel } from '../util/scaleImageToLevel';
|
||||||
|
|
||||||
const MAX_WIDTH = 300;
|
const MAX_WIDTH = 300;
|
||||||
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
||||||
|
@ -39,7 +51,7 @@ export type AttachmentType = {
|
||||||
screenshot?: {
|
screenshot?: {
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
url: string;
|
url?: string;
|
||||||
contentType: MIME.MIMEType;
|
contentType: MIME.MIMEType;
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
@ -51,11 +63,16 @@ export type AttachmentType = {
|
||||||
cdnNumber?: number;
|
cdnNumber?: number;
|
||||||
cdnId?: string;
|
cdnId?: string;
|
||||||
cdnKey?: string;
|
cdnKey?: string;
|
||||||
|
data?: ArrayBuffer;
|
||||||
|
|
||||||
/** Legacy field. Used only for downloading old attachments */
|
/** Legacy field. Used only for downloading old attachments */
|
||||||
id?: number;
|
id?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DownloadedAttachmentType = AttachmentType & {
|
||||||
|
data: ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
type BaseAttachmentDraftType = {
|
type BaseAttachmentDraftType = {
|
||||||
blurHash?: string;
|
blurHash?: string;
|
||||||
contentType: MIME.MIMEType;
|
contentType: MIME.MIMEType;
|
||||||
|
@ -82,13 +99,411 @@ 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;
|
||||||
// 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function migrateDataToFileSystem(
|
||||||
|
attachment: AttachmentType,
|
||||||
|
{
|
||||||
|
writeNewAttachmentData,
|
||||||
|
}: {
|
||||||
|
writeNewAttachmentData: (data: ArrayBuffer) => Promise<string>;
|
||||||
|
}
|
||||||
|
): 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isArrayBuffer(data)) {
|
||||||
|
throw new TypeError(
|
||||||
|
'Expected `attachment.data` to be an array buffer;' +
|
||||||
|
` 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
|
||||||
|
// data: ArrayBuffer
|
||||||
|
// digest: ArrayBuffer
|
||||||
|
// fileName?: string
|
||||||
|
// flags: null
|
||||||
|
// key: ArrayBuffer
|
||||||
|
// size: integer
|
||||||
|
// thumbnail: ArrayBuffer
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Outgoing message attachment fields
|
||||||
|
// {
|
||||||
|
// contentType: MIMEType
|
||||||
|
// data: ArrayBuffer
|
||||||
|
// 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,
|
||||||
|
message?: { sendHQImages?: boolean }
|
||||||
|
): Promise<AttachmentType> {
|
||||||
|
if (!canBeTranscoded(attachment)) {
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we haven't downloaded the attachment yet, we won't have the data
|
||||||
|
if (!attachment.data) {
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataBlob = await arrayBufferToBlob(
|
||||||
|
attachment.data,
|
||||||
|
attachment.contentType
|
||||||
|
);
|
||||||
|
const xcodedDataBlob = await scaleImageToLevel(
|
||||||
|
dataBlob,
|
||||||
|
message ? message.sendHQImages : false
|
||||||
|
);
|
||||||
|
const xcodedDataArrayBuffer = await blobToArrayBuffer(xcodedDataBlob);
|
||||||
|
|
||||||
|
// IMPORTANT: We overwrite the existing `data` `ArrayBuffer` losing the original
|
||||||
|
// 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'),
|
||||||
|
data: xcodedDataArrayBuffer,
|
||||||
|
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 {
|
||||||
|
if (!exports.isValid(attachment)) {
|
||||||
|
logger.error(
|
||||||
|
'Attachment.removeSchemaVersion: Invalid input attachment:',
|
||||||
|
attachment
|
||||||
|
);
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return omit(attachment, 'schemaVersion');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasData(attachment: AttachmentType): boolean {
|
||||||
|
return (
|
||||||
|
attachment.data instanceof ArrayBuffer ||
|
||||||
|
ArrayBuffer.isView(attachment.data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadData(
|
||||||
|
readAttachmentData: (path: string) => Promise<ArrayBuffer>
|
||||||
|
): (attachment?: AttachmentType) => Promise<AttachmentType> {
|
||||||
|
if (!is.function_(readAttachmentData)) {
|
||||||
|
throw new TypeError("'readAttachmentData' must be a function");
|
||||||
|
}
|
||||||
|
|
||||||
|
return async (attachment?: AttachmentType): Promise<AttachmentType> => {
|
||||||
|
if (!isValid(attachment)) {
|
||||||
|
throw new TypeError("'attachment' is not valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAlreadyLoaded = Boolean(attachment.data);
|
||||||
|
if (isAlreadyLoaded) {
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: {
|
||||||
|
writeNewAttachmentData: (data: ArrayBuffer) => Promise<string>;
|
||||||
|
getAbsoluteAttachmentPath: (path: string) => Promise<string>;
|
||||||
|
makeObjectUrl: (data: ArrayBuffer, contentType: MIME.MIMEType) => string;
|
||||||
|
revokeObjectUrl: (path: string) => void;
|
||||||
|
getImageDimensions: (params: {
|
||||||
|
objectUrl: string;
|
||||||
|
logger: LoggerType;
|
||||||
|
}) => { width: number; height: number };
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = await getAbsoluteAttachmentPath(attachment.path);
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
const screenshotPath = await writeNewAttachmentData(screenshotBuffer);
|
||||||
|
|
||||||
|
const thumbnailBuffer = await blobToArrayBuffer(
|
||||||
|
await makeImageThumbnail({
|
||||||
|
size: THUMBNAIL_SIZE,
|
||||||
|
objectUrl: screenshotObjectUrl,
|
||||||
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||||
|
logger,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UI-focused functions
|
// UI-focused functions
|
||||||
|
|
||||||
export function getExtensionForDisplay({
|
export function getExtensionForDisplay({
|
||||||
|
@ -252,7 +667,7 @@ type DimensionsType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getImageDimensions(
|
export function getImageDimensions(
|
||||||
attachment: AttachmentType,
|
attachment: Pick<AttachmentType, 'width' | 'height'>,
|
||||||
forcedWidth?: number
|
forcedWidth?: number
|
||||||
): DimensionsType {
|
): DimensionsType {
|
||||||
const { height, width } = attachment;
|
const { height, width } = attachment;
|
||||||
|
@ -356,28 +771,7 @@ export function getAlt(
|
||||||
|
|
||||||
// Migration-related attachment stuff
|
// Migration-related attachment stuff
|
||||||
|
|
||||||
export type Attachment = {
|
export const isVisualMedia = (attachment: AttachmentType): boolean => {
|
||||||
fileName?: string;
|
|
||||||
flags?: SignalService.AttachmentPointer.Flags;
|
|
||||||
contentType?: MIME.MIMEType;
|
|
||||||
size?: number;
|
|
||||||
data: ArrayBuffer;
|
|
||||||
|
|
||||||
// // Omit unused / deprecated keys:
|
|
||||||
// schemaVersion?: number;
|
|
||||||
// id?: string;
|
|
||||||
// width?: number;
|
|
||||||
// height?: number;
|
|
||||||
// thumbnail?: ArrayBuffer;
|
|
||||||
// key?: ArrayBuffer;
|
|
||||||
// digest?: ArrayBuffer;
|
|
||||||
} & Partial<AttachmentSchemaVersion3>;
|
|
||||||
|
|
||||||
type AttachmentSchemaVersion3 = {
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isVisualMedia = (attachment: Attachment): boolean => {
|
|
||||||
const { contentType } = attachment;
|
const { contentType } = attachment;
|
||||||
|
|
||||||
if (is.undefined(contentType)) {
|
if (is.undefined(contentType)) {
|
||||||
|
@ -391,7 +785,7 @@ export const isVisualMedia = (attachment: Attachment): boolean => {
|
||||||
return MIME.isImage(contentType) || MIME.isVideo(contentType);
|
return MIME.isImage(contentType) || MIME.isVideo(contentType);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isFile = (attachment: Attachment): boolean => {
|
export const isFile = (attachment: AttachmentType): boolean => {
|
||||||
const { contentType } = attachment;
|
const { contentType } = attachment;
|
||||||
|
|
||||||
if (is.undefined(contentType)) {
|
if (is.undefined(contentType)) {
|
||||||
|
@ -409,9 +803,7 @@ export const isFile = (attachment: Attachment): boolean => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isVoiceMessage = (
|
export const isVoiceMessage = (attachment: AttachmentType): boolean => {
|
||||||
attachment: Attachment | AttachmentType
|
|
||||||
): boolean => {
|
|
||||||
const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
|
const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
|
||||||
const hasFlag =
|
const hasFlag =
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
|
@ -438,8 +830,8 @@ export const save = async ({
|
||||||
saveAttachmentToDisk,
|
saveAttachmentToDisk,
|
||||||
timestamp,
|
timestamp,
|
||||||
}: {
|
}: {
|
||||||
attachment: Attachment;
|
attachment: AttachmentType;
|
||||||
index: number;
|
index?: number;
|
||||||
readAttachmentData: (relativePath: string) => Promise<ArrayBuffer>;
|
readAttachmentData: (relativePath: string) => Promise<ArrayBuffer>;
|
||||||
saveAttachmentToDisk: (options: {
|
saveAttachmentToDisk: (options: {
|
||||||
data: ArrayBuffer;
|
data: ArrayBuffer;
|
||||||
|
@ -447,13 +839,15 @@ export const save = async ({
|
||||||
}) => Promise<{ name: string; fullPath: string }>;
|
}) => Promise<{ name: string; fullPath: string }>;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
}): Promise<string | null> => {
|
}): Promise<string | null> => {
|
||||||
if (!attachment.path && !attachment.data) {
|
let data: ArrayBuffer;
|
||||||
|
if (attachment.path) {
|
||||||
|
data = await readAttachmentData(attachment.path);
|
||||||
|
} else if (attachment.data) {
|
||||||
|
data = attachment.data;
|
||||||
|
} else {
|
||||||
throw new Error('Attachment had neither path nor data');
|
throw new Error('Attachment had neither path nor data');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = attachment.path
|
|
||||||
? await readAttachmentData(attachment.path)
|
|
||||||
: attachment.data;
|
|
||||||
const name = getSuggestedFilename({ attachment, timestamp, index });
|
const name = getSuggestedFilename({ attachment, timestamp, index });
|
||||||
|
|
||||||
const result = await saveAttachmentToDisk({
|
const result = await saveAttachmentToDisk({
|
||||||
|
@ -473,7 +867,7 @@ export const getSuggestedFilename = ({
|
||||||
timestamp,
|
timestamp,
|
||||||
index,
|
index,
|
||||||
}: {
|
}: {
|
||||||
attachment: Attachment;
|
attachment: AttachmentType;
|
||||||
timestamp?: number | Date;
|
timestamp?: number | Date;
|
||||||
index?: number;
|
index?: number;
|
||||||
}): string => {
|
}): string => {
|
||||||
|
@ -493,7 +887,7 @@ export const getSuggestedFilename = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFileExtension = (
|
export const getFileExtension = (
|
||||||
attachment: Attachment
|
attachment: AttachmentType
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
if (!attachment.contentType) {
|
if (!attachment.contentType) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
@ -31,3 +31,7 @@ export const isAudio = (value: string): value is MIMEType =>
|
||||||
Boolean(value) && value.startsWith('audio/') && !value.endsWith('aiff');
|
Boolean(value) && value.startsWith('audio/') && !value.endsWith('aiff');
|
||||||
export const isLongMessage = (value: unknown): value is MIMEType =>
|
export const isLongMessage = (value: unknown): value is MIMEType =>
|
||||||
value === LONG_MESSAGE;
|
value === LONG_MESSAGE;
|
||||||
|
|
||||||
|
export const fromString = (value: string): MIMEType => {
|
||||||
|
return value as MIMEType;
|
||||||
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
import { Attachment } from './Attachment';
|
import { AttachmentType } from './Attachment';
|
||||||
import { ContactType } from './Contact';
|
import { ContactType } from './Contact';
|
||||||
import { IndexableBoolean, IndexablePresence } from './IndexedDB';
|
import { IndexableBoolean, IndexablePresence } from './IndexedDB';
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export type IncomingMessage = Readonly<
|
||||||
{
|
{
|
||||||
type: 'incoming';
|
type: 'incoming';
|
||||||
// Required
|
// Required
|
||||||
attachments: Array<Attachment>;
|
attachments: Array<AttachmentType>;
|
||||||
id: string;
|
id: string;
|
||||||
received_at: number;
|
received_at: number;
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export type OutgoingMessage = Readonly<
|
||||||
type: 'outgoing';
|
type: 'outgoing';
|
||||||
|
|
||||||
// Required
|
// Required
|
||||||
attachments: Array<Attachment>;
|
attachments: Array<AttachmentType>;
|
||||||
delivered: number;
|
delivered: number;
|
||||||
delivered_to: Array<string>;
|
delivered_to: Array<string>;
|
||||||
destination: string; // PhoneNumber
|
destination: string; // PhoneNumber
|
||||||
|
|
10
ts/types/errors.ts
Normal file
10
ts/types/errors.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export function toLogFormat(error: unknown): string {
|
||||||
|
if (error instanceof Error && error.stack) {
|
||||||
|
return error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(error);
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import * as IndexedDB from '../IndexedDB';
|
||||||
import { Message, UserMessage } from '../Message';
|
import { Message, UserMessage } from '../Message';
|
||||||
|
|
||||||
const hasAttachment = (
|
const hasAttachment = (
|
||||||
predicate: (value: Attachment.Attachment) => boolean
|
predicate: (value: Attachment.AttachmentType) => boolean
|
||||||
) => (message: UserMessage): IndexedDB.IndexablePresence =>
|
) => (message: UserMessage): IndexedDB.IndexablePresence =>
|
||||||
IndexedDB.toIndexablePresence(message.attachments.some(predicate));
|
IndexedDB.toIndexablePresence(message.attachments.some(predicate));
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export const initializeAttachmentMetadata = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachments = message.attachments.filter(
|
const attachments = message.attachments.filter(
|
||||||
(attachment: Attachment.Attachment) =>
|
(attachment: Attachment.AttachmentType) =>
|
||||||
attachment.contentType !== 'text/x-signal-plain'
|
attachment.contentType !== 'text/x-signal-plain'
|
||||||
);
|
);
|
||||||
const hasAttachments = IndexedDB.toIndexableBoolean(attachments.length > 0);
|
const hasAttachments = IndexedDB.toIndexableBoolean(attachments.length > 0);
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { DownloadAttachmentType } from '../textsecure.d';
|
import { AttachmentType, DownloadedAttachmentType } from '../types/Attachment';
|
||||||
|
|
||||||
import { AttachmentType } from '../types/Attachment';
|
|
||||||
|
|
||||||
export async function downloadAttachment(
|
export async function downloadAttachment(
|
||||||
attachmentData: AttachmentType
|
attachmentData: AttachmentType
|
||||||
): Promise<DownloadAttachmentType | null> {
|
): Promise<DownloadedAttachmentType | null> {
|
||||||
let migratedAttachment: AttachmentType;
|
let migratedAttachment: AttachmentType;
|
||||||
|
|
||||||
const { id: legacyId } = attachmentData;
|
const { id: legacyId } = attachmentData;
|
||||||
|
|
|
@ -1896,10 +1896,24 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "node_modules/blob-util/dist/blob-util.js",
|
"path": "node_modules/blob-util/dist/blob-util.cjs.js",
|
||||||
"line": " bb.append(ary[i]);",
|
"line": " builder.append(parts[i]);",
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2021-07-14T23:12:47.258Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-append(",
|
||||||
|
"path": "node_modules/blob-util/dist/blob-util.es.js",
|
||||||
|
"line": " builder.append(parts[i]);",
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2021-07-14T23:12:47.258Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-append(",
|
||||||
|
"path": "node_modules/blob-util/dist/blob-util.js",
|
||||||
|
"line": " builder.append(parts[i]);",
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2021-07-14T23:12:47.258Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
|
@ -1907,13 +1921,6 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-append(",
|
|
||||||
"path": "node_modules/blob/index.js",
|
|
||||||
"line": " bb.append(ary[i]);",
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/boom/lib/index.js",
|
"path": "node_modules/boom/lib/index.js",
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
AttachmentType,
|
AttachmentType,
|
||||||
InMemoryAttachmentDraftType,
|
InMemoryAttachmentDraftType,
|
||||||
OnDiskAttachmentDraftType,
|
OnDiskAttachmentDraftType,
|
||||||
|
isGIF,
|
||||||
} from '../types/Attachment';
|
} from '../types/Attachment';
|
||||||
import type { StickerPackType as StickerPackDBType } from '../sql/Interface';
|
import type { StickerPackType as StickerPackDBType } from '../sql/Interface';
|
||||||
import * as Stickers from '../types/Stickers';
|
import * as Stickers from '../types/Stickers';
|
||||||
|
@ -63,7 +64,7 @@ const LINK_PREVIEW_TIMEOUT = 60 * 1000;
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
const { Whisper } = window;
|
const { Whisper } = window;
|
||||||
const { Message, MIME, VisualAttachment, Attachment } = window.Signal.Types;
|
const { Message, MIME, VisualAttachment } = window.Signal.Types;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
copyIntoTempDirectory,
|
copyIntoTempDirectory,
|
||||||
|
@ -783,7 +784,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
message.markAttachmentAsCorrupted(options.attachment);
|
message.markAttachmentAsCorrupted(options.attachment);
|
||||||
};
|
};
|
||||||
const showVisualAttachment = (options: {
|
const showVisualAttachment = (options: {
|
||||||
attachment: typeof Attachment;
|
attachment: AttachmentType;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
showSingle?: boolean;
|
showSingle?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -2803,8 +2804,8 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
timestamp,
|
timestamp,
|
||||||
isDangerous,
|
isDangerous,
|
||||||
}: {
|
}: {
|
||||||
attachment: typeof Attachment;
|
attachment: AttachmentType;
|
||||||
timestamp: string;
|
timestamp: number;
|
||||||
isDangerous: boolean;
|
isDangerous: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (isDangerous) {
|
if (isDangerous) {
|
||||||
|
@ -3012,7 +3013,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
attachment,
|
attachment,
|
||||||
messageId,
|
messageId,
|
||||||
}: {
|
}: {
|
||||||
attachment: typeof Attachment;
|
attachment: AttachmentType;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
showSingle?: boolean;
|
showSingle?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
@ -3037,14 +3038,17 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachments = message.get('attachments') || [];
|
const attachments: Array<AttachmentType> = message.get('attachments') || [];
|
||||||
|
|
||||||
|
const loop = isGIF(attachments);
|
||||||
|
|
||||||
const media = attachments
|
const media = attachments
|
||||||
.filter((item: any) => item.thumbnail && !item.pending && !item.error)
|
.filter(item => item.thumbnail && !item.pending && !item.error)
|
||||||
.map((item: any, index: number) => ({
|
.map((item, index) => ({
|
||||||
objectURL: getAbsoluteAttachmentPath(item.path),
|
objectURL: getAbsoluteAttachmentPath(item.path ?? ''),
|
||||||
path: item.path,
|
path: item.path,
|
||||||
contentType: item.contentType,
|
contentType: item.contentType,
|
||||||
|
loop,
|
||||||
index,
|
index,
|
||||||
message,
|
message,
|
||||||
attachment: item,
|
attachment: item,
|
||||||
|
@ -3052,9 +3056,10 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
|
|
||||||
if (media.length === 1) {
|
if (media.length === 1) {
|
||||||
const props = {
|
const props = {
|
||||||
objectURL: getAbsoluteAttachmentPath(path),
|
objectURL: getAbsoluteAttachmentPath(path ?? ''),
|
||||||
contentType,
|
contentType,
|
||||||
caption: attachment.caption,
|
caption: attachment.caption,
|
||||||
|
loop,
|
||||||
onSave: () => {
|
onSave: () => {
|
||||||
const timestamp = message.get('sent_at');
|
const timestamp = message.get('sent_at');
|
||||||
this.downloadAttachment({ attachment, timestamp, message });
|
this.downloadAttachment({ attachment, timestamp, message });
|
||||||
|
@ -3095,6 +3100,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
media,
|
media,
|
||||||
|
loop,
|
||||||
selectedIndex: selectedIndex >= 0 ? selectedIndex : 0,
|
selectedIndex: selectedIndex >= 0 ? selectedIndex : 0,
|
||||||
onSave,
|
onSave,
|
||||||
};
|
};
|
||||||
|
|
41
ts/window.d.ts
vendored
41
ts/window.d.ts
vendored
|
@ -18,7 +18,7 @@ import {
|
||||||
ReactionAttributesType,
|
ReactionAttributesType,
|
||||||
ReactionModelType,
|
ReactionModelType,
|
||||||
} from './model-types.d';
|
} from './model-types.d';
|
||||||
import { TextSecureType, DownloadAttachmentType } from './textsecure.d';
|
import { TextSecureType } from './textsecure.d';
|
||||||
import { Storage } from './textsecure/Storage';
|
import { Storage } from './textsecure/Storage';
|
||||||
import {
|
import {
|
||||||
ChallengeHandler,
|
ChallengeHandler,
|
||||||
|
@ -38,7 +38,7 @@ import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util';
|
||||||
import * as Attachment from './types/Attachment';
|
import * as Attachment from './types/Attachment';
|
||||||
import * as MIME from './types/MIME';
|
import * as MIME from './types/MIME';
|
||||||
import * as Contact from './types/Contact';
|
import * as Contact from './types/Contact';
|
||||||
import * as Errors from '../js/modules/types/errors';
|
import * as Errors from './types/errors';
|
||||||
import { ConversationController } from './ConversationController';
|
import { ConversationController } from './ConversationController';
|
||||||
import { ReduxActions } from './state/types';
|
import { ReduxActions } from './state/types';
|
||||||
import { createStore } from './state/createStore';
|
import { createStore } from './state/createStore';
|
||||||
|
@ -104,7 +104,7 @@ import { Quote } from './components/conversation/Quote';
|
||||||
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
||||||
import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
|
import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
|
||||||
import { MIMEType } from './types/MIME';
|
import { MIMEType } from './types/MIME';
|
||||||
import { AttachmentType } from './types/Attachment';
|
import { DownloadedAttachmentType } from './types/Attachment';
|
||||||
import { ElectronLocaleType } from './util/mapToSupportLocale';
|
import { ElectronLocaleType } from './util/mapToSupportLocale';
|
||||||
import { SignalProtocolStore } from './SignalProtocolStore';
|
import { SignalProtocolStore } from './SignalProtocolStore';
|
||||||
import { Context as SignalContext } from './context';
|
import { Context as SignalContext } from './context';
|
||||||
|
@ -324,8 +324,8 @@ declare global {
|
||||||
copyIntoAttachmentsDirectory: (path: string) => Promise<string>;
|
copyIntoAttachmentsDirectory: (path: string) => Promise<string>;
|
||||||
upgradeMessageSchema: (attributes: unknown) => WhatIsThis;
|
upgradeMessageSchema: (attributes: unknown) => WhatIsThis;
|
||||||
processNewAttachment: (
|
processNewAttachment: (
|
||||||
attachment: DownloadAttachmentType
|
attachment: DownloadedAttachmentType
|
||||||
) => Promise<AttachmentType>;
|
) => Promise<DownloadedAttachmentType>;
|
||||||
|
|
||||||
copyIntoTempDirectory: any;
|
copyIntoTempDirectory: any;
|
||||||
deleteDraftFile: (path: string) => Promise<void>;
|
deleteDraftFile: (path: string) => Promise<void>;
|
||||||
|
@ -339,36 +339,7 @@ declare global {
|
||||||
writeNewDraftData: any;
|
writeNewDraftData: any;
|
||||||
};
|
};
|
||||||
Types: {
|
Types: {
|
||||||
Attachment: {
|
Attachment: typeof Attachment;
|
||||||
save: any;
|
|
||||||
path: string;
|
|
||||||
pending: boolean;
|
|
||||||
flags: number;
|
|
||||||
size: number;
|
|
||||||
screenshot: {
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
thumbnail: {
|
|
||||||
path: string;
|
|
||||||
objectUrl: string;
|
|
||||||
};
|
|
||||||
contentType: MIMEType;
|
|
||||||
error: unknown;
|
|
||||||
caption: string;
|
|
||||||
|
|
||||||
migrateDataToFileSystem: (
|
|
||||||
attachment: WhatIsThis,
|
|
||||||
options: unknown
|
|
||||||
) => WhatIsThis;
|
|
||||||
|
|
||||||
isVoiceMessage: (attachments: unknown) => boolean;
|
|
||||||
isImage: typeof Attachment.isImage;
|
|
||||||
isGIF: typeof Attachment.isGIF;
|
|
||||||
isVideo: typeof Attachment.isVideo;
|
|
||||||
isAudio: typeof Attachment.isAudio;
|
|
||||||
|
|
||||||
getUploadSizeLimitKb: typeof Attachment.getUploadSizeLimitKb;
|
|
||||||
};
|
|
||||||
MIME: typeof MIME;
|
MIME: typeof MIME;
|
||||||
Contact: typeof Contact;
|
Contact: typeof Contact;
|
||||||
Conversation: {
|
Conversation: {
|
||||||
|
|
27
yarn.lock
27
yarn.lock
|
@ -4721,17 +4721,10 @@ bl@^4.0.1, bl@^4.0.3:
|
||||||
inherits "^2.0.4"
|
inherits "^2.0.4"
|
||||||
readable-stream "^3.4.0"
|
readable-stream "^3.4.0"
|
||||||
|
|
||||||
blob-util@1.3.0:
|
blob-util@2.0.2:
|
||||||
version "1.3.0"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-1.3.0.tgz#dbb4e8caffd50b5720d347e1169b6369ba34fe95"
|
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
|
||||||
integrity sha512-cjmYgWj8BQwoX+95rKkWvITL6PiEhSr19sX8qLRu+O6J2qmWmgUvxqhqJn425RFAwLovdDNnsCQ64RRHXjsXSg==
|
integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
|
||||||
dependencies:
|
|
||||||
blob "0.0.4"
|
|
||||||
native-or-lie "1.0.2"
|
|
||||||
|
|
||||||
blob@0.0.4:
|
|
||||||
version "0.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
|
|
||||||
|
|
||||||
block-stream@*:
|
block-stream@*:
|
||||||
version "0.0.9"
|
version "0.0.9"
|
||||||
|
@ -11357,12 +11350,6 @@ levn@~0.3.0:
|
||||||
prelude-ls "~1.1.2"
|
prelude-ls "~1.1.2"
|
||||||
type-check "~0.3.2"
|
type-check "~0.3.2"
|
||||||
|
|
||||||
lie@*:
|
|
||||||
version "3.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.2.0.tgz#4f13f2f8bbb027d383db338c43043545791aa8dc"
|
|
||||||
dependencies:
|
|
||||||
immediate "~3.0.5"
|
|
||||||
|
|
||||||
lie@~3.1.0:
|
lie@~3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
||||||
|
@ -12431,12 +12418,6 @@ napi-build-utils@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508"
|
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508"
|
||||||
integrity sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==
|
integrity sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==
|
||||||
|
|
||||||
native-or-lie@1.0.2:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/native-or-lie/-/native-or-lie-1.0.2.tgz#c870ee0ba0bf0ff11350595d216cfea68a6d8086"
|
|
||||||
dependencies:
|
|
||||||
lie "*"
|
|
||||||
|
|
||||||
natural-compare@^1.4.0:
|
natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue