@slack/bolt
Version:
A framework for building Slack apps, fast.
1,013 lines • 50 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LogLevel = void 0;
const node_util_1 = __importDefault(require("node:util"));
const logger_1 = require("@slack/logger");
const web_api_1 = require("@slack/web-api");
const axios_1 = __importDefault(require("axios"));
const CustomFunction_1 = require("./CustomFunction");
const conversation_store_1 = require("./conversation-store");
const errors_1 = require("./errors");
const helpers_1 = require("./helpers");
const builtin_1 = require("./middleware/builtin");
const process_1 = __importDefault(require("./middleware/process"));
const HTTPReceiver_1 = __importDefault(require("./receivers/HTTPReceiver"));
const SocketModeReceiver_1 = __importDefault(require("./receivers/SocketModeReceiver"));
const types_1 = require("./types");
const utilities_1 = require("./types/utilities");
const packageJson = require('../package.json');
// ----------------------------
// For listener registration methods
// TODO: we have types for this... consolidate
const validViewTypes = ['view_closed', 'view_submission'];
// ----------------------------
// For the constructor
const tokenUsage = 'Apps used in a single workspace can be initialized with a token. Apps used in many workspaces ' +
'should be initialized with oauth installer options or authorize.';
var logger_2 = require("@slack/logger");
Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return logger_2.LogLevel; } });
class WebClientPool {
pool = {};
getOrCreate(token, clientOptions) {
const cachedClient = this.pool[token];
if (typeof cachedClient !== 'undefined') {
return cachedClient;
}
const client = new web_api_1.WebClient(token, clientOptions);
this.pool[token] = client;
return client;
}
}
/**
* A Slack App
*/
class App {
/** Slack Web API client */
client;
clientOptions;
// Some payloads don't have teamId anymore. So we use EnterpriseId in those scenarios
clients = {};
/** Receiver - ingests events from the Slack platform */
receiver;
/** Logger */
logger;
/** Log Level */
logLevel;
/** Authorize */
authorize;
/** Global middleware chain */
middleware;
/** Listener middleware chains */
listeners;
errorHandler;
axios;
installerOptions;
socketMode;
developerMode;
extendedErrorHandler;
hasCustomErrorHandler;
// used when deferInitialization is true
argToken;
// used when deferInitialization is true
argAuthorize;
// used when deferInitialization is true
argAuthorization;
tokenVerificationEnabled;
initialized;
attachFunctionToken;
constructor({ signingSecret = undefined, endpoints = undefined, port = undefined, customRoutes = undefined, agent = undefined, clientTls = undefined, receiver = undefined, convoStore = undefined, token = undefined, appToken = undefined, botId = undefined, botUserId = undefined, authorize = undefined, logger = undefined, logLevel = undefined, ignoreSelf = true, clientOptions = undefined, processBeforeResponse = false, signatureVerification = true, clientId = undefined, clientSecret = undefined, stateSecret = undefined, redirectUri = undefined, installationStore = undefined, scopes = undefined, installerOptions = undefined, socketMode = undefined, developerMode = false, tokenVerificationEnabled = true, extendedErrorHandler = false, deferInitialization = false, attachFunctionToken = true, } = {}) {
/* ------------------------ Developer mode ----------------------------- */
this.developerMode = developerMode;
if (developerMode) {
// Set logLevel to Debug in Developer Mode if one wasn't passed in
this.logLevel = logLevel ?? logger_1.LogLevel.DEBUG;
// Set SocketMode to true if one wasn't passed in
this.socketMode = socketMode ?? true;
}
else {
// If devs aren't using Developer Mode or Socket Mode, set it to false
this.socketMode = socketMode ?? false;
// Set logLevel to Info if one wasn't passed in
this.logLevel = logLevel ?? logger_1.LogLevel.INFO;
}
/* ------------------------ Set logger ----------------------------- */
if (typeof logger === 'undefined') {
// Initialize with the default logger
const consoleLogger = new logger_1.ConsoleLogger();
consoleLogger.setName('bolt-app');
this.logger = consoleLogger;
}
else {
this.logger = logger;
}
if (typeof this.logLevel !== 'undefined' && this.logger.getLevel() !== this.logLevel) {
this.logger.setLevel(this.logLevel);
}
// Error-related properties used to later determine args passed into the error handler
this.hasCustomErrorHandler = false;
this.errorHandler = defaultErrorHandler(this.logger);
this.extendedErrorHandler = extendedErrorHandler;
// Override token with functionBotAccessToken in function-related handlers
this.attachFunctionToken = attachFunctionToken;
/* ------------------------ Set client options ------------------------*/
this.clientOptions = clientOptions !== undefined ? clientOptions : {};
if (agent !== undefined && this.clientOptions.agent === undefined) {
this.clientOptions.agent = agent;
}
if (clientTls !== undefined && this.clientOptions.tls === undefined) {
this.clientOptions.tls = clientTls;
}
if (logLevel !== undefined && logger === undefined) {
// only logLevel is passed
this.clientOptions.logLevel = logLevel;
}
else {
// Since v3.4, WebClient starts sharing logger with App
this.clientOptions.logger = this.logger;
}
// The public WebClient instance (app.client)
// Since v3.4, it can have the passed token in the case of single workspace installation.
this.client = new web_api_1.WebClient(token, this.clientOptions);
this.axios = axios_1.default.create({
httpAgent: agent,
httpsAgent: agent,
// disabling axios' automatic proxy support:
// axios would read from env vars 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,
...clientTls,
});
this.middleware = [];
this.listeners = [];
// Add clientOptions to InstallerOptions to pass them to @slack/oauth
this.installerOptions = {
clientOptions: this.clientOptions,
...installerOptions,
};
if (socketMode && port !== undefined && this.installerOptions.port === undefined) {
// SocketModeReceiver only uses a custom port number when listening for the OAuth flow.
// Therefore, only installerOptions.port is available in the constructor arguments.
this.installerOptions.port = port;
}
if (this.developerMode &&
this.installerOptions &&
(typeof this.installerOptions.callbackOptions === 'undefined' ||
(typeof this.installerOptions.callbackOptions !== 'undefined' &&
typeof this.installerOptions.callbackOptions.failure === 'undefined'))) {
// add a custom failure callback for Developer Mode in case they are using OAuth
this.logger.debug('adding Developer Mode custom OAuth failure handler');
this.installerOptions.callbackOptions = {
failure: (error, _installOptions, _req, res) => {
this.logger.debug(error);
res.writeHead(500, { 'Content-Type': 'text/html' });
res.end(`<html><body><h1>OAuth failed!</h1><div>${escapeHtml(error.code)}</div></body></html>`);
},
};
}
this.receiver = this.initReceiver(receiver, signingSecret, endpoints, port, customRoutes, processBeforeResponse, signatureVerification, clientId, clientSecret, stateSecret, redirectUri, installationStore, scopes, appToken, logger);
/* ------------------------ Set authorize ----------------------------- */
this.tokenVerificationEnabled = tokenVerificationEnabled;
let argAuthorization;
if (token !== undefined) {
argAuthorization = {
botId,
botUserId,
botToken: token,
};
}
if (deferInitialization) {
this.argToken = token;
this.argAuthorize = authorize;
this.argAuthorization = argAuthorization;
this.initialized = false;
// You need to run `await app.init();` on your own
}
else {
this.authorize = this.initAuthorizeInConstructor(token, authorize, argAuthorization);
this.initialized = true;
}
// Conditionally use a global middleware that ignores events (including messages) that are sent from this app
if (ignoreSelf) {
this.use(builtin_1.ignoreSelf);
}
// Use conversation state global middleware
if (convoStore !== false) {
// Use the memory store by default, or another store if provided
const store = convoStore === undefined ? new conversation_store_1.MemoryStore() : convoStore;
this.use((0, conversation_store_1.conversationContext)(store));
}
/* ------------------------ Initialize receiver ------------------------ */
// Should be last to avoid exposing partially initialized app
this.receiver.init(this);
}
async init() {
this.initialized = true;
try {
const initializedAuthorize = this.initAuthorizeIfNoTokenIsGiven(this.argToken, this.argAuthorize);
if (initializedAuthorize !== undefined) {
this.authorize = initializedAuthorize;
return;
}
if (this.argToken !== undefined && this.argAuthorization !== undefined) {
let authorization = this.argAuthorization;
if (this.tokenVerificationEnabled) {
const authTestResult = await this.client.auth.test({ token: this.argToken });
if (authTestResult.ok) {
authorization = {
botUserId: authTestResult.user_id,
botId: authTestResult.bot_id,
botToken: this.argToken,
};
}
}
this.authorize = singleAuthorization(this.client, authorization, this.tokenVerificationEnabled);
this.initialized = true;
}
else {
this.logger.error('Something has gone wrong. Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues');
(0, helpers_1.assertNever)();
}
}
catch (e) {
// Revert the flag change as the initialization failed
this.initialized = false;
throw e;
}
}
get webClientOptions() {
return this.clientOptions;
}
/**
* Register a new middleware, processed in the order registered.
*
* @param m global middleware function
*/
use(m) {
this.middleware.push(m);
return this;
}
/**
* Register Assistant middleware
*
* @param assistant global assistant middleware function
*/
assistant(assistant) {
const m = assistant.getMiddleware();
this.middleware.push(m);
return this;
}
/**
* Register WorkflowStep middleware
*
* @param workflowStep global workflow step middleware function
* @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js
* version.
*/
step(workflowStep) {
const m = workflowStep.getMiddleware();
this.middleware.push(m);
return this;
}
function(callbackId, ...optionOrListeners) {
const options = (0, builtin_1.isSlackEventMiddlewareArgsOptions)(optionOrListeners[0])
? optionOrListeners[0]
: { autoAcknowledge: true };
const listeners = optionOrListeners.filter((optionOrListener) => {
return !(0, builtin_1.isSlackEventMiddlewareArgsOptions)(optionOrListener);
});
const fn = new CustomFunction_1.CustomFunction(callbackId, listeners, options);
this.listeners.push(fn.getListeners());
return this;
}
/**
* Convenience method to call start on the receiver
*
* TODO: should replace HTTPReceiver in type definition with a generic that is constrained to Receiver
*
* @param args receiver-specific start arguments
*/
start(...args) {
if (!this.initialized) {
throw new errors_1.AppInitializationError('This App instance is not yet initialized. Call `await App#init()` before starting the app.');
}
// TODO: HTTPReceiver['start'] should be the actual receiver's return type
return this.receiver.start(...args);
}
// biome-ignore lint/suspicious/noExplicitAny: receivers could accept anything as arguments for stop
stop(...args) {
return this.receiver.stop(...args);
}
event(eventNameOrPattern, ...listeners) {
let invalidEventName = false;
if (typeof eventNameOrPattern === 'string') {
const name = eventNameOrPattern;
invalidEventName = name.startsWith('message.');
}
else if (eventNameOrPattern instanceof RegExp) {
const name = eventNameOrPattern.source;
invalidEventName = name.startsWith('message\\.');
}
if (invalidEventName) {
throw new errors_1.AppInitializationError(`Although the document mentions "${eventNameOrPattern}", it is not a valid event type. Use "message" instead. If you want to filter message events, you can use event.channel_type for it.`);
}
// biome-ignore lint/suspicious/noExplicitAny: FIXME: workaround for TypeScript 4.7 breaking changes
const _listeners = listeners;
this.listeners.push([
builtin_1.onlyEvents,
(0, builtin_1.matchEventType)(eventNameOrPattern),
..._listeners,
]);
}
// TODO: expose a type parameter for overriding the MessageEvent type (just like shortcut() and action() does) https://github.com/slackapi/bolt-js/issues/796
message(...patternsOrMiddleware) {
const messageMiddleware = patternsOrMiddleware.map((patternOrMiddleware) => {
if (typeof patternOrMiddleware === 'string' || node_util_1.default.types.isRegExp(patternOrMiddleware)) {
return (0, builtin_1.matchMessage)(patternOrMiddleware);
}
return patternOrMiddleware;
// biome-ignore lint/suspicious/noExplicitAny: FIXME: workaround for TypeScript 4.7 breaking changes
});
this.listeners.push([
builtin_1.onlyEvents,
(0, builtin_1.matchEventType)('message'),
...messageMiddleware,
]);
}
shortcut(callbackIdOrConstraints, ...listeners) {
const constraints = typeof callbackIdOrConstraints === 'string' || node_util_1.default.types.isRegExp(callbackIdOrConstraints)
? { callback_id: callbackIdOrConstraints }
: callbackIdOrConstraints;
// Fail early if the constraints contain invalid keys
const unknownConstraintKeys = Object.keys(constraints).filter((k) => k !== 'callback_id' && k !== 'type');
if (unknownConstraintKeys.length > 0) {
// TODO:event() will throw an error if you provide an invalid event name; we should align this behaviour.
this.logger.error(`Slack listener cannot be attached using unknown constraint keys: ${unknownConstraintKeys.join(', ')}`);
return;
}
// biome-ignore lint/suspicious/noExplicitAny: FIXME: workaround for TypeScript 4.7 breaking changes
const _listeners = listeners;
this.listeners.push([
builtin_1.onlyShortcuts,
(0, builtin_1.matchConstraints)(constraints),
..._listeners,
]);
}
action(actionIdOrConstraints, ...listeners) {
// Normalize Constraints
const constraints = typeof actionIdOrConstraints === 'string' || node_util_1.default.types.isRegExp(actionIdOrConstraints)
? { action_id: actionIdOrConstraints }
: actionIdOrConstraints;
// Fail early if the constraints contain invalid keys
const unknownConstraintKeys = Object.keys(constraints).filter((k) => k !== 'action_id' && k !== 'block_id' && k !== 'callback_id' && k !== 'type');
if (unknownConstraintKeys.length > 0) {
// TODO:event() will throw an error if you provide an invalid event name; we should align this behaviour.
this.logger.error(`Action listener cannot be attached using unknown constraint keys: ${unknownConstraintKeys.join(', ')}`);
return;
}
// biome-ignore lint/suspicious/noExplicitAny: FIXME: workaround for TypeScript 4.7 breaking changes
const _listeners = listeners;
this.listeners.push([builtin_1.onlyActions, (0, builtin_1.matchConstraints)(constraints), ..._listeners]);
}
command(commandName, ...listeners) {
// biome-ignore lint/suspicious/noExplicitAny: FIXME: workaround for TypeScript 4.7 breaking changes
const _listeners = listeners;
this.listeners.push([
builtin_1.onlyCommands,
(0, builtin_1.matchCommandName)(commandName),
..._listeners,
]);
}
// TODO: reflect the type in constraints to Source
options(actionIdOrConstraints, ...listeners) {
const constraints = typeof actionIdOrConstraints === 'string' || node_util_1.default.types.isRegExp(actionIdOrConstraints)
? { action_id: actionIdOrConstraints }
: actionIdOrConstraints;
// biome-ignore lint/suspicious/noExplicitAny: FIXME: workaround for TypeScript 4.7 breaking changes
const _listeners = listeners;
this.listeners.push([builtin_1.onlyOptions, (0, builtin_1.matchConstraints)(constraints), ..._listeners]);
}
view(callbackIdOrConstraints, ...listeners) {
const constraints = typeof callbackIdOrConstraints === 'string' || node_util_1.default.types.isRegExp(callbackIdOrConstraints)
? { callback_id: callbackIdOrConstraints, type: 'view_submission' }
: callbackIdOrConstraints;
// Fail early if the constraints contain invalid keys
const unknownConstraintKeys = Object.keys(constraints).filter((k) => k !== 'callback_id' && k !== 'type');
if (unknownConstraintKeys.length > 0) {
this.logger.error(`View listener cannot be attached using unknown constraint keys: ${unknownConstraintKeys.join(', ')}`);
return;
}
if (constraints.type !== undefined && !validViewTypes.includes(constraints.type)) {
this.logger.error(`View listener cannot be attached using unknown view event type: ${constraints.type}`);
return;
}
// biome-ignore lint/suspicious/noExplicitAny: FIXME: workaround for TypeScript 4.7 breaking changes
const _listeners = listeners;
this.listeners.push([
builtin_1.onlyViewActions,
(0, builtin_1.matchConstraints)(constraints),
..._listeners,
]);
}
error(errorHandler) {
this.errorHandler = errorHandler;
this.hasCustomErrorHandler = true;
}
/**
* Handles events from the receiver
*/
async processEvent(event) {
const { body, ack } = event;
if (this.developerMode) {
// log the body of the event
// this may contain sensitive info like tokens
this.logger.debug(JSON.stringify(body));
}
// TODO: when generating errors (such as in the say utility) it may become useful to capture the current context,
// or even all of the args, as properties of the error. This would give error handling code some ability to deal
// with "finally" type error situations.
// Introspect the body to determine what type of incoming event is being handled, and any channel context
const { type, conversationId } = (0, helpers_1.getTypeAndConversation)(body);
// If the type could not be determined, warn and exit
if (type === undefined) {
this.logger.warn('Could not determine the type of an incoming event. No listeners will be called.');
return;
}
// From this point on, we assume that body is not just a key-value map, but one of the types of bodies we expect
const bodyArg = body;
// Check if type event with the authorizations object or if it has a top level is_enterprise_install property
const isEnterpriseInstall = (0, helpers_1.isBodyWithTypeEnterpriseInstall)(bodyArg, type);
const source = buildSource(type, conversationId, bodyArg, isEnterpriseInstall);
let authorizeResult;
if (type === helpers_1.IncomingEventType.Event && (0, helpers_1.isEventTypeToSkipAuthorize)(event)) {
authorizeResult = {
enterpriseId: source.enterpriseId,
teamId: source.teamId,
};
}
else {
try {
authorizeResult = await this.authorize(source, bodyArg);
}
catch (error) {
// biome-ignore lint/suspicious/noExplicitAny: errors can be anything
const e = error;
this.logger.warn('Authorization of incoming event did not succeed. No listeners will be called.');
e.code = errors_1.ErrorCode.AuthorizationError;
return this.handleError({
error: e,
logger: this.logger,
body: bodyArg,
context: {
isEnterpriseInstall,
},
});
}
}
// Try to set userId from AuthorizeResult before using one from source
if (authorizeResult.userId === undefined && source.userId !== undefined) {
authorizeResult.userId = source.userId;
}
// Try to set teamId from AuthorizeResult before using one from source
if (authorizeResult.teamId === undefined && source.teamId !== undefined) {
authorizeResult.teamId = source.teamId;
}
// Try to set enterpriseId from AuthorizeResult before using one from source
if (authorizeResult.enterpriseId === undefined && source.enterpriseId !== undefined) {
authorizeResult.enterpriseId = source.enterpriseId;
}
if (typeof event.customProperties !== 'undefined') {
const customProps = event.customProperties;
const builtinKeyDetected = types_1.contextBuiltinKeys.find((key) => key in customProps);
if (typeof builtinKeyDetected !== 'undefined') {
throw new errors_1.InvalidCustomPropertyError('customProperties cannot have the same names with the built-in ones');
}
}
const context = {
...authorizeResult,
...event.customProperties,
isEnterpriseInstall,
retryNum: event.retryNum,
retryReason: event.retryReason,
};
// Extract function-related information and augment context
const { functionExecutionId, functionBotAccessToken, functionInputs } = extractFunctionContext(body);
if (functionExecutionId) {
context.functionExecutionId = functionExecutionId;
if (functionInputs) {
context.functionInputs = functionInputs;
}
if (functionBotAccessToken) {
context.functionBotAccessToken = functionBotAccessToken;
}
}
// Factory for say() utility
// TODO: could this be move out of processEvent, use the same token from below or perhaps even a client from the pool
const createSay = (channelId) => {
const token = selectToken(context, this.attachFunctionToken);
return (message) => {
let postMessageArguments;
if (typeof message === 'string') {
postMessageArguments = { token, text: message, channel: channelId };
}
else {
postMessageArguments = { ...message, token, channel: channelId };
}
return this.client.chat.postMessage(postMessageArguments);
};
};
// Set body and payload
// TODO: this value should eventually conform to AnyMiddlewareArgs
// TODO: remove workflow step stuff in bolt v5
// TODO: can we instead use type predicates in these switch cases to allow for narrowing of the body simultaneously? we have isEvent, isView, isShortcut, isAction already in types/utilities / helpers
let payload;
switch (type) {
case helpers_1.IncomingEventType.Event:
payload = bodyArg.event;
break;
case helpers_1.IncomingEventType.ViewAction:
payload = bodyArg.view;
break;
case helpers_1.IncomingEventType.Shortcut:
payload = bodyArg;
break;
// biome-ignore lint/suspicious/noFallthroughSwitchClause: usually not great, but we do it here
case helpers_1.IncomingEventType.Action:
if (isBlockActionOrInteractiveMessageBody(bodyArg)) {
const { actions } = bodyArg;
[payload] = actions;
break;
}
// If above conditional does not hit, fall through to fallback payload in default block below
default:
payload = bodyArg;
break;
}
// NOTE: the following doesn't work because... distributive?
// const listenerArgs: Partial<AnyMiddlewareArgs> = {
const listenerArgs = {
body: bodyArg,
payload,
};
// Get the client arg
let { client } = this;
const token = selectToken(context, this.attachFunctionToken);
// TODO: this logic should be isolated and tested according to the expected behavior
if (token !== undefined) {
let pool = undefined;
const clientOptionsCopy = { ...this.clientOptions };
if (authorizeResult.teamId !== undefined) {
pool = this.clients[authorizeResult.teamId];
if (pool === undefined) {
pool = this.clients[authorizeResult.teamId] = new WebClientPool();
}
// Add teamId to clientOptions so it can be automatically added to web-api calls
clientOptionsCopy.teamId = authorizeResult.teamId;
}
else if (authorizeResult.enterpriseId !== undefined) {
pool = this.clients[authorizeResult.enterpriseId];
if (pool === undefined) {
pool = this.clients[authorizeResult.enterpriseId] = new WebClientPool();
}
}
if (this.attachFunctionToken && context.functionBotAccessToken) {
// workflow tokens are always unique, they should not be added to the pool
client = new web_api_1.WebClient(token, clientOptionsCopy);
}
else if (pool !== undefined) {
client = pool.getOrCreate(token, clientOptionsCopy);
}
}
// TODO: can we instead use type predicates in these switch cases to allow for narrowing of the body simultaneously? we have isEvent, isView, isShortcut, isAction already in types/utilities / helpers
// Set aliases
if (type === helpers_1.IncomingEventType.Event) {
// TODO: assigning eventListenerArgs by reference to set properties of listenerArgs is error prone, there should be a better way to do this!
const eventListenerArgs = listenerArgs;
eventListenerArgs.event = eventListenerArgs.payload;
if (eventListenerArgs.event.type === 'message') {
const messageEventListenerArgs = eventListenerArgs;
messageEventListenerArgs.message = messageEventListenerArgs.payload;
}
// Add complete() and fail() utilities for function-related interactivity
if (eventListenerArgs.event.type === 'function_executed') {
listenerArgs.complete = (0, CustomFunction_1.createFunctionComplete)(context, client);
listenerArgs.fail = (0, CustomFunction_1.createFunctionFail)(context, client);
listenerArgs.inputs = eventListenerArgs.event.inputs;
}
}
else if (type === helpers_1.IncomingEventType.Action) {
const actionListenerArgs = listenerArgs;
actionListenerArgs.action = actionListenerArgs.payload;
// Add complete() and fail() utilities for function-related interactivity
if (context.functionExecutionId !== undefined) {
listenerArgs.complete = (0, CustomFunction_1.createFunctionComplete)(context, client);
listenerArgs.fail = (0, CustomFunction_1.createFunctionFail)(context, client);
listenerArgs.inputs = context.functionInputs;
}
}
else if (type === helpers_1.IncomingEventType.Command) {
const commandListenerArgs = listenerArgs;
commandListenerArgs.command = commandListenerArgs.payload;
}
else if (type === helpers_1.IncomingEventType.Options) {
const optionListenerArgs = listenerArgs;
optionListenerArgs.options = optionListenerArgs.payload;
}
else if (type === helpers_1.IncomingEventType.ViewAction) {
const viewListenerArgs = listenerArgs;
viewListenerArgs.view = viewListenerArgs.payload;
}
else if (type === helpers_1.IncomingEventType.Shortcut) {
const shortcutListenerArgs = listenerArgs;
shortcutListenerArgs.shortcut = shortcutListenerArgs.payload;
}
// Set say() utility
if (conversationId !== undefined && type !== helpers_1.IncomingEventType.Options) {
listenerArgs.say = createSay(conversationId);
}
// Set respond() utility
if (body.response_url) {
listenerArgs.respond = buildRespondFn(this.axios, body.response_url);
}
else if (typeof body.response_urls !== 'undefined' && body.response_urls.length > 0) {
// This can exist only when view_submission payloads - response_url_enabled: true
listenerArgs.respond = buildRespondFn(this.axios, body.response_urls[0].response_url);
}
// Set ack() utility
if (type !== helpers_1.IncomingEventType.Event) {
listenerArgs.ack = ack;
}
else {
const eventListenerArgs = listenerArgs;
if (eventListenerArgs.event?.type === 'function_executed') {
listenerArgs.ack = ack;
}
else {
// Events API requests are acknowledged right away, since there's no data expected
// Except function_executed events since ack can be handled by the user
await ack();
}
}
// Dispatch event through the global middleware chain
try {
await (0, process_1.default)(this.middleware, listenerArgs, context, client, this.logger, async () => {
// Dispatch the event through the listener middleware chains and aggregate their results
// TODO: change the name of this.middleware and this.listeners to help this make more sense
const listenerResults = this.listeners.map(async (origListenerMiddleware) => {
// Copy the array so modifications don't affect the original
const listenerMiddleware = [...origListenerMiddleware];
// Don't process the last item in the listenerMiddleware array - it will be passed a no-op next fn
const listener = listenerMiddleware.pop();
if (listener === undefined) {
return undefined;
}
return (0, process_1.default)(listenerMiddleware, listenerArgs, context, client, this.logger,
// When all the listener middleware are done processing,
// `listener` here will be called with a noop `next` fn
async () => listener({
...listenerArgs,
context,
client,
logger: this.logger,
next: () => { },
}));
});
const settledListenerResults = await Promise.allSettled(listenerResults);
const rejectedListenerResults = settledListenerResults.filter(utilities_1.isRejected);
if (rejectedListenerResults.length === 1) {
throw rejectedListenerResults[0].reason;
// biome-ignore lint/style/noUselessElse: I think this is a biome issue actually...
}
else if (rejectedListenerResults.length > 1) {
throw new errors_1.MultipleListenerError(rejectedListenerResults.map((rlr) => rlr.reason));
}
});
}
catch (error) {
// biome-ignore lint/suspicious/noExplicitAny: errors can be anything
const e = error;
return this.handleError({
context,
error: e,
logger: this.logger,
body: bodyArg,
});
}
}
/**
* Global error handler. The final destination for all errors (hopefully).
*/
handleError(args) {
const { error, ...rest } = args;
return this.extendedErrorHandler && this.hasCustomErrorHandler
? this.errorHandler({ error: (0, errors_1.asCodedError)(error), ...rest })
: this.errorHandler((0, errors_1.asCodedError)(error));
}
// ---------------------
// Private methods for initialization
// ---------------------
initReceiver(receiver, signingSecret, endpoints, port, customRoutes, processBeforeResponse, signatureVerification, clientId, clientSecret, stateSecret, redirectUri, installationStore, scopes, appToken, logger) {
if (receiver !== undefined) {
// Custom receiver supplied
if (this.socketMode === true && !(receiver instanceof SocketModeReceiver_1.default)) {
throw new errors_1.AppInitializationError('You cannot supply a custom receiver when socketMode is set to true.');
}
return receiver;
}
if (this.socketMode === true) {
if (appToken === undefined) {
throw new errors_1.AppInitializationError('You must provide an appToken when socketMode is set to true. To generate an appToken see: https://api.slack.com/apis/connections/socket#token');
}
this.logger.debug('Initializing SocketModeReceiver');
return new SocketModeReceiver_1.default({
appToken,
clientId,
clientSecret,
stateSecret,
redirectUri,
installationStore,
scopes,
logger,
logLevel: this.logLevel,
installerOptions: this.installerOptions,
customRoutes,
});
}
if (signatureVerification === true && signingSecret === undefined) {
// Using default receiver HTTPReceiver, signature verification enabled, missing signingSecret
throw new errors_1.AppInitializationError('signingSecret is required to initialize the default receiver. Set signingSecret or use a ' +
'custom receiver. You can find your Signing Secret in your Slack App Settings.');
}
this.logger.debug('Initializing HTTPReceiver');
return new HTTPReceiver_1.default({
signingSecret: signingSecret || '',
endpoints,
port,
customRoutes,
processBeforeResponse,
signatureVerification,
clientId,
clientSecret,
stateSecret,
redirectUri,
installationStore,
scopes,
logger,
logLevel: this.logLevel,
installerOptions: this.installerOptions,
});
}
initAuthorizeIfNoTokenIsGiven(token, authorize) {
let usingOauth = false;
const httpReceiver = this.receiver;
if (httpReceiver.installer !== undefined && httpReceiver.installer.authorize !== undefined) {
// This supports using the built-in HTTPReceiver, declaring your own HTTPReceiver
// and theoretically, doing a fully custom (non-Express.js) receiver that implements OAuth
usingOauth = true;
}
if (token !== undefined) {
if (usingOauth || authorize !== undefined) {
throw new errors_1.AppInitializationError(`You cannot provide a token along with either oauth installer options or authorize. ${tokenUsage}`);
}
return undefined;
}
if (authorize === undefined && !usingOauth) {
throw new errors_1.AppInitializationError(`${tokenUsage} \n\nSince you have not provided a token or authorize, you might be missing one or more required oauth installer options. See https://tools.slack.dev/bolt-js/concepts/authenticating-oauth/ for these required fields.\n`);
// biome-ignore lint/style/noUselessElse: I think this is a biome issue actually...
}
else if (authorize !== undefined && usingOauth) {
throw new errors_1.AppInitializationError(`You cannot provide both authorize and oauth installer options. ${tokenUsage}`);
// biome-ignore lint/style/noUselessElse: I think this is a biome issue actually...
}
else if (authorize === undefined && usingOauth) {
// biome-ignore lint/style/noNonNullAssertion: we know installer is truthy here
return httpReceiver.installer.authorize;
// biome-ignore lint/style/noUselessElse: I think this is a biome issue actually...
}
else if (authorize !== undefined && !usingOauth) {
return authorize;
}
return undefined;
}
initAuthorizeInConstructor(token, authorize, authorization) {
const initializedAuthorize = this.initAuthorizeIfNoTokenIsGiven(token, authorize);
if (initializedAuthorize !== undefined) {
return initializedAuthorize;
}
if (token !== undefined && authorization !== undefined) {
return singleAuthorization(this.client, authorization, this.tokenVerificationEnabled);
}
const hasToken = token !== undefined && token.length > 0;
const errorMessage = `Something has gone wrong in #initAuthorizeInConstructor method (hasToken: ${hasToken}, authorize: ${authorize}). Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues`;
this.logger.error(errorMessage);
throw new Error(errorMessage);
}
}
exports.default = App;
function defaultErrorHandler(logger) {
return (error) => {
logger.error(error);
return Promise.reject(error);
};
}
// -----------
// singleAuthorization
function runAuthTestForBotToken(client, authorization) {
// TODO: warn when something needed isn't found
return authorization.botUserId !== undefined && authorization.botId !== undefined
? Promise.resolve({ botUserId: authorization.botUserId, botId: authorization.botId })
: client.auth.test({ token: authorization.botToken }).then((result) => ({
botUserId: result.user_id,
botId: result.bot_id,
}));
}
async function buildAuthorizeResult(isEnterpriseInstall, authTestResult, authorization) {
return { isEnterpriseInstall, botToken: authorization.botToken, ...(await authTestResult) };
}
function singleAuthorization(client, authorization, tokenVerificationEnabled) {
// As Authorize function has a reference to this local variable,
// this local variable can behave as auth.test call result cache for the function
let cachedAuthTestResult;
if (tokenVerificationEnabled) {
// call auth.test immediately
cachedAuthTestResult = runAuthTestForBotToken(client, authorization);
return async ({ isEnterpriseInstall }) => buildAuthorizeResult(isEnterpriseInstall, cachedAuthTestResult, authorization);
}
return async ({ isEnterpriseInstall }) => {
// hold off calling auth.test API until the first access to authorize function
cachedAuthTestResult = runAuthTestForBotToken(client, authorization);
return buildAuthorizeResult(isEnterpriseInstall, cachedAuthTestResult, authorization);
};
}
// ----------------------------
// For processEvent method
/**
* Helper which builds the data structure the authorize hook uses to provide tokens for the context.
*/
function buildSource(type, channelId, body, isEnterpriseInstall) {
// NOTE: potentially something that can be optimized, so that each of these conditions isn't evaluated more than once.
// if this makes it prettier, great! but we should probably check perf before committing to any specific optimization.
const teamId = (() => {
if (type === helpers_1.IncomingEventType.Event) {
const bodyAsEvent = body;
if (Array.isArray(bodyAsEvent.authorizations) &&
bodyAsEvent.authorizations[0] !== undefined &&
bodyAsEvent.authorizations[0].team_id !== null) {
return bodyAsEvent.authorizations[0].team_id;
}
return bodyAsEvent.team_id;
}
if (type === helpers_1.IncomingEventType.Command) {
return body.team_id;
}
const parseTeamId = (bodyAs) => {
// When the app is installed using org-wide deployment, team property will be null
if (typeof bodyAs.team !== 'undefined' && bodyAs.team !== null) {
return bodyAs.team.id;
}
// This is the only place where this function might return undefined
return bodyAs.user.team_id;
};
if (type === helpers_1.IncomingEventType.ViewAction) {
// view_submission/closed payloads can have `view.app_installed_team_id` when a modal view that was opened
// in a different workspace via some operations inside a Slack Connect channel.
const bodyAsView = body;
if (bodyAsView.view.app_installed_team_id) {
return bodyAsView.view.app_installed_team_id;
}
return parseTeamId(bodyAsView);
}
if (type === helpers_1.IncomingEventType.Action ||
type === helpers_1.IncomingEventType.Options ||
type === helpers_1.IncomingEventType.Shortcut) {
const bodyAsActionOrOptionsOrShortcut = body;
return parseTeamId(bodyAsActionOrOptionsOrShortcut);
}
return (0, helpers_1.assertNever)(type);
})();
const enterpriseId = (() => {
if (type === helpers_1.IncomingEventType.Event) {
const bodyAsEvent = body;
if (Array.isArray(bodyAsEvent.authorizations) && bodyAsEvent.authorizations[0] !== undefined) {
// The enterprise_id here can be null when the workspace is not in an Enterprise Grid
const theId = bodyAsEvent.authorizations[0].enterprise_id;
return theId !== null ? theId : undefined;
}
return bodyAsEvent.enterprise_id;
}
if (type === helpers_1.IncomingEventType.Command) {
return body.enterprise_id;
}
if (type === helpers_1.IncomingEventType.Action ||
type === helpers_1.IncomingEventType.Options ||
type === helpers_1.IncomingEventType.ViewAction ||
type === helpers_1.IncomingEventType.Shortcut) {
// NOTE: no type system backed exhaustiveness check within this group of incoming event types
const bodyAsActionOrOptionsOrViewActionOrShortcut = body;
if (typeof bodyAsActionOrOptionsOrViewActionOrShortcut.enterprise !== 'undefined' &&
bodyAsActionOrOptionsOrViewActionOrShortcut.enterprise !== null) {
return bodyAsActionOrOptionsOrViewActionOrShortcut.enterprise.id;
}
// When the app is installed using org-wide deployment, team property will be null
if (typeof bodyAsActionOrOptionsOrViewActionOrShortcut.team !== 'undefined' &&
bodyAsActionOrOptionsOrViewActionOrShortcut.team !== null) {
return bodyAsActionOrOptionsOrViewActionOrShortcut.team.enterprise_id;
}
return undefined;
}
return (0, helpers_1.assertNever)(type);
})();
const userId = (() => {
if (type === helpers_1.IncomingEventType.Event) {
// NOTE: no type system backed exhaustiveness check within this incoming event type
const { event } = body;
if ('user' in event) {
if (typeof event.user === 'string') {
return event.user;
}
if (typeof event.user === 'object') {
return event.user.id;
}
}
if ('channel' in event && typeof event.channel !== 'string' && 'creator' in event.channel) {
return event.channel.creator;
}
if ('subteam' in event && event.subteam.created_by !== undefined) {
return event.subteam.created_by;
}
return undefined;
}
if (type === helpers_1.IncomingEventType.Action ||
type === helpers_1.IncomingEventType.Options ||
type === helpers_1.IncomingEventType.ViewAction ||
type === helpers_1.IncomingEventType.Shortcut) {
// NOTE: no type system backed exhaustiveness check within this incoming event type
const bodyAsActionOrOptionsOrViewActionOrShortcut = body;
return bodyAsActionOrOptionsOrViewActionOrShortcut.user.id;
}
if (type === helpers_1.IncomingEventType.Command) {
return body.user_id;
}
return (0, helpers_1.assertNever)(type);
})();
return {
userId,
isEnterpriseInstall,
teamId: teamId,
enterpriseId: enterpriseId,
conversationId: channelId,
};
}
function isBlockActionOrInteractiveMessageBody(body) {
return body.actions !== undefined;
}
// Returns either a bot token, a user token or a workflow token for client, say()
function selectToken(context, attachFunctionToken) {
if (attachFunctionToken && context.functionBotAccessToken) {
return context.functionBotAccessToken;
}
return context.botToken !== undefined ? context.botToken : context.userToken;
}
function buildRespondFn(axiosInstance, responseUrl) {
return async (message) => {
const normalizedArgs = typeof message === 'string' ? { text: message } : message;
return axiosInstance.post(responseUrl, normalizedArgs);
};
}
function escapeHtml(input) {
if (input) {
return input
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
return '';
}
function extractFunctionContext(body) {
let functionExecutionId = undefined;
let functionBotAccessToken = undefined;
let functionInputs = undefined;
// function_executed event
if (body.event && body.event.type === 'function_executed' && body.event.function_execution_id) {
functionExecutionId = body.event.function_execution_id;
functionBotAccessToken = body.event.bot_access_token;
functionInputs = body.event.inputs;
}
// interactivity (block_actions)
if (body.function_data) {
functionExecutionId = body.function_data.execution_id;
functionBotAccessToken = body.bot_access_token;
functionInputs = body.function_data.inputs;
}
return { functionExecutionId, functionBotAccessToken, functionInputs };
}
// ----------------------------
// Instrumentation
// Don't change the position of the following code
(0, web_api_1.addAppMetadata)({ name: packageJson.name, version: packageJson.version });
//# sourceMappingURL=App.js.ma