slapp
Version:
A module for Slack App integrations
1,061 lines (976 loc) • 30.9 kB
JavaScript
'use strict'
const EventEmitter = require('events')
const Slack = require('slack')
const deap = require('deap/shallow')
const conversationStore = require('./conversation_store')
const Receiver = require('./receiver/')
const logger = require('./logger')
const pathToRegexp = require('path-to-regexp')
const HOUR = 60 * 60
/**
* A Slack App
* @class Slapp
* @api private
*/
class Slapp extends EventEmitter {
/**
* Construct a Slapp, accepts an options object
*
* ##### Parameters
* - `opts.verify_token` Slack Veryify token to validate authenticity of requests coming from Slack
* - `opts.signing_secret` Slack signing secret to check/verify the signature of requests coming from Slack
* - `opts.signing_version` Slack signing version string, defaults to 'v0'
* - `opts.convo_store` Implementation of ConversationStore, defaults to memory
* - `opts.context` `Function (req, res, next)` Required HTTP Middleware function to enrich incoming request with context
* - `opts.log` defaults to `true`, `false` to disable logging
* - `opts.logger` Implementation of a logger, defaults to built-in Slapp command line logger.
* - `opts.colors` defaults to `process.stdout.isTTY`, `true` to enable colors in logging
* - `opts.ignoreSelf` defaults to `true`, `true` to automatically ignore any messages from yourself. This flag requires the context to set `meta.app_bot_id` with the Slack App's bot id.
* - `opts.ignoreBots` defaults to `false`, `true` to ignore any messages from bot users automatically
* - `opts.defaultExpiration` (seconds) defaults to `60 * 60` (1 hour), 0 to never expire
*
* @api private
* @constructor
* @param {Object} opts
* @returns {Object} Slapp
*/
constructor (opts) {
super()
opts = deap.update({
verify_token: process.env.SLACK_VERIFY_TOKEN,
signing_secret: null,
signing_version: 'v0',
convo_store: null,
context: null,
log: true,
logger,
colors: !!process.stdout.isTTY,
ignoreSelf: true,
ignoreBots: false,
defaultExpiration: HOUR
}, opts || {})
if (!opts.context) {
// TODO: Add a link to the github readme section talking about the context function
throw new Error('No context function provided. Please provide a context function to enrich Slack requests with necessary data.')
}
this._middleware = []
this._matchers = []
this._registry = {}
this.verify_token = opts.verify_token
this.log = opts.log
this.colors = opts.colors
this.logger = opts.logger
this.ignoreSelf = opts.ignoreSelf
this.ignoreBots = opts.ignoreBots
this.defaultExpiration = opts.defaultExpiration
// If convo_store is a string, initialize that type of conversation store
// If it's not a sting and it is defined, assume it is an implementation of
// a conversation store
if (!opts.convo_store || typeof opts.convo_store === 'string') {
this.convoStore = conversationStore({ type: opts.convo_store })
} else {
this.convoStore = opts.convo_store
}
this.client = new Slack()
this.receiver = new Receiver(opts)
}
/**
* Initialize app w/ logger, default middleware and receiver listener
*
*
* ##### Returns
* - `this` (chainable)
*
* @api private
*/
init () {
// attach default logging if enabled
if (this.log) {
this.logger(this, {
colors: this.colors
})
}
// call `handle` for each new request
this.receiver
.on('message', this._handle.bind(this))
.on('error', this.emit.bind(this, 'error'))
if (this.ignoreBots) {
this.use(this.ignoreBotsMiddleware())
}
if (this.ignoreSelf) {
this.use(this.ignoreSelfMiddleware())
}
this.use(this.preprocessConversationMiddleware())
return this
}
/**
* Middleware that gets an existing conversation from the conversation store
* or initialize a new one.
*
* @api private
*/
preprocessConversationMiddleware () {
return (msg, next) => {
this.convoStore.get(msg.conversation_id, (err, val) => {
if (err) {
return this.emit('error', err)
}
if (val) {
msg.attachOverrideRoute(val.fnKey, val.state)
}
next()
})
}
}
/**
* Middleware that ignores messages from this bot user (self) when we can tell. Requires the
* meta context to be populated with `app_bot_id`.
*
* @api private
*/
ignoreSelfMiddleware () {
return (msg, next) => {
if (msg.isBot() && msg.isMessage() && msg.body.event.subtype === 'bot_message') {
let bothFalsy = !msg.meta.app_bot_id && !msg.meta.bot_id
let bothEqual = msg.meta.app_bot_id === msg.meta.bot_id
if (!bothFalsy && bothEqual) {
return
}
}
next()
}
}
/**
* Middleware that ignores messages from any bot user
*
* @api private
*/
ignoreBotsMiddleware () {
return (msg, next) => {
if (msg.isBot() && msg.isMessage() && msg.body.event.subtype === 'bot_message') {
return
}
next()
}
}
/**
* Register a new middleware, processed in the order registered.
*
* ##### Parameters
* - `fn`: middleware function `(msg, next) => { }`
*
*
* ##### Returns
* - `this` (chainable)
*
* @param {function} fn
* @api public
*/
use (fn) {
this._middleware.push(fn)
return this
}
/**
* Handle new events (slack events, commands, actions, webhooks, etc.)
*
* ##### Parameters
* - `msg` `Message`
* - `done` `function(err, bool)` Callback called once complete, called with error and boolean indicating message was handled [optional]
*
* @param {Message} msg
* @param {function} done
* @api private
*/
_handle (msg, done) {
done = done || (() => {})
let self = this
let err = msg.verifyProps()
if (err) {
self.emit('error', err)
}
this.emit('info', msg)
msg.attachSlapp(self)
let idx = 0
let next = () => {
let current = idx++
if (self._middleware[current]) {
self._middleware[current](msg, next)
return
}
// is there a conversation override?
if (msg.override) {
self.convoStore.del(msg.conversation_id, (err) => {
if (err) {
self.emit('error', err)
}
// invoking override w/o context explicitly
// don't want to confuse consumers w/ a msg as `this` scope
msg.override.call(null, msg)
done(err || null, true)
})
return
}
// consider the matchers
for (let i = 0; i < self._matchers.length; i++) {
// if match is a regex, text the regex against the text of a message (if it is a message)
let matcher = self._matchers[i]
if (matcher(msg)) {
return done(null, true)
}
}
// no matchers
msg.clearResponse({ close: true })
done(null, false)
}
next()
return this
}
/**
* Attach HTTP routes to an Express app
*
* Routes are:
* - POST `/slack/event`
* - POST `/slack/command`
* - POST `/slack/action`
*
* ##### Parameters
* - `app` instance of Express app or Express.Router
* - `opts.event` `boolean|string` - event route (defaults to `/slack/event`) [optional]
* - `opts.command` `boolean|string` - command route (defaults to `/slack/command`) [optional]
* - `opts.action` `boolean|string` - action route (defaults to `/slack/action`) [optional]
*
*
* ##### Returns
* - `app` reference to Express app or Express.Router passed in
*
*
* Examples:
*
* // would attach all routes w/ default paths
* slapp.attachToExpress(app)
*
* // with options
* slapp.attachToExpress(app, {
* event: true, // would register event route with default of /slack/event
* command: false, // would not register a route for commands
* action: '/slack-action' // custom route for actions
* })
*
* // would only attach a route for events w/ default path
* slapp.attachToExpress(app, {
* event: true
* })
*
* @param {Object} app - instance of Express app
* @param {Object} opts - options for attaching routes
*/
attachToExpress (app, opts) {
return this.receiver.attachToExpress(app, opts)
}
/**
* Register a new function route
*
* ##### Parameters
* - `fnKey` unique key to refer to function
* - `fn` `(msg, state) => {}`
*
*
* ##### Returns
* - `this` (chainable)
*
* @param {string} fnKey
* @param {function} fn
*/
route (fnKey, fn) {
this._registry[fnKey] = fn
return this
}
/**
* Return a registered route
*
* ##### Parameters
* - `fnKey` string - unique key to refer to function
*
*
* ##### Returns
* - `(msg, state) => {}`
*
* @param {string} fnKey
*/
getRoute (fnKey) {
return this._registry[fnKey]
}
/**
* Register a custom Match function (fn)
*
* ##### Returns `true` if there is a match AND you handled the msg.
* Return `false` if there is not a match and you pass on the message.
*
* All of the higher level matching convenience functions
* generate a match function and call `match` to register it.
*
* Only one matcher can return true, and they are executed in the order they are
* defined. Match functions should return as fast as possible because it's important
* that they are efficient. However you may do asyncronous tasks within to
* your hearts content.
*
* ##### Parameters
* - `fn` function - match function `(msg) => { return bool }`
*
*
* ##### Returns
* - `this` (chainable)
*
* @param {function} fn
*/
match (fn) {
this._matchers.push(fn)
return this
}
/**
* Register a new message handler function for the criteria
*
* ##### Parameters
* - `criteria` text that message contains or regex (e.g. "^hi")
* - `typeFilter` [optional] Array for multiple values or string for one value. Valid values are `direct_message`, `direct_mention`, `mention`, `ambient`
* - `callback` function - `(msg, text, [match1], [match2]...) => {}`
*
*
* ##### Returns
* - `this` (chainable)
*
* Example with regex matchers:
*
* slapp.message('^play (song|artist) <([^>]+)>', (msg, text, type, toplay) => {
* // text = 'play artist spotify:track:1yJiE307EBIzOB9kqH1deb'
* // type = 'artist'
* // toplay = 'spotify:track:1yJiE307EBIzOB9kqH1deb'
* }
*
* Example without matchers:
*
* slapp.message('play', (msg, text) => {
* // text = 'play'
* }
*
* Example `msg.body`:
*
* {
* "token":"dxxxxxxxxxxxxxxxxxxxx",
* "team_id":"TXXXXXXXX",
* "api_app_id":"AXXXXXXXX",
* "event":{
* "type":"message",
* "user":"UXXXXXXXX",
* "text":"hello!",
* "ts":"1469130107.000088",
* "channel":"DXXXXXXXX"
* },
* "event_ts":"1469130107.000088",
* "type":"event_callback",
* "authed_users":[
* "UXXXXXXXX"
* ]
* }
*
* @param {(string)} criteria
* @param {(string|Array)} typeFilter
*/
message (criteria, typeFilter, callback) {
if (typeof criteria === 'string') {
criteria = new RegExp(criteria, 'i')
}
if (typeof typeFilter === 'function') {
callback = typeFilter
typeFilter = []
}
if (typeof typeFilter === 'string') {
typeFilter = [typeFilter]
}
let fn = (msg) => {
if (msg.isBaseMessage()) {
let text = msg.stripDirectMention()
let match = text.match(criteria)
if (match && (typeFilter.length === 0 || msg.isAnyOf(typeFilter))) {
callback.apply(null, [msg].concat(match))
return true
}
}
}
this.match(fn)
return this
}
/**
* Register a new event handler for an actionName
*
* ##### Parameters
* - `criteria` the type of event
* - `callback` `(msg) => {}`
*
*
* ##### Returns
* - `this` (chainable)
*
*
* Example `msg` object:
*
* {
* "token":"dxxxxxxxxxxxxxxxxxxxx",
* "team_id":"TXXXXXXXX",
* "api_app_id":"AXXXXXXXX",
* "event":{
* "type":"reaction_added",
* "user":"UXXXXXXXX",
* "item":{
* "type":"message",
* "channel":"DXXXXXXXX",
* "ts":"1469130181.000096"
* },
* "reaction":"grinning"
* },
* "event_ts":"1469131201.822817",
* "type":"event_callback",
* "authed_users":[
* "UXXXXXXXX"
* ]
* }
*
* @param {(string|RegExp)} criteria
* @param {function} callback
*/
event (criteria, callback) {
if (typeof criteria === 'string') {
criteria = new RegExp(`^${criteria}$`, 'i')
}
let fn = (msg) => {
if (msg.type === 'event' && msg.body.event && criteria.test(msg.body.event.type)) {
callback(msg)
return true
}
}
return this.match(fn)
}
/**
* Register a new handler for button or menu actions. The actionValueCriteria
* (optional) for menu options will successfully match if any one of the values
* match the criteria.
*
* The `callbackId` can optionally accept a URL path like pattern matcher that can be
* used to match as well as extract values. For example if `callbackId` is `/myaction/:type/:id`,
* it _will_ match on `/myaction/a-great-action/abcd1234`. And the resulting `Message` object will
* include a `meta.params` object that contains the extracted variables. For example,
* `msg.meta.params.type` ==> `a-great-action` and `msg.meta.params.id` ==> `abcd1234`. This allows
* you to match on dynamic callbackIds while passing data.
*
* Note, `callback_id` values must be properly encoded. We suggest you use `encodeURIComponent` and `decodeURIComponent`.
*
* The underlying module used for matching
* is [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) where there are a lot of examples.
*
*
* ##### Parameters
* - `callbackIdPath` string - may be a simple string or a URL path matcher
* - `actionNameCriteria` string or RegExp - the name of the action [optional]
* - `actionValueCriteria` string or RegExp - the value of the action [optional]
* - `callback` function - `(msg, value) => {}` - value may be a string or array of strings
*
*
* ##### Returns
* - `this` (chainable)
*
* Example:
*
* // match name and value
* slapp.action('dinner_callback', 'drink', 'beer', (msg, val) => {}
* // match name and value either beer or wine
* slapp.action('dinner_callback', 'drink', '(beer|wine)', (msg, val) => {}
* // match name drink, any value
* slapp.action('dinner_callback', 'drink', (msg, val) => {}
* // match dinner_callback, any name or value
* slapp.action('dinner_callback', 'drink', (msg, val) => {}
* // match with regex
* slapp.action('dinner_callback', /^drink$/, /^b[e]{2}r$/, (msg, val) => {}
* // callback_id matcher
* slapp.action('/dinner_callback/:drink', (msg, val) => {}
*
* Example button action `msg.body` object:
*
* {
* "actions":[
* {
* "name":"answer",
* "value":":wine_glass:"
* }
* ],
* "callback_id":"in_or_out_callback",
* "team":{
* "id":"TXXXXXXXX",
* "domain":"companydomain"
* },
* "channel":{
* "id":"DXXXXXXXX",
* "name":"directmessage"
* },
* "user":{
* "id":"UXXXXXXXX",
* "name":"mike.brevoort"
* },
* "action_ts":"1469129995.067370",
* "message_ts":"1469129988.000084",
* "attachment_id":"1",
* "token":"dxxxxxxxxxxxxxxxxxxxx",
* "original_message":{
* "text":"What?",
* "username":"In or Out",
* "bot_id":"BXXXXXXXX",
* "attachments":[
* {
* "callback_id":"in_or_out_callback",
* "fallback":"Pick one",
* "id":1,
* "actions":[
* {
* "id":"1",
* "name":"answer",
* "text":":beer:",
* "type":"button",
* "value":":beer:",
* "style":""
* },
* {
* "id":"2",
* "name":"answer",
* "text":":beers:",
* "type":"button",
* "value":":wine:",
* "style":""
* },
* ]
* },
* {
* "text":":beers: • mike.brevoort",
* "id":2,
* "fallback":"who picked beers"
* }
* ],
* "type":"message",
* "subtype":"bot_message",
* "ts":"1469129988.000084"
* },
* "response_url":"https://hooks.slack.com/actions/TXXXXXXXX/111111111111/txxxxxxxxxxxxxxxxxxxx"
* }
*
*
* Example menu action `msg.body` object:
*
* {
* "actions": [
* {
* "name": "winners_list",
* "selected_options": [
* {
* "value": "U061F1ZUR"
* }
* ]
* }
* ],
* "callback_id": "select_simple_1234",
* "team": {
* "id": "T012AB0A1",
* "domain": "pocket-calculator"
* },
* "channel": {
* "id": "C012AB3CD",
* "name": "general"
* },
* "user": {
* "id": "U012A1BCD",
* "name": "musik"
* },
* "action_ts": "1481579588.685999",
* "message_ts": "1481579582.000003",
* "attachment_id": "1",
* "token": "verification_token_string",
* "original_message": {
* "text": "It's time to nominate the channel of the week",
* "bot_id": "B08BCU62D",
* "attachments": [
* {
* "callback_id": "select_simple_1234",
* "fallback": "Upgrade your Slack client to use messages like these.",
* "id": 1,
* "color": "3AA3E3",
* "actions": [
* {
* "id": "1",
* "name": "channels_list",
* "text": "Which channel changed your life this week?",
* "type": "select",
* "data_source": "channels"
* }
* ]
* }
* ],
* "type": "message",
* "subtype": "bot_message",
* "ts": "1481579582.000003"
* },
* "response_url": "https://hooks.slack.com/actions/T012AB0A1/1234567890/JpmK0yzoZ5eRiqfeduTBYXWQ"
* }
*
* @param {string} callbackId
* @param {(string|RegExp)} actionNameCriteria
* @param {(string|RegExp)} actionValueCriteria
* @param {function} callback
*/
action (callbackIdPath, actionNameCriteria, actionValueCriteria, callback) {
if (typeof actionValueCriteria === 'function') {
callback = actionValueCriteria
actionValueCriteria = /.*/
}
if (typeof actionNameCriteria === 'function') {
callback = actionNameCriteria
actionNameCriteria = /.*/
actionValueCriteria = /.*/
}
if (typeof actionNameCriteria === 'string') {
actionNameCriteria = new RegExp(`^${actionNameCriteria}$`, 'i')
}
if (typeof actionValueCriteria === 'string') {
actionValueCriteria = new RegExp(`^${actionValueCriteria}$`, 'i')
}
var keys = []
var callbackIdCriteria = pathToRegexp(callbackIdPath, keys)
let fn = (msg) => {
if (msg.type !== 'action' || !msg.body.actions) return
let callbackIdMatch = callbackIdCriteria.exec(msg.body.callback_id)
if (!callbackIdMatch) return
let params = {}
keys.forEach((key, i) => {
params[key.name] = callbackIdMatch[i + 1]
})
msg.meta.params = params
// Don't know how to handle multiple actions in the area. As far as this writing, this isn't ever
// expected to happen. Best way to handle this uncertainty is to loop until we find a match and then stop
for (let i = 0; i < msg.body.actions.length; i++) {
let action = msg.body.actions[i]
if (actionNameCriteria.test(action.name)) {
// test for menu options. There could be multiple options returned so attempt to match
// on any of them and if any one matches, we'll consider this a match.
if (Array.isArray(action.selected_options)) {
if (action.selected_options.find(option => actionValueCriteria.test(option.value))) {
callback(msg, action.selected_options.map(it => it.value))
return true
}
}
// test for message actions
if (actionValueCriteria.test(action.value)) {
callback(msg, action.value)
return true
}
}
}
}
return this.match(fn)
}
/**
* Register a new handler for a [message action](https://api.slack.com/actions).
*
* The `callbackId` should match the "Callback ID" registered in the message action.
*
*
* ##### Parameters
* - `callbackId` string
* - `callback` function - `(msg, message) => {}` - message
*
*
* ##### Returns
* - `this` (chainable)
*
* Example:
*
* // match on callback_id
* slapp.messageAction('launch_message_action', (msg, message) => {}
*
*
* Example message action `msg.body` object:
*
* {
* "token": "Nj2rfC2hU8mAfgaJLemZgO7H",
* "callback_id": "chirp_message",
* "type": "message_action",
* "trigger_id": "13345224609.8534564800.6f8ab1f53e13d0cd15f96106292d5536",
* "response_url": "https://hooks.slack.com/app-actions/T0MJR11A4/21974584944/yk1S9ndf35Q1flupVG5JbpM6",
* "team": {
* "id": "T0MJRM1A7",
* "domain": "pandamonium",
* },
* "channel": {
* "id": "D0LFFBKLZ",
* "name": "cats"
* },
* "user": {
* "id": "U0D15K92L",
* "name": "dr_maomao"
* },
* "message": {
* "type": "message",
* "user": "U0MJRG1AL",
* "ts": "1516229207.000133",
* "text": "World's smallest big cat! <https://youtube.com/watch?v=W86cTIoMv2U>"
* }
* }
*
*
* @param {string} callbackId
* @param {function} callback
*/
messageAction (callbackId, callback) {
let fn = (msg) => {
if (msg.type !== 'action' || msg.body.type !== 'message_action') return
if (msg.body.callback_id !== callbackId) return
callback(msg, msg.body.message)
return true
}
return this.match(fn)
}
/**
* Register a new interactive message options handler
*
* `options` accepts a `callbackIdPath` like `action`. See `action` for details.
*
* ##### Parameters
* - `callbackIdPath` string - may be a simple string or a URL path matcher
* - `actionNameCriteria` string or RegExp - the name of the action [optional]
* - `actionValueCriteria` string or RegExp - the value of the action [optional]
* - `callback` function - `(msg, value) => {}` - value is the current value of the option (e.g. partially typed)
*
*
* ##### Returns
* - `this` (chainable)
*
* Example matching callback only
*
* slapp.options('my_callback', (msg, value) => {}
*
*
* Example with name matcher
*
* slapp.options('my_callback', 'my_name', (msg, value) => {}
*
*
* Example with RegExp matcher criteria:
*
* slapp.options('my_callback', /my_n.+/, (msg, value) => {}
*
* Example with callback_id path criteria:
*
* slapp.options('/my_callback/:id', (msg, value) => {}
*
*
*
* Example `msg.body` object:
*
* {
* "name": "musik",
* "value": "",
* "callback_id": "select_remote_1234",
* "team": {
* "id": "T012AB0A1",
* "domain": "pocket-calculator"
* },
* "channel": {
* "id": "C012AB3CD",
* "name": "general"
* },
* "user": {
* "id": "U012A1BCD",
* "name": "musik"
* },
* "action_ts": "1481670445.010908",
* "message_ts": "1481670439.000007",
* "attachment_id": "1",
* "token": "verification_token_string"
* }
**
* @param {string} callbackId
* @param {(string|RegExp)} actionNameCriteria
* @param {(string|RegExp)} actionValueCriteria
* @param {function} callback
*/
options (callbackIdPath, actionNameCriteria, callback) {
if (typeof actionNameCriteria === 'function') {
callback = actionNameCriteria
actionNameCriteria = /.*/
}
if (typeof actionNameCriteria === 'string') {
actionNameCriteria = new RegExp(`^${actionNameCriteria}$`, 'i')
}
var keys = []
var callbackIdCriteria = pathToRegexp(callbackIdPath, keys)
let fn = (msg) => {
if (msg.type !== 'options') return false
let callbackIdMatch = callbackIdCriteria.exec(msg.body.callback_id)
if (!callbackIdMatch) return
let params = {}
keys.forEach((key, i) => {
params[key.name] = callbackIdMatch[i + 1]
})
msg.meta.params = params
if (actionNameCriteria.test(msg.body.name)) {
callback(msg, msg.body.value)
return true
}
}
return this.match(fn)
}
/**
* Register a new slash command handler
*
* ##### Parameters
* - `command` string - the slash command (e.g. "/doit")
* - `criteria` string or RegExp (e.g "/^create.+$/") [optional]
* - `callback` function - `(msg) => {}`
*
*
* ##### Returns
* - `this` (chainable)
*
* Example without parameters:
*
* // "/acommand"
* slapp.command('acommand', (msg) => {
* }
*
*
* Example with RegExp matcher criteria:
*
* // "/acommand create flipper"
* slapp.command('acommand', 'create (.*)'(msg, text, name) => {
* // text = 'create flipper'
* // name = 'flipper'
* }
*
*
* Example `msg` object:
*
* {
* "type":"command",
* "body":{
* "token":"xxxxxxxxxxxxxxxxxxx",
* "team_id":"TXXXXXXXX",
* "team_domain":"teamxxxxxxx",
* "channel_id":"Dxxxxxxxx",
* "channel_name":"directmessage",
* "user_id":"Uxxxxxxxx",
* "user_name":"xxxx.xxxxxxxx",
* "command":"/doit",
* "text":"whatever was typed after command",
* "response_url":"https://hooks.slack.com/commands/TXXXXXXXX/111111111111111111111111111"
* },
* "resource":{
* "app_token":"xoxp-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX",
* "app_user_id":"UXXXXXXXX",
* "bot_token":"xoxb-XXXXXXXXXX-XXXXXXXXXXXXXXXXXXXX",
* "bot_user_id":"UXXXXXXXX"
* },
* "meta":{
* "user_id":"UXXXXXXXX",
* "channel_id":"DXXXXXXXX",
* "team_id":"TXXXXXXXX"
* },
* }
* @param {string} command
* @param {(string|RegExp)} criteria
* @param {function} callback
*/
command (command, criteria, callback) {
if (typeof criteria === 'function') {
callback = criteria
criteria = /.*/
}
if (typeof criteria === 'string') {
criteria = new RegExp(criteria, 'i')
}
if (typeof command === 'string') {
command = new RegExp(`^${command}$`, 'i')
}
let fn = (msg) => {
if (msg.type === 'command' && msg.body.command && msg.body.command.match(command)) {
let text = msg.body.text || ''
let match = text.match(criteria)
if (match) {
callback.apply(null, [msg].concat(match))
return true
}
}
}
return this.match(fn)
}
/**
* Register a dialog submission handler for the given callback_id
*
* ##### Parameters
* - `callbackId` string - the callback_id of the form
* - `callback` function - `(msg, submission) => {}`
*
*
* ##### Returns
* - `this` (chainable)
*
* Example;
*
* // "/acommand"
* slapp.command('my_callback_id', (msg, submission) => {
* submission.prop_name_1
* }
*
*
* Example `msg` object:
*
* {
* "type":"action",
* "body":{
* "type": "dialog_submission",
* "submission": {
* "answer": "two",
* "feedback": "test"
* },
* "callback_id": "xyz",
* "team": {
* "id": "T1PR9DEFS",
* "domain": "aslackdomain"
* },
* "user": {
* "id": "U1ABCDEF",
* "name": "mikebrevoort"
* },
* "channel": {
* "id": "C1PR520RRR",
* "name": "random"
* },
* "action_ts": "1503445940.478855"
* },
* }
* @param {string} callbackId
* @param {function} callback
*/
dialog (callbackIdPath, callback) {
var keys = []
var callbackIdCriteria = pathToRegexp(callbackIdPath, keys)
let fn = (msg) => {
if (msg.type !== 'action' || msg.body.type !== 'dialog_submission') return
let callbackIdMatch = callbackIdCriteria.exec(msg.body.callback_id)
if (!callbackIdMatch) return
let params = {}
keys.forEach((key, i) => {
params[key.name] = callbackIdMatch[i + 1]
})
msg.meta.params = params
callback(msg, msg.body.submission)
return true
}
return this.match(fn)
}
}
module.exports = Slapp