UNPKG

@slack/web-api

Version:

Official library for using the Slack Platform's Web API

750 lines 38.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var g = generator.apply(thisArg, _arguments || []), i, q = []; return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i; function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; } function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } function fulfill(value) { resume("next", value); } function reject(value) { resume("throw", value); } function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } }; var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildThreadTsWarningMessage = exports.WebClient = exports.WebClientEvent = void 0; const querystring_1 = require("querystring"); const path_1 = require("path"); const is_stream_1 = __importDefault(require("is-stream")); const p_queue_1 = __importDefault(require("p-queue")); const p_retry_1 = __importStar(require("p-retry")); const axios_1 = __importDefault(require("axios")); const form_data_1 = __importDefault(require("form-data")); const is_electron_1 = __importDefault(require("is-electron")); const zlib_1 = __importDefault(require("zlib")); const util_1 = require("util"); const methods_1 = require("./methods"); const instrument_1 = require("./instrument"); const errors_1 = require("./errors"); const logger_1 = require("./logger"); const retry_policies_1 = require("./retry-policies"); const helpers_1 = __importDefault(require("./helpers")); const file_upload_1 = require("./file-upload"); /* * Helpers */ const defaultFilename = 'Untitled'; const defaultPageSize = 200; const noopPageReducer = () => undefined; var WebClientEvent; (function (WebClientEvent) { // TODO: safe to rename this to conform to PascalCase enum type naming convention? // eslint-disable-next-line @typescript-eslint/naming-convention WebClientEvent["RATE_LIMITED"] = "rate_limited"; })(WebClientEvent = exports.WebClientEvent || (exports.WebClientEvent = {})); /** * A client for Slack's Web API * * This client provides an alias for each {@link https://api.slack.com/methods|Web API method}. Each method is * a convenience wrapper for calling the {@link WebClient#apiCall} method using the method name as the first parameter. */ class WebClient extends methods_1.Methods { /** * @param token - An API token to authenticate/authorize with Slack (usually start with `xoxp`, `xoxb`) */ constructor(token, { slackApiUrl = 'https://slack.com/api/', logger = undefined, logLevel = undefined, maxRequestConcurrency = 100, retryConfig = retry_policies_1.tenRetriesInAboutThirtyMinutes, agent = undefined, tls = undefined, timeout = 0, rejectRateLimitedCalls = false, headers = {}, teamId = undefined, } = {}) { super(); this.token = token; this.slackApiUrl = slackApiUrl; this.retryConfig = retryConfig; this.requestQueue = new p_queue_1.default({ concurrency: maxRequestConcurrency }); // NOTE: may want to filter the keys to only those acceptable for TLS options this.tlsConfig = tls !== undefined ? tls : {}; this.rejectRateLimitedCalls = rejectRateLimitedCalls; this.teamId = teamId; // Logging if (typeof logger !== 'undefined') { this.logger = logger; if (typeof logLevel !== 'undefined') { this.logger.debug('The logLevel given to WebClient was ignored as you also gave logger'); } } else { this.logger = (0, logger_1.getLogger)(WebClient.loggerName, logLevel !== null && logLevel !== void 0 ? logLevel : logger_1.LogLevel.INFO, logger); } // eslint-disable-next-line no-param-reassign if (this.token && !headers.Authorization) headers.Authorization = `Bearer ${this.token}`; this.axios = axios_1.default.create({ timeout, baseURL: slackApiUrl, headers: (0, is_electron_1.default)() ? headers : Object.assign({ 'User-Agent': (0, instrument_1.getUserAgent)() }, headers), httpAgent: agent, httpsAgent: agent, transformRequest: [this.serializeApiCallOptions.bind(this)], validateStatus: () => true, maxRedirects: 0, // disabling axios' automatic proxy support: // axios would read from envvars to configure a proxy automatically, but it doesn't support TLS destinations. // for compatibility with https://api.slack.com, and for a larger set of possible proxies (SOCKS or other // protocols), users of this package should use the `agent` option to configure a proxy. proxy: false, }); // serializeApiCallOptions will always determine the appropriate content-type delete this.axios.defaults.headers.post['Content-Type']; this.logger.debug('initialized'); } /** * Generic method for calling a Web API method * * @param method - the Web API method to call {@link https://api.slack.com/methods} * @param options - options */ async apiCall(method, options = {}) { this.logger.debug(`apiCall('${method}') start`); warnDeprecations(method, this.logger); warnIfFallbackIsMissing(method, this.logger, options); warnIfThreadTsIsNotString(method, this.logger, options); if (typeof options === 'string' || typeof options === 'number' || typeof options === 'boolean') { throw new TypeError(`Expected an options argument but instead received a ${typeof options}`); } (0, file_upload_1.warnIfNotUsingFilesUploadV2)(method, this.logger); if (method === 'files.uploadV2') return this.filesUploadV2(options); const headers = {}; if (options.token) headers.Authorization = `Bearer ${options.token}`; const response = await this.makeRequest(method, Object.assign({ team_id: this.teamId }, options), headers); const result = await this.buildResult(response); this.logger.debug(`http request result: ${JSON.stringify(result)}`); // log warnings in response metadata if (result.response_metadata !== undefined && result.response_metadata.warnings !== undefined) { result.response_metadata.warnings.forEach(this.logger.warn.bind(this.logger)); } // log warnings and errors in response metadata messages // related to https://api.slack.com/changelog/2016-09-28-response-metadata-is-on-the-way if (result.response_metadata !== undefined && result.response_metadata.messages !== undefined) { result.response_metadata.messages.forEach((msg) => { const errReg = /\[ERROR\](.*)/; const warnReg = /\[WARN\](.*)/; if (errReg.test(msg)) { const errMatch = msg.match(errReg); if (errMatch != null) { this.logger.error(errMatch[1].trim()); } } else if (warnReg.test(msg)) { const warnMatch = msg.match(warnReg); if (warnMatch != null) { this.logger.warn(warnMatch[1].trim()); } } }); } // If result's content is gzip, "ok" property is not returned with successful response // TODO: look into simplifying this code block to only check for the second condition // if an { ok: false } body applies for all API errors if (!result.ok && (response.headers['content-type'] !== 'application/gzip')) { throw (0, errors_1.platformErrorFromResult)(result); } else if ('ok' in result && result.ok === false) { throw (0, errors_1.platformErrorFromResult)(result); } this.logger.debug(`apiCall('${method}') end`); return result; } paginate(method, options, shouldStop, reduce) { if (!methods_1.cursorPaginationEnabledMethods.has(method)) { this.logger.warn(`paginate() called with method ${method}, which is not known to be cursor pagination enabled.`); } const pageSize = (() => { if (options !== undefined && typeof options.limit === 'number') { const { limit } = options; // eslint-disable-next-line no-param-reassign delete options.limit; return limit; } return defaultPageSize; })(); function generatePages() { return __asyncGenerator(this, arguments, function* generatePages_1() { // when result is undefined, that signals that the first of potentially many calls has not yet been made let result; // paginationOptions stores pagination options not already stored in the options argument let paginationOptions = { limit: pageSize, }; if (options !== undefined && options.cursor !== undefined) { paginationOptions.cursor = options.cursor; } // NOTE: test for the situation where you're resuming a pagination using and existing cursor while (result === undefined || paginationOptions !== undefined) { // eslint-disable-next-line no-await-in-loop result = yield __await(this.apiCall(method, Object.assign(options !== undefined ? options : {}, paginationOptions))); yield yield __await(result); paginationOptions = paginationOptionsForNextPage(result, pageSize); } }); } if (shouldStop === undefined) { return generatePages.call(this); } const pageReducer = (reduce !== undefined) ? reduce : noopPageReducer; let index = 0; return (async () => { // Unroll the first iteration of the iterator // This is done primarily because in order to satisfy the type system, we need a variable that is typed as A // (shown as accumulator before), but before the first iteration all we have is a variable typed A | undefined. // Unrolling the first iteration allows us to deal with undefined as a special case. var _a, e_1, _b, _c; const pageIterator = generatePages.call(this); const firstIteratorResult = await pageIterator.next(undefined); // Assumption: there will always be at least one result in a paginated API request // if (firstIteratorResult.done) { return; } const firstPage = firstIteratorResult.value; let accumulator = pageReducer(undefined, firstPage, index); index += 1; if (shouldStop(firstPage)) { return accumulator; } try { // Continue iteration // eslint-disable-next-line no-restricted-syntax for (var _d = true, pageIterator_1 = __asyncValues(pageIterator), pageIterator_1_1; pageIterator_1_1 = await pageIterator_1.next(), _a = pageIterator_1_1.done, !_a;) { _c = pageIterator_1_1.value; _d = false; try { const page = _c; accumulator = pageReducer(accumulator, page, index); if (shouldStop(page)) { return accumulator; } index += 1; } finally { _d = true; } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (!_d && !_a && (_b = pageIterator_1.return)) await _b.call(pageIterator_1); } finally { if (e_1) throw e_1.error; } } return accumulator; })(); } /* eslint-disable no-trailing-spaces */ /** * This wrapper method provides an easy way to upload files using the following endpoints: * * **#1**: For each file submitted with this method, submit filenames * and file metadata to {@link https://api.slack.com/methods/files.getUploadURLExternal files.getUploadURLExternal} to request a URL to * which to send the file data to and an id for the file * * **#2**: for each returned file `upload_url`, upload corresponding file to * URLs returned from step 1 (e.g. https://files.slack.com/upload/v1/...\") * * **#3**: Complete uploads {@link https://api.slack.com/methods/files.completeUploadExternal files.completeUploadExternal} * * **#4**: Unless `request_file_info` set to false, call {@link https://api.slack.com/methods/files.info files.info} for * each file uploaded and returns that data. Requires that your app have `files:read` scope. * @param options */ async filesUploadV2(options) { var _a; this.logger.debug('files.uploadV2() start'); // 1 const fileUploads = await this.getAllFileUploads(options); const fileUploadsURLRes = await this.fetchAllUploadURLExternal(fileUploads); // set the upload_url and file_id returned from Slack fileUploadsURLRes.forEach((res, idx) => { fileUploads[idx].upload_url = res.upload_url; fileUploads[idx].file_id = res.file_id; }); // 2 await this.postFileUploadsToExternalURL(fileUploads, options); // 3 const completion = await this.completeFileUploads(fileUploads); // 4 let res = completion; if ((_a = options.request_file_info) !== null && _a !== void 0 ? _a : true) { res = await this.getFileInfo(fileUploads); } return { ok: true, files: res }; } /** * For each file submitted with this method, submits filenames * and file metadata to files.getUploadURLExternal to request a URL to * which to send the file data to and an id for the file * @param fileUploads */ async fetchAllUploadURLExternal(fileUploads) { return Promise.all(fileUploads.map((upload) => { /* eslint-disable @typescript-eslint/consistent-type-assertions */ const options = { filename: upload.filename, length: upload.length, alt_text: upload.alt_text, snippet_type: upload.snippet_type, }; return this.files.getUploadURLExternal(options); })); } /** * Complete uploads. * @param fileUploads * @returns */ async completeFileUploads(fileUploads) { const toComplete = Object.values((0, file_upload_1.getAllFileUploadsToComplete)(fileUploads)); return Promise.all(toComplete.map((job) => this.files.completeUploadExternal(job))); } /** * Call {@link https://api.slack.com/methods/files.info files.info} for * each file uploaded and returns relevant data. Requires that your app have `files:read` scope, to * turn off, set `request_file_info` set to false. * @param fileUploads * @returns */ async getFileInfo(fileUploads) { /* eslint-disable @typescript-eslint/no-non-null-assertion */ return Promise.all(fileUploads.map((job) => this.files.info({ file: job.file_id }))); } /** * for each returned file upload URL, upload corresponding file * @param fileUploads * @returns */ async postFileUploadsToExternalURL(fileUploads, options) { return Promise.all(fileUploads.map(async (upload) => { const { upload_url, file_id, filename, data } = upload; // either file or content will be defined const body = data; // try to post to external url if (upload_url) { const headers = {}; if (options.token) headers.Authorization = `Bearer ${options.token}`; const uploadRes = await this.makeRequest(upload_url, { body, }, headers); if (uploadRes.status !== 200) { return Promise.reject(Error(`Failed to upload file (id:${file_id}, filename: ${filename})`)); } const returnData = { ok: true, body: uploadRes.data }; return Promise.resolve(returnData); } return Promise.reject(Error(`No upload url found for file (id: ${file_id}, filename: ${filename}`)); })); } /** * @param options All file uploads arguments * @returns An array of file upload entries */ async getAllFileUploads(options) { let fileUploads = []; // add single file data to uploads if file or content exists at the top level if (options.file || options.content) { fileUploads.push(await (0, file_upload_1.getFileUploadJob)(options, this.logger)); } // add multiple files data when file_uploads is supplied if (options.file_uploads) { fileUploads = fileUploads.concat(await (0, file_upload_1.getMultipleFileUploadJobs)(options, this.logger)); } return fileUploads; } /** * Low-level function to make a single API request. handles queuing, retries, and http-level errors */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async makeRequest(url, body, headers = {}) { // TODO: better input types - remove any const task = () => this.requestQueue.add(async () => { const requestURL = (url.startsWith('https' || 'http')) ? url : `${this.axios.getUri() + url}`; this.logger.debug(`http request url: ${requestURL}`); this.logger.debug(`http request body: ${JSON.stringify(redact(body))}`); this.logger.debug(`http request headers: ${JSON.stringify(redact(headers))}`); try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const config = Object.assign({ headers }, this.tlsConfig); // admin.analytics.getFile returns a binary response // To be able to parse it, it should be read as an ArrayBuffer if (url.endsWith('admin.analytics.getFile')) { config.responseType = 'arraybuffer'; } const response = await this.axios.post(url, body, config); this.logger.debug('http response received'); if (response.status === 429) { const retrySec = parseRetryHeaders(response); if (retrySec !== undefined) { this.emit(WebClientEvent.RATE_LIMITED, retrySec); if (this.rejectRateLimitedCalls) { throw new p_retry_1.AbortError((0, errors_1.rateLimitedErrorWithDelay)(retrySec)); } this.logger.info(`API Call failed due to rate limiting. Will retry in ${retrySec} seconds.`); // pause the request queue and then delay the rejection by the amount of time in the retry header this.requestQueue.pause(); // NOTE: if there was a way to introspect the current RetryOperation and know what the next timeout // would be, then we could subtract that time from the following delay, knowing that it the next // attempt still wouldn't occur until after the rate-limit header has specified. an even better // solution would be to subtract the time from only the timeout of this next attempt of the // RetryOperation. this would result in the staying paused for the entire duration specified in the // header, yet this operation not having to pay the timeout cost in addition to that. await (0, helpers_1.default)(retrySec * 1000); // resume the request queue and throw a non-abort error to signal a retry this.requestQueue.start(); // TODO: We may want to have more detailed info such as team_id, params except tokens, and so on. throw Error(`A rate limit was exceeded (url: ${url}, retry-after: ${retrySec})`); } else { // TODO: turn this into some CodedError throw new p_retry_1.AbortError(new Error(`Retry header did not contain a valid timeout (url: ${url}, retry-after header: ${response.headers['retry-after']})`)); } } // Slack's Web API doesn't use meaningful status codes besides 429 and 200 if (response.status !== 200) { throw (0, errors_1.httpErrorFromResponse)(response); } return response; } catch (error) { // To make this compatible with tsd, casting here instead of `catch (error: any)` // eslint-disable-next-line @typescript-eslint/no-explicit-any const e = error; this.logger.warn('http request failed', e.message); if (e.request) { throw (0, errors_1.requestErrorWithOriginal)(e); } throw error; } }); return (0, p_retry_1.default)(task, this.retryConfig); } /** * Transforms options (a simple key-value object) into an acceptable value for a body. This can be either * a string, used when posting with a content-type of url-encoded. Or, it can be a readable stream, used * when the options contain a binary (a stream or a buffer) and the upload should be done with content-type * multipart/form-data. * * @param options - arguments for the Web API method * @param headers - a mutable object representing the HTTP headers for the outgoing request */ // eslint-disable-next-line @typescript-eslint/no-explicit-any serializeApiCallOptions(options, headers) { // The following operation both flattens complex objects into a JSON-encoded strings and searches the values for // binary content let containsBinaryData = false; // eslint-disable-next-line @typescript-eslint/no-explicit-any const flattened = Object.entries(options).map(([key, value]) => { if (value === undefined || value === null) { return []; } let serializedValue = value; if (Buffer.isBuffer(value) || (0, is_stream_1.default)(value)) { containsBinaryData = true; } else if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') { // if value is anything other than string, number, boolean, binary data, a Stream, or a Buffer, then encode it // as a JSON string. serializedValue = JSON.stringify(value); } return [key, serializedValue]; }); // A body with binary content should be serialized as multipart/form-data if (containsBinaryData) { this.logger.debug('Request arguments contain binary data'); const form = flattened.reduce((frm, [key, value]) => { if (Buffer.isBuffer(value) || (0, is_stream_1.default)(value)) { const opts = {}; opts.filename = (() => { // attempt to find filename from `value`. adapted from: // https://github.com/form-data/form-data/blob/028c21e0f93c5fefa46a7bbf1ba753e4f627ab7a/lib/form_data.js#L227-L230 // formidable and the browser add a name property // fs- and request- streams have path property // eslint-disable-next-line @typescript-eslint/no-explicit-any const streamOrBuffer = value; if (typeof streamOrBuffer.name === 'string') { return (0, path_1.basename)(streamOrBuffer.name); } if (typeof streamOrBuffer.path === 'string') { return (0, path_1.basename)(streamOrBuffer.path); } return defaultFilename; })(); frm.append(key, value, opts); } else if (key !== undefined && value !== undefined) { frm.append(key, value); } return frm; }, new form_data_1.default()); // Copying FormData-generated headers into headers param // not reassigning to headers param since it is passed by reference and behaves as an inout param Object.entries(form.getHeaders()).forEach(([header, value]) => { // eslint-disable-next-line no-param-reassign headers[header] = value; }); return form; } // Otherwise, a simple key-value object is returned // eslint-disable-next-line no-param-reassign headers['Content-Type'] = 'application/x-www-form-urlencoded'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const initialValue = {}; return (0, querystring_1.stringify)(flattened.reduce((accumulator, [key, value]) => { if (key !== undefined && value !== undefined) { accumulator[key] = value; } return accumulator; }, initialValue)); } /** * Processes an HTTP response into a WebAPICallResult by performing JSON parsing on the body and merging relevant * HTTP headers into the object. * @param response - an http response */ // eslint-disable-next-line class-methods-use-this async buildResult(response) { let { data } = response; const isGzipResponse = response.headers['content-type'] === 'application/gzip'; // Check for GZIP response - if so, it is a successful response from admin.analytics.getFile if (isGzipResponse) { // admin.analytics.getFile will return a Buffer that can be unzipped try { const unzippedData = await new Promise((resolve, reject) => { zlib_1.default.unzip(data, (err, buf) => { if (err) { return reject(err); } return resolve(buf.toString().split('\n')); }); }).then((res) => res) .catch((err) => { throw err; }); const fileData = []; if (Array.isArray(unzippedData)) { unzippedData.forEach((dataset) => { if (dataset && dataset.length > 0) { fileData.push(JSON.parse(dataset)); } }); } data = { file_data: fileData }; } catch (err) { data = { ok: false, error: err }; } } else if (!isGzipResponse && response.request.path === '/api/admin.analytics.getFile') { // if it isn't a Gzip response but is from the admin.analytics.getFile request, // decode the ArrayBuffer to JSON read the error data = JSON.parse(new util_1.TextDecoder().decode(data)); } if (typeof data === 'string') { // response.data can be a string, not an object for some reason try { data = JSON.parse(data); } catch (_) { // failed to parse the string value as JSON data data = { ok: false, error: data }; } } if (data.response_metadata === undefined) { data.response_metadata = {}; } // add scopes metadata from headers if (response.headers['x-oauth-scopes'] !== undefined) { data.response_metadata.scopes = response.headers['x-oauth-scopes'].trim().split(/\s*,\s*/); } if (response.headers['x-accepted-oauth-scopes'] !== undefined) { data.response_metadata.acceptedScopes = response.headers['x-accepted-oauth-scopes'].trim().split(/\s*,\s*/); } // add retry metadata from headers const retrySec = parseRetryHeaders(response); if (retrySec !== undefined) { data.response_metadata.retryAfter = retrySec; } return data; } } exports.WebClient = WebClient; /** * The name used to prefix all logging generated from this object */ WebClient.loggerName = 'WebClient'; exports.default = WebClient; /** * Determines an appropriate set of cursor pagination options for the next request to a paginated API method. * @param previousResult - the result of the last request, where the next cursor might be found. * @param pageSize - the maximum number of additional items to fetch in the next request. */ function paginationOptionsForNextPage(previousResult, pageSize) { if (previousResult !== undefined && previousResult.response_metadata !== undefined && previousResult.response_metadata.next_cursor !== undefined && previousResult.response_metadata.next_cursor !== '') { return { limit: pageSize, cursor: previousResult.response_metadata.next_cursor, }; } return undefined; } /** * Extract the amount of time (in seconds) the platform has recommended this client wait before sending another request * from a rate-limited HTTP response (statusCode = 429). */ function parseRetryHeaders(response) { if (response.headers['retry-after'] !== undefined) { const retryAfter = parseInt(response.headers['retry-after'], 10); if (!Number.isNaN(retryAfter)) { return retryAfter; } } return undefined; } /** * Log a warning when using a deprecated method * @param method api method being called * @param logger instance of web clients logger */ function warnDeprecations(method, logger) { const deprecatedConversationsMethods = ['channels.', 'groups.', 'im.', 'mpim.']; const deprecatedMethods = ['admin.conversations.whitelist.']; const isDeprecatedConversations = deprecatedConversationsMethods.some((depMethod) => { const re = new RegExp(`^${depMethod}`); return re.test(method); }); const isDeprecated = deprecatedMethods.some((depMethod) => { const re = new RegExp(`^${depMethod}`); return re.test(method); }); if (isDeprecatedConversations) { logger.warn(`${method} is deprecated. Please use the Conversations API instead. For more info, go to https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api`); } else if (isDeprecated) { logger.warn(`${method} is deprecated. Please check on https://api.slack.com/methods for an alternative.`); } } /** * Log a warning when using chat.postMessage without text argument or attachments with fallback argument * @param method api method being called * @param logger instance of we clients logger * @param options arguments for the Web API method */ function warnIfFallbackIsMissing(method, logger, options) { const targetMethods = ['chat.postEphemeral', 'chat.postMessage', 'chat.scheduleMessage', 'chat.update']; const isTargetMethod = targetMethods.includes(method); const hasAttachments = (args) => Array.isArray(args.attachments) && args.attachments.length; const missingAttachmentFallbackDetected = (args) => Array.isArray(args.attachments) && args.attachments.some((attachment) => !attachment.fallback || attachment.fallback.trim() === ''); const isEmptyText = (args) => args.text === undefined || args.text === null || args.text === ''; const buildMissingTextWarning = () => `The top-level \`text\` argument is missing in the request payload for a ${method} call - ` + 'It\'s a best practice to always provide a `text` argument when posting a message. ' + 'The `text` is used in places where the content cannot be rendered such as: ' + 'system push notifications, assistive technology such as screen readers, etc.'; const buildMissingFallbackWarning = () => `Additionally, the attachment-level \`fallback\` argument is missing in the request payload for a ${method} call - ` + 'To avoid this warning, it is recommended to always provide a top-level `text` argument when posting a message. ' + 'Alternatively, you can provide an attachment-level `fallback` argument, though this is now considered a legacy field (see https://api.slack.com/reference/messaging/attachments#legacy_fields for more details).'; if (isTargetMethod && typeof options === 'object') { if (hasAttachments(options)) { if (missingAttachmentFallbackDetected(options) && isEmptyText(options)) { logger.warn(buildMissingTextWarning()); logger.warn(buildMissingFallbackWarning()); } } else if (isEmptyText(options)) { logger.warn(buildMissingTextWarning()); } } } /** * Log a warning when thread_ts is not a string * @param method api method being called * @param logger instance of web clients logger * @param options arguments for the Web API method */ function warnIfThreadTsIsNotString(method, logger, options) { const targetMethods = ['chat.postEphemeral', 'chat.postMessage', 'chat.scheduleMessage', 'files.upload']; const isTargetMethod = targetMethods.includes(method); if (isTargetMethod && (options === null || options === void 0 ? void 0 : options.thread_ts) !== undefined && typeof (options === null || options === void 0 ? void 0 : options.thread_ts) !== 'string') { logger.warn(buildThreadTsWarningMessage(method)); } } function buildThreadTsWarningMessage(method) { return `The given thread_ts value in the request payload for a ${method} call is a float value. We highly recommend using a string value instead.`; } exports.buildThreadTsWarningMessage = buildThreadTsWarningMessage; /** * Takes an object and redacts specific items * @param body * @returns */ function redact(body) { const flattened = Object.entries(body).map(([key, value]) => { // no value provided if (value === undefined || value === null) { return []; } let serializedValue = value; // redact possible tokens if (key.match(/.*token.*/) !== null || key.match(/[Aa]uthorization/)) { serializedValue = '[[REDACTED]]'; } // when value is buffer or stream we can avoid logging it if (Buffer.isBuffer(value) || (0, is_stream_1.default)(value)) { serializedValue = '[[BINARY VALUE OMITTED]]'; } else if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') { serializedValue = JSON.stringify(value); } return [key, serializedValue]; }); // return as object const initialValue = {}; return flattened.reduce((accumulator, [key, value]) => { if (key !== undefined && value !== undefined) { accumulator[key] = value; } return accumulator; }, initialValue); } //# sourceMappingURL=WebClient.js.map