session-desktop/ts/session/utils/Attachments.ts
2021-10-27 13:57:14 +11:00

163 lines
4.5 KiB
TypeScript

import * as crypto from 'crypto';
import { Attachment } from '../../types/Attachment';
import {
AttachmentPointer,
AttachmentPointerWithUrl,
PreviewWithAttachmentUrl,
Quote,
QuotedAttachmentWithUrl,
} from '../messages/outgoing/visibleMessage/VisibleMessage';
import { FSv2 } from '../../fileserver';
import { addAttachmentPadding } from '../crypto/BufferPadding';
import _ from 'lodash';
interface UploadParams {
attachment: Attachment;
isAvatar?: boolean;
isRaw?: boolean;
shouldPad?: boolean;
}
export interface RawPreview {
url: string;
title?: string;
image: Attachment;
}
export interface RawQuoteAttachment {
contentType?: string;
fileName?: string;
thumbnail?: Attachment;
}
export interface RawQuote {
id: number;
author: string;
text?: string;
attachments?: Array<RawQuoteAttachment>;
}
// tslint:disable-next-line: no-unnecessary-class
export class AttachmentFsV2Utils {
private constructor() {}
public static async uploadToFsV2(params: UploadParams): Promise<AttachmentPointerWithUrl> {
const { attachment, isRaw = false, shouldPad = false } = params;
if (typeof attachment !== 'object' || attachment == null) {
throw new Error('Invalid attachment passed.');
}
if (!(attachment.data instanceof ArrayBuffer)) {
throw new TypeError(
`\`attachment.data\` must be an \`ArrayBuffer\`; got: ${typeof attachment.data}`
);
}
const pointer: AttachmentPointer = {
contentType: attachment.contentType || undefined,
size: attachment.size,
fileName: attachment.fileName,
flags: attachment.flags,
caption: attachment.caption,
};
let attachmentData: ArrayBuffer;
if (isRaw) {
attachmentData = attachment.data;
} else {
pointer.key = new Uint8Array(crypto.randomBytes(64));
const iv = new Uint8Array(crypto.randomBytes(16));
const dataToEncrypt = !shouldPad ? attachment.data : addAttachmentPadding(attachment.data);
const data = await window.textsecure.crypto.encryptAttachment(
dataToEncrypt,
pointer.key.buffer,
iv.buffer
);
pointer.digest = new Uint8Array(data.digest);
attachmentData = data.ciphertext;
}
// use file server v2
if (FSv2.useFileServerAPIV2Sending) {
const uploadToV2Result = await FSv2.uploadFileToFsV2(attachmentData);
if (uploadToV2Result) {
const pointerWithUrl: AttachmentPointerWithUrl = {
...pointer,
id: uploadToV2Result.fileId,
url: uploadToV2Result.fileUrl,
};
return pointerWithUrl;
}
window?.log?.warn('upload to file server v2 failed');
throw new Error(`upload to file server v2 of ${attachment.fileName} failed`);
}
throw new Error('Only v2 fileserver upload is supported');
}
public static async uploadAttachmentsToFsV2(
attachments: Array<Attachment>
): Promise<Array<AttachmentPointerWithUrl>> {
const promises = (attachments || []).map(async attachment =>
this.uploadToFsV2({
attachment,
shouldPad: true,
})
);
return Promise.all(promises);
}
public static async uploadLinkPreviewsToFsV2(
previews: Array<RawPreview>
): Promise<Array<PreviewWithAttachmentUrl>> {
const promises = (previews || []).map(async preview => {
// some links does not have an image associated, and it makes the whole message fail to send
if (!preview.image) {
window.log.warn('tried to upload file to fsv2 without image.. skipping');
return preview as any;
}
const image = await this.uploadToFsV2({
attachment: preview.image,
});
return {
...preview,
image,
};
});
return _.compact(await Promise.all(promises));
}
public static async uploadQuoteThumbnailsToFsV2(quote?: RawQuote): Promise<Quote | undefined> {
if (!quote) {
return undefined;
}
const promises = (quote.attachments ?? []).map(async attachment => {
let thumbnail: AttachmentPointer | undefined;
if (attachment.thumbnail) {
thumbnail = await this.uploadToFsV2({
attachment: attachment.thumbnail,
});
}
if (!thumbnail) {
return attachment;
}
return {
...attachment,
thumbnail,
url: thumbnail.url,
id: thumbnail.id,
} as QuotedAttachmentWithUrl;
});
const attachments = _.compact(await Promise.all(promises));
return {
...quote,
attachments,
};
}
}