UNPKG

42.5 kBMarkdownView Raw
1[![Sponsored by Beep Boop](https://img.shields.io/badge/%E2%9D%A4%EF%B8%8F_sponsored_by-%E2%9C%A8_Robots%20%26%20Pencils_%E2%9C%A8-FB6CBE.svg)](https://beepboophq.com)
2[![Build Status](https://travis-ci.org/BeepBoopHQ/slapp.svg)](https://travis-ci.org/BeepBoopHQ/slapp)
3[![Coverage Status](https://coveralls.io/repos/github/BeepBoopHQ/slapp/badge.svg)](https://coveralls.io/github/BeepBoopHQ/slapp)
4
5# Slapp
6Slapp is a node.js module for creating Slack integrations from simple slash commands to complex bots. It is specifically for Slack --not a generic bot framework-- because we believe the best restaurants in the world are not buffets. 🍴😉
7
8Slapp heavily favors the new HTTP based [Slack Events API](https://api.slack.com/events-api) over [Realtime Messaging API](https://api.slack.com/rtm) websockets for creating more scalable and manageable bots. It supports simple conversation flows with state managed out of process to survive restarts and horizontal scaling. Conversation flows aren't just message based but may include any Slack event, [interactive buttons](https://api.slack.com/docs/message-buttons), [slash commands](https://api.slack.com/slash-commands), etc.
9
10Slapp is built on a strong foundation with a test suite with 100% test coverage and depends on the [smallwins/slack](https://github.com/smallwins/slack) client.
11
12Here is a basic example:
13```js
14const Slapp = require('slapp')
15const BeepBoopContext = require('slapp-context-beepboop')
16if (!process.env.PORT) throw Error('PORT missing but required')
17
18var slapp = Slapp({ context: BeepBoopContext() })
19
20slapp.message('^(hi|hello|hey).*', ['direct_mention', 'direct_message'], (msg, text, greeting) => {
21 msg
22 .say(`${greeting}, how are you?`)
23 .route('handleHowAreYou') // where to route the next msg in the conversation
24})
25
26// register a route handler
27slapp.route('handleHowAreYou', (msg) => {
28 // respond with a random entry from array
29 msg.say(['Me too', 'Noted', 'That is interesting'])
30})
31
32// attach handlers to an Express app
33slapp.attachToExpress(require('express')()).listen(process.env.PORT)
34```
35
36## Install
37
38```
39npm install --save slapp
40```
41
42## Getting Started
43We recommend you watch this [quick tutorial](https://www.youtube.com/watch?v=q9iMeRbrgpw) on how to get started with Slapp on BeepBoop! It'll talk you through some of these key points:
44
45* Creating your first Slapp application
46* Adding your application to [Beep Boop](https://beepboophq.com)
47* Setting up a Slack App ready to work with Slapp / Beep Boop
48
49Even if you're not using Beep Boop the video should help you understand how to get your Slack App setup properly so you can make the most of Slapp.
50
51## Setup
52You can call the Slapp function with the following options:
53```js
54const Slapp = require('slapp')
55const ConvoStore = require('slapp-convo-beepboop')
56const BeepBoopContext = require('slapp-context-beepboop')
57
58var slapp = Slapp({
59 verify_token: process.env.SLACK_VERIFY_TOKEN,
60 convo_store: ConvoStore(),
61 context: BeepBoopContext(),
62 log: true,
63 colors: true
64})
65```
66
67### Context Lookup
68One of the challenges with writing a multi-team Slack app is that you need to make sure you have the appropriate tokens and meta-data for a team when you get a message from them. This lets you make api calls on behalf of that team in response to incoming messages from Slack. You typically collect and store this meta-data during the **Add to Slack** OAuth flow. If you're running on [Beep Boop][beepboop], this data is saved for you automatically. Slapp has a required `context` option that gives you a convenient hook to load that team-specific meta-data and enrich the message with it. While you can add whatever meta-data you have about a team in this function, there are a few required properties that need to be set on `req.slapp.meta` for Slapp to process requests:
69
70+ `app_token` - **required** OAuth `access_token` property
71+ `bot_token` - **required if you have a bot user** OAuth `bot.bot_access_token` property
72+ `bot_user_id` - **required if you have a bot user** OAuth `bot.bot_user_id` property
73+ `app_bot_id` - **required if you have a bot user and use ignoreSelf option** Profile call with bot token, `users.profile.bot_id` property
74
75The incoming request from Slack has been parsed and normalized by the time the `context` function runs, and is available via `req.slapp`. You can rely on this data in your `context` function to assist you in looking up the necessary tokens and meta-data.
76
77`req.slapp` has the following structure:
78```js
79{
80 type: 'event|command|action',
81 body: {}, // original payload from Slack
82 meta: {
83 user_id: '<USER_ID>',
84 channel_id: '<CHANNEL_ID>',
85 team_id: '<TEAM_ID>'
86 }
87}
88```
89
90If you're running on [Beep Boop][beepboop], these values are stored and added automatically for you, otherwise you'll need to set these properties on `req.slapp.meta` with data retreived from wherever you're storing your OAuth data. That might look something like this:
91```js
92// your database module...
93var myDB = require('./my-db')
94
95var slapp = Slapp({
96 context (req, res, next) {
97 var meta = req.slapp.meta
98
99 myDB.getTeamData(meta.team_id, (err, data) => {
100 if (err) {
101 console.error('Error loading team data: ', err)
102 return res.send(err)
103 }
104
105 // mixin necessary team meta-data
106 req.slapp.meta = Object.assign(req.slapp.meta, {
107 app_token: data.app_token,
108 bot_token: data.bot_token,
109 bot_user_id: data.bot_user_id,
110 // you can add your own team meta-data as well
111 other_value: data.other_value
112 })
113 })
114 }
115})
116```
117
118### Message Middleware
119Slapp supports middleware for incoming events, allowing you to stop the propagation
120of the event by not calling `next()`, passively observing, or appending metadata
121to the message by adding properties to `msg.meta`. Middleware is processed in the
122order it is added.
123
124Register new middleware with `use`:
125
126```
127slapp.use(fn(msg, next))
128```
129
130For example, simple middleware that logs all incoming messages:
131
132```
133slapp.use((msg, next) => {
134 console.log(msg)
135 next()
136})
137```
138
139Or that does some validation:
140
141```
142slapp.use((msg, next) => {
143 if (valid) {
144 next()
145 } else {
146 console.error('uh oh')
147 }
148})
149```
150
151## Slack Events
152Listen for any Slack event with `slapp.event(event_name, (msg) => {})`.
153
154```js
155// add a smile reaction by the bot for any message reacted to
156slapp.event('reaction_added', (msg) => {
157 let token = msg.meta.bot_token
158 let timestamp = msg.body.event.item.ts
159 let channel = msg.body.event.item.channel
160 slapp.client.reactions.add({token, name: 'smile', channel, timestamp}, (err) => {
161 if (err) console.log('Error adding reaction', err)
162 })
163})
164```
165
166![Slack Events Demo](https://storage.googleapis.com/beepboophq/_assets/slackapp/demo-event.gif)
167
168## Slack Event Messages
169A message is just a subtype of Slack event but has a special convenience method `slapp.message(regex, [types], (msg) => {})`:
170
171```js
172slapp.message('goodnight', 'mention', (msg) => {
173 msg.say('sweet dreams :crescent_moon: ')
174})
175```
176![Slack Message Demo](https://storage.googleapis.com/beepboophq/_assets/slackapp/demo-message.gif)
177
178
179## Interactive Messages
180`msg.say()` may be passed text, an array of text values (one is chosen randomly), or an object to be sent to [`chat.postMessage`](https://api.slack.com/methods/chat.postMessage). It defaults to the current channel and the bot user token (or app token if there is not bot user). Here's an example of using `msg.say()` to send an interactive message and registering a handler to receive the button action:
181
182```js
183slapp.message('yesno', (msg) => {
184 msg.say({
185 text: '',
186 attachments: [
187 {
188 text: '',
189 fallback: 'Yes or No?',
190 callback_id: 'yesno_callback',
191 actions: [
192 { name: 'answer', text: 'Yes', type: 'button', value: 'yes' },
193 { name: 'answer', text: 'No', type: 'button', value: 'no' }
194 ]
195 }]
196 })
197})
198
199slapp.action('yesno_callback', 'answer', (msg, value) => {
200 msg.respond(msg.body.response_url, `${value} is a good choice!`)
201})
202```
203
204![Interactive Message Demo](https://storage.googleapis.com/beepboophq/_assets/slackapp/demo-interactive.gif)
205
206## Slash Commands
207
208```js
209slapp.command('/inorout', /^in/, (msg) => {
210 // `respond` is used for actions or commands and uses the `response_url` provided by the
211 // incoming request from Slack
212 msg.respond(`Glad you are in ${match}!`)
213})
214```
215
216![Slash Command Demo](https://storage.googleapis.com/beepboophq/_assets/slackapp/demo-slash.gif)
217
218
219You can also match on text after the command similar to messages like this:
220```js
221slapp.command('/inorout', 'create (.*)', (msg, text, question) => {
222 // if "/inorout create Who is in?" is received:
223 // text = create Who is in?
224 // question = Who is in?
225})
226```
227
228## Conversations and Bots
229With Slapp you can use the Slack Events API to create bots much like you would with a
230a Realtime Messaging API socket. Events over HTTP may be not necessarily be received by
231the same process if you are running multiple instances of your app behind a load balancer;
232therefore your Slapp process should be stateless. And thus conversation state should be
233stored out of process.
234
235You can pass a conversation store implementation into the Slapp factory with the `convo_store` option. If you are using [Beep Boop](https://beepboophq.com), you should use `require('slapp-convo-beepboop')()` and it will be handled for you. Otherwise, a conversation store needs to implement these three functions:
236
237```js
238 set (id, params, callback) {} // callback(err)
239 get (id, callback) // callback(err, val)
240 del (id, callback) {} // callback(err)
241```
242
243The [in memory implementation](https://github.com/BeepBoopHQ/slapp/blob/master/src/conversation_store/memory.js) can be used for testing and as an example when creating your own implementation.
244
245### What is a conversation?
246A conversation is scoped by the combination of Slack Team, Channel, and User. When
247you register a new route handler (see below), it will only be invoked when receiving
248a message from the same team in the same channel by the same user.
249
250### Conversation Routing
251Conversations use a very simple routing mechanism. Within any msg handler you may
252call `msg.route` to designate a handler for the next msg received in a conversation.
253The handler must be preregistered with the same key through `slapp.route`.
254
255For example, if we register a route handler under the key `handleGoodDay`:
256
257```js
258slapp.route('handleGoodDay', (msg) => {
259 msg.say(':expressionless:')
260})
261```
262
263We can route to that in a `msg` handler like this:
264
265```js
266slapp.message('^hi', 'direct_message', (msg) => {
267 msg.say('Are you having a good day?').route('handleGoodDay')
268})
269```
270
271The route handler will get called for this conversation no matter what type of event
272it is. This means you can use any slack events, slash commands interactive message actions,
273and the like in your conversation flows. If a route handler is registered, it will
274supercede any other matcher.
275
276### Conversation State and Expiration
277When specifying a route handler with `msg.route` you can optionally pass an arbitrary
278object and expiration time in seconds.
279
280Consider the example below. If a user says "do it" in a direct message then ask
281for confirmation using an interactive message. If they do something other than
282answer by pressing a button, redirect them to choose one of the options, yes or no.
283When they choose, handle the response accordingly.
284
285Notice the `state` object that is passed to `msg.route` and into `slapp.route`. Each time `msg.route` is called an expiration time of 60 seconds is set. If
286there is not activity by the user for 60 seconds, we expire the conversation flow.
287
288
289```js
290// if a user says "do it" in a DM
291slapp.message('do it', 'direct_message', (msg) => {
292 var state = { requested: Date.now() }
293 // respond with an interactive message with buttons Yes and No
294 msg
295 .say({
296 text: '',
297 attachments: [
298 {
299 text: 'Are you sure?',
300 fallback: 'Are you sure?',
301 callback_id: 'doit_confirm_callback',
302 actions: [
303 { name: 'answer', text: 'Yes', type: 'button', value: 'yes' },
304 { name: 'answer', text: 'No', type: 'button', value: 'no' }
305 ]
306 }]
307 })
308 // handle the response with this route passing state
309 // and expiring the conversation after 60 seconds
310 .route('handleDoitConfirmation', state, 60)
311})
312
313slapp.route('handleDoitConfirmation', (msg, state) => {
314 // if they respond with anything other than a button selection,
315 // get them back on track
316 if (msg.type !== 'action') {
317 msg
318 .say('Please choose a Yes or No button :wink:')
319 // notice we have to declare the next route to handle the response
320 // every time. Pass along the state and expire the conversation
321 // 60 seconds from now.
322 .route('handleDoitConfirmation', state, 60)
323 return
324 }
325
326 let answer = msg.body.actions[0].value
327 if (answer !== 'yes') {
328 // the answer was not affirmative
329 msg.respond(msg.body.response_url, {
330 text: `OK, not doing it. Whew that was close :cold_sweat:`,
331 delete_original: true
332 })
333 // notice we did NOT specify a route because the conversation is over
334 return
335 }
336
337 // use the state that's been passed through the flow to figure out the
338 // elapsed time
339 var elapsed = (Date.now() - state.requested)/1000
340 msg.respond(msg.body.response_url, {
341 text: `You requested me to do it ${elapsed} seconds ago`,
342 delete_original: true
343 })
344
345 // simulate doing some work and send a confirmation.
346 setTimeout(() => {
347 msg.say('I "did it"')
348 }, 3000)
349})
350```
351
352![Conversation Demo](https://storage.googleapis.com/beepboophq/_assets/slackapp/demo-doit.gif)
353
354## Custom Logging
355You can pass in your own custom logger instead of using the built-in logger. A custom logger would implement:
356
357```js
358(app, opts) => {
359 app
360 .on('info', (msg) => {
361 ...
362 })
363 .on('error', (err) => {
364 ...
365 })
366}
367```
368The `msg` is the same as the Message type. `opts` includes the `opts.colors` passed into Slapp initially.
369
370# API
371
372# slapp
373
374 - [slapp()](#slappoptsobject)
375
376## slapp(opts:Object)
377
378 Create a new Slapp, accepts an options object
379
380 Parameters
381 - `opts.verify_token` Slack Veryify token to validate authenticity of requests coming from Slack
382 - `opts.signing_secret` Slack signing secret to check/verify the signature of requests coming from Slack
383 - `opts.signing_version` Slack signing version string, defaults to 'v0'
384 - `opts.convo_store` Implementation of ConversationStore, defaults to memory
385 - `opts.context` `Function (req, res, next)` HTTP Middleware function to enrich incoming request with context
386 - `opts.log` defaults to `true`, `false` to disable logging
387 - `opts.logger` Implementation of a logger, defaults to built-in Slapp command line logger.
388 - `opts.colors` defaults to `process.stdout.isTTY`, `true` to enable colors in logging
389 - `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 users.profile.bot_id.
390 - `opts.ignoreBots` defaults to `false`, `true` to ignore any messages from bot users automatically
391
392 Example
393
394
395```js
396 var Slapp = require('slapp')
397 var BeepBoopConvoStore = require('slapp-convo-beepboop')
398 var BeepBoopContext = require('slapp-context-beepboop')
399 var slapp = Slapp({
400 record: 'out.jsonl',
401 context: BeepBoopContext(),
402 convo_store: BeepBoopConvoStore({ debug: true })
403 })
404```
405
406
407
408# Slapp
409
410 - [Slapp.use()](#slappusefnfunction)
411 - [Slapp.attachToExpress()](#slappattachtoexpressappobjectoptsobject)
412 - [Slapp.route()](#slapproutefnkeystringfnfunction)
413 - [Slapp.getRoute()](#slappgetroutefnkeystring)
414 - [Slapp.match()](#slappmatchfnfunction)
415 - [Slapp.message()](#slappmessagecriteriastringtypefilterstringarray)
416 - [Slapp.event()](#slappeventcriteriastringregexpcallbackfunction)
417 - [Slapp.action()](#slappactioncallbackidstringactionnamecriteriastringregexpactionvaluecriteriastringregexpcallbackfunction)
418 - [Slapp.messageAction()](#slappmessageactioncallbackidstringcallbackfunction)
419 - [Slapp.options()](#slappoptionscallbackidstringactionnamecriteriastringregexpactionvaluecriteriastringregexpcallbackfunction)
420 - [Slapp.command()](#slappcommandcommandstringcriteriastringregexpcallbackfunction)
421 - [Slapp.dialog()](#slappdialogcallbackidstringcallbackfunction)
422
423## Slapp.use(fn:function)
424
425 Register a new middleware, processed in the order registered.
426
427#### Parameters
428 - `fn`: middleware function `(msg, next) => { }`
429
430
431#### Returns
432 - `this` (chainable)
433
434## Slapp.attachToExpress(app:Object, opts:Object)
435
436 Attach HTTP routes to an Express app
437
438 Routes are:
439 - POST `/slack/event`
440 - POST `/slack/command`
441 - POST `/slack/action`
442
443#### Parameters
444 - `app` instance of Express app or Express.Router
445 - `opts.event` `boolean|string` - event route (defaults to `/slack/event`) [optional]
446 - `opts.command` `boolean|string` - command route (defaults to `/slack/command`) [optional]
447 - `opts.action` `boolean|string` - action route (defaults to `/slack/action`) [optional]
448
449
450#### Returns
451 - `app` reference to Express app or Express.Router passed in
452
453
454 Examples:
455
456```js
457 // would attach all routes w/ default paths
458 slapp.attachToExpress(app)
459```
460
461
462```js
463 // with options
464 slapp.attachToExpress(app, {
465 event: true, // would register event route with default of /slack/event
466 command: false, // would not register a route for commands
467 action: '/slack-action' // custom route for actions
468 })
469```
470
471
472```js
473 // would only attach a route for events w/ default path
474 slapp.attachToExpress(app, {
475 event: true
476 })
477```
478
479## Slapp.route(fnKey:string, fn:function)
480
481 Register a new function route
482
483#### Parameters
484 - `fnKey` unique key to refer to function
485 - `fn` `(msg, state) => {}`
486
487
488#### Returns
489 - `this` (chainable)
490
491## Slapp.getRoute(fnKey:string)
492
493 Return a registered route
494
495#### Parameters
496 - `fnKey` string - unique key to refer to function
497
498
499#### Returns
500 - `(msg, state) => {}`
501
502## Slapp.match(fn:function)
503
504 Register a custom Match function (fn)
505
506#### Returns `true` if there is a match AND you handled the msg.
507 Return `false` if there is not a match and you pass on the message.
508
509 All of the higher level matching convenience functions
510 generate a match function and call `match` to register it.
511
512 Only one matcher can return true, and they are executed in the order they are
513 defined. Match functions should return as fast as possible because it's important
514 that they are efficient. However you may do asyncronous tasks within to
515 your hearts content.
516
517#### Parameters
518 - `fn` function - match function `(msg) => { return bool }`
519
520
521#### Returns
522 - `this` (chainable)
523
524## Slapp.message(criteria:string, typeFilter:string|Array)
525
526 Register a new message handler function for the criteria
527
528#### Parameters
529 - `criteria` text that message contains or regex (e.g. "^hi")
530 - `typeFilter` [optional] Array for multiple values or string for one value. Valid values are `direct_message`, `direct_mention`, `mention`, `ambient`
531 - `callback` function - `(msg, text, [match1], [match2]...) => {}`
532
533
534#### Returns
535 - `this` (chainable)
536
537 Example with regex matchers:
538
539```js
540 slapp.message('^play (song|artist) <([^>]+)>', (msg, text, type, toplay) => {
541 // text = 'play artist spotify:track:1yJiE307EBIzOB9kqH1deb'
542 // type = 'artist'
543 // toplay = 'spotify:track:1yJiE307EBIzOB9kqH1deb'
544 }
545```
546
547
548 Example without matchers:
549
550```js
551 slapp.message('play', (msg, text) => {
552 // text = 'play'
553 }
554```
555
556
557 Example `msg.body`:
558
559```js
560 {
561 "token":"dxxxxxxxxxxxxxxxxxxxx",
562 "team_id":"TXXXXXXXX",
563 "api_app_id":"AXXXXXXXX",
564 "event":{
565 "type":"message",
566 "user":"UXXXXXXXX",
567 "text":"hello!",
568 "ts":"1469130107.000088",
569 "channel":"DXXXXXXXX"
570 },
571 "event_ts":"1469130107.000088",
572 "type":"event_callback",
573 "authed_users":[
574 "UXXXXXXXX"
575 ]
576 }
577```
578
579## Slapp.event(criteria:string|RegExp, callback:function)
580
581 Register a new event handler for an actionName
582
583#### Parameters
584 - `criteria` the type of event
585 - `callback` `(msg) => {}`
586
587
588#### Returns
589 - `this` (chainable)
590
591
592 Example `msg` object:
593
594```js
595 {
596 "token":"dxxxxxxxxxxxxxxxxxxxx",
597 "team_id":"TXXXXXXXX",
598 "api_app_id":"AXXXXXXXX",
599 "event":{
600 "type":"reaction_added",
601 "user":"UXXXXXXXX",
602 "item":{
603 "type":"message",
604 "channel":"DXXXXXXXX",
605 "ts":"1469130181.000096"
606 },
607 "reaction":"grinning"
608 },
609 "event_ts":"1469131201.822817",
610 "type":"event_callback",
611 "authed_users":[
612 "UXXXXXXXX"
613 ]
614 }
615```
616
617## Slapp.action(callbackId:string, actionNameCriteria:string|RegExp, actionValueCriteria:string|RegExp, callback:function)
618
619 Register a new handler for button or menu actions. The actionValueCriteria
620 (optional) for menu options will successfully match if any one of the values
621 match the criteria.
622
623 The `callbackId` can optionally accept a URL path like pattern matcher that can be
624 used to match as well as extract values. For example if `callbackId` is `/myaction/:type/:id`,
625 it _will_ match on `/myaction/a-great-action/abcd1234`. And the resulting `Message` object will
626 include a `meta.params` object that contains the extracted variables. For example,
627 `msg.meta.params.type` ==> `a-great-action` and `msg.meta.params.id` ==> `abcd1234`. This allows
628 you to match on dynamic callbackIds while passing data.
629
630 Note, `callback_id` values must be properly encoded. We suggest you use `encodeURIComponent` and `decodeURIComponent`.
631
632 The underlying module used for matching
633 is [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) where there are a lot of examples.
634
635
636#### Parameters
637 - `callbackIdPath` string - may be a simple string or a URL path matcher
638 - `actionNameCriteria` string or RegExp - the name of the action [optional]
639 - `actionValueCriteria` string or RegExp - the value of the action [optional]
640 - `callback` function - `(msg, value) => {}` - value may be a string or array of strings
641
642
643#### Returns
644 - `this` (chainable)
645
646 Example:
647
648```js
649 // match name and value
650 slapp.action('dinner_callback', 'drink', 'beer', (msg, val) => {}
651 // match name and value either beer or wine
652 slapp.action('dinner_callback', 'drink', '(beer|wine)', (msg, val) => {}
653 // match name drink, any value
654 slapp.action('dinner_callback', 'drink', (msg, val) => {}
655 // match dinner_callback, any name or value
656 slapp.action('dinner_callback', 'drink', (msg, val) => {}
657 // match with regex
658 slapp.action('dinner_callback', /^drink$/, /^b[e]{2}r$/, (msg, val) => {}
659 // callback_id matcher
660 slapp.action('/dinner_callback/:drink', (msg, val) => {}
661```
662
663
664 Example button action `msg.body` object:
665
666```js
667 {
668 "actions":[
669 {
670 "name":"answer",
671 "value":":wine_glass:"
672 }
673 ],
674 "callback_id":"in_or_out_callback",
675 "team":{
676 "id":"TXXXXXXXX",
677 "domain":"companydomain"
678 },
679 "channel":{
680 "id":"DXXXXXXXX",
681 "name":"directmessage"
682 },
683 "user":{
684 "id":"UXXXXXXXX",
685 "name":"mike.brevoort"
686 },
687 "action_ts":"1469129995.067370",
688 "message_ts":"1469129988.000084",
689 "attachment_id":"1",
690 "token":"dxxxxxxxxxxxxxxxxxxxx",
691 "original_message":{
692 "text":"What?",
693 "username":"In or Out",
694 "bot_id":"BXXXXXXXX",
695 "attachments":[
696 {
697 "callback_id":"in_or_out_callback",
698 "fallback":"Pick one",
699 "id":1,
700 "actions":[
701 {
702 "id":"1",
703 "name":"answer",
704 "text":":beer:",
705 "type":"button",
706 "value":":beer:",
707 "style":""
708 },
709 {
710 "id":"2",
711 "name":"answer",
712 "text":":beers:",
713 "type":"button",
714 "value":":wine:",
715 "style":""
716 },
717 ]
718 },
719 {
720 "text":":beers: • mike.brevoort",
721 "id":2,
722 "fallback":"who picked beers"
723 }
724 ],
725 "type":"message",
726 "subtype":"bot_message",
727 "ts":"1469129988.000084"
728 },
729 "response_url":"https://hooks.slack.com/actions/TXXXXXXXX/111111111111/txxxxxxxxxxxxxxxxxxxx"
730 }
731```
732
733
734
735 Example menu action `msg.body` object:
736
737```js
738 {
739 "actions": [
740 {
741 "name": "winners_list",
742 "selected_options": [
743 {
744 "value": "U061F1ZUR"
745 }
746 ]
747 }
748 ],
749 "callback_id": "select_simple_1234",
750 "team": {
751 "id": "T012AB0A1",
752 "domain": "pocket-calculator"
753 },
754 "channel": {
755 "id": "C012AB3CD",
756 "name": "general"
757 },
758 "user": {
759 "id": "U012A1BCD",
760 "name": "musik"
761 },
762 "action_ts": "1481579588.685999",
763 "message_ts": "1481579582.000003",
764 "attachment_id": "1",
765 "token": "verification_token_string",
766 "original_message": {
767 "text": "It's time to nominate the channel of the week",
768 "bot_id": "B08BCU62D",
769 "attachments": [
770 {
771 "callback_id": "select_simple_1234",
772 "fallback": "Upgrade your Slack client to use messages like these.",
773 "id": 1,
774 "color": "3AA3E3",
775 "actions": [
776 {
777 "id": "1",
778 "name": "channels_list",
779 "text": "Which channel changed your life this week?",
780 "type": "select",
781 "data_source": "channels"
782 }
783 ]
784 }
785 ],
786 "type": "message",
787 "subtype": "bot_message",
788 "ts": "1481579582.000003"
789 },
790 "response_url": "https://hooks.slack.com/actions/T012AB0A1/1234567890/JpmK0yzoZ5eRiqfeduTBYXWQ"
791 }
792```
793
794## Slapp.messageAction(callbackId:string, callback:function)
795
796 Register a new handler for a [message action](https://api.slack.com/actions).
797
798 The `callbackId` should match the "Callback ID" registered in the message action.
799
800
801#### Parameters
802 - `callbackId` string
803 - `callback` function - `(msg, message) => {}` - message
804
805
806#### Returns
807 - `this` (chainable)
808
809 Example:
810
811```js
812 // match on callback_id
813 slapp.messageAction('launch_message_action', (msg, message) => {}
814```
815
816
817
818 Example message action `msg.body` object:
819
820```js
821 {
822 "token": "Nj2rfC2hU8mAfgaJLemZgO7H",
823 "callback_id": "chirp_message",
824 "type": "message_action",
825 "trigger_id": "13345224609.8534564800.6f8ab1f53e13d0cd15f96106292d5536",
826 "response_url": "https://hooks.slack.com/app-actions/T0MJR11A4/21974584944/yk1S9ndf35Q1flupVG5JbpM6",
827 "team": {
828 "id": "T0MJRM1A7",
829 "domain": "pandamonium",
830 },
831 "channel": {
832 "id": "D0LFFBKLZ",
833 "name": "cats"
834 },
835 "user": {
836 "id": "U0D15K92L",
837 "name": "dr_maomao"
838 },
839 "message": {
840 "type": "message",
841 "user": "U0MJRG1AL",
842 "ts": "1516229207.000133",
843 "text": "World's smallest big cat! <https://youtube.com/watch?v=W86cTIoMv2U>"
844 }
845 }
846```
847
848## Slapp.options(callbackId:string, actionNameCriteria:string|RegExp, actionValueCriteria:string|RegExp, callback:function)
849
850 Register a new interactive message options handler
851
852 `options` accepts a `callbackIdPath` like `action`. See `action` for details.
853
854#### Parameters
855 - `callbackIdPath` string - may be a simple string or a URL path matcher
856 - `actionNameCriteria` string or RegExp - the name of the action [optional]
857 - `actionValueCriteria` string or RegExp - the value of the action [optional]
858 - `callback` function - `(msg, value) => {}` - value is the current value of the option (e.g. partially typed)
859
860
861#### Returns
862 - `this` (chainable)
863
864 Example matching callback only
865
866```js
867 slapp.options('my_callback', (msg, value) => {}
868```
869
870
871
872 Example with name matcher
873
874```js
875 slapp.options('my_callback', 'my_name', (msg, value) => {}
876```
877
878
879
880 Example with RegExp matcher criteria:
881
882```js
883 slapp.options('my_callback', /my_n.+/, (msg, value) => {}
884```
885
886
887 Example with callback_id path criteria:
888
889```js
890 slapp.options('/my_callback/:id', (msg, value) => {}
891```
892
893
894
895
896 Example `msg.body` object:
897
898```js
899 {
900 "name": "musik",
901 "value": "",
902 "callback_id": "select_remote_1234",
903 "team": {
904 "id": "T012AB0A1",
905 "domain": "pocket-calculator"
906 },
907 "channel": {
908 "id": "C012AB3CD",
909 "name": "general"
910 },
911 "user": {
912 "id": "U012A1BCD",
913 "name": "musik"
914 },
915 "action_ts": "1481670445.010908",
916 "message_ts": "1481670439.000007",
917 "attachment_id": "1",
918 "token": "verification_token_string"
919 }
920```
921
922 *
923
924## Slapp.command(command:string, criteria:string|RegExp, callback:function)
925
926 Register a new slash command handler
927
928#### Parameters
929 - `command` string - the slash command (e.g. "/doit")
930 - `criteria` string or RegExp (e.g "/^create.+$/") [optional]
931 - `callback` function - `(msg) => {}`
932
933
934#### Returns
935 - `this` (chainable)
936
937 Example without parameters:
938
939```js
940 // "/acommand"
941 slapp.command('acommand', (msg) => {
942 }
943```
944
945
946
947 Example with RegExp matcher criteria:
948
949```js
950 // "/acommand create flipper"
951 slapp.command('acommand', 'create (.*)'(msg, text, name) => {
952 // text = 'create flipper'
953 // name = 'flipper'
954 }
955```
956
957
958
959 Example `msg` object:
960
961```js
962 {
963 "type":"command",
964 "body":{
965 "token":"xxxxxxxxxxxxxxxxxxx",
966 "team_id":"TXXXXXXXX",
967 "team_domain":"teamxxxxxxx",
968 "channel_id":"Dxxxxxxxx",
969 "channel_name":"directmessage",
970 "user_id":"Uxxxxxxxx",
971 "user_name":"xxxx.xxxxxxxx",
972 "command":"/doit",
973 "text":"whatever was typed after command",
974 "response_url":"https://hooks.slack.com/commands/TXXXXXXXX/111111111111111111111111111"
975 },
976 "resource":{
977 "app_token":"xoxp-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX",
978 "app_user_id":"UXXXXXXXX",
979 "bot_token":"xoxb-XXXXXXXXXX-XXXXXXXXXXXXXXXXXXXX",
980 "bot_user_id":"UXXXXXXXX"
981 },
982 "meta":{
983 "user_id":"UXXXXXXXX",
984 "channel_id":"DXXXXXXXX",
985 "team_id":"TXXXXXXXX"
986 },
987 }
988```
989
990## Slapp.dialog(callbackId:string, callback:function)
991
992 Register a dialog submission handler for the given callback_id
993
994#### Parameters
995 - `callbackId` string - the callback_id of the form
996 - `callback` function - `(msg, submission) => {}`
997
998
999#### Returns
1000 - `this` (chainable)
1001
1002 Example;
1003
1004```js
1005 // "/acommand"
1006 slapp.command('my_callback_id', (msg, submission) => {
1007 submission.prop_name_1
1008 }
1009```
1010
1011
1012
1013 Example `msg` object:
1014
1015```js
1016 {
1017 "type":"action",
1018 "body":{
1019 "type": "dialog_submission",
1020 "submission": {
1021 "answer": "two",
1022 "feedback": "test"
1023 },
1024 "callback_id": "xyz",
1025 "team": {
1026 "id": "T1PR9DEFS",
1027 "domain": "aslackdomain"
1028 },
1029 "user": {
1030 "id": "U1ABCDEF",
1031 "name": "mikebrevoort"
1032 },
1033 "channel": {
1034 "id": "C1PR520RRR",
1035 "name": "random"
1036 },
1037 "action_ts": "1503445940.478855"
1038 },
1039 }
1040```
1041
1042
1043
1044# Message
1045
1046
1047A Message object is created for every incoming Slack event, slash command, and interactive message action.
1048It is generally always passed as `msg`.
1049
1050`msg` has three main top level properties
1051- `type` - one of `event`, `command`, `action`
1052- `body` - the unmodified payload of the original event
1053- `meta` - derived or normalized properties and anything appended by middleware.
1054
1055`meta` should at least have these properties
1056- `app_token` - token for the user for the app
1057- `app_user_id` - userID for the user who install ed the app
1058- `bot_token` - token for a bot user of the app
1059- `bot_user_id` - userID of the bot user of the app
1060
1061
1062 - [Message.constructor()](#messageconstructortypestringbodyobjectmetaobject)
1063 - [Message.hasResponse()](#messagehasresponse)
1064 - [Message.route()](#messageroutefnkeystringstateobjectsecondstoexpirenumber)
1065 - [Message.cancel()](#messagecancel)
1066 - [Message.say()](#messagesayinputstringobjectarraycallbackfunction)
1067 - [Message.respond()](#messagerespondresponseurlstringinputstringobjectarraycallbackfunction)
1068 - [Message.thread()](#messagethread)
1069 - [Message.unthread()](#messageunthread)
1070 - [Message._request()](#message_request)
1071 - [Message.isBot()](#messageisbot)
1072 - [Message.isBaseMessage()](#messageisbasemessage)
1073 - [Message.isThreaded()](#messageisthreaded)
1074 - [Message.isDirectMention()](#messageisdirectmention)
1075 - [Message.isDirectMessage()](#messageisdirectmessage)
1076 - [Message.isMention()](#messageismention)
1077 - [Message.isAmbient()](#messageisambient)
1078 - [Message.isAnyOf()](#messageisanyofofarray)
1079 - [Message.isAuthedTeam()](#messageisauthedteam)
1080 - [Message.usersMentioned()](#messageusersmentioned)
1081 - [Message.channelsMentioned()](#messagechannelsmentioned)
1082 - [Message.subteamGroupsMentioned()](#messagesubteamgroupsmentioned)
1083 - [Message.everyoneMentioned()](#messageeveryonementioned)
1084 - [Message.channelMentioned()](#messagechannelmentioned)
1085 - [Message.hereMentioned()](#messageherementioned)
1086 - [Message.linksMentioned()](#messagelinksmentioned)
1087 - [Message.stripDirectMention()](#messagestripdirectmention)
1088
1089## Message.constructor(type:string, body:Object, meta:Object)
1090
1091 Construct a new Message
1092
1093#### Parameters
1094 - `type` the type of message (event, command, action, etc.)
1095
1096## Message.hasResponse()
1097
1098 May this message be responded to with `msg.respond` because the originating
1099 event included a `response_url`. If `hasResponse` returns false, you may
1100 still call `msg.respond` while explicitly passing a `response_url`.
1101
1102#### Returns `true` if `msg.respond` may be called on this message, implicitly.
1103
1104## Message.route(fnKey:string, state:Object, secondsToExpire:number)
1105
1106 Register the next function to route to in a conversation.
1107
1108 The route should be registered already through `slapp.route`
1109
1110#### Parameters
1111 - `fnKey` `string`
1112 - `state` `object` arbitrary data to be passed back to your function [optional]
1113 - `secondsToExpire` `number` - number of seconds to wait for the next message in the conversation before giving up. Default 60 minutes [optional]
1114
1115
1116#### Returns
1117 - `this` (chainable)
1118
1119## Message.cancel()
1120
1121 Explicity cancel pending `route` registration.
1122
1123## Message.say(input:string|Object|Array, callback:function)
1124
1125 Send a message through [`chat.postmessage`](https://api.slack.com/methods/chat.postMessage).
1126
1127 The current channel and inferred tokens are used as defaults. `input` maybe a
1128 `string`, `Object` or mixed `Array` of `strings` and `Objects`. If a string,
1129 the value will be set to `text` of the `chat.postmessage` object. Otherwise pass
1130 a [`chat.postmessage`](https://api.slack.com/methods/chat.postMessage) `Object`.
1131 If the current message is part of a thread, the new message will remain
1132 in the thread. To control if a message is threaded or not you can use the
1133 `msg.thread()` and `msg.unthread()` functions.
1134
1135 If `input` is an `Array`, a random value in the array will be selected.
1136
1137#### Parameters
1138 - `input` the payload to send, maybe a string, Object or Array.
1139 - `callback` (err, data) => {}
1140
1141
1142#### Returns
1143 - `this` (chainable)
1144
1145## Message.respond([responseUrl]:string, input:string|Object|Array, callback:function)
1146
1147 Respond to a Slash command, interactive message action, or interactive message options request.
1148
1149 Slash commands and message actions responses should be passed a [`chat.postmessage`](https://api.slack.com/methods/chat.postMessage)
1150 payload. If `respond` is called within 3000ms (2500ms actually with a 500ms buffer) of the original request,
1151 the original request will be responded to instead or using the `response_url`. This will keep the
1152 action button spinner in sync with an awaiting update and is about 25% more responsive when tested.
1153
1154 `input` options are the same as [`say`](#messagesay)
1155
1156
1157 If a response to an interactive message options request then an array of options should be passed
1158 like:
1159
1160```js
1161 {
1162 "options": [
1163 { "text": "value" },
1164 { "text": "value" }
1165 ]
1166 }
1167```
1168
1169
1170
1171#### Parameters
1172 - `responseUrl` string - URL provided by a Slack interactive message action or slash command [optional]
1173 - `input` the payload to send, maybe a string, Object or Array.
1174 - `callback` (err, data) => {}
1175
1176 Example:
1177
1178```js
1179 // responseUrl implied from body.response_url if this is an action or command
1180 msg.respond('thanks!', (err) => {})
1181```
1182
1183
1184```js
1185 // responseUrl explicitly provided
1186 msg.respond(responseUrl, 'thanks!', (err) => {})
1187```
1188
1189
1190```js
1191 // input provided as object
1192 msg.respond({ text: 'thanks!' }, (err) => {})
1193```
1194
1195
1196```js
1197 // input provided as Array
1198 msg.respond(['thanks!', 'I :heart: u'], (err) => {})
1199```
1200
1201
1202
1203#### Returns
1204 - `this` (chainable)
1205
1206## Message.thread()
1207
1208 Ensures all subsequent messages created are under a thread of the current message
1209
1210 Example:
1211
1212```js
1213 // current msg is not part of a thread (i.e. does not have thread_ts set)
1214 msg.
1215 .say('This message will not be part of the thread and will be in the channel')
1216 .thread()
1217 .say('This message will remain in the thread')
1218 .say('This will also be in the thread')
1219```
1220
1221
1222#### Returns
1223 - `this` (chainable)
1224
1225## Message.unthread()
1226
1227 Ensures all subsequent messages created are not part of a thread
1228
1229 Example:
1230
1231```js
1232 // current msg is part of a thread (i.e. has thread_ts set)
1233 msg.
1234 .say('This message will remain in the thread')
1235 .unthread()
1236 .say('This message will not be part of the thread and will be in the channel')
1237 .say('This will also not be part of the thread')
1238```
1239
1240
1241
1242#### Returns
1243 - `this` (chainable)
1244
1245## Message._request()
1246
1247 istanbul ignore next
1248
1249## Message.isBot()
1250
1251 Is this from a bot user?
1252
1253#### Returns `bool` true if `this` is a message from a bot user
1254
1255## Message.isBaseMessage()
1256
1257 Is this an `event` of type `message` without any [subtype](https://api.slack.com/events/message)?
1258
1259
1260#### Returns `bool` true if `this` is a message event type with no subtype
1261
1262## Message.isThreaded()
1263
1264 Is this an `event` of type `message` without any [subtype](https://api.slack.com/events/message)?
1265
1266
1267#### Returns `bool` true if `this` is an event that is part of a thread
1268
1269## Message.isDirectMention()
1270
1271 Is this a message that is a direct mention ("@botusername: hi there", "@botusername goodbye!")
1272
1273
1274#### Returns `bool` true if `this` is a direct mention
1275
1276## Message.isDirectMessage()
1277
1278 Is this a message in a direct message channel (one on one)
1279
1280
1281#### Returns `bool` true if `this` is a direct message
1282
1283## Message.isMention()
1284
1285 Is this a message where the bot user mentioned anywhere in the message.
1286 Only checks for mentions of the bot user and does not consider any other users.
1287
1288
1289#### Returns `bool` true if `this` mentions the bot user
1290
1291## Message.isAmbient()
1292
1293 Is this a message that's not a direct message or that mentions that bot at
1294 all (other users could be mentioned)
1295
1296
1297#### Returns `bool` true if `this` is an ambient message
1298
1299## Message.isAnyOf(of:Array)
1300
1301 Is this a message that matches any one of the filters
1302
1303#### Parameters
1304 - `messageFilters` Array - any of `direct_message`, `direct_mention`, `mention` and `ambient`
1305
1306
1307#### Returns `bool` true if `this` is a message that matches any of the filters
1308
1309## Message.isAuthedTeam()
1310
1311 Return true if the event "team_id" is included in the "authed_teams" array.
1312 In other words, this event originated from a team who has installed your app
1313 versus a team who is sharing a channel with a team who has installed the app
1314 but in fact hasn't installed the app into that team explicitly.
1315 There are some events that do not include an "authed_teams" property. In these
1316 cases, error on the side of claiming this IS from an authed team.
1317
1318#### Returns an Array of user IDs
1319
1320## Message.usersMentioned()
1321
1322 Return the user IDs of any users mentioned in the message
1323
1324#### Returns an Array of user IDs
1325
1326## Message.channelsMentioned()
1327
1328 Return the channel IDs of any channels mentioned in the message
1329
1330#### Returns an Array of channel IDs
1331
1332## Message.subteamGroupsMentioned()
1333
1334 Return the IDs of any subteams (groups) mentioned in the message
1335
1336#### Returns an Array of subteam IDs
1337
1338## Message.everyoneMentioned()
1339
1340 Was "@everyone" mentioned in the message
1341
1342#### Returns `bool` true if `@everyone` was mentioned
1343
1344## Message.channelMentioned()
1345
1346 Was the current "@channel" mentioned in the message
1347
1348#### Returns `bool` true if `@channel` was mentioned
1349
1350## Message.hereMentioned()
1351
1352 Was the "@here" mentioned in the message
1353
1354#### Returns `bool` true if `@here` was mentioned
1355
1356## Message.linksMentioned()
1357
1358 Return the URLs of any links mentioned in the message
1359
1360#### Returns `Array:string` of URLs of links mentioned in the message
1361
1362## Message.stripDirectMention()
1363
1364 Strip the direct mention prefix from the message text and return it. The
1365 original text is not modified
1366
1367
1368#### Returns `string` original `text` of message with a direct mention of the bot
1369 user removed. For example, `@botuser hi` or `@botuser: hi` would produce `hi`.
1370 `@notbotuser hi` would produce `@notbotuser hi`
1371
1372
1373# Contributing
1374
1375We adore contributions. Please include the details of the proposed changes in a Pull Request and ensure `npm test` passes. 👻
1376
1377### Scripts
1378- `npm test` - runs linter and tests with coverage
1379- `npm run unit` - runs unit tests without coverage
1380- `npm run lint` - just runs JS standard linter
1381- `npm run coverage` - runs tests with coverage
1382- `npm run lcov` - runs tests with coverage and output lcov report
1383- `npm run docs` - regenerates API docs in this README.md
1384
1385# License
1386MIT Copyright (c) 2016 Beep Boop, Robots & Pencils
1387
1388[beepboop]: https://beepboophq.com