From 604eb1aad52730e8c66a929e92d576c49a10a7e0 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Wed, 9 Sep 2020 17:06:14 -0700 Subject: [PATCH 01/10] Added initial SocketModeReceiver --- package.json | 1 + src/ExpressReceiver.ts | 19 +++- src/SocketModeReceiver.ts | 176 ++++++++++++++++++++++++++++++++++++++ src/index.ts | 2 + 4 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 src/SocketModeReceiver.ts diff --git a/package.json b/package.json index f9c3a1eb0..25a5cbe15 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "dependencies": { "@slack/logger": "^2.0.0", "@slack/oauth": "^1.4.0", + "@slack/socket-mode": "feat-socket-mode", "@slack/types": "^1.9.0", "@slack/web-api": "^5.14.0", "@types/express": "^4.16.1", diff --git a/src/ExpressReceiver.ts b/src/ExpressReceiver.ts index cca9653e2..0104eb27a 100644 --- a/src/ExpressReceiver.ts +++ b/src/ExpressReceiver.ts @@ -6,7 +6,7 @@ import rawBody from 'raw-body'; import querystring from 'querystring'; import crypto from 'crypto'; import tsscmp from 'tsscmp'; -import { Logger, ConsoleLogger } from '@slack/logger'; +import { Logger, ConsoleLogger, LogLevel } from '@slack/logger'; import { InstallProvider, CallbackOptions, InstallProviderOptions, InstallURLOptions } from '@slack/oauth'; import App from './App'; import { ReceiverAuthenticityError, ReceiverMultipleAckError } from './errors'; @@ -17,6 +17,7 @@ import { AnyMiddlewareArgs, Receiver, ReceiverEvent } from './types'; export interface ExpressReceiverOptions { signingSecret: string; logger?: Logger; + logLevel?: LogLevel; endpoints?: | string | { @@ -65,7 +66,8 @@ export default class ExpressReceiver implements Receiver { constructor({ signingSecret = '', - logger = new ConsoleLogger(), + logger = undefined, + logLevel = LogLevel.INFO, endpoints = { events: '/slack/events' }, processBeforeResponse = false, clientId = undefined, @@ -79,15 +81,22 @@ export default class ExpressReceiver implements Receiver { // TODO: what about starting an https server instead of http? what about other options to create the server? this.server = createServer(this.app); + if (typeof logger !== 'undefined') { + this.logger = logger; + } else { + this.logger = new ConsoleLogger(); + this.logger.setLevel(logLevel); + } + const expressMiddleware: RequestHandler[] = [ - verifySignatureAndParseRawBody(logger, signingSecret), + verifySignatureAndParseRawBody(this.logger, signingSecret), respondToSslCheck, respondToUrlVerification, this.requestHandler.bind(this), ]; this.processBeforeResponse = processBeforeResponse; - this.logger = logger; + const endpointList = typeof endpoints === 'string' ? [endpoints] : Object.values(endpoints); this.router = Router(); endpointList.forEach((endpoint) => { @@ -104,6 +113,8 @@ export default class ExpressReceiver implements Receiver { clientSecret, stateSecret, installationStore, + logLevel, + logger, // pass logger that was passed in constructor, not one created locally stateStore: installerOptions.stateStore, authVersion: installerOptions.authVersion!, clientOptions: installerOptions.clientOptions, diff --git a/src/SocketModeReceiver.ts b/src/SocketModeReceiver.ts new file mode 100644 index 000000000..5b674eb3e --- /dev/null +++ b/src/SocketModeReceiver.ts @@ -0,0 +1,176 @@ +/* eslint-disable @typescript-eslint/explicit-member-accessibility, @typescript-eslint/strict-boolean-expressions */ +import { SocketModeClient } from '@slack/socket-mode'; +import { createServer } from 'http'; +import { Logger, ConsoleLogger, LogLevel } from '@slack/logger'; +import { InstallProvider, CallbackOptions, InstallProviderOptions, InstallURLOptions } from '@slack/oauth'; +import App from './App'; +import { Receiver, ReceiverEvent } from './types'; + +// TODO: we throw away the key names for endpoints, so maybe we should use this interface. is it better for migrations? +// if that's the reason, let's document that with a comment. +export interface SocketModeReceiverOptions { + logger?: Logger; + logLevel?: LogLevel; + clientId?: string; + clientSecret?: string; + stateSecret?: InstallProviderOptions['stateSecret']; // required when using default stateStore + installationStore?: InstallProviderOptions['installationStore']; // default MemoryInstallationStore + scopes?: InstallURLOptions['scopes']; + installerOptions?: InstallerOptions; + token?: string; // App Level Token +} + +// Additional Installer Options +interface InstallerOptions { + stateStore?: InstallProviderOptions['stateStore']; // default ClearStateStore + authVersion?: InstallProviderOptions['authVersion']; // default 'v2' + metadata?: InstallURLOptions['metadata']; + installPath?: string; + redirectUriPath?: string; + callbackOptions?: CallbackOptions; + userScopes?: InstallURLOptions['userScopes']; + clientOptions?: InstallProviderOptions['clientOptions']; + authorizationUrl?: InstallProviderOptions['authorizationUrl']; + port?: number; // used to create a server when doing OAuth +} + +/** + * Receives Events, Slash Commands, and Actions of a web socket connection + */ +export default class SocketModeReceiver implements Receiver { + /* Express app */ + public client: SocketModeClient; + + private bolt: App | undefined; + + private logger: Logger; + + public installer: InstallProvider | undefined = undefined; + + constructor({ + token = undefined, + logger = undefined, + logLevel = LogLevel.INFO, + clientId = undefined, + clientSecret = undefined, + stateSecret = undefined, + installationStore = undefined, + scopes = undefined, + installerOptions = {}, + }: SocketModeReceiverOptions) { + this.client = new SocketModeClient({ + token, + logLevel, + clientOptions: installerOptions.clientOptions, + }); + + // const expressMiddleware: RequestHandler[] = [ + // TODO: Should we still be verifying Signature? + // verifySignatureAndParseRawBody(logger, signingSecret), + // respondToSslCheck, + // respondToUrlVerification, + // this.requestHandler.bind(this), + // ]; + + if (typeof logger !== 'undefined') { + this.logger = logger; + } else { + this.logger = new ConsoleLogger(); + this.logger.setLevel(logLevel); + } + + if ( + clientId !== undefined && + clientSecret !== undefined && + (stateSecret !== undefined || installerOptions.stateStore !== undefined) + ) { + this.installer = new InstallProvider({ + clientId, + clientSecret, + stateSecret, + installationStore, + logLevel, + logger, // pass logger that was passed in constructor, not one created locally + stateStore: installerOptions.stateStore, + authVersion: installerOptions.authVersion!, + clientOptions: installerOptions.clientOptions, + authorizationUrl: installerOptions.authorizationUrl, + }); + } + + // Add OAuth routes to receiver + if (this.installer !== undefined) { + // use default or passed in redirect path + const redirectUriPath = + installerOptions.redirectUriPath === undefined ? '/slack/oauth_redirect' : installerOptions.redirectUriPath; + + // use default or passed in installPath + const installPath = installerOptions.installPath === undefined ? '/slack/install' : installerOptions.installPath; + + const server = createServer(async (req, res) => { + if (req.url === redirectUriPath) { + // call installer.handleCallback to wrap up the install flow + await this.installer!.handleCallback(req, res); + } + + if (req.url === installPath) { + try { + const url = await this.installer!.generateInstallUrl({ + metadata: installerOptions.metadata, + scopes: scopes!, + userScopes: installerOptions.userScopes, + }); + res.writeHead(200, {}); + res.end(``); + } catch (err) { + throw new Error(err); + } + } + }); + + const port = installerOptions.port === undefined ? 3000 : installerOptions.port; + this.logger.info(`listening on port ${port} for OAuth`); + this.logger.info(`Go to http://localhost:${port}/slack/install to initiate OAuth flow`); + // use port 3000 by default + server.listen(port); + } + + this.client.on('slack_event', async ({ ack, body }) => { + const event: ReceiverEvent = { + body, + ack, + }; + await this.bolt?.processEvent(event); + }); + } + + public init(bolt: App): void { + this.bolt = bolt; + } + + public start(): Promise { + return new Promise((resolve, reject) => { + try { + // start socket mode client + this.client.start(); + resolve(); + } catch (error) { + reject(error); + } + }); + } + + public stop(): Promise { + return new Promise((resolve, reject) => { + try { + this.client.disconnect(); + resolve(); + } catch (error) { + reject(error); + } + }); + } +} diff --git a/src/index.ts b/src/index.ts index 7951c22c4..84184cd9b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,8 @@ export { export { default as ExpressReceiver, ExpressReceiverOptions } from './ExpressReceiver'; +export { default as SocketModeReceiver } from './SocketModeReceiver'; + export * from './errors'; export * from './middleware/builtin'; export * from './types'; From 2b47cf50abf67e61ad94bf1010aacf5c22ee0958 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Mon, 14 Dec 2020 22:01:27 -0800 Subject: [PATCH 02/10] added socketMode flag to AppOptions --- src/App.ts | 22 ++++++++++++++++++++++ src/SocketModeReceiver.ts | 14 +++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/App.ts b/src/App.ts index 4ec0254e3..5932840aa 100644 --- a/src/App.ts +++ b/src/App.ts @@ -5,6 +5,7 @@ import util from 'util'; import { WebClient, ChatPostMessageArguments, addAppMetadata, WebClientOptions } from '@slack/web-api'; import { Logger, LogLevel, ConsoleLogger } from '@slack/logger'; import axios, { AxiosInstance } from 'axios'; +import SocketModeReceiver from './SocketModeReceiver'; import ExpressReceiver, { ExpressReceiverOptions } from './ExpressReceiver'; import { ignoreSelf as ignoreSelfMiddleware, @@ -67,6 +68,7 @@ export interface AppOptions { clientTls?: Pick; convoStore?: ConversationStore | false; token?: AuthorizeResult['botToken']; // either token or authorize + appToken?: string; // TODO should this be included in AuthorizeResult botId?: AuthorizeResult['botId']; // only used when authorize is not defined, shortcut for fetching botUserId?: AuthorizeResult['botUserId']; // only used when authorize is not defined, shortcut for fetching authorize?: Authorize; // either token or authorize @@ -75,6 +77,7 @@ export interface AppOptions { logLevel?: LogLevel; ignoreSelf?: boolean; clientOptions?: Pick; + socketMode?: boolean; } export { LogLevel, Logger } from '@slack/logger'; @@ -181,6 +184,7 @@ export default class App { receiver = undefined, convoStore = undefined, token = undefined, + appToken = undefined, botId = undefined, botUserId = undefined, authorize = undefined, @@ -195,6 +199,7 @@ export default class App { installationStore = undefined, scopes = undefined, installerOptions = undefined, + socketMode = false, }: AppOptions = {}) { if (typeof logger === 'undefined') { // Initialize with the default logger @@ -236,6 +241,22 @@ export default class App { // Check for required arguments of ExpressReceiver if (receiver !== undefined) { this.receiver = receiver; + } else if (socketMode) { + if (appToken === undefined) { + throw new AppInitializationError('You must provide an appToken when using socketMode'); + } + this.logger.debug('Initializing SocketModeReceiver'); + // Create default SocketModeReceiver + this.receiver = new SocketModeReceiver({ + appToken, + clientId, + clientSecret, + stateSecret, + installationStore, + scopes, + installerOptions: this.installerOptions, + logger: this.logger, + }); } else if (signingSecret === undefined) { // No custom receiver throw new AppInitializationError( @@ -243,6 +264,7 @@ export default class App { 'custom receiver.', ); } else { + this.logger.debug('Initializing ExpressReceiver'); // Create default ExpressReceiver this.receiver = new ExpressReceiver({ signingSecret, diff --git a/src/SocketModeReceiver.ts b/src/SocketModeReceiver.ts index 5b674eb3e..8bbbda134 100644 --- a/src/SocketModeReceiver.ts +++ b/src/SocketModeReceiver.ts @@ -17,7 +17,7 @@ export interface SocketModeReceiverOptions { installationStore?: InstallProviderOptions['installationStore']; // default MemoryInstallationStore scopes?: InstallURLOptions['scopes']; installerOptions?: InstallerOptions; - token?: string; // App Level Token + appToken?: string; // App Level Token } // Additional Installer Options @@ -41,14 +41,14 @@ export default class SocketModeReceiver implements Receiver { /* Express app */ public client: SocketModeClient; - private bolt: App | undefined; + private app: App | undefined; private logger: Logger; public installer: InstallProvider | undefined = undefined; constructor({ - token = undefined, + appToken = undefined, logger = undefined, logLevel = LogLevel.INFO, clientId = undefined, @@ -59,7 +59,7 @@ export default class SocketModeReceiver implements Receiver { installerOptions = {}, }: SocketModeReceiverOptions) { this.client = new SocketModeClient({ - token, + appToken, logLevel, clientOptions: installerOptions.clientOptions, }); @@ -143,12 +143,12 @@ export default class SocketModeReceiver implements Receiver { body, ack, }; - await this.bolt?.processEvent(event); + await this.app?.processEvent(event); }); } - public init(bolt: App): void { - this.bolt = bolt; + public init(app: App): void { + this.app = app; } public start(): Promise { From 7131ec1a5b0437719aa03a8810fec0a9099707e6 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Tue, 15 Dec 2020 15:25:14 -0800 Subject: [PATCH 03/10] added socket_mode doc to basic concepts --- docs/_basic/socket_mode.md | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/_basic/socket_mode.md diff --git a/docs/_basic/socket_mode.md b/docs/_basic/socket_mode.md new file mode 100644 index 000000000..e72f82cc9 --- /dev/null +++ b/docs/_basic/socket_mode.md @@ -0,0 +1,64 @@ +--- +title: Using Socket Mode +lang: en +slug: socket-mode +order: 16 +--- + +
+With the introduction of [Socket Mode](ADD api.slack.com link when ready), Bolt for JavaScript introduced a new `SocketModeReceiver` in `@slack/bolt@3.0.0`. With Socket Mode, instead of creating a server with endpoints that Slack sends payloads too, the app will instead connect to Slack via a websocket connection and receive data from Slack over the socket connection. Make sure to enable Socket Mode in your app configuration settings. + +To use the new `SocketModeReceiver`, just pass in `socketMode:true` and `appToken:YOUR_APP_TOKEN` when intializing App. You can get your App Token in your app configuration settings under the **Basic Information** section. +
+ +```javascript +const { App } = require('@slack/bolt'); + +const app = new App({ + token: process.env.BOT_TOKEN + socketMode: true, + appToken: process.env.APP_TOKEN, +}); + +(async () => { + await app.start(); + console.log('⚡️ Bolt app started'); +})(); +``` + +
+ +

Custom SocketMode Receiver

+
+ +
+You can define a custom `SocketModeReceiver` by importing it from `@slack/bolt`. + +
+ +```javascript +const { App, SocketModeReceiver } = require('@slack/bolt'); + +const socketModeReceiver = new SocketModeReceiver({ + appToken: process.env.APP_TOKEN, + + // enable the following if you want to use OAuth + // clientId: process.env.CLIENT_ID, + // clientSecret: process.env.CLIENT_SECRET, + // stateSecret: 'my-state-secret', + // scopes: ['channels:read', 'chat:write', 'app_mentions:read', 'channels:manage', 'commands'], +}); + +const app = new App({ + receiver: socketModeReceiver, + // disable token line below if using OAuth + token: process.env.BOT_TOKEN +}); + +(async () => { + await app.start(); + console.log('⚡️ Bolt app started'); +})(); +``` + +
From f6bceb4ded6085cc1a8d2b421f71d6ca7ec4a997 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Tue, 5 Jan 2021 14:53:12 -0800 Subject: [PATCH 04/10] Apply suggestions from code review Co-authored-by: Kazuhiro Sera --- src/App.ts | 2 +- src/SocketModeReceiver.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/App.ts b/src/App.ts index 5932840aa..658cfdb6c 100644 --- a/src/App.ts +++ b/src/App.ts @@ -243,7 +243,7 @@ export default class App { this.receiver = receiver; } else if (socketMode) { if (appToken === undefined) { - throw new AppInitializationError('You must provide an appToken when using socketMode'); + throw new AppInitializationError('You must provide an appToken when using Socket Mode'); } this.logger.debug('Initializing SocketModeReceiver'); // Create default SocketModeReceiver diff --git a/src/SocketModeReceiver.ts b/src/SocketModeReceiver.ts index 8bbbda134..850f6a5bc 100644 --- a/src/SocketModeReceiver.ts +++ b/src/SocketModeReceiver.ts @@ -17,7 +17,7 @@ export interface SocketModeReceiverOptions { installationStore?: InstallProviderOptions['installationStore']; // default MemoryInstallationStore scopes?: InstallURLOptions['scopes']; installerOptions?: InstallerOptions; - appToken?: string; // App Level Token + appToken: string; // App Level Token } // Additional Installer Options @@ -48,7 +48,7 @@ export default class SocketModeReceiver implements Receiver { public installer: InstallProvider | undefined = undefined; constructor({ - appToken = undefined, + appToken, logger = undefined, logLevel = LogLevel.INFO, clientId = undefined, @@ -65,7 +65,6 @@ export default class SocketModeReceiver implements Receiver { }); // const expressMiddleware: RequestHandler[] = [ - // TODO: Should we still be verifying Signature? // verifySignatureAndParseRawBody(logger, signingSecret), // respondToSslCheck, // respondToUrlVerification, From 222224157f80267b0b900b14d333e9b9f280d8fd Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Wed, 6 Jan 2021 16:38:12 -0800 Subject: [PATCH 05/10] cleaned up logger code --- src/App.ts | 6 ++++-- src/SocketModeReceiver.ts | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/App.ts b/src/App.ts index 658cfdb6c..269b7d43c 100644 --- a/src/App.ts +++ b/src/App.ts @@ -254,8 +254,9 @@ export default class App { stateSecret, installationStore, scopes, + logger, + logLevel, installerOptions: this.installerOptions, - logger: this.logger, }); } else if (signingSecret === undefined) { // No custom receiver @@ -275,8 +276,9 @@ export default class App { stateSecret, installationStore, scopes, + logger, + logLevel, installerOptions: this.installerOptions, - logger: this.logger, }); } diff --git a/src/SocketModeReceiver.ts b/src/SocketModeReceiver.ts index 850f6a5bc..c33fb030d 100644 --- a/src/SocketModeReceiver.ts +++ b/src/SocketModeReceiver.ts @@ -61,6 +61,7 @@ export default class SocketModeReceiver implements Receiver { this.client = new SocketModeClient({ appToken, logLevel, + logger, clientOptions: installerOptions.clientOptions, }); From 88f36fc9cad84b8c91ea811ae66f81b53b0c98ff Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Thu, 7 Jan 2021 10:58:58 -0800 Subject: [PATCH 06/10] Update docs/_basic/socket_mode.md Co-authored-by: Kazuhiro Sera --- docs/_basic/socket_mode.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_basic/socket_mode.md b/docs/_basic/socket_mode.md index e72f82cc9..0c2acd1e9 100644 --- a/docs/_basic/socket_mode.md +++ b/docs/_basic/socket_mode.md @@ -6,9 +6,9 @@ order: 16 ---
-With the introduction of [Socket Mode](ADD api.slack.com link when ready), Bolt for JavaScript introduced a new `SocketModeReceiver` in `@slack/bolt@3.0.0`. With Socket Mode, instead of creating a server with endpoints that Slack sends payloads too, the app will instead connect to Slack via a websocket connection and receive data from Slack over the socket connection. Make sure to enable Socket Mode in your app configuration settings. +With the introduction of [Socket Mode](ADD api.slack.com link when ready), Bolt for JavaScript introduced a new `SocketModeReceiver` in `@slack/bolt@3.0.0`. With Socket Mode, instead of creating a server with endpoints that Slack sends payloads too, the app will instead connect to Slack via a WebSocket connection and receive data from Slack over the socket connection. Make sure to enable Socket Mode in your app configuration settings. -To use the new `SocketModeReceiver`, just pass in `socketMode:true` and `appToken:YOUR_APP_TOKEN` when intializing App. You can get your App Token in your app configuration settings under the **Basic Information** section. +To use the new `SocketModeReceiver`, just pass in `socketMode:true` and `appToken:YOUR_APP_TOKEN` when initializing App. You can get your App Token in your app configuration settings under the **Basic Information** section.
```javascript From 881f3fd24723afafdaf1893ccf4333699b09f3cb Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Fri, 8 Jan 2021 14:33:04 -0800 Subject: [PATCH 07/10] added socket-mode example --- docs/_basic/socket_mode.md | 2 +- examples/socket-mode/README.md | 70 +++++++++++ examples/socket-mode/app.js | 185 ++++++++++++++++++++++++++++++ examples/socket-mode/package.json | 15 +++ 4 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 examples/socket-mode/README.md create mode 100644 examples/socket-mode/app.js create mode 100644 examples/socket-mode/package.json diff --git a/docs/_basic/socket_mode.md b/docs/_basic/socket_mode.md index 0c2acd1e9..83f541da6 100644 --- a/docs/_basic/socket_mode.md +++ b/docs/_basic/socket_mode.md @@ -6,7 +6,7 @@ order: 16 ---
-With the introduction of [Socket Mode](ADD api.slack.com link when ready), Bolt for JavaScript introduced a new `SocketModeReceiver` in `@slack/bolt@3.0.0`. With Socket Mode, instead of creating a server with endpoints that Slack sends payloads too, the app will instead connect to Slack via a WebSocket connection and receive data from Slack over the socket connection. Make sure to enable Socket Mode in your app configuration settings. +With the introduction of [Socket Mode](https://api.slack.com/socket-mode), Bolt for JavaScript introduced a new `SocketModeReceiver` in `@slack/bolt@3.0.0`. With Socket Mode, instead of creating a server with endpoints that Slack sends payloads too, the app will instead connect to Slack via a WebSocket connection and receive data from Slack over the socket connection. Make sure to enable Socket Mode in your app configuration settings. To use the new `SocketModeReceiver`, just pass in `socketMode:true` and `appToken:YOUR_APP_TOKEN` when initializing App. You can get your App Token in your app configuration settings under the **Basic Information** section.
diff --git a/examples/socket-mode/README.md b/examples/socket-mode/README.md new file mode 100644 index 000000000..91196b6d2 --- /dev/null +++ b/examples/socket-mode/README.md @@ -0,0 +1,70 @@ +# Bolt-js Socket Mode Test App + +This is a quick example app to test socket-mode with bolt-js. + +If using OAuth, local development requires a public URL where Slack can send requests. In this guide, we'll be using [`ngrok`](https://ngrok.com/download). Checkout [this guide](https://api.slack.com/tutorials/tunneling-with-ngrok) for setting it up. OAuth installation is only needed for public distribution. For internal apps, we recommend installing via your app config. + +Before we get started, make sure you have a development workspace where you have permissions to install apps. If you don’t have one setup, go ahead and [create one](https://slack.com/create). You also need to [create a new app](https://api.slack.com/apps?new_app=1) if you haven’t already. You will need to enable Socket Mode and generate an App Level Token. + +## Install Dependencies + +``` +npm install +``` + +## Install app to workspace + +In your [**App Config** Page](https://api.slack.com/apps), go to **OAuth & Permissions** and add the `channels:read`, `app_mentions:read`, `commands` and `chat:write` permissions. Click **install App** to install the app to your workspace and generate a bot token. + +Then go to the **Socket Mode** section in App Config to enable it. + +Go to **Basic Information** section in App Config and generate a `App Level Token` with the `connections:write` scope. + +Navigate to the **App Home** page in your app config and enable it. + +Lastly, in the **Events Subscription** page, click **Subscribe to bot events** and add `app_home_opened`, `app_mentioned`, and `message.channels`. + +## Setup Environment Variables + +This app requires you setup a few environment variables. You can get these values by navigating to your [**App Config** Page](https://api.slack.com/apps). + +``` +// can get this from OAuth & Permission page in app config +export BOT_TOKEN=YOUR_SLACK_BOT_TOKEN +// can generate the app token from basic information page in app config +export APP_TOKEN=YOUR_SLACK_APP_TOKEN + +// if using OAuth, also export the following +export CLIENT_ID=YOUR_SLACK_CLIENT_ID +export CLIENT_SECRET=YOUR_SLACK_CLIENT_SECRET +``` + +## Run the App + +Start the app with the following command: + +``` +npm start +``` + +### Running with OAuth + +Only implement OAuth if you plan to distribute your application publicly. Uncomment out the OAuth specific comments in the code. If you are on dev instance, you will have to uncomment out those options as well. + +Start `ngrok` so we can access the app on an external network and create a `redirect url` for OAuth. + +``` +ngrok http 3000 +``` + +This should output a forwarding address for `http` and `https`. Take note of the `https` one. It should look something like the following: + +``` +Forwarding https://3cb89939.ngrok.io -> http://localhost:3000 +``` + +Then navigate to **OAuth & Permissions** in your App Config and add a Redirect Url. The redirect URL should be set to your `ngrok` forwarding address with the `slack/oauth_redirect` path appended. ex: + +``` +https://3cb89939.ngrok.io/slack/oauth_redirect +``` diff --git a/examples/socket-mode/app.js b/examples/socket-mode/app.js new file mode 100644 index 000000000..2439d7382 --- /dev/null +++ b/examples/socket-mode/app.js @@ -0,0 +1,185 @@ +const { App, LogLevel, SocketModeReceiver } = require('@slack/bolt'); + +const clientOptions = { + // enable this for dev instance + // slackApiUrl: 'https://dev.slack.com/api/' +}; + +// const socketModeReceiver = new SocketModeReceiver({ +// appToken: process.env.APP_TOKEN, +// installerOptions: { +// clientOptions, +// // use the following when running against a dev instance and using OAuth +// // authorizationUrl: 'https://dev.slack.com/oauth/v2/authorize', +// }, + +// // enable the following if you want to use OAuth +// // clientId: process.env.CLIENT_ID, +// // clientSecret: process.env.CLIENT_SECRET, +// // stateSecret: 'my-state-secret', +// // scopes: ['channels:read', 'chat:write', 'app_mentions:read', 'channels:manage', 'commands'], + +// logLevel: LogLevel.DEBUG, +// }); + +const app = new App({ + // receiver: socketModeReceiver, + token: process.env.BOT_TOKEN, //disable this if enabling OAuth in socketModeReceiver + // logLevel: LogLevel.DEBUG, + clientOptions, + appToken: process.env.APP_TOKEN, + socketMode: true, +}); + +(async () => { + await app.start(); + console.log('⚡️ Bolt app started'); +})(); + +// Publish a App Home +app.event('app_home_opened', async ({ event, client }) => { + await client.views.publish({ + user_id: event.user, + view: { + "type":"home", + "blocks":[ + { + "type": "section", + "block_id": "section678", + "text": { + "type": "mrkdwn", + "text": "App Home Published" + }, + } + ] + }, + }); +}); + +// Message Shortcut example +app.shortcut('launch_msg_shortcut', async ({ shortcut, body, ack, context, client }) => { + await ack(); + console.log(shortcut); +}); + +// Global Shortcut example +// setup global shortcut in App config with `launch_shortcut` as callback id +// add `commands` scope +app.shortcut('launch_shortcut', async ({ shortcut, body, ack, context, client }) => { + try { + // Acknowledge shortcut request + await ack(); + + // Call the views.open method using one of the built-in WebClients + const result = await client.views.open({ + trigger_id: shortcut.trigger_id, + view: { + type: "modal", + title: { + type: "plain_text", + text: "My App" + }, + close: { + type: "plain_text", + text: "Close" + }, + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "About the simplest modal you could conceive of :smile:\n\nMaybe or ." + } + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "Psssst this modal was designed using " + } + ] + } + ] + } + }); + } + catch (error) { + console.error(error); + } +}); + + +// subscribe to 'app_mention' event in your App config +// need app_mentions:read and chat:write scopes +app.event('app_mention', async ({ event, context, client, say }) => { + try { + await say({"blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": `Thanks for the mention <@${event.user}>! Click my fancy button` + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Button", + "emoji": true + }, + "value": "click_me_123", + "action_id": "first_button" + } + } + ]}); + } + catch (error) { + console.error(error); + } +}); + +// subscribe to `message.channels` event in your App Config +// need channels:read scope +app.message('hello', async ({ message, say }) => { + // say() sends a message to the channel where the event was triggered + // no need to directly use 'chat.postMessage', no need to include token + await say({"blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": `Thanks for the mention <@${message.user}>! Click my fancy button` + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Button", + "emoji": true + }, + "value": "click_me_123", + "action_id": "first_button" + } + } + ]}); +}); + +// Listen and respond to button click +app.action('first_button', async({action, ack, say, context}) => { + console.log('button clicked'); + console.log(action); + // acknowledge the request right away + await ack(); + await say('Thanks for clicking the fancy button'); +}); + +// Listen to slash command +// need to add commands permission +// create slash command in App Config +app.command('/socketslash', async ({ command, ack, say }) => { + // Acknowledge command request + await ack(); + + await say(`${command.text}`); +}); diff --git a/examples/socket-mode/package.json b/examples/socket-mode/package.json new file mode 100644 index 000000000..d4730de7e --- /dev/null +++ b/examples/socket-mode/package.json @@ -0,0 +1,15 @@ +{ + "name": "bolt-socket-mode-example", + "version": "1.0.0", + "description": "Example app using socket mode", + "main": "index.js", + "scripts": { + "start": "node app.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Slack Technologies, Inc.", + "license": "MIT", + "dependencies": { + "@slack/bolt": "feat-socket-mode" + } +} From 45cbbac9391a2644cf879dbba5dd64887b90ee87 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Sun, 10 Jan 2021 23:02:43 -0800 Subject: [PATCH 08/10] added initial Developer Mode --- examples/socket-mode/README.md | 2 +- src/App.ts | 60 ++++++++++++++++++++++++++++++---- src/SocketModeReceiver.ts | 24 ++++++-------- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/examples/socket-mode/README.md b/examples/socket-mode/README.md index 91196b6d2..fee478f9a 100644 --- a/examples/socket-mode/README.md +++ b/examples/socket-mode/README.md @@ -1,6 +1,6 @@ # Bolt-js Socket Mode Test App -This is a quick example app to test socket-mode with bolt-js. +This is a quick example app to test [Socket Mode](https://api.slack.com/socket-mode) with bolt-js. If using OAuth, local development requires a public URL where Slack can send requests. In this guide, we'll be using [`ngrok`](https://ngrok.com/download). Checkout [this guide](https://api.slack.com/tutorials/tunneling-with-ngrok) for setting it up. OAuth installation is only needed for public distribution. For internal apps, we recommend installing via your app config. diff --git a/src/App.ts b/src/App.ts index 269b7d43c..e39de9f68 100644 --- a/src/App.ts +++ b/src/App.ts @@ -78,6 +78,7 @@ export interface AppOptions { ignoreSelf?: boolean; clientOptions?: Pick; socketMode?: boolean; + developerMode?: boolean; } export { LogLevel, Logger } from '@slack/logger'; @@ -161,6 +162,9 @@ export default class App { /** Logger */ private logger: Logger; + /** Log Level */ + private logLevel: LogLevel; + /** Authorize */ private authorize!: Authorize; @@ -176,6 +180,10 @@ export default class App { private installerOptions: ExpressReceiverOptions['installerOptions']; + private socketMode: boolean; + + private developerMode: boolean; + constructor({ signingSecret = undefined, endpoints = undefined, @@ -199,8 +207,23 @@ export default class App { installationStore = undefined, scopes = undefined, installerOptions = undefined, - socketMode = false, + socketMode = undefined, + developerMode = false, }: AppOptions = {}) { + // this.logLevel = logLevel; + this.developerMode = developerMode; + if (developerMode) { + // Set logLevel to Debug in Developer Mode if one wasn't passed in + this.logLevel = logLevel ?? 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 ?? LogLevel.INFO; + } + if (typeof logger === 'undefined') { // Initialize with the default logger const consoleLogger = new ConsoleLogger(); @@ -209,8 +232,8 @@ export default class App { } else { this.logger = logger; } - if (typeof logLevel !== 'undefined' && this.logger.getLevel() !== logLevel) { - this.logger.setLevel(logLevel); + if (typeof this.logLevel !== 'undefined' && this.logger.getLevel() !== this.logLevel) { + this.logger.setLevel(this.logLevel); } this.errorHandler = defaultErrorHandler(this.logger); this.clientOptions = { @@ -238,10 +261,28 @@ export default class App { ...installerOptions, }; + 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(`

OAuth failed!

${error}
`); + }, + }; + } + // Check for required arguments of ExpressReceiver if (receiver !== undefined) { this.receiver = receiver; - } else if (socketMode) { + } else if (this.socketMode) { if (appToken === undefined) { throw new AppInitializationError('You must provide an appToken when using Socket Mode'); } @@ -255,7 +296,7 @@ export default class App { installationStore, scopes, logger, - logLevel, + logLevel: this.logLevel, installerOptions: this.installerOptions, }); } else if (signingSecret === undefined) { @@ -277,7 +318,7 @@ export default class App { installationStore, scopes, logger, - logLevel, + logLevel: this.logLevel, installerOptions: this.installerOptions, }); } @@ -543,6 +584,13 @@ export default class App { */ public async processEvent(event: ReceiverEvent): Promise { 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. diff --git a/src/SocketModeReceiver.ts b/src/SocketModeReceiver.ts index c33fb030d..5f189e929 100644 --- a/src/SocketModeReceiver.ts +++ b/src/SocketModeReceiver.ts @@ -65,13 +65,6 @@ export default class SocketModeReceiver implements Receiver { clientOptions: installerOptions.clientOptions, }); - // const expressMiddleware: RequestHandler[] = [ - // verifySignatureAndParseRawBody(logger, signingSecret), - // respondToSslCheck, - // respondToUrlVerification, - // this.requestHandler.bind(this), - // ]; - if (typeof logger !== 'undefined') { this.logger = logger; } else { @@ -108,12 +101,10 @@ export default class SocketModeReceiver implements Receiver { const installPath = installerOptions.installPath === undefined ? '/slack/install' : installerOptions.installPath; const server = createServer(async (req, res) => { - if (req.url === redirectUriPath) { + if (req.url !== undefined && req.url.startsWith(redirectUriPath)) { // call installer.handleCallback to wrap up the install flow - await this.installer!.handleCallback(req, res); - } - - if (req.url === installPath) { + await this.installer!.handleCallback(req, res, installerOptions.callbackOptions); + } else if (req.url !== undefined && req.url.startsWith(installPath)) { try { const url = await this.installer!.generateInstallUrl({ metadata: installerOptions.metadata, @@ -128,12 +119,17 @@ export default class SocketModeReceiver implements Receiver { } catch (err) { throw new Error(err); } + } else { + this.logger.error(`Tried to reach ${req.url} which isn't a`); + // Return 404 because we don't support route + res.writeHead(404, {}); + res.end(`route ${req.url} doesn't exist!`); } }); const port = installerOptions.port === undefined ? 3000 : installerOptions.port; - this.logger.info(`listening on port ${port} for OAuth`); - this.logger.info(`Go to http://localhost:${port}/slack/install to initiate OAuth flow`); + this.logger.debug(`listening on port ${port} for OAuth`); + this.logger.debug(`Go to http://localhost:${port}${installPath} to initiate OAuth flow`); // use port 3000 by default server.listen(port); } From b2064b9902825a6b683d2adb126e0848b8ee937b Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Mon, 11 Jan 2021 18:32:45 -0800 Subject: [PATCH 09/10] Apply suggestions from code review Co-authored-by: Shay DeWael --- docs/_basic/socket_mode.md | 4 ++-- examples/socket-mode/README.md | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/_basic/socket_mode.md b/docs/_basic/socket_mode.md index 83f541da6..c0f3bbd18 100644 --- a/docs/_basic/socket_mode.md +++ b/docs/_basic/socket_mode.md @@ -6,9 +6,9 @@ order: 16 ---
-With the introduction of [Socket Mode](https://api.slack.com/socket-mode), Bolt for JavaScript introduced a new `SocketModeReceiver` in `@slack/bolt@3.0.0`. With Socket Mode, instead of creating a server with endpoints that Slack sends payloads too, the app will instead connect to Slack via a WebSocket connection and receive data from Slack over the socket connection. Make sure to enable Socket Mode in your app configuration settings. +[Socket Mode](https://api.slack.com/socket-mode) allows your app to connect and receive data from Slack via a WebSocket connection. To handle the connection, Bolt for JavaScript includes a `SocketModeReceiver` (in `@slack/bolt@3.0.0` and higher). Before using Socket Mode, be sure to enable it within your app configuration. -To use the new `SocketModeReceiver`, just pass in `socketMode:true` and `appToken:YOUR_APP_TOKEN` when initializing App. You can get your App Token in your app configuration settings under the **Basic Information** section. +To use the `SocketModeReceiver`, just pass in `socketMode:true` and `appToken:YOUR_APP_TOKEN` when initializing `App`. You can get your App Token in your app configuration under the **Basic Information** section.
```javascript diff --git a/examples/socket-mode/README.md b/examples/socket-mode/README.md index fee478f9a..99643af76 100644 --- a/examples/socket-mode/README.md +++ b/examples/socket-mode/README.md @@ -1,4 +1,4 @@ -# Bolt-js Socket Mode Test App +# Bolt for JavaScript Socket Mode Test App This is a quick example app to test [Socket Mode](https://api.slack.com/socket-mode) with bolt-js. @@ -14,19 +14,19 @@ npm install ## Install app to workspace -In your [**App Config** Page](https://api.slack.com/apps), go to **OAuth & Permissions** and add the `channels:read`, `app_mentions:read`, `commands` and `chat:write` permissions. Click **install App** to install the app to your workspace and generate a bot token. +In your [app configuration](https://api.slack.com/apps), go to **OAuth & Permissions** and add the `channels:read`, `app_mentions:read`, `commands`, and `chat:write` permissions. Click **Install App** to install the app to your workspace and generate a bot token. -Then go to the **Socket Mode** section in App Config to enable it. +Next, navigate to the **Socket Mode** section and toggle the **Enable Socket Mode** button to start receiving events over a WebSocket connection. -Go to **Basic Information** section in App Config and generate a `App Level Token` with the `connections:write` scope. +Next, click on **Basic Information** and generate a `App Level Token` with the `connections:write` scope. -Navigate to the **App Home** page in your app config and enable it. +Then navigate to **App Home**. Under **Show tabs**, toggle the **Home tab** option. -Lastly, in the **Events Subscription** page, click **Subscribe to bot events** and add `app_home_opened`, `app_mentioned`, and `message.channels`. +Lastly, in **Events Subscription**, click **Subscribe to bot events** and add `app_home_opened`, `app_mentioned`, and `message.channels`. ## Setup Environment Variables -This app requires you setup a few environment variables. You can get these values by navigating to your [**App Config** Page](https://api.slack.com/apps). +This app requires you setup a few environment variables. You can find these values in your [app configuration](https://api.slack.com/apps). ``` // can get this from OAuth & Permission page in app config @@ -57,13 +57,13 @@ Start `ngrok` so we can access the app on an external network and create a `redi ngrok http 3000 ``` -This should output a forwarding address for `http` and `https`. Take note of the `https` one. It should look something like the following: +This output should include a forwarding address for `http` and `https` (we'll use the `https` one). It should look something like the following: ``` Forwarding https://3cb89939.ngrok.io -> http://localhost:3000 ``` -Then navigate to **OAuth & Permissions** in your App Config and add a Redirect Url. The redirect URL should be set to your `ngrok` forwarding address with the `slack/oauth_redirect` path appended. ex: +Then navigate to **OAuth & Permissions** in your app configuration and click **Add a Redirect URL**. The redirect URL should be set to your `ngrok` forwarding address with the `slack/oauth_redirect` path appended. ex: ``` https://3cb89939.ngrok.io/slack/oauth_redirect From ea3b645d1890ead7d71e83a409aa882984b1b5e2 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Mon, 11 Jan 2021 21:35:13 -0800 Subject: [PATCH 10/10] updated based on feedback --- docs/_basic/socket_mode.md | 2 +- examples/socket-mode/README.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/_basic/socket_mode.md b/docs/_basic/socket_mode.md index c0f3bbd18..58a3e496f 100644 --- a/docs/_basic/socket_mode.md +++ b/docs/_basic/socket_mode.md @@ -8,7 +8,7 @@ order: 16
[Socket Mode](https://api.slack.com/socket-mode) allows your app to connect and receive data from Slack via a WebSocket connection. To handle the connection, Bolt for JavaScript includes a `SocketModeReceiver` (in `@slack/bolt@3.0.0` and higher). Before using Socket Mode, be sure to enable it within your app configuration. -To use the `SocketModeReceiver`, just pass in `socketMode:true` and `appToken:YOUR_APP_TOKEN` when initializing `App`. You can get your App Token in your app configuration under the **Basic Information** section. +To use the `SocketModeReceiver`, just pass in `socketMode:true` and `appToken:YOUR_APP_TOKEN` when initializing `App`. You can get your App Level Token in your app configuration under the **Basic Information** section.
```javascript diff --git a/examples/socket-mode/README.md b/examples/socket-mode/README.md index 99643af76..693104802 100644 --- a/examples/socket-mode/README.md +++ b/examples/socket-mode/README.md @@ -1,8 +1,8 @@ # Bolt for JavaScript Socket Mode Test App -This is a quick example app to test [Socket Mode](https://api.slack.com/socket-mode) with bolt-js. +This is a quick example app to test [Socket Mode](https://api.slack.com/socket-mode) with Bolt for JavaScript. -If using OAuth, local development requires a public URL where Slack can send requests. In this guide, we'll be using [`ngrok`](https://ngrok.com/download). Checkout [this guide](https://api.slack.com/tutorials/tunneling-with-ngrok) for setting it up. OAuth installation is only needed for public distribution. For internal apps, we recommend installing via your app config. +If using OAuth, Slack requires a public URL where it can send requests. In this guide, we'll be using [`ngrok`](https://ngrok.com/download). Checkout [this guide](https://api.slack.com/tutorials/tunneling-with-ngrok) for setting it up. OAuth installation is only needed for public distribution. For internal apps, we recommend installing via your app configuration. Before we get started, make sure you have a development workspace where you have permissions to install apps. If you don’t have one setup, go ahead and [create one](https://slack.com/create). You also need to [create a new app](https://api.slack.com/apps?new_app=1) if you haven’t already. You will need to enable Socket Mode and generate an App Level Token. @@ -29,9 +29,9 @@ Lastly, in **Events Subscription**, click **Subscribe to bot events** and add `a This app requires you setup a few environment variables. You can find these values in your [app configuration](https://api.slack.com/apps). ``` -// can get this from OAuth & Permission page in app config +// can get this from OAuth & Permission page in app configuration export BOT_TOKEN=YOUR_SLACK_BOT_TOKEN -// can generate the app token from basic information page in app config +// can generate the app level token from basic information page in app configuration export APP_TOKEN=YOUR_SLACK_APP_TOKEN // if using OAuth, also export the following @@ -49,9 +49,9 @@ npm start ### Running with OAuth -Only implement OAuth if you plan to distribute your application publicly. Uncomment out the OAuth specific comments in the code. If you are on dev instance, you will have to uncomment out those options as well. +Only implement OAuth if you plan to distribute your application across multiple workspaces. Uncomment out the OAuth specific comments in the code. If you are on dev instance, you will have to uncomment out those options as well. -Start `ngrok` so we can access the app on an external network and create a `redirect url` for OAuth. +Start `ngrok` so we can access the app on an external network and create a redirect URL for OAuth. ``` ngrok http 3000