UNPKG

@slack/bolt

Version:

A framework for building Slack apps, fast.

189 lines 9.26 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const node_http_1 = require("node:http"); const node_url_1 = require("node:url"); const logger_1 = require("@slack/logger"); const oauth_1 = require("@slack/oauth"); const socket_mode_1 = require("@slack/socket-mode"); const path_to_regexp_1 = require("path-to-regexp"); const SocketModeFunctions_1 = require("./SocketModeFunctions"); const SocketModeResponseAck_1 = require("./SocketModeResponseAck"); const custom_routes_1 = require("./custom-routes"); const verify_redirect_opts_1 = require("./verify-redirect-opts"); /** * Receives Events, Slash Commands, and Actions of a web socket connection */ class SocketModeReceiver { /* Express app */ client; app; logger; installer = undefined; httpServer; httpServerPort; routes; processEventErrorHandler; constructor({ appToken, logger = undefined, logLevel = logger_1.LogLevel.INFO, clientId = undefined, clientSecret = undefined, stateSecret = undefined, redirectUri = undefined, installationStore = undefined, scopes = undefined, installerOptions = {}, customRoutes = [], customPropertiesExtractor = (_args) => ({}), processEventErrorHandler = SocketModeFunctions_1.defaultProcessEventErrorHandler, }) { this.client = new socket_mode_1.SocketModeClient({ appToken, logLevel, logger, clientOptions: installerOptions.clientOptions, }); this.logger = logger ?? (() => { const defaultLogger = new logger_1.ConsoleLogger(); defaultLogger.setLevel(logLevel); return defaultLogger; })(); this.routes = (0, custom_routes_1.buildReceiverRoutes)(customRoutes); this.processEventErrorHandler = processEventErrorHandler; // Verify redirect options if supplied, throws coded error if invalid (0, verify_redirect_opts_1.verifyRedirectOpts)({ redirectUri, redirectUriPath: installerOptions.redirectUriPath }); if (clientId !== undefined && clientSecret !== undefined && (installerOptions.stateVerification === false || // state store not needed stateSecret !== undefined || installerOptions.stateStore !== undefined) // user provided state store ) { this.installer = new oauth_1.InstallProvider({ clientId, clientSecret, stateSecret, installationStore, logLevel, logger, // pass logger that was passed in constructor, not one created locally directInstall: installerOptions.directInstall, stateStore: installerOptions.stateStore, stateVerification: installerOptions.stateVerification, legacyStateVerification: installerOptions.legacyStateVerification, stateCookieName: installerOptions.stateCookieName, stateCookieExpirationSeconds: installerOptions.stateCookieExpirationSeconds, renderHtmlForInstallPath: installerOptions.renderHtmlForInstallPath, authVersion: installerOptions.authVersion, clientOptions: installerOptions.clientOptions, authorizationUrl: installerOptions.authorizationUrl, }); } // Add OAuth and/or custom routes to receiver if (this.installer !== undefined || customRoutes.length) { const installPath = installerOptions.installPath === undefined ? '/slack/install' : installerOptions.installPath; this.httpServerPort = installerOptions.port === undefined ? 3000 : installerOptions.port; this.httpServer = (0, node_http_1.createServer)(async (req, res) => { // biome-ignore lint/style/noNonNullAssertion: method should always be defined for an HTTP request right? const method = req.method.toUpperCase(); // Handle OAuth-related requests if (this.installer) { // create install url options const installUrlOptions = { metadata: installerOptions.metadata, scopes: scopes ?? [], userScopes: installerOptions.userScopes, redirectUri, }; // Installation has been initiated const redirectUriPath = installerOptions.redirectUriPath === undefined ? '/slack/oauth_redirect' : installerOptions.redirectUriPath; if (req.url?.startsWith(redirectUriPath)) { const { stateVerification, callbackOptions } = installerOptions; if (stateVerification === false) { // if stateVerification is disabled make install options available to handler // since they won't be encoded in the state param of the generated url await this.installer.handleCallback(req, res, callbackOptions, installUrlOptions); } else { await this.installer.handleCallback(req, res, callbackOptions); } return; } // Visiting the installation endpoint if (req.url?.startsWith(installPath)) { const { installPathOptions } = installerOptions; await this.installer.handleInstallPath(req, res, installPathOptions, installUrlOptions); return; } } // Handle request for custom routes if (customRoutes.length && req.url) { // NOTE: the domain and scheme are irrelevant here. // The URL object is only used to safely obtain the path to match const { pathname: path } = new node_url_1.URL(req.url, 'http://localhost'); const routes = Object.keys(this.routes); for (let i = 0; i < routes.length; i += 1) { const route = routes[i]; const matchRegex = (0, path_to_regexp_1.match)(route, { decode: decodeURIComponent }); const pathMatch = matchRegex(path); if (pathMatch && this.routes[route][method] !== undefined) { const params = pathMatch.params; const message = Object.assign(req, { params }); this.routes[route][method](message, res); return; } } } this.logger.info(`An unhandled HTTP request (${req.method}) made to ${req.url} was ignored`); res.writeHead(404, {}); res.end(); }); this.logger.debug(`Listening for HTTP requests on port ${this.httpServerPort}`); if (this.installer) { this.logger.debug(`Go to http://localhost:${this.httpServerPort}${installPath} to initiate OAuth flow`); } } this.client.on('slack_event', async (args) => { const { body, retry_num, retry_reason } = args; const ack = new SocketModeResponseAck_1.SocketModeResponseAck({ logger: this.logger, socketModeClientAck: args.ack }); const event = { body, ack: ack.bind(), retryNum: retry_num, retryReason: retry_reason, customProperties: customPropertiesExtractor(args), }; try { await this.app?.processEvent(event); } catch (error) { const shouldBeAcked = await this.processEventErrorHandler({ error: error, logger: this.logger, event, }); if (shouldBeAcked) { await event.ack(); } } }); } init(app) { this.app = app; } start() { if (this.httpServer !== undefined) { // This HTTP server is only for the OAuth flow support this.httpServer.listen(this.httpServerPort); } // start socket mode client return this.client.start(); } stop() { if (this.httpServer !== undefined) { // This HTTP server is only for the OAuth flow support this.httpServer.close((error) => { if (error) this.logger.error(`Failed to shutdown the HTTP server for OAuth flow: ${error}`); }); } return new Promise((resolve, reject) => { try { this.client.disconnect(); resolve(); } catch (error) { reject(error); } }); } } exports.default = SocketModeReceiver; //# sourceMappingURL=SocketModeReceiver.js.map