import http , { RequestListener , Agent } from 'http' ; import axios , { AxiosInstance } from 'axios' ; import isString from 'lodash.isstring' ; import isRegExp from 'lodash.isregexp' ; import isFunction from 'lodash.isfunction' ; import isPlainObject from 'lodash.isplainobject' ; import debugFactory from 'debug' ; import { RequestHandler } from 'express' ; import { ErrorCode , CodedError } from './errors' ; import { createHTTPHandler } from './http-handler' ; import { packageIdentifier , promiseTimeout , isFalsy } from './util' ; const debug = debugFactory ( '@slack/interactive-messages:adapter' ) ; /**
 * The type of stored constraints.
 */ const enum StoredConstraintsType { Action = 'action' , Shortcut = 'shortcut' , Options = 'options' , ViewSubmission = 'view_submission' , ViewClosed = 'view_closed' } /** Some HTTP response statuses. */ export enum ResponseStatus { Ok = 200 , Failure = 500 } param matchingConstraints returns function formatMatchingConstraints < C extends AnyConstraints > ( matchingConstraints ) { if ( matchingConstraints === undefined || matchingConstraints === null ) { throw new TypeError ( 'Constraints cannot be undefined or null' ) ; } let ret = { } ; if ( ! isPlainObject ( matchingConstraints ) ) { ret . callbackId = matchingConstraints as string | RegExp ; } else { ret = { ... matchingConstraints as C } ; } return ret as C ; } param matchingConstraints returns function validateConstraints ( matchingConstraints ) { if ( ! isFalsy ( matchingConstraints . callbackId ) && ! ( isString ( matchingConstraints . callbackId ) || isRegExp ( matchingConstraints . callbackId ) ) ) { return new TypeError ( 'Callback ID must be a string or RegExp' ) ; } if ( ! hasBlockRelatedConstraints ( matchingConstraints ) ) { return false ; } if ( ! isFalsy ( matchingConstraints . blockId ) && ! ( isString ( matchingConstraints . blockId ) || isRegExp ( matchingConstraints . blockId ) ) ) { return new TypeError ( 'Block ID must be a string or RegExp' ) ; } if ( ! isFalsy ( matchingConstraints . actionId ) && ! ( isString ( matchingConstraints . actionId ) || isRegExp ( matchingConstraints . actionId ) ) ) { return new TypeError ( 'Action ID must be a string or RegExp' ) ; } return false ; } param matchingConstraints returns function validateOptionsConstraints ( optionsConstraints ) { if ( ! isFalsy ( optionsConstraints . within ) && ! ( optionsConstraints . within === 'interactive_message' || optionsConstraints . within === 'block_actions' || optionsConstraints . within === 'dialog' ) ) { return new TypeError ( 'Within must be \'block_actions\', \'interactive_message\' or \'dialog\'' ) ; } return false ; } param viewConstraints returns function validateViewConstraints ( viewConstraints ) { if ( viewConstraints . externalId === null || ( ( ! isFalsy ( viewConstraints . externalId ) && ! ( isString ( viewConstraints . externalId ) || isRegExp ( viewConstraints . externalId ) ) ) ) ) { return new TypeError ( 'External ID must be a string or RegExp' ) ; } if ( viewConstraints . viewId === null || ( ! isFalsy ( viewConstraints . viewId ) && ! isString ( viewConstraints . viewId ) ) ) { return new TypeError ( 'View ID must be a string' ) ; } return false ; } /**
 * An adapter for Slack's interactive message components such as buttons, menus, and dialogs.
 */ export default class SlackMessageAdapter { /**
     * Slack app signing secret used to authenticate request
     */ public signingSecret : string ; /**
     * The number of milliseconds to wait before flushing a synchronous response to an incoming request and falling back
     * to an asynchronous response.
     */ public syncResponseTimeout : number ; /**
     * Whether or not promises that resolve after the syncResponseTimeout can fallback to a request for the response_url.
     * This only works in cases where the semantic meaning of the response and the response_url are the same.
     */ public lateResponseFallbackEnabled : boolean ; private callbacks : [ StoredConstraints , Callback ] [ ] ; private axios : AxiosInstance ; private server ? : http . Server ; param signingSecret param options . syncResponseTimeout param options . lateResponseFallbackEnabled public constructor ( signingSecret , { syncResponseTimeout = 2500 , lateResponseFallbackEnabled = true , agent = undefined , } = { } ) { if ( ! isString ( signingSecret ) ) { throw new TypeError ( 'SlackMessageAdapter needs a signing secret' ) ; } if ( syncResponseTimeout > 3000 || syncResponseTimeout < 1 ) { throw new TypeError ( 'syncResponseTimeout must be between 1 and 3000' ) ; } this . signingSecret = signingSecret ; this . syncResponseTimeout = syncResponseTimeout ; this . lateResponseFallbackEnabled = lateResponseFallbackEnabled ; this . callbacks = [ ] ; this . axios = axios . create ( { headers : { 'User-Agent' : packageIdentifier ( ) , } , httpAgent : agent , httpsAgent : agent , proxy : false , } ) ; debug ( 'instantiated' ) ; } returns public async createServer ( ) { return http . createServer ( this . requestListener ( ) ) ; } returns public start ( port ) { return this . createServer ( ) . then ( ( server ) => new Promise ( ( resolve , reject ) => { this . server = server ; server . on ( 'error' , reject ) ; server . listen ( port , ( ) => resolve ( server ) ) ; debug ( 'server started - port: %s' , port ) ; } ) ) ; } returns public stop ( ) { return new Promise ( ( resolve , reject ) => { if ( ! isFalsy ( this . server ) ) { this . server . close ( ( error ) => { delete this . server ; if ( ! isFalsy ( error ) ) { reject ( error ) ; } else { resolve ( ) ; } } ) ; } else { reject ( new Error ( 'SlackMessageAdapter cannot stop when it did not start a server' ) ) ; } } ) ; } returns public expressMiddleware ( ) { const requestListener = this . requestListener ( ) ; return ( req , res , _next ) => { requestListener ( req , res ) ; } ; } /**
     * Create a request listener function that handles HTTP requests, verifies requests and dispatches responses
     */ public requestListener ( ) { return createHTTPHandler ( this ) ; } param matchingConstraints param callback returns public action ( matchingConstraints , callback ) { const actionConstraints = formatMatchingConstraints ( matchingConstraints ) ; const error = validateConstraints ( actionConstraints ) ; if ( error ) { debug ( 'action could not be registered: %s' , error . message ) ; throw error ; } const storableConstraints = Object . assign ( actionConstraints , { handlerType : StoredConstraintsType . Action as const , } ) ; return this . registerCallback ( storableConstraints , callback ) ; } public shortcut ( matchingConstraints , callback ) { const shortcutConstraints = formatMatchingConstraints ( matchingConstraints ) ; const error = validateConstraints ( shortcutConstraints ) ; if ( error ) { debug ( 'shortcut could not be registered: %s' , error . message ) ; throw error ; } const storableConstraints = Object . assign ( shortcutConstraints , { handlerType : StoredConstraintsType . Shortcut as const , } ) ; return this . registerCallback ( storableConstraints , callback ) ; } param matchingConstraints param callback returns public options ( matchingConstraints , callback ) { const optionsConstraints = formatMatchingConstraints ( matchingConstraints ) ; const error = validateConstraints ( optionsConstraints ) || validateOptionsConstraints ( optionsConstraints ) ; if ( error ) { debug ( 'options could not be registered: %s' , error . message ) ; throw error ; } const storableConstraints = Object . assign ( optionsConstraints , { handlerType : StoredConstraintsType . Options as const , } ) ; return this . registerCallback ( storableConstraints , callback ) ; } param matchingConstraints param callback returns public viewSubmission ( matchingConstraints , callback ) { const viewConstraints = formatMatchingConstraints ( matchingConstraints ) ; const error = validateConstraints ( viewConstraints ) || validateViewConstraints ( viewConstraints ) ; if ( error ) { debug ( 'view submission could not be registered: %s' , error . message ) ; throw error ; } const storableConstraints = Object . assign ( viewConstraints , { handlerType : StoredConstraintsType . ViewSubmission as const , } ) ; return this . registerCallback ( storableConstraints , callback ) ; } param matchingConstraints param callback returns public viewClosed ( matchingConstraints , callback ) { const viewConstraints = formatMatchingConstraints ( matchingConstraints ) ; const error = validateConstraints ( viewConstraints ) || validateViewConstraints ( viewConstraints ) ; if ( error ) { debug ( 'view closed could not be registered: %s' , error . message ) ; throw error ; } const storableConstraints = Object . assign ( viewConstraints , { handlerType : StoredConstraintsType . ViewClosed as const , } ) ; return this . registerCallback ( storableConstraints , callback ) ; } remarks internal returns public dispatch ( payload ) { const callback = this . matchCallback ( payload ) ; if ( isFalsy ( callback ) ) { debug ( 'dispatch could not find a handler' ) ; debug ( { payload } ) ; return undefined ; } debug ( 'dispatching to handler' ) ; const [ , callbackFn ] = callback ; const respond = payload . response_url ? ( message ) : Promise < any > => { if ( typeof ( message as any ) . then === 'function' ) { throw new TypeError ( 'Cannot use a Promise as the parameter for respond()' ) ; } debug ( 'sending async response' ) ; return this . axios . post ( payload . response_url , message ) ; } : undefined ; let callbackResult ; try { callbackResult = callbackFn . call ( this , payload , respond as Respond ) ; } catch ( error ) { debug ( 'callback error: %o' , error ) ; return Promise . resolve ( { status : ResponseStatus . Failure } ) ; } if ( ! isFalsy ( callbackResult ) ) { return promiseTimeout ( this . syncResponseTimeout , callbackResult ) . then ( ( content ) => ( { content , status : ResponseStatus . Ok } ) ) . catch < DispatchResult > ( ( error ) => { if ( error . code === ErrorCode . PromiseTimeout ) { if ( ! this . lateResponseFallbackEnabled || respond === undefined || payload . type === 'dialog_submission' ) { debug ( 'WARNING: The response Promise did not resolve under the timeout.' ) ; return ( callbackResult as Promise < any > ) . then ( ( content ) => ( { content , status : ResponseStatus . Ok } ) ) . catch ( ( ) => ( { status : ResponseStatus . Failure } ) ) ; } ( callbackResult as Promise < any > ) . then ( respond ) . catch ( ( callbackError ) => { debug ( 'ERROR: Promise was late and failed. Use `.catch()` to handle errors.' ) ; throw callbackError ; } ) ; return { status : ResponseStatus . Ok } ; } return { status : ResponseStatus . Failure } ; } ) ; } return Promise . resolve ( { status : 200 } ) ; } private registerCallback ( constraints , callback ) { if ( ! isFunction ( callback ) ) { debug ( 'did not register callback because its not a function' ) ; throw new TypeError ( 'callback must be a function' ) ; } this . callbacks . push ( [ constraints , callback ] ) ; return this ; } private matchCallback ( payload ) { return this . callbacks . find ( ( [ constraints ] ) => { if ( ! isFalsy ( constraints . callbackId ) ) { const callbackId = ( ( constraints . handlerType === StoredConstraintsType . ViewSubmission || constraints . handlerType === StoredConstraintsType . ViewClosed ) && payload . view ) ? payload . view . callback_id : payload . callback_id ; if ( isString ( constraints . callbackId ) && callbackId !== constraints . callbackId ) { return false ; } if ( isRegExp ( constraints . callbackId ) && ! ( constraints . callbackId as RegExp ) . test ( callbackId ) ) { return false ; } } if ( constraints . handlerType === StoredConstraintsType . Action ) { if ( ! ( payload . actions || payload . submission || payload . message ) ) { return false ; } const action = payload . actions ? payload . actions [ 0 ] : { } ; if ( ! isFalsy ( constraints . blockId ) ) { if ( isString ( constraints . blockId ) && action . block_id !== constraints . blockId ) { return false ; } if ( isRegExp ( constraints . blockId ) && ! ( constraints . blockId as RegExp ) . test ( action . block_id ) ) { return false ; } } if ( ! isFalsy ( constraints . actionId ) ) { if ( isString ( constraints . actionId ) && action . action_id !== constraints . actionId ) { return false ; } if ( isRegExp ( constraints . actionId ) && ! ( constraints . actionId as RegExp ) . test ( action . action_id ) ) { return false ; } } const type = action . type || payload . type || ( action . selected_options && 'select' ) ; if ( ! type ) { debug ( 'no type found in dispatched action' ) ; } if ( ! isFalsy ( constraints . type ) && constraints . type !== type ) { return false ; } if ( 'unfurl' in constraints && ( ( constraints . unfurl && ! payload . is_app_unfurl ) || ( ! constraints . unfurl && payload . is_app_unfurl ) ) ) { return false ; } } if ( constraints . handlerType === StoredConstraintsType . Options ) { if ( ! ( 'name' in payload || ( payload . type && payload . type === 'block_suggestion' ) ) ) { return false ; } if ( ! isFalsy ( constraints . blockId ) ) { if ( isString ( constraints . blockId ) && payload . block_id !== constraints . blockId ) { return false ; } if ( isRegExp ( constraints . blockId ) && ! ( constraints . blockId as RegExp ) . test ( payload . block_id ) ) { return false ; } } if ( ! isFalsy ( constraints . actionId ) ) { if ( isString ( constraints . actionId ) && payload . action_id !== constraints . actionId ) { return false ; } if ( isRegExp ( constraints . actionId ) && ! ( constraints . actionId as RegExp ) . test ( payload . action_id ) ) { return false ; } } if ( ! isFalsy ( constraints . within ) ) { if ( constraints . within === 'interactive_message' && payload . type !== 'interactive_message' ) { return false ; } if ( constraints . within === 'block_actions' && payload . type !== 'block_suggestion' ) { return false ; } if ( constraints . within === 'dialog' && payload . type !== 'dialog_suggestion' ) { return false ; } } } if ( constraints . handlerType === StoredConstraintsType . ViewSubmission || constraints . handlerType === StoredConstraintsType . ViewClosed ) { if ( ! isFalsy ( payload . type ) && ( ( constraints . handlerType === StoredConstraintsType . ViewSubmission && payload . type !== 'view_submission' ) || ( constraints . handlerType === StoredConstraintsType . ViewClosed && payload . type !== 'view_closed' ) ) ) { return false ; } if ( isFalsy ( payload . view ) ) { return false ; } if ( ! isFalsy ( constraints . viewId ) && payload . view . id !== constraints . viewId ) { return false ; } if ( ! isFalsy ( constraints . externalId ) ) { if ( isString ( constraints . externalId ) && payload . view . external_id !== constraints . externalId ) { return false ; } if ( isRegExp ( constraints . externalId ) && ! ( constraints . externalId as RegExp ) . test ( payload . view . external_id ) ) { return false ; } } } return true ; } ) ; } } /**
 * The result of a call to  SlackMessageAdapter .
  export interface DispatchResult { status : ResponseStatus ; content ? : any ; } /**
 * Options for constructing  SlackMessageAdapter .
  export interface MessageAdapterOptions { syncResponseTimeout ? : number ; lateResponseFallbackEnabled ? : boolean ; agent ? : Agent ; } /**
 * Constraints on when to call an action handler.
 */ export interface ActionConstraints { /**
     * A string or RegExp to match against the `callback_id`
     */ callbackId ? : string | RegExp ; /**
     * A string or RegExp to match against the `block_id`
     */ blockId ? : string | RegExp ; /**
     * A string or RegExp to match against the `action_id`
     */ actionId ? : string | RegExp ; /**
     * Valid types include all
     * [actions block elements](https://api.slack.com/reference/messaging/interactive-components),
     * `select` only for menu selections, or `dialog_submission` only for dialog submissions
     */ type ? : string ; /**
     * When `true` only match actions from an unfurl
     */ unfurl ? : boolean ; } /**
 * Constraints on when to call an shortcut handler.
 */ export interface ShortcutConstraints { /**
     * A string or RegExp to match against the `callback_id`
     */ callbackId ? : string | RegExp ; /**
     * Valid type includes shortcut
     */ type ? : 'shortcut' ; } /**
 * Constraints on when to call an options handler.
 */ export interface OptionsConstraints { /**
     * A string or RegExp to match against the `callback_id`
     */ callbackId ? : string | RegExp ; /**
     * A string or RegExp to match against the `block_id`
     */ blockId ? : string | RegExp ; /**
     * A string or RegExp to match against the `action_id`
     */ actionId ? : string | RegExp ; /**
     * The source of options request.
     */ within : 'block_actions' | 'interactive_message' | 'dialog' ; } /**
 * Constraints on when to call a view submission or view closed handler.
 */ export interface ViewConstraints { /**
     * A string or RegExp to match against the `callback_id`
     */ callbackId ? : string | RegExp ; /**
     * A string to match against the `external_id`
     */ externalId ? : string | RegExp ; /**
     * A string to match against the `view_id`
     */ viewId ? : string ; } export type AnyConstraints = ActionConstraints | OptionsConstraints | ViewConstraints | ShortcutConstraints ; /**
 * Internal storage type that describes the constraints of an ActionHandler or OptionsHandler.
 */ export type StoredConstraints = ( { handlerType : StoredConstraintsType . Action ; } & ActionConstraints ) | ( { handlerType : StoredConstraintsType . Shortcut ; } & ShortcutConstraints ) | ( { handlerType : StoredConstraintsType . Options ; } & OptionsConstraints ) | ( { handlerType : StoredConstraintsType . ViewSubmission ; } & ViewConstraints ) | ( { handlerType : StoredConstraintsType . ViewClosed ; } & ViewConstraints ) ; param message returns export type Respond = ( message : any ) => Promise < unknown > ; param payload param respond returns export type ActionHandler = ( payload : any , respond : Respond ) => any | Promise < any > | undefined ; /**
 * A handler function for global shortcuts.
 *
 * TODO: describe the payload and return values more specifically?
 */ export type ShortcutHandler = ( payload : any ) => any | Promise < any > | undefined ; param payload returns export type OptionsHandler = ( payload : any ) => any | Promise < any > | undefined ; /**
 * A handler function for view submission requests.
 *
 * TODO: describe the payload and return values more specifically?
 */ export type ViewSubmissionHandler = ( payload : any ) => any | Promise < any > | undefined ; /**
 * A handler function for view closed requests.
 *
 * TODO: describe the payload and return values more specifically?
 */ export type ViewClosedHandler = ( payload : any ) => void ; type Callback = ActionHandler | OptionsHandler | ViewSubmissionHandler | ViewClosedHandler ; function hasBlockRelatedConstraints ( constraints ) { const asBlockRelatedConstraints = constraints as ( ActionConstraints | OptionsConstraints ) ; return ! ( isFalsy ( asBlockRelatedConstraints . blockId ) && isFalsy ( asBlockRelatedConstraints . actionId ) ) ; }