UNPKG

@slack/web-api

Version:

Official library for using the Slack Platform's Web API

375 lines 17.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getFileUploadJob = getFileUploadJob; exports.getMultipleFileUploadJobs = getMultipleFileUploadJobs; exports.getFileData = getFileData; exports.getFileDataLength = getFileDataLength; exports.getFileDataAsStream = getFileDataAsStream; exports.getAllFileUploadsToComplete = getAllFileUploadsToComplete; exports.warnIfNotUsingFilesUploadV2 = warnIfNotUsingFilesUploadV2; exports.warnIfChannels = warnIfChannels; exports.errorIfChannelsCsv = errorIfChannelsCsv; exports.errorIfInvalidOrMissingFileData = errorIfInvalidOrMissingFileData; exports.warnIfMissingOrInvalidFileNameAndDefault = warnIfMissingOrInvalidFileNameAndDefault; exports.warnIfLegacyFileType = warnIfLegacyFileType; exports.buildMissingFileIdError = buildMissingFileIdError; exports.buildFileSizeErrorMsg = buildFileSizeErrorMsg; exports.buildLegacyFileTypeWarning = buildLegacyFileTypeWarning; exports.buildMissingFileNameWarning = buildMissingFileNameWarning; exports.buildMissingExtensionWarning = buildMissingExtensionWarning; exports.buildLegacyMethodWarning = buildLegacyMethodWarning; exports.buildGeneralFilesUploadWarning = buildGeneralFilesUploadWarning; exports.buildFilesUploadMissingMessage = buildFilesUploadMissingMessage; exports.buildChannelsWarning = buildChannelsWarning; exports.buildMultipleChannelsErrorMsg = buildMultipleChannelsErrorMsg; exports.buildInvalidFilesUploadParamError = buildInvalidFilesUploadParamError; const node_fs_1 = require("node:fs"); const node_stream_1 = require("node:stream"); const errors_1 = require("./errors"); function getFileUploadJob(options, logger) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d; // Validate parameters warnIfLegacyFileType(options, logger); warnIfChannels(options, logger); errorIfChannelsCsv(options); const fileName = warnIfMissingOrInvalidFileNameAndDefault(options, logger); const fileData = yield getFileData(options); const fileDataBytesLength = getFileDataLength(fileData); const fileUploadJob = { // supplied by user alt_text: options.alt_text, blocks: options.blocks, channel_id: (_a = options.channels) !== null && _a !== void 0 ? _a : options.channel_id, filename: (_b = options.filename) !== null && _b !== void 0 ? _b : fileName, initial_comment: options.initial_comment, snippet_type: options.snippet_type, title: (_d = (_c = options.title) !== null && _c !== void 0 ? _c : options.filename) !== null && _d !== void 0 ? _d : fileName, // default title to filename unless otherwise specified // calculated data: fileData, length: fileDataBytesLength, }; if ('thread_ts' in options) { fileUploadJob.thread_ts = options.thread_ts; } if ('token' in options) { fileUploadJob.token = options.token; } if ('content' in options) { return Object.assign({ content: options.content }, fileUploadJob); } if ('file' in options) { return Object.assign({ file: options.file }, fileUploadJob); } throw (0, errors_1.errorWithCode)(new Error('Either a file or content field is required for valid file upload. You must supply one'), errors_1.ErrorCode.FileUploadInvalidArgumentsError); }); } /** * 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 */ function getMultipleFileUploadJobs(options, logger) { return __awaiter(this, void 0, void 0, function* () { if ('file_uploads' in options) { // 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 { blocks, channel_id, channels, initial_comment, thread_ts } = upload; if (blocks || 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. const uploadJobArgs = Object.assign(Object.assign({}, upload), { blocks: options.blocks, channels: options.channels, channel_id: options.channel_id, initial_comment: options.initial_comment }); if ('thread_ts' in options) { uploadJobArgs.thread_ts = options.thread_ts; } if ('token' in options) { uploadJobArgs.token = options.token; } if ('content' in upload) { return getFileUploadJob(Object.assign({ content: upload.content }, uploadJobArgs), logger); } if ('file' in upload) { return getFileUploadJob(Object.assign({ file: upload.file }, uploadJobArgs), logger); } throw (0, errors_1.errorWithCode)(new Error('Either a file or content field is required for valid file upload. You must supply one'), errors_1.ErrorCode.FileUploadInvalidArgumentsError); })); } throw new Error(buildFilesUploadMissingMessage()); }); } // Helpers to build the FileUploadJob /** * Returns a single file upload's data * @param options * @returns Binary data representation of file */ function getFileData(options) { return __awaiter(this, void 0, void 0, function* () { errorIfInvalidOrMissingFileData(options); if ('file' in options) { const { file } = options; // 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, node_fs_1.readFileSync)(file); return dataBuffer; } catch (_err) { 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 = yield getFileDataAsStream(file); if (data) return data; } if ('content' in options) return Buffer.from(options.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); }); } function getFileDataLength(data) { if (data) { return Buffer.byteLength(data, 'utf8'); } throw (0, errors_1.errorWithCode)(new Error(buildFileSizeErrorMsg()), errors_1.ErrorCode.FileUploadReadFileDataError); } function getFileDataAsStream(readable) { return __awaiter(this, void 0, void 0, function* () { const chunks = []; return new Promise((resolve, reject) => { readable.on('readable', () => { let chunk = readable.read(); while (chunk !== null) { chunks.push(chunk); chunk = readable.read(); } }); readable.on('end', () => { if (chunks.length > 0) { const content = Buffer.concat(chunks); resolve(content); } else { reject(Error('No data in supplied file')); } }); }); }); } /** * Filters through all fileUploads and groups them into jobs for completion * based on combination of channel_id, thread_ts, initial_comment, blocks. * {@link https://docs.slack.dev/reference/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 = {}; for (const upload of fileUploads) { const { blocks, channel_id, thread_ts, initial_comment, file_id, title } = upload; if (file_id) { const compareString = `:::${channel_id}:::${thread_ts}:::${initial_comment}:::${JSON.stringify(blocks)}`; // biome-ignore lint/suspicious/noPrototypeBuiltins: TODO use hasOwn instead of hasOwnProperty if (!Object.prototype.hasOwnProperty.call(toComplete, compareString)) { toComplete[compareString] = { files: [{ id: file_id, title }], channel_id, blocks, initial_comment, }; if (thread_ts && channel_id) { const fileThreadDestinationArgument = { channel_id, thread_ts, }; toComplete[compareString] = Object.assign(Object.assign({}, toComplete[compareString]), fileThreadDestinationArgument); } if ('token' in upload) { toComplete[compareString].token = upload.token; } } else { toComplete[compareString].files.push({ id: file_id, title, }); } } else { throw new Error(buildMissingFileIdError()); } } return toComplete; } // 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()); } /** * `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()); } /** * 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); } } /** * Checks for either a file or content property and errors if missing * @param options */ function errorIfInvalidOrMissingFileData(options) { const hasFile = 'file' in options; const hasContent = 'content' in options; if (!(hasFile || hasContent) || (hasFile && hasContent)) { 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); } if ('file' in options) { const { file } = options; if (file && !(typeof file === 'string' || Buffer.isBuffer(file) || file instanceof node_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' in options && options.content && typeof options.content !== 'string') { throw (0, errors_1.errorWithCode)(new Error('content must be a string'), errors_1.ErrorCode.FileUploadInvalidArgumentsError); } } /** * @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; } /** * `filetype` param is no longer supported and will be ignored * @param options * @param logger */ function warnIfLegacyFileType(options, logger) { if (options.filetype) { logger.warn(buildLegacyFileTypeWarning()); } } // Validation message utilities function buildMissingFileIdError() { return 'Missing required file id for file upload completion'; } function buildFileSizeErrorMsg() { return 'There was an issue calculating the size of your file'; } 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'); } 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'); } function buildMissingExtensionWarning(filename) { return `filename supplied '${filename}' may be missing a proper extension. Missing extenions may result in unexpected unfurl behavior when shared`; } function buildLegacyMethodWarning(method) { return `${method} may cause some issues like timeouts for relatively large files.`; } function buildGeneralFilesUploadWarning() { return ('Our latest recommendation is to use client.files.uploadV2() method, ' + 'which is mostly compatible and much stabler, instead.'); } function buildFilesUploadMissingMessage() { return 'Something went wrong with processing file_uploads'; } 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')."); } function buildMultipleChannelsErrorMsg() { return 'Sharing files with multiple channels is no longer supported in v2. Share files in each channel separately instead.'; } function buildInvalidFilesUploadParamError() { return ('You may supply file_uploads only for a single channel, message, or thread respectively. ' + 'Therefore, please supply any channel_id, initial_comment or blocks, or thread_ts in the top-layer.'); } //# sourceMappingURL=file-upload.js.map