@slack/web-api
Version:
Official library for using the Slack Platform's Web API
327 lines • 14.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildInvalidFilesUploadParamError = exports.buildMultipleChannelsErrorMsg = exports.buildChannelsWarning = exports.buildFilesUploadMissingMessage = exports.buildGeneralFilesUploadWarning = exports.buildLegacyMethodWarning = exports.buildMissingExtensionWarning = exports.buildMissingFileNameWarning = exports.buildLegacyFileTypeWarning = exports.buildFileSizeErrorMsg = exports.buildMissingFileIdError = exports.warnIfLegacyFileType = exports.warnIfMissingOrInvalidFileNameAndDefault = exports.errorIfInvalidOrMissingFileData = exports.errorIfChannelsCsv = exports.warnIfChannels = exports.warnIfNotUsingFilesUploadV2 = exports.getAllFileUploadsToComplete = exports.getFileDataAsStream = exports.getFileDataLength = exports.getFileData = exports.getMultipleFileUploadJobs = exports.getFileUploadJob = void 0;
const fs_1 = require("fs");
const stream_1 = require("stream");
const errors_1 = require("./errors");
/**
* Returns a fileUploadJob used to represent the of the file upload job and
* required metadata.
* @param options Options provided by user
* @param channelId optional channel id to share file with, omitted, channel is private
* @returns
*/
async function getFileUploadJob(options, logger) {
var _a, _b, _c, _d;
// Validate parameters
warnIfLegacyFileType(options, logger);
warnIfChannels(options, logger);
errorIfChannelsCsv(options);
const fileName = warnIfMissingOrInvalidFileNameAndDefault(options, logger);
const fileData = await getFileData(options);
const fileDataBytesLength = getFileDataLength(fileData);
const fileUploadJob = {
// supplied by user
alt_text: options.alt_text,
channel_id: (_a = options.channels) !== null && _a !== void 0 ? _a : options.channel_id,
content: options.content,
file: options.file,
filename: (_b = options.filename) !== null && _b !== void 0 ? _b : fileName,
initial_comment: options.initial_comment,
snippet_type: options.snippet_type,
thread_ts: options.thread_ts,
title: (_c = options.title) !== null && _c !== void 0 ? _c : ((_d = options.filename) !== null && _d !== void 0 ? _d : fileName),
// calculated
data: fileData,
length: fileDataBytesLength,
};
return fileUploadJob;
}
exports.getFileUploadJob = getFileUploadJob;
/**
* Returns an array of files upload entries when `file_uploads` is supplied.
* **Note**
* file_uploads should be set when multiple files are intended to be attached to a
* single message. To support this, we handle options supplied with
* top level `initial_comment`, `thread_ts`, `channel_id` and `file_uploads` parameters.
* ```javascript
* const res = await client.files.uploadV2({
* initial_comment: 'Here are the files!',
* thread_ts: '1223313423434.131321',
* channel_id: 'C12345',
* file_uploads: [
* {
* file: './test/fixtures/test-txt.txt',
* filename: 'test-txt.txt',
* },
* {
* file: './test/fixtures/test-png.png',
* filename: 'test-png.png',
* },
* ],
* });
* ```
* @param options provided by user
*/
async function getMultipleFileUploadJobs(options, logger) {
if (options.file_uploads) {
// go through each file_upload and create a job for it
return Promise.all(options.file_uploads.map((upload) => {
// ensure no omitted properties included in files_upload entry
// these properties are valid only at the top-level, not
// inside file_uploads.
const { channel_id, channels, initial_comment, thread_ts } = upload;
if (channel_id || channels || initial_comment || thread_ts) {
throw (0, errors_1.errorWithCode)(new Error(buildInvalidFilesUploadParamError()), errors_1.ErrorCode.FileUploadInvalidArgumentsError);
}
// takes any channel_id, initial_comment and thread_ts
// supplied at the top level.
return getFileUploadJob(Object.assign(Object.assign({}, upload), { channels: options.channels, channel_id: options.channel_id, initial_comment: options.initial_comment, thread_ts: options.thread_ts }), logger);
}));
}
throw new Error(buildFilesUploadMissingMessage());
}
exports.getMultipleFileUploadJobs = getMultipleFileUploadJobs;
// Helpers to build the FileUploadJob
/**
* Returns a single file upload's data
* @param options
* @returns Binary data representation of file
*/
async function getFileData(options) {
errorIfInvalidOrMissingFileData(options);
const { file, content } = options;
if (file) {
// try to handle as buffer
if (Buffer.isBuffer(file))
return file;
// try to handle as filepath
if (typeof file === 'string') {
// try to read file as if the string was a file path
try {
const dataBuffer = (0, fs_1.readFileSync)(file);
return dataBuffer;
}
catch (error) {
throw (0, errors_1.errorWithCode)(new Error(`Unable to resolve file data for ${file}. Please supply a filepath string, or binary data Buffer or String directly.`), errors_1.ErrorCode.FileUploadInvalidArgumentsError);
}
}
// try to handle as Readable
const data = await getFileDataAsStream(file);
if (data)
return data;
}
if (content)
return Buffer.from(content);
// general catch-all error
throw (0, errors_1.errorWithCode)(new Error('There was an issue getting the file data for the file or content supplied'), errors_1.ErrorCode.FileUploadReadFileDataError);
}
exports.getFileData = getFileData;
function getFileDataLength(data) {
if (data) {
return Buffer.byteLength(data, 'utf8');
}
throw (0, errors_1.errorWithCode)(new Error(buildFileSizeErrorMsg()), errors_1.ErrorCode.FileUploadReadFileDataError);
}
exports.getFileDataLength = getFileDataLength;
async function getFileDataAsStream(readable) {
const chunks = [];
return new Promise((resolve, reject) => {
readable.on('readable', () => {
let chunk;
/* eslint-disable no-cond-assign */
while ((chunk = readable.read()) !== null) {
chunks.push(chunk);
}
if (chunks.length > 0) {
const content = Buffer.concat(chunks);
resolve(content);
}
else {
reject(Error('No data in supplied file'));
}
});
});
}
exports.getFileDataAsStream = getFileDataAsStream;
/**
* Filters through all fileUploads and groups them into jobs for completion
* based on combination of channel_id, thread_ts, initial_comment.
* {@link https://api.slack.com/methods/files.completeUploadExternal files.completeUploadExternal} allows for multiple
* files to be uploaded with a message (`initial_comment`), and as a threaded message (`thread_ts`)
* In order to be grouped together, file uploads must have like properties.
* @param fileUploads
* @returns
*/
function getAllFileUploadsToComplete(fileUploads) {
const toComplete = {};
fileUploads.forEach((upload) => {
const { channel_id, thread_ts, initial_comment, file_id, title } = upload;
if (file_id) {
const compareString = `:::${channel_id}:::${thread_ts}:::${initial_comment}`;
if (!Object.prototype.hasOwnProperty.call(toComplete, compareString)) {
toComplete[compareString] = {
files: [{ id: file_id, title }],
channel_id,
initial_comment,
thread_ts,
};
}
else {
toComplete[compareString].files.push({
id: file_id,
title,
});
}
}
else {
throw new Error(buildMissingFileIdError());
}
});
return toComplete;
}
exports.getAllFileUploadsToComplete = getAllFileUploadsToComplete;
// Validation
/**
* Advise to use the files.uploadV2 method over legacy files.upload method and over
* lower-level utilities.
* @param method
* @param logger
*/
function warnIfNotUsingFilesUploadV2(method, logger) {
const targetMethods = ['files.upload'];
const isTargetMethod = targetMethods.includes(method);
if (method === 'files.upload')
logger.warn(buildLegacyMethodWarning(method));
if (isTargetMethod)
logger.info(buildGeneralFilesUploadWarning());
}
exports.warnIfNotUsingFilesUploadV2 = warnIfNotUsingFilesUploadV2;
/**
* `channels` param is supported but only when a single channel is specified.
* @param options
* @param logger
*/
function warnIfChannels(options, logger) {
if (options.channels)
logger.warn(buildChannelsWarning());
}
exports.warnIfChannels = warnIfChannels;
/**
* v1 files.upload supported `channels` parameter provided as a comma-separated
* string of values, e.g. 'C1234,C5678'. V2 no longer supports this csv value.
* You may still supply `channels` with a single channel string value e.g. 'C1234'
* but it is highly encouraged to supply `channel_id` instead.
* @param options
*/
function errorIfChannelsCsv(options) {
const channels = options.channels ? options.channels.split(',') : [];
if (channels.length > 1) {
throw (0, errors_1.errorWithCode)(new Error(buildMultipleChannelsErrorMsg()), errors_1.ErrorCode.FileUploadInvalidArgumentsError);
}
}
exports.errorIfChannelsCsv = errorIfChannelsCsv;
/**
* Checks for either a file or content property and errors if missing
* @param options
*/
function errorIfInvalidOrMissingFileData(options) {
const { file, content } = options;
if (!(file || content) || (file && content)) {
throw (0, errors_1.errorWithCode)(new Error('Either a file or content field is required for valid file upload. You cannot supply both'), errors_1.ErrorCode.FileUploadInvalidArgumentsError);
}
/* eslint-disable @typescript-eslint/no-explicit-any */
if (file && !(typeof file === 'string' || Buffer.isBuffer(file) || file instanceof stream_1.Readable)) {
throw (0, errors_1.errorWithCode)(new Error('file must be a valid string path, buffer or Readable'), errors_1.ErrorCode.FileUploadInvalidArgumentsError);
}
if (content && typeof content !== 'string') {
throw (0, errors_1.errorWithCode)(new Error('content must be a string'), errors_1.ErrorCode.FileUploadInvalidArgumentsError);
}
}
exports.errorIfInvalidOrMissingFileData = errorIfInvalidOrMissingFileData;
/**
* @param options
* @param logger
* @returns filename if it exists
*/
function warnIfMissingOrInvalidFileNameAndDefault(options, logger) {
var _a;
const DEFAULT_FILETYPE = 'txt';
const DEFAULT_FILENAME = `file.${(_a = options.filetype) !== null && _a !== void 0 ? _a : DEFAULT_FILETYPE}`;
const { filename } = options;
if (!filename) {
// Filename was an optional property in legacy method
logger.warn(buildMissingFileNameWarning());
return DEFAULT_FILENAME;
}
if (filename.split('.').length < 2) {
// likely filename is missing extension
logger.warn(buildMissingExtensionWarning(filename));
}
return filename;
}
exports.warnIfMissingOrInvalidFileNameAndDefault = warnIfMissingOrInvalidFileNameAndDefault;
/**
* `filetype` param is no longer supported and will be ignored
* @param options
* @param logger
*/
function warnIfLegacyFileType(options, logger) {
if (options.filetype) {
logger.warn(buildLegacyFileTypeWarning());
}
}
exports.warnIfLegacyFileType = warnIfLegacyFileType;
// Validation message utilities
function buildMissingFileIdError() {
return 'Missing required file id for file upload completion';
}
exports.buildMissingFileIdError = buildMissingFileIdError;
function buildFileSizeErrorMsg() {
return 'There was an issue calculating the size of your file';
}
exports.buildFileSizeErrorMsg = buildFileSizeErrorMsg;
function buildLegacyFileTypeWarning() {
return 'filetype is no longer a supported field in files.uploadV2.' +
' \nPlease remove this field. To indicate file type, please do so via the required filename property' +
' using the appropriate file extension, e.g. image.png, text.txt';
}
exports.buildLegacyFileTypeWarning = buildLegacyFileTypeWarning;
function buildMissingFileNameWarning() {
return 'filename is a required field for files.uploadV2. \n For backwards compatibility and ease of migration, ' +
'defaulting the filename. For best experience and consistent unfurl behavior, you' +
' should set the filename property with correct file extension, e.g. image.png, text.txt';
}
exports.buildMissingFileNameWarning = buildMissingFileNameWarning;
function buildMissingExtensionWarning(filename) {
return `filename supplied '${filename}' may be missing a proper extension. Missing extenions may result in unexpected unfurl behavior when shared`;
}
exports.buildMissingExtensionWarning = buildMissingExtensionWarning;
function buildLegacyMethodWarning(method) {
return `${method} may cause some issues like timeouts for relatively large files.`;
}
exports.buildLegacyMethodWarning = buildLegacyMethodWarning;
function buildGeneralFilesUploadWarning() {
return 'Our latest recommendation is to use client.files.uploadV2() method, ' +
'which is mostly compatible and much stabler, instead.';
}
exports.buildGeneralFilesUploadWarning = buildGeneralFilesUploadWarning;
function buildFilesUploadMissingMessage() {
return 'Something went wrong with processing file_uploads';
}
exports.buildFilesUploadMissingMessage = buildFilesUploadMissingMessage;
function buildChannelsWarning() {
return 'Although the \'channels\' parameter is still supported for smoother migration from legacy files.upload, ' +
'we recommend using the new channel_id parameter with a single str value instead (e.g. \'C12345\').';
}
exports.buildChannelsWarning = buildChannelsWarning;
function buildMultipleChannelsErrorMsg() {
return 'Sharing files with multiple channels is no longer supported in v2. Share files in each channel separately instead.';
}
exports.buildMultipleChannelsErrorMsg = buildMultipleChannelsErrorMsg;
function buildInvalidFilesUploadParamError() {
return 'You may supply file_uploads only for a single channel, comment, thread respectively. ' +
'Therefore, please supply any channel_id, initial_comment, thread_ts in the top-layer.';
}
exports.buildInvalidFilesUploadParamError = buildInvalidFilesUploadParamError;
//# sourceMappingURL=file-upload.js.map