UNPKG

@slack/socket-mode

Version:

Official library for using the Slack Platform's Socket Mode API

378 lines 18.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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); 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()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SocketModeClient = void 0; const web_api_1 = require("@slack/web-api"); const eventemitter3_1 = require("eventemitter3"); const package_json_1 = __importDefault(require("../package.json")); const SlackWebSocket_1 = require("./SlackWebSocket"); const UnrecoverableSocketModeStartError_1 = require("./UnrecoverableSocketModeStartError"); const errors_1 = require("./errors"); const logger_1 = __importStar(require("./logger")); // Lifecycle events as described in the README var State; (function (State) { State["Connecting"] = "connecting"; State["Connected"] = "connected"; State["Reconnecting"] = "reconnecting"; State["Disconnecting"] = "disconnecting"; State["Disconnected"] = "disconnected"; State["Authenticated"] = "authenticated"; })(State || (State = {})); /** * A Socket Mode Client allows programs to communicate with the * [Slack Platform's Events API](https://api.slack.com/events-api) over WebSocket connections. * This object uses the EventEmitter pattern to dispatch incoming events * and has a built in send method to acknowledge incoming events over the WebSocket connection. */ class SocketModeClient extends eventemitter3_1.EventEmitter { constructor({ logger = undefined, logLevel = undefined, autoReconnectEnabled = true, pingPongLoggingEnabled = false, clientPingTimeout = 5000, serverPingTimeout = 30000, appToken = '', clientOptions = {}, } = { appToken: '' }) { super(); /** * Internal count for managing the reconnection state */ this.numOfConsecutiveReconnectionFailures = 0; this.customLoggerProvided = false; /** * Sentinel tracking if user invoked `disconnect()`; for enforcing shutting down of client * even if `autoReconnectEnabled` is `true`. */ this.shuttingDown = false; if (!appToken) { throw new Error('Must provide an App-Level Token when initializing a Socket Mode Client'); } this.pingPongLoggingEnabled = pingPongLoggingEnabled; this.clientPingTimeoutMS = clientPingTimeout; this.serverPingTimeoutMS = serverPingTimeout; // Setup the logger if (typeof logger !== 'undefined') { this.customLoggerProvided = true; this.logger = logger; if (typeof logLevel !== 'undefined') { this.logger.debug('The logLevel given to Socket Mode was ignored as you also gave logger'); } } else { this.logger = logger_1.default.getLogger(SocketModeClient.loggerName, logLevel !== null && logLevel !== void 0 ? logLevel : logger_1.LogLevel.INFO, logger); } this.webClientOptions = clientOptions; if (this.webClientOptions.retryConfig === undefined) { // For faster retries of apps.connections.open API calls for reconnecting this.webClientOptions.retryConfig = { retries: 100, factor: 1.3 }; } this.webClient = new web_api_1.WebClient('', Object.assign({ logger, logLevel: this.logger.getLevel(), headers: { Authorization: `Bearer ${appToken}` } }, clientOptions)); this.autoReconnectEnabled = autoReconnectEnabled; // bind to error, message and close events emitted from the web socket this.on('error', (err) => { this.logger.error(`WebSocket error! ${err}`); }); this.on('close', () => { // Underlying WebSocket connection was closed, possibly reconnect. if (!this.shuttingDown && this.autoReconnectEnabled) { this.delayReconnectAttempt(this.start); } else { // If reconnect is disabled or user explicitly called `disconnect()`, emit a disconnected state. this.emit(State.Disconnected); } }); this.on('ws_message', this.onWebSocketMessage.bind(this)); this.logger.debug('The Socket Mode client has successfully initialized'); } // PUBLIC METHODS /** * Start a Socket Mode session app. * This method must be called before any messages can be sent or received, * or to disconnect the client via the `disconnect` method. */ start() { return __awaiter(this, void 0, void 0, function* () { // python equiv: SocketModeClient.connect this.shuttingDown = false; this.logger.debug('Starting Socket Mode session ...'); // create a socket connection using SlackWebSocket this.websocket = new SlackWebSocket_1.SlackWebSocket({ url: yield this.retrieveWSSURL(), // web socket events relevant to this client will be emitted into the instance of this class // see bottom of constructor for where we bind to these events client: this, logLevel: this.logger.getLevel(), logger: this.customLoggerProvided ? this.logger : undefined, httpAgent: this.webClientOptions.agent, clientPingTimeoutMS: this.clientPingTimeoutMS, serverPingTimeoutMS: this.serverPingTimeoutMS, pingPongLoggingEnabled: this.pingPongLoggingEnabled, }); // Return a promise that resolves with the connection information return new Promise((resolve, reject) => { var _a; // biome-ignore lint/suspicious/noExplicitAny: untyped connection callback parameters let connectedCallback = (_res) => { }; // biome-ignore lint/suspicious/noExplicitAny: untyped connection callback parameters let disconnectedCallback = (_err) => { }; connectedCallback = (result) => { this.removeListener(State.Disconnected, disconnectedCallback); resolve(result); }; disconnectedCallback = (err) => { this.removeListener(State.Connected, connectedCallback); reject(err); }; this.once(State.Connected, connectedCallback); this.once(State.Disconnected, disconnectedCallback); this.emit(State.Connecting); (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.connect(); }); }); } /** * End a Socket Mode session. After this method is called no messages will be sent or received * unless you call start() again later. */ disconnect() { this.shuttingDown = true; this.logger.debug('Manually disconnecting this Socket Mode client'); this.emit(State.Disconnecting); return new Promise((resolve, _reject) => { var _a; if (!this.websocket) { this.emit(State.Disconnected); resolve(); } else { // Resolve (or reject) on disconnect this.once(State.Disconnected, resolve); (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.disconnect(); } }); } // PRIVATE/PROTECTED METHODS /** * Initiates a reconnect, taking into account configurable delays and number of reconnect attempts and failures. * Accepts a callback to invoke after any calculated delays. */ delayReconnectAttempt(cb) { this.numOfConsecutiveReconnectionFailures += 1; const msBeforeRetry = this.clientPingTimeoutMS * this.numOfConsecutiveReconnectionFailures; this.logger.debug(`Before trying to reconnect, this client will wait for ${msBeforeRetry} milliseconds`); return new Promise((res, _rej) => { setTimeout(() => { if (this.shuttingDown) { this.logger.debug('Client shutting down, will not attempt reconnect.'); } else { this.logger.debug('Continuing with reconnect...'); this.emit(State.Reconnecting); cb.apply(this).then(res); } }, msBeforeRetry); }); } /** * Retrieves a new WebSocket URL to connect to. */ retrieveWSSURL() { return __awaiter(this, void 0, void 0, function* () { // python equiv: BaseSocketModeClient.issue_new_wss_url try { this.logger.debug('Going to retrieve a new WSS URL ...'); const resp = yield this.webClient.apps.connections.open({}); if (!resp.url) { const msg = `apps.connections.open did not return a URL! (response: ${resp})`; this.logger.error(msg); throw new Error(msg); } this.numOfConsecutiveReconnectionFailures = 0; this.emit(State.Authenticated, resp); return resp.url; } catch (error) { // TODO: Python catches rate limit errors when interacting with this API: https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/socket_mode/client.py#L51 this.logger.error(`Failed to retrieve a new WSS URL (error: ${error})`); const err = error; let isRecoverable = true; if (err.code === web_api_1.ErrorCode.PlatformError && Object.values(UnrecoverableSocketModeStartError_1.UnrecoverableSocketModeStartError).includes(err.data.error)) { isRecoverable = false; } else if (err.code === web_api_1.ErrorCode.RequestError) { isRecoverable = false; } else if (err.code === web_api_1.ErrorCode.HTTPError) { isRecoverable = false; } if (this.autoReconnectEnabled && isRecoverable) { return yield this.delayReconnectAttempt(this.retrieveWSSURL); } throw error; } }); } /** * `onmessage` handler for the client's WebSocket. * This will parse the payload and dispatch the application-relevant events for each incoming message. * Mediates: * - raising the State.Connected event (when Slack sends a type:hello message) * - disconnecting the underlying socket (when Slack sends a type:disconnect message) */ onWebSocketMessage(data, isBinary) { return __awaiter(this, void 0, void 0, function* () { var _a; if (isBinary) { this.logger.debug('Unexpected binary message received, ignoring.'); return; } const payload = data.toString(); // TODO: should we redact things in here? this.logger.debug(`Received a message on the WebSocket: ${payload}`); // Parse message into slack event let event; try { event = JSON.parse(payload); } catch (parseError) { // Prevent application from crashing on a bad message, but log an error to bring attention this.logger.debug(`Unable to parse an incoming WebSocket message (will ignore): ${parseError}, ${payload}`); return; } // Slack has finalized the handshake with a hello message; we are good to go. if (event.type === 'hello') { this.emit(State.Connected); return; } // Slack is recycling the pod handling the connection (or otherwise requires the client to reconnect) if (event.type === 'disconnect') { this.logger.debug(`Received "${event.type}" (${event.reason}) message - disconnecting.${this.autoReconnectEnabled ? ' Will reconnect.' : ''}`); (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.disconnect(); return; } // Define Ack, a helper method for acknowledging events incoming from Slack const ack = (response) => __awaiter(this, void 0, void 0, function* () { if (this.logger.getLevel() === logger_1.LogLevel.DEBUG) { this.logger.debug(`Calling ack() - type: ${event.type}, envelope_id: ${event.envelope_id}, data: ${JSON.stringify(response)}`); } yield this.send(event.envelope_id, response); }); // For events_api messages, expose the type of the event if (event.type === 'events_api') { this.emit(event.payload.event.type, { ack, envelope_id: event.envelope_id, body: event.payload, event: event.payload.event, retry_num: event.retry_attempt, retry_reason: event.retry_reason, accepts_response_payload: event.accepts_response_payload, }); } else { // Emit just ack and body for all other types of messages this.emit(event.type, { ack, envelope_id: event.envelope_id, body: event.payload, accepts_response_payload: event.accepts_response_payload, }); } // Emitter for all slack events // (this can be used in tools like bolt-js) this.emit('slack_event', { ack, envelope_id: event.envelope_id, type: event.type, body: event.payload, retry_num: event.retry_attempt, retry_reason: event.retry_reason, accepts_response_payload: event.accepts_response_payload, }); }); } /** * Method for sending an outgoing message of an arbitrary type over the WebSocket connection. * Primarily used to send acknowledgements back to slack for incoming events * @param id the envelope id * @param body the message body or string text */ send(id, body = {}) { const _body = typeof body === 'string' ? { text: body } : body; const message = { envelope_id: id, payload: Object.assign({}, _body) }; return new Promise((resolve, reject) => { var _a; const wsState = (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.readyState; this.logger.debug(`send() method was called (WebSocket state: ${wsState ? SlackWebSocket_1.WS_READY_STATES[wsState] : 'uninitialized'})`); if (this.websocket === undefined) { this.logger.error('Failed to send a message as the client is not connected'); reject((0, errors_1.sendWhileDisconnectedError)()); } else if (!this.websocket.isActive()) { this.logger.error('Failed to send a message as the client has no active connection'); reject((0, errors_1.sendWhileNotReadyError)()); } else { this.emit('outgoing_message', message); const flatMessage = JSON.stringify(message); this.logger.debug(`Sending a WebSocket message: ${flatMessage}`); this.websocket.send(flatMessage, (error) => { if (error) { this.logger.error(`Failed to send a WebSocket message (error: ${error})`); return reject((0, errors_1.websocketErrorWithOriginal)(error)); } return resolve(); }); } }); } } exports.SocketModeClient = SocketModeClient; /** * The name used to prefix all logging generated from this class */ SocketModeClient.loggerName = 'SocketModeClient'; /* Instrumentation */ (0, web_api_1.addAppMetadata)({ name: package_json_1.default.name, version: package_json_1.default.version }); exports.default = SocketModeClient; //# sourceMappingURL=SocketModeClient.js.map