From 03a07eadf3b435d9e963d043fe38ace9a85e8280 Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Thu, 1 Oct 2020 23:51:41 -0700 Subject: [PATCH 01/31] add support for https servers, with server options, and listen options --- src/App.ts | 76 +++++++++++++++++++++--------------------- src/ExpressReceiver.ts | 50 +++++++++++++++++++-------- 2 files changed, 74 insertions(+), 52 deletions(-) diff --git a/src/App.ts b/src/App.ts index b9f1a5903..33bd243b1 100644 --- a/src/App.ts +++ b/src/App.ts @@ -236,7 +236,7 @@ export default class App { // No custom receiver throw new AppInitializationError( 'Signing secret not found, so could not initialize the default receiver. Set a signing secret or use a ' + - 'custom receiver.', + 'custom receiver.', ); } else { // Create default ExpressReceiver @@ -326,12 +326,12 @@ export default class App { /** * Convenience method to call start on the receiver * - * TODO: args could be defined using a generic constraint from the receiver type + * TODO: should replace ExpressReceiver in type definition with a generic that is constrained to Receiver * * @param args receiver-specific start arguments */ - public start(...args: any[]): Promise { - return this.receiver.start(...args); + public start(...args: Parameters): ReturnType { + return this.receiver.start(...args) as ReturnType; } public stop(...args: any[]): Promise { @@ -565,19 +565,19 @@ export default class App { type === IncomingEventType.Event ? (bodyArg as SlackEventMiddlewareArgs['body']).event : type === IncomingEventType.ViewAction - ? (bodyArg as SlackViewMiddlewareArgs['body']).view - : type === IncomingEventType.Shortcut - ? (bodyArg as SlackShortcutMiddlewareArgs['body']) - : type === IncomingEventType.Action && - isBlockActionOrInteractiveMessageBody(bodyArg as SlackActionMiddlewareArgs['body']) - ? (bodyArg as SlackActionMiddlewareArgs['body']).actions[0] - : (bodyArg as ( - | Exclude< - AnyMiddlewareArgs, - SlackEventMiddlewareArgs | SlackActionMiddlewareArgs | SlackViewMiddlewareArgs - > - | SlackActionMiddlewareArgs> - )['body']), + ? (bodyArg as SlackViewMiddlewareArgs['body']).view + : type === IncomingEventType.Shortcut + ? (bodyArg as SlackShortcutMiddlewareArgs['body']) + : type === IncomingEventType.Action && + isBlockActionOrInteractiveMessageBody(bodyArg as SlackActionMiddlewareArgs['body']) + ? (bodyArg as SlackActionMiddlewareArgs['body']).actions[0] + : (bodyArg as ( + | Exclude< + AnyMiddlewareArgs, + SlackEventMiddlewareArgs | SlackActionMiddlewareArgs | SlackViewMiddlewareArgs + > + | SlackActionMiddlewareArgs> + )['body']), }; // Set aliases @@ -721,13 +721,13 @@ function buildSource( type === IncomingEventType.Options || type === IncomingEventType.ViewAction || type === IncomingEventType.Shortcut - ? ((body as ( + ? ((body as ( | SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs | SlackShortcutMiddlewareArgs )['body']).team.id as string) - : assertNever(type), + : assertNever(type), enterpriseId: type === IncomingEventType.Event || type === IncomingEventType.Command ? ((body as (SlackEventMiddlewareArgs | SlackCommandMiddlewareArgs)['body']).enterprise_id as string) @@ -735,35 +735,35 @@ function buildSource( type === IncomingEventType.Options || type === IncomingEventType.ViewAction || type === IncomingEventType.Shortcut - ? ((body as ( + ? ((body as ( | SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs | SlackShortcutMiddlewareArgs )['body']).team.enterprise_id as string) - : undefined, + : undefined, userId: type === IncomingEventType.Event ? typeof (body as SlackEventMiddlewareArgs['body']).event.user === 'string' ? ((body as SlackEventMiddlewareArgs['body']).event.user as string) : typeof (body as SlackEventMiddlewareArgs['body']).event.user === 'object' - ? ((body as SlackEventMiddlewareArgs['body']).event.user.id as string) - : (body as SlackEventMiddlewareArgs['body']).event.channel !== undefined && - (body as SlackEventMiddlewareArgs['body']).event.channel.creator !== undefined - ? ((body as SlackEventMiddlewareArgs['body']).event.channel.creator as string) - : (body as SlackEventMiddlewareArgs['body']).event.subteam !== undefined && - (body as SlackEventMiddlewareArgs['body']).event.subteam.created_by !== undefined - ? ((body as SlackEventMiddlewareArgs['body']).event.subteam.created_by as string) - : undefined + ? ((body as SlackEventMiddlewareArgs['body']).event.user.id as string) + : (body as SlackEventMiddlewareArgs['body']).event.channel !== undefined && + (body as SlackEventMiddlewareArgs['body']).event.channel.creator !== undefined + ? ((body as SlackEventMiddlewareArgs['body']).event.channel.creator as string) + : (body as SlackEventMiddlewareArgs['body']).event.subteam !== undefined && + (body as SlackEventMiddlewareArgs['body']).event.subteam.created_by !== undefined + ? ((body as SlackEventMiddlewareArgs['body']).event.subteam.created_by as string) + : undefined : type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewAction || type === IncomingEventType.Shortcut - ? ((body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).user + ? ((body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).user .id as string) - : type === IncomingEventType.Command - ? ((body as SlackCommandMiddlewareArgs['body']).user_id as string) - : undefined, + : type === IncomingEventType.Command + ? ((body as SlackCommandMiddlewareArgs['body']).user_id as string) + : undefined, conversationId: channelId, }; // tslint:enable:max-line-length @@ -794,11 +794,11 @@ function singleTeamAuthorization( authorization.botUserId !== undefined && authorization.botId !== undefined ? Promise.resolve({ botUserId: authorization.botUserId, botId: authorization.botId }) : client.auth.test({ token: authorization.botToken }).then((result) => { - return { - botUserId: result.user_id as string, - botId: result.bot_id as string, - }; - }); + return { + botUserId: result.user_id as string, + botId: result.bot_id as string, + }; + }); return async () => { return { botToken: authorization.botToken, ...(await identifiers) }; diff --git a/src/ExpressReceiver.ts b/src/ExpressReceiver.ts index cca9653e2..8c17fc484 100644 --- a/src/ExpressReceiver.ts +++ b/src/ExpressReceiver.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/explicit-member-accessibility, @typescript-eslint/strict-boolean-expressions */ -import { createServer, Server } from 'http'; +import { createServer, Server, ServerOptions } from 'http'; +import { createServer as createHttpsServer, Server as HTTPSServer, ServerOptions as HTTPSServerOptions } from 'https'; +import { ListenOptions } from 'net'; import express, { Request, Response, Application, RequestHandler, Router } from 'express'; import rawBody from 'raw-body'; import querystring from 'querystring'; @@ -18,10 +20,10 @@ export interface ExpressReceiverOptions { signingSecret: string; logger?: Logger; endpoints?: - | string - | { - [endpointType: string]: string; - }; + | string + | { + [endpointType: string]: string; + }; processBeforeResponse?: boolean; clientId?: string; clientSecret?: string; @@ -51,7 +53,7 @@ export default class ExpressReceiver implements Receiver { /* Express app */ public app: Application; - private server: Server; + private server?: Server; private bolt: App | undefined; @@ -76,8 +78,6 @@ export default class ExpressReceiver implements Receiver { installerOptions = {}, }: ExpressReceiverOptions) { this.app = express(); - // TODO: what about starting an https server instead of http? what about other options to create the server? - this.server = createServer(this.app); const expressMiddleware: RequestHandler[] = [ verifySignatureAndParseRawBody(logger, signingSecret), @@ -146,7 +146,7 @@ export default class ExpressReceiver implements Receiver { if (!isAcknowledged) { this.logger.error( 'An incoming event was not acknowledged within 3 seconds. ' + - 'Ensure that the ack() argument is called in a listener.', + 'Ensure that the ack() argument is called in a listener.', ); } // tslint:disable-next-line: align @@ -201,15 +201,24 @@ export default class ExpressReceiver implements Receiver { this.bolt = bolt; } - // TODO: the arguments should be defined as the arguments of Server#listen() - // TODO: the return value should be defined as a type that both http and https servers inherit from, or a union - public start(port: number): Promise { + public start(port: number): Promise; + public start(portOrListenOptions: number | ListenOptions, serverOptions?: ServerOptions): Promise; + public start(portOrListenOptions: number | ListenOptions, httpsServerOptions?: HTTPSServerOptions): Promise; + public start(portOrListenOptions: number | ListenOptions, serverOptions: ServerOptions | HTTPSServerOptions = {}): Promise { + let createServerFn: (typeof createServer | typeof createHttpsServer) = createServer; + + // Decide which kind of server, HTTP or HTTPS, by search for any keys in the serverOptions that are exclusive to HTTPS + if (Object.keys(serverOptions).filter(k => httpsOptionKeys.includes(k)).length > 0) { + createServerFn = createHttpsServer; + } + + this.server = createServerFn(serverOptions, this.app); + return new Promise((resolve, reject) => { try { - // TODO: what about other listener options? // TODO: what about asynchronous errors? should we attach a handler for this.server.on('error', ...)? // if so, how can we check for only errors related to listening, as opposed to later errors? - this.server.listen(port, () => { + this.server!.listen(portOrListenOptions, () => { resolve(this.server); }); } catch (error) { @@ -222,6 +231,9 @@ export default class ExpressReceiver implements Receiver { // generic types public stop(): Promise { return new Promise((resolve, reject) => { + if (this.server === undefined) { + return reject(new Error('The receiver was not started.')); + } // TODO: what about synchronous errors? this.server.close((error) => { if (error !== undefined) { @@ -366,3 +378,13 @@ function parseRequestBody(stringBody: string, contentType: string | undefined): return JSON.parse(stringBody); } + +// Option keys for tls.createServer() and tls.createSecureContext(), exclusive of those for http.createServer() +const httpsOptionKeys = [ + 'ALPNProtocols', 'clientCertEngine', 'enableTrace', 'handshakeTimeout', 'rejectUnauthorized', 'requestCert', + 'sessionTimeout', 'SNICallback', 'ticketKeys', 'pskCallback', 'pskIdentityHint', + + 'ca', 'cert', 'sigalgs', 'ciphers', 'clientCertEngine', 'crl', 'dhparam', 'ecdhCurve', 'honorCipherOrder', 'key', + 'privateKeyEngine', 'privateKeyIdentifier', 'maxVersion', 'minVersion', 'passphrase', 'pfx', 'secureOptions', + 'secureProtocol', 'sessionIdContext', +] From ffa7a9a00b1dfe2d1b6fb084ea6ca10121d112fa Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Fri, 2 Oct 2020 02:08:51 -0700 Subject: [PATCH 02/31] adds asynchronous error handling and server state cleanup --- src/ExpressReceiver.ts | 59 +++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/src/ExpressReceiver.ts b/src/ExpressReceiver.ts index 8c17fc484..9d1da709b 100644 --- a/src/ExpressReceiver.ts +++ b/src/ExpressReceiver.ts @@ -212,18 +212,53 @@ export default class ExpressReceiver implements Receiver { createServerFn = createHttpsServer; } + if (this.server !== undefined) { + // TODO: CodedError + return Promise.reject(new Error('The receiver cannot be started because it was already started.')); + } + this.server = createServerFn(serverOptions, this.app); return new Promise((resolve, reject) => { - try { - // TODO: what about asynchronous errors? should we attach a handler for this.server.on('error', ...)? - // if so, how can we check for only errors related to listening, as opposed to later errors? - this.server!.listen(portOrListenOptions, () => { - resolve(this.server); - }); - } catch (error) { - reject(error); + if (this.server === undefined) { + // TODO: CodedError + throw new Error('The receiver cannot be started because private state was mutated. Please report this to ' + + 'the maintainers.'); } + + this.server.on('error', (error) => { + if (this.server === undefined) { + // TODO: CodedError + throw new Error('The receiver cannot be started because private state was mutated. Please report this ' + + 'to the maintainers.'); + } + + this.server.close(); + + // If the error event occurs before listening completes (like EADDRINUSE), this works well. However, if the + // error event happens some after the Promise is already resolved, the error would be silently swallowed up. + // The documentation doesn't describe any specific errors that can occur after listening has started, so this + // feels safe. + reject(error); + }); + + this.server.on('close', () => { + // Not removing all listeners because consumers could have added their own `close` event listener, and those + // should be called. If the consumer doesn't dispose of any references to the server properly, this would be + // a memory leak. + // this.server?.removeAllListeners(); + this.server = undefined; + }); + + this.server.listen(portOrListenOptions, () => { + if (this.server === undefined) { + // TODO: CodedError + return reject(new Error('The receiver cannot be started because private state was mutated. Please report ' + + 'this to the maintainers.')); + } + + resolve(this.server); + }); }); } @@ -232,13 +267,13 @@ export default class ExpressReceiver implements Receiver { public stop(): Promise { return new Promise((resolve, reject) => { if (this.server === undefined) { - return reject(new Error('The receiver was not started.')); + // TODO: CodedError + return reject(new Error('The receiver cannot be stopped because it was not started.')); } - // TODO: what about synchronous errors? this.server.close((error) => { + // NOTE: close listener added in start() is responsible for unsetting this.server if (error !== undefined) { - reject(error); - return; + return reject(error); } resolve(); From 989178f88bd89f35062d4247fb6b0c12b1f40f31 Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Fri, 2 Oct 2020 02:17:44 -0700 Subject: [PATCH 03/31] new coded error type --- src/ExpressReceiver.ts | 25 ++++++++++--------------- src/errors.ts | 5 +++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/ExpressReceiver.ts b/src/ExpressReceiver.ts index 9d1da709b..05555b1ee 100644 --- a/src/ExpressReceiver.ts +++ b/src/ExpressReceiver.ts @@ -11,7 +11,7 @@ import tsscmp from 'tsscmp'; import { Logger, ConsoleLogger } from '@slack/logger'; import { InstallProvider, CallbackOptions, InstallProviderOptions, InstallURLOptions } from '@slack/oauth'; import App from './App'; -import { ReceiverAuthenticityError, ReceiverMultipleAckError } from './errors'; +import { ReceiverAuthenticityError, ReceiverMultipleAckError, ReceiverInconsistentStateError } from './errors'; import { AnyMiddlewareArgs, 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? @@ -213,24 +213,19 @@ export default class ExpressReceiver implements Receiver { } if (this.server !== undefined) { - // TODO: CodedError - return Promise.reject(new Error('The receiver cannot be started because it was already started.')); + return Promise.reject(new ReceiverInconsistentStateError('The receiver cannot be started because it was already started.')); } this.server = createServerFn(serverOptions, this.app); return new Promise((resolve, reject) => { if (this.server === undefined) { - // TODO: CodedError - throw new Error('The receiver cannot be started because private state was mutated. Please report this to ' + - 'the maintainers.'); + throw new ReceiverInconsistentStateError(missingServerErrorDescription); } this.server.on('error', (error) => { if (this.server === undefined) { - // TODO: CodedError - throw new Error('The receiver cannot be started because private state was mutated. Please report this ' + - 'to the maintainers.'); + throw new ReceiverInconsistentStateError(missingServerErrorDescription); } this.server.close(); @@ -252,9 +247,7 @@ export default class ExpressReceiver implements Receiver { this.server.listen(portOrListenOptions, () => { if (this.server === undefined) { - // TODO: CodedError - return reject(new Error('The receiver cannot be started because private state was mutated. Please report ' + - 'this to the maintainers.')); + return reject(new ReceiverInconsistentStateError(missingServerErrorDescription)); } resolve(this.server); @@ -267,8 +260,7 @@ export default class ExpressReceiver implements Receiver { public stop(): Promise { return new Promise((resolve, reject) => { if (this.server === undefined) { - // TODO: CodedError - return reject(new Error('The receiver cannot be stopped because it was not started.')); + return reject(new ReceiverInconsistentStateError('The receiver cannot be stopped because it was not started.')); } this.server.close((error) => { // NOTE: close listener added in start() is responsible for unsetting this.server @@ -422,4 +414,7 @@ const httpsOptionKeys = [ 'ca', 'cert', 'sigalgs', 'ciphers', 'clientCertEngine', 'crl', 'dhparam', 'ecdhCurve', 'honorCipherOrder', 'key', 'privateKeyEngine', 'privateKeyIdentifier', 'maxVersion', 'minVersion', 'passphrase', 'pfx', 'secureOptions', 'secureProtocol', 'sessionIdContext', -] +]; + +const missingServerErrorDescription = 'The receiver cannot be started because private state was mutated. Please ' + + 'report this to the maintainers.'; diff --git a/src/errors.ts b/src/errors.ts index d45f7477c..2303a8a39 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -11,6 +11,7 @@ export enum ErrorCode { ReceiverMultipleAckError = 'slack_bolt_receiver_ack_multiple_error', ReceiverAuthenticityError = 'slack_bolt_receiver_authenticity_error', + ReceiverInconsistentStateError = 'slack_bolt_receiver_inconsistent_state_error', MultipleListenerError = 'slack_bolt_multiple_listener_error', @@ -70,6 +71,10 @@ export class ReceiverAuthenticityError extends Error implements CodedError { public code = ErrorCode.ReceiverAuthenticityError; } +export class ReceiverInconsistentStateError extends Error implements CodedError { + public code = ErrorCode.ReceiverAuthenticityError; +} + export class MultipleListenerError extends Error implements CodedError { public code = ErrorCode.MultipleListenerError; From 13c9eaa9d1f02547e514f864d02e49f0334d6ff9 Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Fri, 2 Oct 2020 09:34:12 -0700 Subject: [PATCH 04/31] add note for future improvement --- src/ExpressReceiver.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ExpressReceiver.ts b/src/ExpressReceiver.ts index 05555b1ee..d904eed42 100644 --- a/src/ExpressReceiver.ts +++ b/src/ExpressReceiver.ts @@ -201,6 +201,7 @@ export default class ExpressReceiver implements Receiver { this.bolt = bolt; } + // TODO: can this method be defined as generic instead of using overloads? public start(port: number): Promise; public start(portOrListenOptions: number | ListenOptions, serverOptions?: ServerOptions): Promise; public start(portOrListenOptions: number | ListenOptions, httpsServerOptions?: HTTPSServerOptions): Promise; From 84e93f35b7939a5e0f9cc60813b467849f9fdde7 Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Mon, 19 Oct 2020 00:24:47 -0700 Subject: [PATCH 05/31] prettier formatting --- src/App.ts | 70 +++++++++++++++++++++--------------------- src/ExpressReceiver.ts | 70 ++++++++++++++++++++++++++++++------------ 2 files changed, 86 insertions(+), 54 deletions(-) diff --git a/src/App.ts b/src/App.ts index 33bd243b1..e88157681 100644 --- a/src/App.ts +++ b/src/App.ts @@ -236,7 +236,7 @@ export default class App { // No custom receiver throw new AppInitializationError( 'Signing secret not found, so could not initialize the default receiver. Set a signing secret or use a ' + - 'custom receiver.', + 'custom receiver.', ); } else { // Create default ExpressReceiver @@ -565,19 +565,19 @@ export default class App { type === IncomingEventType.Event ? (bodyArg as SlackEventMiddlewareArgs['body']).event : type === IncomingEventType.ViewAction - ? (bodyArg as SlackViewMiddlewareArgs['body']).view - : type === IncomingEventType.Shortcut - ? (bodyArg as SlackShortcutMiddlewareArgs['body']) - : type === IncomingEventType.Action && - isBlockActionOrInteractiveMessageBody(bodyArg as SlackActionMiddlewareArgs['body']) - ? (bodyArg as SlackActionMiddlewareArgs['body']).actions[0] - : (bodyArg as ( - | Exclude< - AnyMiddlewareArgs, - SlackEventMiddlewareArgs | SlackActionMiddlewareArgs | SlackViewMiddlewareArgs - > - | SlackActionMiddlewareArgs> - )['body']), + ? (bodyArg as SlackViewMiddlewareArgs['body']).view + : type === IncomingEventType.Shortcut + ? (bodyArg as SlackShortcutMiddlewareArgs['body']) + : type === IncomingEventType.Action && + isBlockActionOrInteractiveMessageBody(bodyArg as SlackActionMiddlewareArgs['body']) + ? (bodyArg as SlackActionMiddlewareArgs['body']).actions[0] + : (bodyArg as ( + | Exclude< + AnyMiddlewareArgs, + SlackEventMiddlewareArgs | SlackActionMiddlewareArgs | SlackViewMiddlewareArgs + > + | SlackActionMiddlewareArgs> + )['body']), }; // Set aliases @@ -721,13 +721,13 @@ function buildSource( type === IncomingEventType.Options || type === IncomingEventType.ViewAction || type === IncomingEventType.Shortcut - ? ((body as ( + ? ((body as ( | SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs | SlackShortcutMiddlewareArgs )['body']).team.id as string) - : assertNever(type), + : assertNever(type), enterpriseId: type === IncomingEventType.Event || type === IncomingEventType.Command ? ((body as (SlackEventMiddlewareArgs | SlackCommandMiddlewareArgs)['body']).enterprise_id as string) @@ -735,35 +735,35 @@ function buildSource( type === IncomingEventType.Options || type === IncomingEventType.ViewAction || type === IncomingEventType.Shortcut - ? ((body as ( + ? ((body as ( | SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs | SlackShortcutMiddlewareArgs )['body']).team.enterprise_id as string) - : undefined, + : undefined, userId: type === IncomingEventType.Event ? typeof (body as SlackEventMiddlewareArgs['body']).event.user === 'string' ? ((body as SlackEventMiddlewareArgs['body']).event.user as string) : typeof (body as SlackEventMiddlewareArgs['body']).event.user === 'object' - ? ((body as SlackEventMiddlewareArgs['body']).event.user.id as string) - : (body as SlackEventMiddlewareArgs['body']).event.channel !== undefined && - (body as SlackEventMiddlewareArgs['body']).event.channel.creator !== undefined - ? ((body as SlackEventMiddlewareArgs['body']).event.channel.creator as string) - : (body as SlackEventMiddlewareArgs['body']).event.subteam !== undefined && - (body as SlackEventMiddlewareArgs['body']).event.subteam.created_by !== undefined - ? ((body as SlackEventMiddlewareArgs['body']).event.subteam.created_by as string) - : undefined + ? ((body as SlackEventMiddlewareArgs['body']).event.user.id as string) + : (body as SlackEventMiddlewareArgs['body']).event.channel !== undefined && + (body as SlackEventMiddlewareArgs['body']).event.channel.creator !== undefined + ? ((body as SlackEventMiddlewareArgs['body']).event.channel.creator as string) + : (body as SlackEventMiddlewareArgs['body']).event.subteam !== undefined && + (body as SlackEventMiddlewareArgs['body']).event.subteam.created_by !== undefined + ? ((body as SlackEventMiddlewareArgs['body']).event.subteam.created_by as string) + : undefined : type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewAction || type === IncomingEventType.Shortcut - ? ((body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).user + ? ((body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).user .id as string) - : type === IncomingEventType.Command - ? ((body as SlackCommandMiddlewareArgs['body']).user_id as string) - : undefined, + : type === IncomingEventType.Command + ? ((body as SlackCommandMiddlewareArgs['body']).user_id as string) + : undefined, conversationId: channelId, }; // tslint:enable:max-line-length @@ -794,11 +794,11 @@ function singleTeamAuthorization( authorization.botUserId !== undefined && authorization.botId !== undefined ? Promise.resolve({ botUserId: authorization.botUserId, botId: authorization.botId }) : client.auth.test({ token: authorization.botToken }).then((result) => { - return { - botUserId: result.user_id as string, - botId: result.bot_id as string, - }; - }); + return { + botUserId: result.user_id as string, + botId: result.bot_id as string, + }; + }); return async () => { return { botToken: authorization.botToken, ...(await identifiers) }; diff --git a/src/ExpressReceiver.ts b/src/ExpressReceiver.ts index d904eed42..311467d79 100644 --- a/src/ExpressReceiver.ts +++ b/src/ExpressReceiver.ts @@ -20,10 +20,10 @@ export interface ExpressReceiverOptions { signingSecret: string; logger?: Logger; endpoints?: - | string - | { - [endpointType: string]: string; - }; + | string + | { + [endpointType: string]: string; + }; processBeforeResponse?: boolean; clientId?: string; clientSecret?: string; @@ -145,8 +145,7 @@ export default class ExpressReceiver implements Receiver { setTimeout(() => { if (!isAcknowledged) { this.logger.error( - 'An incoming event was not acknowledged within 3 seconds. ' + - 'Ensure that the ack() argument is called in a listener.', + 'An incoming event was not acknowledged within 3 seconds. Ensure that the ack() argument is called in a listener.', ); } // tslint:disable-next-line: align @@ -204,17 +203,25 @@ export default class ExpressReceiver implements Receiver { // TODO: can this method be defined as generic instead of using overloads? public start(port: number): Promise; public start(portOrListenOptions: number | ListenOptions, serverOptions?: ServerOptions): Promise; - public start(portOrListenOptions: number | ListenOptions, httpsServerOptions?: HTTPSServerOptions): Promise; - public start(portOrListenOptions: number | ListenOptions, serverOptions: ServerOptions | HTTPSServerOptions = {}): Promise { - let createServerFn: (typeof createServer | typeof createHttpsServer) = createServer; + public start( + portOrListenOptions: number | ListenOptions, + httpsServerOptions?: HTTPSServerOptions, + ): Promise; + public start( + portOrListenOptions: number | ListenOptions, + serverOptions: ServerOptions | HTTPSServerOptions = {}, + ): Promise { + let createServerFn: typeof createServer | typeof createHttpsServer = createServer; // Decide which kind of server, HTTP or HTTPS, by search for any keys in the serverOptions that are exclusive to HTTPS - if (Object.keys(serverOptions).filter(k => httpsOptionKeys.includes(k)).length > 0) { + if (Object.keys(serverOptions).filter((k) => httpsOptionKeys.includes(k)).length > 0) { createServerFn = createHttpsServer; } if (this.server !== undefined) { - return Promise.reject(new ReceiverInconsistentStateError('The receiver cannot be started because it was already started.')); + return Promise.reject( + new ReceiverInconsistentStateError('The receiver cannot be started because it was already started.'), + ); } this.server = createServerFn(serverOptions, this.app); @@ -409,13 +416,38 @@ function parseRequestBody(stringBody: string, contentType: string | undefined): // Option keys for tls.createServer() and tls.createSecureContext(), exclusive of those for http.createServer() const httpsOptionKeys = [ - 'ALPNProtocols', 'clientCertEngine', 'enableTrace', 'handshakeTimeout', 'rejectUnauthorized', 'requestCert', - 'sessionTimeout', 'SNICallback', 'ticketKeys', 'pskCallback', 'pskIdentityHint', - - 'ca', 'cert', 'sigalgs', 'ciphers', 'clientCertEngine', 'crl', 'dhparam', 'ecdhCurve', 'honorCipherOrder', 'key', - 'privateKeyEngine', 'privateKeyIdentifier', 'maxVersion', 'minVersion', 'passphrase', 'pfx', 'secureOptions', - 'secureProtocol', 'sessionIdContext', + 'ALPNProtocols', + 'clientCertEngine', + 'enableTrace', + 'handshakeTimeout', + 'rejectUnauthorized', + 'requestCert', + 'sessionTimeout', + 'SNICallback', + 'ticketKeys', + 'pskCallback', + 'pskIdentityHint', + + 'ca', + 'cert', + 'sigalgs', + 'ciphers', + 'clientCertEngine', + 'crl', + 'dhparam', + 'ecdhCurve', + 'honorCipherOrder', + 'key', + 'privateKeyEngine', + 'privateKeyIdentifier', + 'maxVersion', + 'minVersion', + 'passphrase', + 'pfx', + 'secureOptions', + 'secureProtocol', + 'sessionIdContext', ]; -const missingServerErrorDescription = 'The receiver cannot be started because private state was mutated. Please ' + - 'report this to the maintainers.'; +const missingServerErrorDescription = + 'The receiver cannot be started because private state was mutated. Please report this to the maintainers.'; From dc9f85f06727b14fc33562c166ac622fc4575c6e Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Mon, 19 Oct 2020 23:37:11 -0700 Subject: [PATCH 06/31] WIP: update tests for new server options --- src/App.spec.ts | 79 ++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/src/App.spec.ts b/src/App.spec.ts index be71c2074..d04e09739 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -11,7 +11,6 @@ import App, { ViewConstraints } from './App'; import { WebClientOptions, WebClient } from '@slack/web-api'; import { WorkflowStep } from './WorkflowStep'; -// TODO: swap out rewiremock for proxyquire to see if it saves execution time // Utility functions const noop = () => Promise.resolve(undefined); const noopMiddleware = async ({ next }: { next: NextFn }) => { @@ -68,7 +67,7 @@ describe('App', () => { assert(authorizeCallback.notCalled, 'Should not call the authorize callback on instantiation'); assert.instanceOf(app, App); }); - it('should fail without a token for single team authorization or authorize callback or oauth installer', async () => { + it('should fail without a token for single team authorization, authorize callback, nor oauth installer', async () => { // Arrange const App = await importApp(); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match @@ -243,22 +242,28 @@ describe('App', () => { }); describe('#start', () => { - it('should pass calls through to receiver', async () => { - // Arrange - const dummyReturn = Symbol(); - const dummyParams = [Symbol(), Symbol()]; - const fakeReceiver = new FakeReceiver(); - const App = await importApp(); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match - const app = new App({ receiver: fakeReceiver, authorize: noopAuthorize }); - fakeReceiver.start = sinon.fake.returns(dummyReturn); - - // Act - const actualReturn = await app.start(...dummyParams); - - // Assert - assert.deepEqual(actualReturn, dummyReturn); - assert.deepEqual(dummyParams, fakeReceiver.start.firstCall.args); - }); + // The following test case depends on a definition of App that is generic on its Receiver type. This will be + // addressed in the future. It cannot even be left uncommented with the `it.skip()` global because it will fail + // TypeScript compilation as written. + // it('should pass calls through to receiver', async () => { + // // Arrange + // const dummyReturn = Symbol(); + // const dummyParams = [Symbol(), Symbol()]; + // const fakeReceiver = new FakeReceiver(); + // const App = await importApp(); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match + // const app = new App({ receiver: fakeReceiver, authorize: noopAuthorize }); + // fakeReceiver.start = sinon.fake.returns(dummyReturn); + + // // Act + // const actualReturn = await app.start(...dummyParams); + + // // Assert + // assert.deepEqual(actualReturn, dummyReturn); + // assert.deepEqual(dummyParams, fakeReceiver.start.firstCall.args); + // }); + + // TODO: another test case to take the place of the one above (for coverage until the definition of App is made + // generic). }); describe('#stop', () => { @@ -793,53 +798,53 @@ describe('App', () => { await ackFn(); await next!(); }); - app.shortcut({ callback_id: 'message_action_callback_id' }, async ({}) => { + app.shortcut({ callback_id: 'message_action_callback_id' }, async ({ }) => { await shortcutFn(); }); - app.shortcut({ type: 'message_action', callback_id: 'another_message_action_callback_id' }, async ({}) => { + app.shortcut({ type: 'message_action', callback_id: 'another_message_action_callback_id' }, async ({ }) => { await shortcutFn(); }); - app.shortcut({ type: 'message_action', callback_id: 'does_not_exist' }, async ({}) => { + app.shortcut({ type: 'message_action', callback_id: 'does_not_exist' }, async ({ }) => { await shortcutFn(); }); - app.shortcut({ callback_id: 'shortcut_callback_id' }, async ({}) => { + app.shortcut({ callback_id: 'shortcut_callback_id' }, async ({ }) => { await shortcutFn(); }); - app.shortcut({ type: 'shortcut', callback_id: 'another_shortcut_callback_id' }, async ({}) => { + app.shortcut({ type: 'shortcut', callback_id: 'another_shortcut_callback_id' }, async ({ }) => { await shortcutFn(); }); - app.shortcut({ type: 'shortcut', callback_id: 'does_not_exist' }, async ({}) => { + app.shortcut({ type: 'shortcut', callback_id: 'does_not_exist' }, async ({ }) => { await shortcutFn(); }); - app.action('block_action_id', async ({}) => { + app.action('block_action_id', async ({ }) => { await actionFn(); }); - app.action({ callback_id: 'interactive_message_callback_id' }, async ({}) => { + app.action({ callback_id: 'interactive_message_callback_id' }, async ({ }) => { await actionFn(); }); - app.action({ callback_id: 'dialog_submission_callback_id' }, async ({}) => { + app.action({ callback_id: 'dialog_submission_callback_id' }, async ({ }) => { await actionFn(); }); - app.view('view_callback_id', async ({}) => { + app.view('view_callback_id', async ({ }) => { await viewFn(); }); - app.view({ callback_id: 'view_callback_id', type: 'view_closed' }, async ({}) => { + app.view({ callback_id: 'view_callback_id', type: 'view_closed' }, async ({ }) => { await viewFn(); }); - app.options('external_select_action_id', async ({}) => { + app.options('external_select_action_id', async ({ }) => { await optionsFn(); }); - app.options({ callback_id: 'dialog_suggestion_callback_id' }, async ({}) => { + app.options({ callback_id: 'dialog_suggestion_callback_id' }, async ({ }) => { await optionsFn(); }); - app.event('app_home_opened', async ({}) => { + app.event('app_home_opened', async ({ }) => { /* noop */ }); - app.message('hello', async ({}) => { + app.message('hello', async ({ }) => { /* noop */ }); - app.command('/echo', async ({}) => { + app.command('/echo', async ({ }) => { /* noop */ }); @@ -849,7 +854,7 @@ describe('App', () => { type: 'view_submission', unknown_key: 'should be detected', } as any) as ViewConstraints; - app.view(invalidViewConstraints1, async ({}) => { + app.view(invalidViewConstraints1, async ({ }) => { /* noop */ }); assert.isTrue(fakeLogger.error.called); @@ -861,7 +866,7 @@ describe('App', () => { type: undefined, unknown_key: 'should be detected', } as any) as ViewConstraints; - app.view(invalidViewConstraints2, async ({}) => { + app.view(invalidViewConstraints2, async ({ }) => { /* noop */ }); assert.isTrue(fakeLogger.error.called); @@ -1386,7 +1391,7 @@ async function importApp( function withNoopWebClient(): Override { return { '@slack/web-api': { - WebClient: class {}, + WebClient: class { }, }, }; } From efe35d7228b2db8a96e9d06746a11ea3a3f3400a Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Thu, 12 Nov 2020 17:14:25 -0500 Subject: [PATCH 07/31] wip --- src/ExpressReceiver.spec.ts | 108 ++++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 10 deletions(-) diff --git a/src/ExpressReceiver.spec.ts b/src/ExpressReceiver.spec.ts index 30bc508ae..69166823e 100644 --- a/src/ExpressReceiver.spec.ts +++ b/src/ExpressReceiver.spec.ts @@ -1,10 +1,13 @@ // tslint:disable:no-implicit-dependencies import 'mocha'; -import { Logger, LogLevel } from '@slack/logger'; +import sinon, { SinonFakeTimers, SinonSpy } from 'sinon'; import { assert } from 'chai'; +import { Override, mergeOverrides } from './test-helpers'; +import rewiremock from 'rewiremock'; +import { Logger, LogLevel } from '@slack/logger'; import { Request, Response } from 'express'; -import sinon, { SinonFakeTimers } from 'sinon'; import { Readable } from 'stream'; +import { EventEmitter } from 'events'; import ExpressReceiver, { respondToSslCheck, @@ -51,6 +54,7 @@ describe('ExpressReceiver', () => { } describe('constructor', () => { + // NOTE: it would be more informative to test known valid combinations of options, as well as invalid combinations it('should accept supported arguments', async () => { const receiver = new ExpressReceiver({ signingSecret: 'my-secret', @@ -70,16 +74,67 @@ describe('ExpressReceiver', () => { }); }); - describe('start/stop', () => { - it('should be available', async () => { - const receiver = new ExpressReceiver({ - signingSecret: 'my-secret', - logger: noopLogger, - }); + describe('#start()', () => { + beforeEach(function () { + this.fakeServer = new FakeServer(); + this.fakeCreateServer = sinon.fake.returns(this.fakeServer); + }); + + it('should start listening for requests using the built-in HTTP server', async function () { + // Arrange + const overrides = mergeOverrides( + withHttpCreateServer(this.fakeCreateServer), + withHttpsCreateServer(sinon.fake.throws('Should not be used.')), + ); + const ExpressReceiver = await importExpressReceiver(overrides); + const receiver = new ExpressReceiver({ signingSecret: '' }); + const port = 12345; + + // Act + const server = await receiver.start(port); + + // Assert + assert(this.fakeCreateServer.calledOnce); + assert.strictEqual(server, this.fakeServer); + assert(this.fakeServer.listen.calledWith(port)); + }); + it('should start listening for requests using the built-in HTTPS (TLS) server when given TLS server options', async function () { + // Arrange + const overrides = mergeOverrides( + withHttpCreateServer(sinon.fake.throws('Should not be used.')), + withHttpsCreateServer(this.fakeCreateServer), + ); + const ExpressReceiver = await importExpressReceiver(overrides); + const receiver = new ExpressReceiver({ signingSecret: '' }); + const port = 12345; + const tlsOptions = { key: '', cert: '' }; + + // Act + const server = await receiver.start(port, tlsOptions); - await receiver.start(9999); - await receiver.stop(); + // Assert + assert(this.fakeCreateServer.calledOnceWith(tlsOptions)); + assert.strictEqual(server, this.fakeServer); + assert(this.fakeServer.listen.calledWith(port)); }); + + it('should reject with an error when the built-in HTTP server fails to listen (such as EADDRINUSE)', async function () { + // Arrange + this. + }); + it('should reject with an error when starting and the server was already previously started'); + }); + + describe('#stop', () => { + it('should stop listening for requests when a built-in HTTP server is already started'); + it('should reject when a built-in HTTP server is not started'); + }); + + describe('state management for built-in server', () => { + it('should be able to start after it was stopped', () => { + // TODO: assert that listeners on the 'close' event still get called + }) + }); describe('built-in middleware', () => { @@ -487,3 +542,36 @@ describe('ExpressReceiver', () => { }); }); }); + +/* Testing Harness */ + +// Loading the system under test using overrides +async function importExpressReceiver( + overrides: Override = {}, +): Promise { + return (await rewiremock.module(() => import('./ExpressReceiver'), overrides)).default; +} + +// Composable overrides +function withHttpCreateServer(spy: SinonSpy): Override { + return { + http: { + createServer: spy, + }, + }; +} + +function withHttpsCreateServer(spy: SinonSpy): Override { + return { + https: { + createServer: spy, + }, + }; +} + +// Fakes +class FakeServer extends EventEmitter { + public on = sinon.fake(); + public listen = sinon.fake((...args: any[]) => { setImmediate(() => { args[1](); }); }); + public close = sinon.fake(); +} From d20a942302050a190d81578e950876bfefbb6a5c Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Mon, 11 Jan 2021 23:19:55 -0800 Subject: [PATCH 08/31] added migration guide for v3 --- .github/workflows/ci-build.yml | 2 +- docs/_tutorials/migration_v3.md | 117 ++++++++++++++++++++++++++++ docs/_tutorials/using-typescript.md | 2 +- package.json | 4 +- 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 docs/_tutorials/migration_v3.md diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index f6e9776db..8fe0fcbb0 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 14.x] + node-version: [12.x, 14.x] steps: - uses: actions/checkout@v2 diff --git a/docs/_tutorials/migration_v3.md b/docs/_tutorials/migration_v3.md new file mode 100644 index 000000000..4473bbc33 --- /dev/null +++ b/docs/_tutorials/migration_v3.md @@ -0,0 +1,117 @@ +--- +title: Migrating to V3 +order: 2 +slug: migration-v3 +lang: en +layout: tutorial +permalink: /tutorial/migration-v3 +--- +# Migrating to v3.x + +
+This guide will walk you through the process of updating your app from using `@slack/bolt@2.x` to `@slack/bolt@3.x`. There are a few changes you'll need to make but for most apps, these changes can be applied in 5 - 15 minutes. + +*Note: Make sure to checkout our [support schedule](#slackbolt1x-support-schedule) for `@slack/bolt@2.x` if you don't plan on upgrading right away* +
+ +--- + +### Org Wide App Installation Changes to InstallationStore & orgAuthorize + +In [Bolt for JavaScript 2.5.0](https://github.com/slackapi/bolt-js/releases/tag/%40slack%2Fbolt%402.5.0), we introduced support for [org wide app installations](https://api.slack.com/enterprise/apps). To add support to your applications, two new methods were introduced to the Installation Store used during OAuth, `fetchOrgInstallation` & `storeOrgInstallation`. With `@slack/bolt@3.x`, we have dropped support for these two new methods to better align with Bolt for Python and Bolt for Java. See the code samples below for the recommended changes to migrate. + +Before: + +```javascript +installationStore: { + storeInstallation: async (installation) => { + // change the line below so it saves to your database + return await database.set(installation.team.id, installation); + }, + fetchInstallation: async (InstallQuery) => { + // change the line below so it fetches from your database + return await database.get(InstallQuery.teamId); + }, + storeOrgInstallation: async (installation) => { + // include this method if you want your app to support org wide installations + // change the line below so it saves to your database + return await database.set(installation.enterprise.id, installation); + }, + fetchOrgInstallation: async (InstallQuery) => { + // include this method if you want your app to support org wide installations + // change the line below so it fetches from your database + return await database.get(InstallQuery.enterpriseId); + }, + }, +``` + +After: + +```javascript +installationStore: { + storeInstallation: async (installation) => { + if (installation.isEnterpriseInstall) { + // support for org wide app installation + return await database.set(installation.enterprise.id, installation); + } else { + // single team app installation + return await database.set(installation.team.id, installation); + } + throw new Error('Failed saving installation data to installationStore'); + }, + fetchInstallation: async (InstallQuery) => { + // replace database.get so it fetches from your database + if (InstallQuery.isEnterpriseInstall && InstallQuery.enterpriseId !== undefined) { + // org wide app installation lookup + return await database.get(InstallQuery.enterpriseId); + } + if (InstallQuery.teamId !== undefined) { + // single team app installation lookup + return await database.get(InstallQuery.teamId); + } + throw new Error('Failed fetching installation'); + }, + }, +``` + +Along with this change, we have also dropped support for `orgAuthorize`, and instead recommend developers to use `authorize` for both the single workspace installs and org wide app installs (if you are not using the built-in OAuth or providing a token when initializing App). See the code sample below for migration steps: + +Before: + +```javascript +const app = new App({ authorize: authorizeFn, orgAuthorize: orgAuthorizeFn, signingSecret: process.env.SLACK_SIGNING_SECRET }); + +const authorizeFn = async ({ teamId, enterpriseId}) => { + // Use teamId to fetch installation details from database +} + +const orgAuthorizeFn = async ({ teamId, enterpriseId }) => { + // Use enterpriseId to fetch installation details from database +} +``` + +After: +```javascript +const app = new App({ authorize: authorizeFn, signingSecret: process.env.SLACK_SIGNING_SECRET }); + +const authorizeFn = async ({ teamId, enterpriseId, isEnterpriseInstall}) => { + // if isEnterpriseInstall is true, use enterpriseId to fetch installation details from database + // else, use teamId to fetch installation details from database +} +``` + +### HTTP Receiver as default + +In `@slack/bolt@3.x`, we have introduced a new default [`HTTPReceiver`](https://github.com/slackapi/bolt-js/issues/670) which replaces the previous default `ExpressReceiver`. This will allow Bolt for JavaScript apps to easily work with other popular web frameworks (Hapi.js, Koa, etc). `ExpressReceiver` is still being shipped with Bolt for JavaScript and their may be some usecases that aren't supported by the new `HTTPReceiver` that `ExpressReceiver` supported. One usecase that isn't supported by `HTTPReceiver` is creating custom routes (ex: create a route to do a health check). For these usecases, we recommend continuing to use `ExpressReceiver` by importing it and create your own instance of it to pass into `App`. See [our documentation on adding custom http routes](https://slack.dev/bolt-js/concepts#custom-routes) for a code sample. + +### @slack/bolt@2.x support schedule + +`@slack/bolt@2.x` will be deprecated on **January 12th, 2020**. We will only implement **critical bug fixes** until the official end of life date and close non critical issues and pull requests, which is slated for **May 31st, 2021**. At this time, development will fully stop for `@slack/bolt@2.x` and all remaining open issues and pull requests will be closed. + +### Minimum Node Version + +`@slack/bolt@3.x` requires a minimum Node version of `12.13.0` and minimum npm version of `6.12.0` . + +### Minimum TypeScript Version + +As outlined in our [using TypeScript guide](https://slack.dev/bolt/tutorial/using-typescript), `@slack/bolt@3.x` requires a minimum TypeScript version of `4.1`. diff --git a/docs/_tutorials/using-typescript.md b/docs/_tutorials/using-typescript.md index 03b5d127b..311714f4b 100644 --- a/docs/_tutorials/using-typescript.md +++ b/docs/_tutorials/using-typescript.md @@ -14,4 +14,4 @@ This page helps describe how to use this package from a project that also uses T ### Minimum version -The latest major version of `@slack/bolt` is supported to build against a minimum TypeScript version of v3.7. +The latest major version of `@slack/bolt` is supported to build against a minimum TypeScript version of v4.1. diff --git a/package.json b/package.json index c111f1cde..482924610 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "dist/**/*" ], "engines": { - "node": ">=10.13.0", - "npm": ">=6.4.1" + "node": ">=12.13.0", + "npm": ">=6.12.0" }, "scripts": { "prepare": "npm run build", From e06e9798bcd52af121ba3cd9ae82c42b46e16961 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Tue, 12 Jan 2021 00:23:31 -0800 Subject: [PATCH 09/31] Update docs/_tutorials/migration_v3.md Co-authored-by: Ankur Oberoi --- .github/workflows/ci-build.yml | 2 +- docs/_tutorials/migration_v3.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 8fe0fcbb0..99f8ccf20 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x] + node-version: [12.x, 14.x, 15.x] steps: - uses: actions/checkout@v2 diff --git a/docs/_tutorials/migration_v3.md b/docs/_tutorials/migration_v3.md index 4473bbc33..94646665f 100644 --- a/docs/_tutorials/migration_v3.md +++ b/docs/_tutorials/migration_v3.md @@ -102,7 +102,7 @@ const authorizeFn = async ({ teamId, enterpriseId, isEnterpriseInstall}) => { ### HTTP Receiver as default -In `@slack/bolt@3.x`, we have introduced a new default [`HTTPReceiver`](https://github.com/slackapi/bolt-js/issues/670) which replaces the previous default `ExpressReceiver`. This will allow Bolt for JavaScript apps to easily work with other popular web frameworks (Hapi.js, Koa, etc). `ExpressReceiver` is still being shipped with Bolt for JavaScript and their may be some usecases that aren't supported by the new `HTTPReceiver` that `ExpressReceiver` supported. One usecase that isn't supported by `HTTPReceiver` is creating custom routes (ex: create a route to do a health check). For these usecases, we recommend continuing to use `ExpressReceiver` by importing it and create your own instance of it to pass into `App`. See [our documentation on adding custom http routes](https://slack.dev/bolt-js/concepts#custom-routes) for a code sample. +In `@slack/bolt@3.x`, we have introduced a new default [`HTTPReceiver`](https://github.com/slackapi/bolt-js/issues/670) which replaces the previous default `ExpressReceiver`. This will allow Bolt for JavaScript apps to easily work with other popular web frameworks (Hapi.js, Koa, etc). `ExpressReceiver` is still being shipped with Bolt for JavaScript and `HTTPReceiver` will not provide all the same functionality. One use case that isn't supported by `HTTPReceiver` is creating custom routes (ex: create a route to do a health check). For these use cases, we recommend continuing to use `ExpressReceiver` by importing the class, and creating your own instance of it, and passing this instance into the constructor of `App`. See [our documentation on adding custom http routes](https://slack.dev/bolt-js/concepts#custom-routes) for an example. ### @slack/bolt@2.x support schedule From 7b05dcbf008f3298f6aa47878f3217e3fbd581b5 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Thu, 7 Jan 2021 12:33:05 -0800 Subject: [PATCH 10/31] removed orgAuthorize --- docs/_basic/authenticating_oauth.md | 31 +++++--- docs/_basic/ja_authenticating_oauth.md | 31 +++++--- docs/_tutorials/reference.md | 5 +- src/App.spec.ts | 106 +------------------------ src/App.ts | 35 +++----- 5 files changed, 51 insertions(+), 157 deletions(-) diff --git a/docs/_basic/authenticating_oauth.md b/docs/_basic/authenticating_oauth.md index 92af5f68b..c51ab910f 100644 --- a/docs/_basic/authenticating_oauth.md +++ b/docs/_basic/authenticating_oauth.md @@ -16,7 +16,7 @@ Bolt for JavaScript does not support OAuth for [custom receivers](#receiver). If To learn more about the OAuth installation flow with Slack, [read the API documentation](https://api.slack.com/authentication/oauth-v2). -To add support for [org wide installations](https://api.slack.com/enterprise/apps), you will need Bolt for JavaScript version `2.5.0` or newer. You will have to update your `installationStore` to include `storeOrgInstallation` and `fetchOrgInstallation` methods. Lastly, make sure you have enabled org wide installations in your app configuration settings under **Org Level Apps**. +To add support for [org wide installations](https://api.slack.com/enterprise/apps), you will need Bolt for JavaScript version `3.0.0` or newer. Make sure you have enabled org wide installations in your app configuration settings under **Org Level Apps**. ```javascript @@ -29,21 +29,26 @@ const app = new App({ installationStore: { storeInstallation: async (installation) => { // change the line below so it saves to your database - return await database.set(installation.team.id, installation); + if (installation.isEnterpriseInstall) { + // support for org wide app installation + return await database.set(installation.enterprise.id, installation); + } else { + // single team app installation + return await database.set(installation.team.id, installation); + } + throw new Error('Failed saving installation data to installationStore'); }, fetchInstallation: async (InstallQuery) => { // change the line below so it fetches from your database - return await database.get(InstallQuery.teamId); - }, - storeOrgInstallation: async (installation) => { - // include this method if you want your app to support org wide installations - // change the line below so it saves to your database - return await database.set(installation.enterprise.id, installation); - }, - fetchOrgInstallation: async (InstallQuery) => { - // include this method if you want your app to support org wide installations - // change the line below so it fetches from your database - return await database.get(InstallQuery.enterpriseId); + if (InstallQuery.isEnterpriseInstall && InstallQuery.enterpriseId !== undefined) { + // org wide app installation lookup + return await database.get(InstallQuery.enterpriseId); + } + if (InstallQuery.teamId !== undefined) { + // single team app installation lookup + return await database.get(InstallQuery.teamId); + } + throw new Error('Failed fetching installation'); }, }, }); diff --git a/docs/_basic/ja_authenticating_oauth.md b/docs/_basic/ja_authenticating_oauth.md index 3a5ac1c28..dd88202c9 100644 --- a/docs/_basic/ja_authenticating_oauth.md +++ b/docs/_basic/ja_authenticating_oauth.md @@ -15,7 +15,7 @@ Bolt for JavaScript は `slack/install` というパスも生成します。こ Slack の OAuth インストールフローについてもっと知りたい場合は [API ドキュメント](https://api.slack.com/authentication/oauth-v2)を参照してください。 -[Enterprise Grid の OrG 全体へのインストール](https://api.slack.com/enterprise/apps)への対応を追加する場合、Bolt for JavaScript のバージョン 2.5.0 以上を利用してください。また、`installationStore` に `storeOrgInstallation`、`fetchOrgInstallation` というメソッドを追加する必要があります。そして、最後に Slack アプリの設定画面で **Org Level Apps** の設定が有効になっていることを忘れずに確認するようにしてください。 +[Enterprise Grid の OrG 全体へのインストール](https://api.slack.com/enterprise/apps)への対応を追加する場合、Bolt for JavaScript のバージョン 3.0.0 以上を利用してください。そして、最後に Slack アプリの設定画面で **Org Level Apps** の設定が有効になっていることを忘れずに確認するようにしてください。 ```javascript @@ -28,21 +28,26 @@ const app = new App({ installationStore: { storeInstallation: async (installation) => { // 実際のデータベースに保存するために、ここのコードを変更 - return await database.set(installation.team.id, installation); + if (installation.isEnterpriseInstall) { + // support for org wide app installation + return await database.set(installation.enterprise.id, installation); + } else { + // single team app installation + return await database.set(installation.team.id, installation); + } + throw new Error('Failed saving installation data to installationStore'); }, fetchInstallation: async (InstallQuery) => { // 実際のデータベースから取得するために、ここのコードを変更 - return await database.get(InstallQuery.teamId); - }, - storeOrgInstallation: async (installation) => { - // OrG 全体へのインストールに対応する場合はこのメソッドも追加 - // 実際のデータベースから取得するために、ここのコードを変更 - return await database.set(installation.enterprise.id, installation); - }, - fetchOrgInstallation: async (InstallQuery) => { - // OrG 全体へのインストールに対応する場合はこのメソッドも追加 - // 実際のデータベースから取得するために、ここのコードを変更 - return await database.get(InstallQuery.enterpriseId); + if (InstallQuery.isEnterpriseInstall && InstallQuery.enterpriseId !== undefined) { + // org wide app installation lookup + return await database.get(InstallQuery.enterpriseId); + } + if (InstallQuery.teamId !== undefined) { + // single team app installation lookup + return await database.get(InstallQuery.teamId); + } + throw new Error('Failed fetching installation'); }, }, }); diff --git a/docs/_tutorials/reference.md b/docs/_tutorials/reference.md index bbd3fdbda..0f66173ee 100644 --- a/docs/_tutorials/reference.md +++ b/docs/_tutorials/reference.md @@ -93,7 +93,7 @@ Bolt includes a collection of initialization options to customize apps. There ar | `clientId` | The client ID `string` from your app's configuration which is [required to configure OAuth](/bolt-js/concepts#authenticating-oauth). | | `clientSecret` | The client secret `string` from your app's configuration which is [required to configure OAuth](/bolt-js/concepts#authenticating-oauth). | | `stateSecret` | Recommended parameter (`string`) that's passed when [configuring OAuth](/bolt-js/concepts#authenticating-oauth) to prevent CSRF attacks | -| `installationStore` | Defines how to save and fetch installation data when [configuring OAuth](/bolt-js/concepts#authenticating-oauth). Contains two methods: `fetchInstallation` and `storeInstallation`. If you want org-app support, you should also add `fetchOrgInstallation` and `storeOrgInstallation`. The default `installationStore` is an in-memory store. | +| `installationStore` | Defines how to save and fetch installation data when [configuring OAuth](/bolt-js/concepts#authenticating-oauth). Contains two methods: `fetchInstallation` and `storeInstallation`. The default `installationStore` is an in-memory store. | | `scopes` | Array of scopes that your app will request [within the OAuth process](/bolt-js/concepts#authenticating-oauth). | | `installerOptions` | Optional object that can be used to customize [the default OAuth support](/bolt-js/concepts#authenticating-oauth). Read more in the OAuth documentation. | @@ -109,8 +109,7 @@ App options are passed into the `App` constructor. | `token` | A `string` from your app's configuration (under "Settings" > "Install App") required for calling the Web API. May not be passed when using `authorize`, `orgAuthorize`, or OAuth. | | `botId` | Can only be used when `authorize` is not defined. The optional `botId` is the ID for your bot token (ex: `B12345`) which can be used to ignore messages sent by your app. If a `xoxb-` token is passed to your app, this value will automatically be retrieved by your app calling the [`auth.test` method](https://api.slack.com/methods/auth.test). | | `botUserId` | Can only be used wihen `authorize` is not defined. The optional `botUserId` is distinct from the `botId`, as it's the user ID associated with your bot user used to identify direct mentions. If a `xoxb-` token is passed to your app, this value will automatically be retrieved by your app calling the [`auth.test` method](https://api.slack.com/methods/auth.test). | -| `authorize` | Function for multi-team installations that determines which token is associated with the incoming event. The `authorize` function is passed source data that always contains a `teamId`, and sometimes contains a `userId`, `conversationId`, `enterpriseId`, and `isEnterpriseInstall` (depending which information the incoming event contains). An `authorize` function should either return a `botToken`, `botId`, and `botUserId`, or could return a `userToken`. If using [built-in OAuth support](/bolt-js/concepts#authenticating-oauth), an `authorize` function will automatically be created so you do not need to pass one in. More information about `authorization` functions can be found on | -| `orgAuthorize` | Function similar to `authorize`, but used for apps installed to an entire enterprise organization. The `orgAuthorize` function is passed source data that always contains an `enterpriseId`, and sometimes contains a `teamId`, `userId`, `conversationId`, and `isEnterpriseInstall` (depending which information the incoming event contains). The `orgAuthorize` function should return the same information as the `authorize` function, with the addition of a `teamId` and `enterpriseId`. +| `authorize` | Function for multi-team installations that determines which token is associated with the incoming event. The `authorize` function is passed source data that sometimes contains a `userId`, `conversationId`, `enterpriseId`, `teamId` and `isEnterpriseInstall` (depending which information the incoming event contains). An `authorize` function should either return a `botToken`, `botId`, and `botUserId`, or could return a `userToken`. If using [built-in OAuth support](/bolt-js/concepts#authenticating-oauth), an `authorize` function will automatically be created so you do not need to pass one in. More information about `authorization` functions can be found on | | `logger` | Option that allows you to pass a custom logger rather than using the built-in one. Loggers must implement specific methods ([the `Logger` interface](https://github.com/slackapi/node-slack-sdk/blob/main/packages/logger/src/index.ts)), which includes `setLevel(level: LogLevel)`, `getLevel()`, `setName(name: string)`, `debug(...msgs: any[])`, `info(...msgs: any[])`, `warn(...msgs: any[])`, and `error(...msgs: any[])`. More information about logging are [in the documentation](/bolt-js/concepts#logging) | | `logLevel` | Option to control how much or what kind of information is logged. The `LogLevel` export contains the possible levels–in order of most to least information: `DEBUG`, `INFO`, `WARN`, and `ERROR`. By default, `logLevel` is set to `INFO`. More information on logging can be found [in the documentation](/bolt-js/concepts#logging). | | `ignoreSelf` | `boolean` to enable a middleware function that ignores any messages coming from your app. Requires a `botId`. Defaults to `true`. | diff --git a/src/App.spec.ts b/src/App.spec.ts index 6e0eadf50..54af55ee0 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -68,32 +68,6 @@ describe('App', () => { assert(authorizeCallback.notCalled, 'Should not call the authorize callback on instantiation'); assert.instanceOf(app, App); }); - it('should succeed with an orgAuthorize callback', async () => { - // Arrange - const authorizeCallback = sinon.fake(); - const App = await importApp(); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match - - // Act - const app = new App({ orgAuthorize: authorizeCallback, signingSecret: '' }); - - // Assert - assert(authorizeCallback.notCalled, 'Should not call the orgAuthorize callback on instantiation'); - assert.instanceOf(app, App); - }); - it('should succeed with an authorize and orgAuthorize callback', async () => { - // Arrange - const authorizeCallback = sinon.fake(); - const orgAuthorizeCallback = sinon.fake(); - const App = await importApp(); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match - - // Act - const app = new App({ orgAuthorize: orgAuthorizeCallback, authorize: authorizeCallback, signingSecret: '' }); - - // Assert - assert(authorizeCallback.notCalled, 'Should not call the authorize callback on instantiation'); - assert(orgAuthorizeCallback.notCalled, 'Should not call the orgAuthorize callback on instantiation'); - assert.instanceOf(app, App); - }); it('should fail without a token for single team authorization or authorize callback or oauth installer', async () => { // Arrange const App = await importApp(); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match @@ -123,22 +97,6 @@ describe('App', () => { assert(authorizeCallback.notCalled); } }); - it('should fail when both a token and orgAuthorize callback are specified', async () => { - // Arrange - const authorizeCallback = sinon.fake(); - const App = await importApp(); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match - - // Act - try { - // eslint-disable-line @typescript-eslint/no-unused-expressions - new App({ token: '', orgAuthorize: authorizeCallback, signingSecret: '' }); - assert.fail(); - } catch (error) { - // Assert - assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); - assert(authorizeCallback.notCalled); - } - }); it('should fail when both a token is specified and OAuthInstaller is initialized', async () => { // Arrange const authorizeCallback = sinon.fake(); @@ -171,28 +129,6 @@ describe('App', () => { assert(authorizeCallback.notCalled); } }); - it('should fail when both a orgAuthorize callback is specified and OAuthInstaller is initialized', async () => { - // Arrange - const authorizeCallback = sinon.fake(); - const App = await importApp(); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match - - // Act - try { - // eslint-disable-line @typescript-eslint/no-unused-expressions - new App({ - orgAuthorize: authorizeCallback, - clientId: '', - clientSecret: '', - stateSecret: '', - signingSecret: '', - }); - assert.fail(); - } catch (error) { - // Assert - assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); - assert(authorizeCallback.notCalled); - } - }); describe('with a custom receiver', () => { it('should succeed with no signing secret', async () => { // Arrange @@ -1203,8 +1139,8 @@ describe('App', () => { assert(fakeErrorHandler.notCalled); }); - // This test confirms orgAuthorize is being used for org events - it('should acknowledge any of possible org events', async () => { + // This test confirms authorize is being used for org events + it('should acknowledge any possible org events', async () => { // Arrange const ackFn = sinon.fake.resolves({}); const actionFn = sinon.fake.resolves({}); @@ -1220,7 +1156,7 @@ describe('App', () => { const app = new App({ logger: fakeLogger, receiver: fakeReceiver, - orgAuthorize: sinon.fake.resolves(dummyAuthorizationResult), + authorize: sinon.fake.resolves(dummyAuthorizationResult), }); app.use(async ({ next }) => { @@ -1311,42 +1247,6 @@ describe('App', () => { assert.equal(ackFn.callCount, dummyReceiverEvents.length); assert(fakeErrorHandler.notCalled); }); - - it('should fail because no orgAuthorize was defined to handle org install events', async () => { - // Arrange - const ackFn = sinon.fake.resolves({}); - const actionFn = sinon.fake.resolves({}); - const overrides = buildOverrides([withNoopWebClient()]); - const App = await importApp(overrides); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match - const dummyReceiverEvents = createOrgAppReceiverEvents(); - - // Act - const fakeLogger = createFakeLogger(); - // only passed in authorize and not orgAuthorize - const app = new App({ - logger: fakeLogger, - receiver: fakeReceiver, - authorize: sinon.fake.resolves(dummyAuthorizationResult), - }); - - app.use(async ({ next }) => { - await ackFn(); - await next!(); - }); - app.action('block_action_id', async ({}) => { - await actionFn(); - }); - - app.error(fakeErrorHandler); - await Promise.all(dummyReceiverEvents.map((event) => fakeReceiver.sendEvent(event))); - - // Assert - assert.equal(actionFn.callCount, 0); - assert.equal(ackFn.callCount, 0); - assert.equal(fakeErrorHandler.callCount, dummyReceiverEvents.length); - assert.instanceOf(fakeErrorHandler.firstCall.args[0], Error); - assert.propertyVal(fakeErrorHandler.firstCall.args[0], 'code', ErrorCode.AuthorizationError); - }); }); describe('respond()', () => { diff --git a/src/App.ts b/src/App.ts index 3fdbe76ac..4ec0254e3 100644 --- a/src/App.ts +++ b/src/App.ts @@ -69,8 +69,7 @@ export interface AppOptions { token?: AuthorizeResult['botToken']; // either token or authorize 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 - orgAuthorize?: Authorize; // either token or orgAuthorize + authorize?: Authorize; // either token or authorize receiver?: Receiver; logger?: Logger; logLevel?: LogLevel; @@ -160,10 +159,7 @@ export default class App { private logger: Logger; /** Authorize */ - private authorize!: Authorize; - - /** Org Authorize */ - private orgAuthorize!: Authorize; + private authorize!: Authorize; /** Global middleware chain */ private middleware: Middleware[]; @@ -188,7 +184,6 @@ export default class App { botId = undefined, botUserId = undefined, authorize = undefined, - orgAuthorize = undefined, logger = undefined, logLevel = undefined, ignoreSelf = true, @@ -274,32 +269,22 @@ export default class App { } if (token !== undefined) { - if (authorize !== undefined || orgAuthorize !== undefined || usingOauth) { + if (authorize !== undefined || usingOauth) { throw new AppInitializationError( - `token as well as authorize, orgAuthorize, or oauth installer options were provided. ${tokenUsage}`, + `token as well as authorize or oauth installer options were provided. ${tokenUsage}`, ); } this.authorize = singleAuthorization(this.client, { botId, botUserId, botToken: token }); - this.orgAuthorize = singleAuthorization(this.client, { botId, botUserId, botToken: token }); - } else if (authorize === undefined && orgAuthorize === undefined && !usingOauth) { + } else if (authorize === undefined && !usingOauth) { throw new AppInitializationError( - `No token, no authorize, no orgAuthorize, and no oauth installer options provided. ${tokenUsage}`, + `No token, no authorize, and no oauth installer options provided. ${tokenUsage}`, ); - } else if ((authorize !== undefined || orgAuthorize !== undefined) && usingOauth) { + } else if (authorize !== undefined && usingOauth) { throw new AppInitializationError(`Both authorize options and oauth installer options provided. ${tokenUsage}`); - } else if (authorize === undefined && orgAuthorize === undefined && usingOauth) { + } else if (authorize === undefined && usingOauth) { this.authorize = (this.receiver as ExpressReceiver).installer!.authorize; - this.orgAuthorize = (this.receiver as ExpressReceiver).installer!.authorize; - } else if (authorize === undefined && orgAuthorize !== undefined && !usingOauth) { - // only supporting org installs - this.orgAuthorize = orgAuthorize; - } else if (authorize !== undefined && orgAuthorize === undefined && !usingOauth) { - // only supporting non org installs - this.authorize = authorize; - } else if (authorize !== undefined && orgAuthorize !== undefined && !usingOauth) { - // supporting both org installs and non org installs + } else if (authorize !== undefined && !usingOauth) { this.authorize = authorize; - this.orgAuthorize = orgAuthorize; } else { this.logger.error('Never should have reached this point, please report to the team'); assertNever(); @@ -555,7 +540,7 @@ export default class App { let authorizeResult: AuthorizeResult; try { if (source.isEnterpriseInstall) { - authorizeResult = await this.orgAuthorize(source as AuthorizeSourceData, bodyArg); + authorizeResult = await this.authorize(source as AuthorizeSourceData, bodyArg); } else { authorizeResult = await this.authorize(source as AuthorizeSourceData, bodyArg); } From 6726bc272b46f1b0b6f6d87146f292b3ec383af8 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Fri, 8 Jan 2021 17:24:03 -0800 Subject: [PATCH 11/31] Apply suggestions from code review Co-authored-by: Kazuhiro Sera --- docs/_basic/ja_authenticating_oauth.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/_basic/ja_authenticating_oauth.md b/docs/_basic/ja_authenticating_oauth.md index dd88202c9..25aebf4c3 100644 --- a/docs/_basic/ja_authenticating_oauth.md +++ b/docs/_basic/ja_authenticating_oauth.md @@ -15,7 +15,7 @@ Bolt for JavaScript は `slack/install` というパスも生成します。こ Slack の OAuth インストールフローについてもっと知りたい場合は [API ドキュメント](https://api.slack.com/authentication/oauth-v2)を参照してください。 -[Enterprise Grid の OrG 全体へのインストール](https://api.slack.com/enterprise/apps)への対応を追加する場合、Bolt for JavaScript のバージョン 3.0.0 以上を利用してください。そして、最後に Slack アプリの設定画面で **Org Level Apps** の設定が有効になっていることを忘れずに確認するようにしてください。 +[Enterprise Grid の OrG 全体へのインストール](https://api.slack.com/enterprise/apps)への対応を追加する場合、Bolt for JavaScript のバージョン 3.0.0 以上を利用してください。また Slack アプリの設定画面で **Org Level Apps** の設定が有効になっていることを確認してください。 ```javascript @@ -29,10 +29,10 @@ const app = new App({ storeInstallation: async (installation) => { // 実際のデータベースに保存するために、ここのコードを変更 if (installation.isEnterpriseInstall) { - // support for org wide app installation + // OrG 全体へのインストールに対応する場合 return await database.set(installation.enterprise.id, installation); } else { - // single team app installation + // 単独のワークスペースへのインストールの場合 return await database.set(installation.team.id, installation); } throw new Error('Failed saving installation data to installationStore'); @@ -40,11 +40,11 @@ const app = new App({ fetchInstallation: async (InstallQuery) => { // 実際のデータベースから取得するために、ここのコードを変更 if (InstallQuery.isEnterpriseInstall && InstallQuery.enterpriseId !== undefined) { - // org wide app installation lookup + // OrG 全体へのインストール情報の参照 return await database.get(InstallQuery.enterpriseId); } if (InstallQuery.teamId !== undefined) { - // single team app installation lookup + // 単独のワークスペースへのインストール情報の参照 return await database.get(InstallQuery.teamId); } throw new Error('Failed fetching installation'); From b565f1e56d31277bb77619a7ef1b1bbe283fbb9b Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Tue, 5 Jan 2021 15:33:18 -0800 Subject: [PATCH 12/31] updated minimum node version to 12.13.0, updated minimum typescript to 4.1 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 482924610..72b095a0f 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@slack/types": "^1.9.0", "@slack/web-api": "^5.14.0", "@types/express": "^4.16.1", - "@types/node": ">=10", + "@types/node": ">=12", "@types/promise.allsettled": "^1.0.3", "axios": "^0.21.1", "express": "^4.16.4", @@ -79,7 +79,7 @@ "ts-node": "^8.1.0", "tsd": "^0.13.1", "tslint-config-airbnb": "^5.11.1", - "typescript": "^3.7.2" + "typescript": "^4.1.0" }, "tsd": { "directory": "types-tests" From c88d755500e9fc8825aee57f663bed6d651b9601 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Wed, 9 Sep 2020 17:06:14 -0700 Subject: [PATCH 13/31] 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 72b095a0f..76372cddc 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 554a8cbcc22bbf089454d05da425e39c82fb67cf Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Mon, 14 Dec 2020 22:01:27 -0800 Subject: [PATCH 14/31] 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 d3d3555729f581237a85f3a8014f078b8b80027d Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Tue, 15 Dec 2020 15:25:14 -0800 Subject: [PATCH 15/31] 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 3abf15be013a533b8fe08bbb7f8c562b0dc34469 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Tue, 5 Jan 2021 14:53:12 -0800 Subject: [PATCH 16/31] 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 810f5c8730d5537ebfcb7a4c635f559f195ddc3f Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Wed, 6 Jan 2021 16:38:12 -0800 Subject: [PATCH 17/31] 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 6150ef14af91591b30b498f408f2993de8356eaf Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Thu, 7 Jan 2021 10:58:58 -0800 Subject: [PATCH 18/31] 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 c2317a25a996e84b70bfef5f8ec111d0437f27df Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Fri, 8 Jan 2021 14:33:04 -0800 Subject: [PATCH 19/31] 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 40da39b07343e50517f99d022733779ad119579a Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Sun, 10 Jan 2021 23:02:43 -0800 Subject: [PATCH 20/31] 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 0f1ca4358a73088e0fa64b2ff335dab448dac8e9 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Mon, 11 Jan 2021 18:32:45 -0800 Subject: [PATCH 21/31] 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 b1d863dda1520b120b258fff42f1fae70a2361d4 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Mon, 11 Jan 2021 21:35:13 -0800 Subject: [PATCH 22/31] 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 From 1c06c19c1968b885492de83927bf0e23e745eaa3 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Tue, 12 Jan 2021 00:32:44 -0800 Subject: [PATCH 23/31] incorporated feedback into migration guide --- docs/_tutorials/migration_v3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_tutorials/migration_v3.md b/docs/_tutorials/migration_v3.md index 94646665f..9118aba91 100644 --- a/docs/_tutorials/migration_v3.md +++ b/docs/_tutorials/migration_v3.md @@ -18,7 +18,7 @@ This guide will walk you through the process of updating your app from using `@s ### Org Wide App Installation Changes to InstallationStore & orgAuthorize -In [Bolt for JavaScript 2.5.0](https://github.com/slackapi/bolt-js/releases/tag/%40slack%2Fbolt%402.5.0), we introduced support for [org wide app installations](https://api.slack.com/enterprise/apps). To add support to your applications, two new methods were introduced to the Installation Store used during OAuth, `fetchOrgInstallation` & `storeOrgInstallation`. With `@slack/bolt@3.x`, we have dropped support for these two new methods to better align with Bolt for Python and Bolt for Java. See the code samples below for the recommended changes to migrate. +In [Bolt for JavaScript 2.5.0](https://github.com/slackapi/bolt-js/releases/tag/%40slack%2Fbolt%402.5.0), we introduced support for [org wide app installations](https://api.slack.com/enterprise/apps). To add support to your applications, two new methods were introduced to the Installation Store used during OAuth, `fetchOrgInstallation` & `storeOrgInstallation`. With `@slack/bolt@3.x`, we have dropped support for these two new methods for a simpler interface and to be better aligned with Bolt for Python and Bolt for Java. See the code samples below for the recommended changes to migrate. Before: From 678fdec9e77e0277bbcbbbabe5ea3a0bcb673947 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 12 Jan 2021 17:45:20 +0900 Subject: [PATCH 24/31] Add Japanese version to #745 --- docs/_tutorials/ja_migration_v3.md | 114 +++++++++++++++++++++++++++++ docs/_tutorials/migration_v3.md | 2 +- 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 docs/_tutorials/ja_migration_v3.md diff --git a/docs/_tutorials/ja_migration_v3.md b/docs/_tutorials/ja_migration_v3.md new file mode 100644 index 000000000..7dcbef097 --- /dev/null +++ b/docs/_tutorials/ja_migration_v3.md @@ -0,0 +1,114 @@ +--- +title: 3.x マイグレーションガイド +order: 2 +slug: migration-v3 +lang: ja +layout: tutorial +permalink: /tutorial/migration-v3 +--- +# 3.x マイグレーションガイド + +
+このガイドは Bolt 2.x を利用しているアプリを 3.x にアップグレードするための手順について説明します。いくつかの変更が必要とはなりますが、ほとんどのアプリの場合で、おそらく対応に必要な時間は 5 〜 15 分程度です。 + +*注: もしすぐにアップグレードをしない場合は、[Bolt 2.x に関するサポートスケジュール](#slackbolt2x-support-schedule)をご確認ください* +
+ +--- + +### InstallationStore と orgAuthorize での OrG レベルでのインストール対応に関する変更 + +[Bolt for JavaScript 2.5.0](https://github.com/slackapi/bolt-js/releases/tag/%40slack%2Fbolt%402.5.0) で、私たちは [OrG レベルでのインストール](https://api.slack.com/enterprise/apps)のサポートを追加しました。このサポートをあなたのアプリケーションに追加するには、OAuth フローの中で使用される `fetchOrgInstallation`、`storeOrgInstallation` という二つの新しいメソッドを導入する必要がありました。 3.x では、よりシンプルなインタフェースの実現と Bolt for Python、Bolt for Java との互換性を考慮して、これらの二つの新しいメソッドのサポートを廃止しました。マイグレーションに必要となる変更については以下のコード例を参考にしてください。 + +これまで: + +```javascript +installationStore: { + storeInstallation: async (installation) => { + // change the line below so it saves to your database + return await database.set(installation.team.id, installation); + }, + fetchInstallation: async (InstallQuery) => { + // change the line below so it fetches from your database + return await database.get(InstallQuery.teamId); + }, + storeOrgInstallation: async (installation) => { + // include this method if you want your app to support org wide installations + // change the line below so it saves to your database + return await database.set(installation.enterprise.id, installation); + }, + fetchOrgInstallation: async (InstallQuery) => { + // include this method if you want your app to support org wide installations + // change the line below so it fetches from your database + return await database.get(InstallQuery.enterpriseId); + }, + }, +``` + +これから: + +```javascript +installationStore: { + storeInstallation: async (installation) => { + if (installation.isEnterpriseInstall) { + // support for org wide app installation + return await database.set(installation.enterprise.id, installation); + } else { + // single team app installation + return await database.set(installation.team.id, installation); + } + throw new Error('Failed saving installation data to installationStore'); + }, + fetchInstallation: async (InstallQuery) => { + // replace database.get so it fetches from your database + if (InstallQuery.isEnterpriseInstall && InstallQuery.enterpriseId !== undefined) { + // org wide app installation lookup + return await database.get(InstallQuery.enterpriseId); + } + if (InstallQuery.teamId !== undefined) { + // single team app installation lookup + return await database.get(InstallQuery.teamId); + } + throw new Error('Failed fetching installation'); + }, + }, +``` + +この変更に合わせて `orgAuthorize` 関数のサポートも廃止しました。もし、組み込みの OAuth 機能を利用されていない場合は、代わりに `authorize` だけを単一のワークスペースへのインストールでも OrG レベルでのインストールでも使うように変更することを推奨します。マイグレーションの手順については、以下のコード例を参考にしてください。 + +これまで: + +```javascript +const app = new App({ authorize: authorizeFn, orgAuthorize: orgAuthorizeFn, signingSecret: process.env.SLACK_SIGNING_SECRET }); +const authorizeFn = async ({ teamId, enterpriseId}) => { + // Use teamId to fetch installation details from database +} +const orgAuthorizeFn = async ({ teamId, enterpriseId }) => { + // Use enterpriseId to fetch installation details from database +} +``` + +これから: +```javascript +const app = new App({ authorize: authorizeFn, signingSecret: process.env.SLACK_SIGNING_SECRET }); +const authorizeFn = async ({ teamId, enterpriseId, isEnterpriseInstall}) => { + // if isEnterpriseInstall is true, use enterpriseId to fetch installation details from database + // else, use teamId to fetch installation details from database +} +``` + +### デフォルトのレシーバーを HTTPReceiver に変更 + +3.x から新しい [`HTTPReceiver`](https://github.com/slackapi/bolt-js/issues/670) というレシーバーを導入し、デフォルトのレシーバー実装を、これまでの `ExpressReceiver` からこのレシーバーに変更します。この変更は、Bolt for JavaScript を Express.js 以外の人気のある Web フレームワーク(Hapi.js や Koa など)とともに動作させることを容易にします。`ExpressReceiver` は引き続き Bolt for JavaScript のリリースに含まれます。また、`HTTPReceiver` は `ExpressReceiver` が提供する全ての機能を提供するわけではありません。例えば、一つのユースケースとしては、`HTTPReceiver` ではカスタムの HTTP ルート(例: ヘルスチェックのための URL を追加する)を追加する機能はサポートされていません。このようなユースケースに対応するためには、引き続き `ExpressReceiver` を利用することを推奨します。その場合はクラスを import して、インスタンス化したものを `App` のコンストラクタに渡してください。詳細は[カスタム HTTP ルートの追加](https://slack.dev/bolt-js/ja-jp/concepts#custom-routes)を参考にしてください。 + +### Bolt 2.x のサポートスケジュール + +`@slack/bolt@2.x` は **2021 年 1 月 12 日** より非推奨となります。それまでの期間はケースバイケースでバグ修正や新機能のバックポートを対応を継続します。`@slack/bolt@1.x` が非推奨となった後は、End of life(正式サポートの終了日)まで **クリティカルなバグ修正のみ** を実装し、クリティカルではない issue や pull request はクローズします。End of life は **2021 年 5 月 31 日** の予定です。この日からは `@slack/bolt@2.x` の開発は完全に終了となり、残っている open issue や pull request もクローズされます。 + +### Node の最低必須バージョン + +`@slack/bolt@3.x` は Node は `12.13.0` 以上、npm は `6.12.0` 以上が必須バージョンです。 + +### TypeScript の最低必須バージョン + +[TypeScript 利用ガイド]({{ site.url | append: site.baseurl }}/ja-jp/tutorial/using-typescript) でも説明していますが、`@slack/bolt@3.x` は TypeScirpt 4.1 以上が必須バージョンです。 diff --git a/docs/_tutorials/migration_v3.md b/docs/_tutorials/migration_v3.md index 9118aba91..8663c5e0e 100644 --- a/docs/_tutorials/migration_v3.md +++ b/docs/_tutorials/migration_v3.md @@ -106,7 +106,7 @@ In `@slack/bolt@3.x`, we have introduced a new default [`HTTPReceiver`](https:// ### @slack/bolt@2.x support schedule -`@slack/bolt@2.x` will be deprecated on **January 12th, 2020**. We will only implement **critical bug fixes** until the official end of life date and close non critical issues and pull requests, which is slated for **May 31st, 2021**. At this time, development will fully stop for `@slack/bolt@2.x` and all remaining open issues and pull requests will be closed. +`@slack/bolt@2.x` will be deprecated on **January 12th, 2021**. We will only implement **critical bug fixes** until the official end of life date and close non critical issues and pull requests, which is slated for **May 31st, 2021**. At this time, development will fully stop for `@slack/bolt@2.x` and all remaining open issues and pull requests will be closed. ### Minimum Node Version From 5d990f6da1ccea217b38127b65d0467960d560b1 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 12 Jan 2021 17:52:15 +0900 Subject: [PATCH 25/31] Update docs/_tutorials/ja_migration_v3.md --- docs/_tutorials/ja_migration_v3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_tutorials/ja_migration_v3.md b/docs/_tutorials/ja_migration_v3.md index 7dcbef097..4efcc2adc 100644 --- a/docs/_tutorials/ja_migration_v3.md +++ b/docs/_tutorials/ja_migration_v3.md @@ -103,7 +103,7 @@ const authorizeFn = async ({ teamId, enterpriseId, isEnterpriseInstall}) => { ### Bolt 2.x のサポートスケジュール -`@slack/bolt@2.x` は **2021 年 1 月 12 日** より非推奨となります。それまでの期間はケースバイケースでバグ修正や新機能のバックポートを対応を継続します。`@slack/bolt@1.x` が非推奨となった後は、End of life(正式サポートの終了日)まで **クリティカルなバグ修正のみ** を実装し、クリティカルではない issue や pull request はクローズします。End of life は **2021 年 5 月 31 日** の予定です。この日からは `@slack/bolt@2.x` の開発は完全に終了となり、残っている open issue や pull request もクローズされます。 +`@slack/bolt@2.x` は **2021 年 1 月 12 日** より非推奨となります。それまでの期間はケースバイケースでバグ修正や新機能のバックポートを対応を継続します。`@slack/bolt@2.x` が非推奨となった後は、End of life(正式サポートの終了日)まで **クリティカルなバグ修正のみ** を実装し、クリティカルではない issue や pull request はクローズします。End of life は **2021 年 5 月 31 日** の予定です。この日からは `@slack/bolt@2.x` の開発は完全に終了となり、残っている open issue や pull request もクローズされます。 ### Node の最低必須バージョン From d36024de51d7f7979039cd7e6b31b2758f4576aa Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 12 Jan 2021 20:57:56 +0900 Subject: [PATCH 26/31] Add missing Japanese documents for v3 --- docs/_basic/ja_socket_mode.md | 65 ++++++++++++++++++++++++++ docs/_tutorials/ja_using-typescript.md | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 docs/_basic/ja_socket_mode.md diff --git a/docs/_basic/ja_socket_mode.md b/docs/_basic/ja_socket_mode.md new file mode 100644 index 000000000..8d5c7fdfa --- /dev/null +++ b/docs/_basic/ja_socket_mode.md @@ -0,0 +1,65 @@ +--- +title: ソケットモードの使用 +lang: ja +slug: socket-mode +order: 16 +--- + +
+[ソケットモード](https://api.slack.com/socket-mode) は、アプリに WebSocket での接続と、そのコネクション経由でのデータ受信を可能とします。コネクションをハンドリングするために @slack/bolt@3.0.0` 以上では `SokcetModeReceiver` というレシーバーが提供されています。ソケットモードを使う前に、アプリの管理画面でソケットモードの機能が有効になっているコオを確認しておいてください。 + +`SocketModeReceiver` を使う方法は `App` インスタンスの初期化時にコンストラクターに `socketMode: true` と `appToken: YOUR_APP_TOKEN` を渡すだけです。App Level Token は、アプリ管理画面の **Basic Information** セクションから取得できます。 +
+ +```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'); +})(); +``` + +
+ +

ソケットモードレシーバーのカスタム初期化

+
+ +
+ +以下のように `@slack/bolt` から `SocketModeReceiver` を import して、カスタムされたインスタンスとして定義することができます。 + +
+ +```javascript +const { App, SocketModeReceiver } = require('@slack/bolt'); + +const socketModeReceiver = new SocketModeReceiver({ + appToken: process.env.APP_TOKEN, + + // 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, + // OAuth を使うなら以下の token 指定は不要です + token: process.env.BOT_TOKEN +}); + +(async () => { + await app.start(); + console.log('⚡️ Bolt app started'); +})(); +``` + +
diff --git a/docs/_tutorials/ja_using-typescript.md b/docs/_tutorials/ja_using-typescript.md index 96f36390f..f1ffbd23d 100644 --- a/docs/_tutorials/ja_using-typescript.md +++ b/docs/_tutorials/ja_using-typescript.md @@ -15,4 +15,4 @@ permalink: /ja-jp/tutorial/using-typescript ### 最低必須バージョン -`@slack/bolt` の最新のメジャーバージョンは TypeScript 3.7 以上での利用をサポートしています。 +`@slack/bolt` の最新のメジャーバージョンは TypeScript 4.1 以上での利用をサポートしています。 From 41b28612e3a26890624ff27cf4cba1f537c1c82b Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Tue, 12 Jan 2021 14:53:20 -0800 Subject: [PATCH 27/31] updated release steps in maintainers guide --- .github/maintainers_guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/maintainers_guide.md b/.github/maintainers_guide.md index ecc80acc6..408995118 100644 --- a/.github/maintainers_guide.md +++ b/.github/maintainers_guide.md @@ -51,7 +51,7 @@ When documentation is in a beta state, it requires a new, distinct collection of 2. Merge into main repository * Create a pull request with the commit that was just made. Be certain to include the tag. For - example: `git push username main:rel-v1.0.8 && git push --tags username`. + example: `git push username main --tags`. * Once tests pass and a reviewer has approved, merge the pull request. You will also want to update your local `main` branch. * Push the new tag up to origin `git push --tags origin`. From 87f891fff6cf246324303505dbcc71a8f540d9e6 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Tue, 12 Jan 2021 14:55:21 -0800 Subject: [PATCH 28/31] updated dependencies --- examples/socket-mode/package.json | 2 +- package.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/socket-mode/package.json b/examples/socket-mode/package.json index d4730de7e..b082479a1 100644 --- a/examples/socket-mode/package.json +++ b/examples/socket-mode/package.json @@ -10,6 +10,6 @@ "author": "Slack Technologies, Inc.", "license": "MIT", "dependencies": { - "@slack/bolt": "feat-socket-mode" + "@slack/bolt": "3.0.0" } } diff --git a/package.json b/package.json index 76372cddc..4de7ee14c 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,11 @@ "url": "https://github.com/slackapi/bolt-js/issues" }, "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", + "@slack/logger": "^3.0.0", + "@slack/oauth": "^2.0.0", + "@slack/socket-mode": "1.0.0", + "@slack/types": "^2.0.0", + "@slack/web-api": "^6.0.0", "@types/express": "^4.16.1", "@types/node": ">=12", "@types/promise.allsettled": "^1.0.3", From f0f618034c91a3ddc970f5b183def2ca26d81be0 Mon Sep 17 00:00:00 2001 From: Steve Gill Date: Tue, 12 Jan 2021 16:00:39 -0800 Subject: [PATCH 29/31] Fix merge conflict issue --- src/App.spec.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/App.spec.ts b/src/App.spec.ts index cc464b535..245bd04c0 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -67,32 +67,6 @@ describe('App', () => { assert(authorizeCallback.notCalled, 'Should not call the authorize callback on instantiation'); assert.instanceOf(app, App); }); - it('should succeed with an orgAuthorize callback', async () => { - // Arrange - const authorizeCallback = sinon.fake(); - const App = await importApp(); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match - - // Act - const app = new App({ orgAuthorize: authorizeCallback, signingSecret: '' }); - - // Assert - assert(authorizeCallback.notCalled, 'Should not call the orgAuthorize callback on instantiation'); - assert.instanceOf(app, App); - }); - it('should succeed with an authorize and orgAuthorize callback', async () => { - // Arrange - const authorizeCallback = sinon.fake(); - const orgAuthorizeCallback = sinon.fake(); - const App = await importApp(); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match - - // Act - const app = new App({ orgAuthorize: orgAuthorizeCallback, authorize: authorizeCallback, signingSecret: '' }); - - // Assert - assert(authorizeCallback.notCalled, 'Should not call the authorize callback on instantiation'); - assert(orgAuthorizeCallback.notCalled, 'Should not call the orgAuthorize callback on instantiation'); - assert.instanceOf(app, App); - }); it('should fail without a token for single team authorization, authorize callback, nor oauth installer', async () => { // Arrange const App = await importApp(); // eslint-disable-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match From d955feef86d12bdb6fcc4c0953047a2ad3713980 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 13 Jan 2021 11:20:44 +0900 Subject: [PATCH 30/31] Fix Socket Mode documents --- docs/_basic/ja_socket_mode.md | 2 +- docs/_basic/socket_mode.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_basic/ja_socket_mode.md b/docs/_basic/ja_socket_mode.md index 8d5c7fdfa..70cfefc4e 100644 --- a/docs/_basic/ja_socket_mode.md +++ b/docs/_basic/ja_socket_mode.md @@ -15,7 +15,7 @@ order: 16 const { App } = require('@slack/bolt'); const app = new App({ - token: process.env.BOT_TOKEN + token: process.env.BOT_TOKEN, socketMode: true, appToken: process.env.APP_TOKEN, }); diff --git a/docs/_basic/socket_mode.md b/docs/_basic/socket_mode.md index 58a3e496f..34cd950ca 100644 --- a/docs/_basic/socket_mode.md +++ b/docs/_basic/socket_mode.md @@ -15,7 +15,7 @@ To use the `SocketModeReceiver`, just pass in `socketMode:true` and `appToken:YO const { App } = require('@slack/bolt'); const app = new App({ - token: process.env.BOT_TOKEN + token: process.env.BOT_TOKEN, socketMode: true, appToken: process.env.APP_TOKEN, }); From ad6a32ffc91acb8b98454e7f3b47c7eab0b249ad Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Wed, 13 Jan 2021 10:20:59 -0800 Subject: [PATCH 31/31] prettier --- src/App.spec.ts | 74 ++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/App.spec.ts b/src/App.spec.ts index ec62a2628..245bd04c0 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -1056,53 +1056,53 @@ describe('App', () => { await ackFn(); await next!(); }); - app.shortcut({ callback_id: 'message_action_callback_id' }, async ({ }) => { + app.shortcut({ callback_id: 'message_action_callback_id' }, async ({}) => { await shortcutFn(); }); - app.shortcut({ type: 'message_action', callback_id: 'another_message_action_callback_id' }, async ({ }) => { + app.shortcut({ type: 'message_action', callback_id: 'another_message_action_callback_id' }, async ({}) => { await shortcutFn(); }); - app.shortcut({ type: 'message_action', callback_id: 'does_not_exist' }, async ({ }) => { + app.shortcut({ type: 'message_action', callback_id: 'does_not_exist' }, async ({}) => { await shortcutFn(); }); - app.shortcut({ callback_id: 'shortcut_callback_id' }, async ({ }) => { + app.shortcut({ callback_id: 'shortcut_callback_id' }, async ({}) => { await shortcutFn(); }); - app.shortcut({ type: 'shortcut', callback_id: 'another_shortcut_callback_id' }, async ({ }) => { + app.shortcut({ type: 'shortcut', callback_id: 'another_shortcut_callback_id' }, async ({}) => { await shortcutFn(); }); - app.shortcut({ type: 'shortcut', callback_id: 'does_not_exist' }, async ({ }) => { + app.shortcut({ type: 'shortcut', callback_id: 'does_not_exist' }, async ({}) => { await shortcutFn(); }); - app.action('block_action_id', async ({ }) => { + app.action('block_action_id', async ({}) => { await actionFn(); }); - app.action({ callback_id: 'interactive_message_callback_id' }, async ({ }) => { + app.action({ callback_id: 'interactive_message_callback_id' }, async ({}) => { await actionFn(); }); - app.action({ callback_id: 'dialog_submission_callback_id' }, async ({ }) => { + app.action({ callback_id: 'dialog_submission_callback_id' }, async ({}) => { await actionFn(); }); - app.view('view_callback_id', async ({ }) => { + app.view('view_callback_id', async ({}) => { await viewFn(); }); - app.view({ callback_id: 'view_callback_id', type: 'view_closed' }, async ({ }) => { + app.view({ callback_id: 'view_callback_id', type: 'view_closed' }, async ({}) => { await viewFn(); }); - app.options('external_select_action_id', async ({ }) => { + app.options('external_select_action_id', async ({}) => { await optionsFn(); }); - app.options({ callback_id: 'dialog_suggestion_callback_id' }, async ({ }) => { + app.options({ callback_id: 'dialog_suggestion_callback_id' }, async ({}) => { await optionsFn(); }); - app.event('app_home_opened', async ({ }) => { + app.event('app_home_opened', async ({}) => { /* noop */ }); - app.message('hello', async ({ }) => { + app.message('hello', async ({}) => { /* noop */ }); - app.command('/echo', async ({ }) => { + app.command('/echo', async ({}) => { /* noop */ }); @@ -1112,7 +1112,7 @@ describe('App', () => { type: 'view_submission', unknown_key: 'should be detected', } as any) as ViewConstraints; - app.view(invalidViewConstraints1, async ({ }) => { + app.view(invalidViewConstraints1, async ({}) => { /* noop */ }); assert.isTrue(fakeLogger.error.called); @@ -1124,7 +1124,7 @@ describe('App', () => { type: undefined, unknown_key: 'should be detected', } as any) as ViewConstraints; - app.view(invalidViewConstraints2, async ({ }) => { + app.view(invalidViewConstraints2, async ({}) => { /* noop */ }); assert.isTrue(fakeLogger.error.called); @@ -1165,53 +1165,53 @@ describe('App', () => { await ackFn(); await next!(); }); - app.shortcut({ callback_id: 'message_action_callback_id' }, async ({ }) => { + app.shortcut({ callback_id: 'message_action_callback_id' }, async ({}) => { await shortcutFn(); }); - app.shortcut({ type: 'message_action', callback_id: 'another_message_action_callback_id' }, async ({ }) => { + app.shortcut({ type: 'message_action', callback_id: 'another_message_action_callback_id' }, async ({}) => { await shortcutFn(); }); - app.shortcut({ type: 'message_action', callback_id: 'does_not_exist' }, async ({ }) => { + app.shortcut({ type: 'message_action', callback_id: 'does_not_exist' }, async ({}) => { await shortcutFn(); }); - app.shortcut({ callback_id: 'shortcut_callback_id' }, async ({ }) => { + app.shortcut({ callback_id: 'shortcut_callback_id' }, async ({}) => { await shortcutFn(); }); - app.shortcut({ type: 'shortcut', callback_id: 'another_shortcut_callback_id' }, async ({ }) => { + app.shortcut({ type: 'shortcut', callback_id: 'another_shortcut_callback_id' }, async ({}) => { await shortcutFn(); }); - app.shortcut({ type: 'shortcut', callback_id: 'does_not_exist' }, async ({ }) => { + app.shortcut({ type: 'shortcut', callback_id: 'does_not_exist' }, async ({}) => { await shortcutFn(); }); - app.action('block_action_id', async ({ }) => { + app.action('block_action_id', async ({}) => { await actionFn(); }); - app.action({ callback_id: 'interactive_message_callback_id' }, async ({ }) => { + app.action({ callback_id: 'interactive_message_callback_id' }, async ({}) => { await actionFn(); }); - app.action({ callback_id: 'dialog_submission_callback_id' }, async ({ }) => { + app.action({ callback_id: 'dialog_submission_callback_id' }, async ({}) => { await actionFn(); }); - app.view('view_callback_id', async ({ }) => { + app.view('view_callback_id', async ({}) => { await viewFn(); }); - app.view({ callback_id: 'view_callback_id', type: 'view_closed' }, async ({ }) => { + app.view({ callback_id: 'view_callback_id', type: 'view_closed' }, async ({}) => { await viewFn(); }); - app.options('external_select_action_id', async ({ }) => { + app.options('external_select_action_id', async ({}) => { await optionsFn(); }); - app.options({ callback_id: 'dialog_suggestion_callback_id' }, async ({ }) => { + app.options({ callback_id: 'dialog_suggestion_callback_id' }, async ({}) => { await optionsFn(); }); - app.event('app_home_opened', async ({ }) => { + app.event('app_home_opened', async ({}) => { /* noop */ }); - app.message('hello', async ({ }) => { + app.message('hello', async ({}) => { /* noop */ }); - app.command('/echo', async ({ }) => { + app.command('/echo', async ({}) => { /* noop */ }); @@ -1221,7 +1221,7 @@ describe('App', () => { type: 'view_submission', unknown_key: 'should be detected', } as any) as ViewConstraints; - app.view(invalidViewConstraints1, async ({ }) => { + app.view(invalidViewConstraints1, async ({}) => { /* noop */ }); assert.isTrue(fakeLogger.error.called); @@ -1233,7 +1233,7 @@ describe('App', () => { type: undefined, unknown_key: 'should be detected', } as any) as ViewConstraints; - app.view(invalidViewConstraints2, async ({ }) => { + app.view(invalidViewConstraints2, async ({}) => { /* noop */ }); assert.isTrue(fakeLogger.error.called); @@ -1758,7 +1758,7 @@ async function importApp( function withNoopWebClient(): Override { return { '@slack/web-api': { - WebClient: class { }, + WebClient: class {}, }, }; }