slackrb 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +29 -0
  5. data/.rubocop_todo.yml +49 -0
  6. data/CHANGELOG.md +219 -0
  7. data/Dangerfile +5 -0
  8. data/Gemfile +20 -0
  9. data/LICENSE.md +22 -0
  10. data/README.md +766 -0
  11. data/Rakefile +21 -0
  12. data/lib/config/application.rb +14 -0
  13. data/lib/config/boot.rb +8 -0
  14. data/lib/config/environment.rb +5 -0
  15. data/lib/slack-ruby-bot/about.rb +9 -0
  16. data/lib/slack-ruby-bot/app.rb +56 -0
  17. data/lib/slack-ruby-bot/bot.rb +19 -0
  18. data/lib/slack-ruby-bot/client.rb +65 -0
  19. data/lib/slack-ruby-bot/commands/about.rb +14 -0
  20. data/lib/slack-ruby-bot/commands/base.rb +158 -0
  21. data/lib/slack-ruby-bot/commands/help.rb +43 -0
  22. data/lib/slack-ruby-bot/commands/hi.rb +16 -0
  23. data/lib/slack-ruby-bot/commands/support/attrs.rb +36 -0
  24. data/lib/slack-ruby-bot/commands/support/help.rb +84 -0
  25. data/lib/slack-ruby-bot/commands/support/match.rb +23 -0
  26. data/lib/slack-ruby-bot/commands/unknown.rb +13 -0
  27. data/lib/slack-ruby-bot/commands.rb +7 -0
  28. data/lib/slack-ruby-bot/config.rb +38 -0
  29. data/lib/slack-ruby-bot/hooks/hello.rb +35 -0
  30. data/lib/slack-ruby-bot/hooks/hook_support.rb +45 -0
  31. data/lib/slack-ruby-bot/hooks/message.rb +56 -0
  32. data/lib/slack-ruby-bot/hooks/set.rb +45 -0
  33. data/lib/slack-ruby-bot/hooks.rb +6 -0
  34. data/lib/slack-ruby-bot/mvc/controller/base.rb +172 -0
  35. data/lib/slack-ruby-bot/mvc/model/base.rb +27 -0
  36. data/lib/slack-ruby-bot/mvc/mvc.rb +7 -0
  37. data/lib/slack-ruby-bot/mvc/view/base.rb +30 -0
  38. data/lib/slack-ruby-bot/mvc.rb +3 -0
  39. data/lib/slack-ruby-bot/rspec/support/bots_for_tests.rb +35 -0
  40. data/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/it_behaves_like_a_slack_bot.rb +16 -0
  41. data/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/not_respond.rb +25 -0
  42. data/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_error.rb +36 -0
  43. data/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_message.rb +34 -0
  44. data/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_messages.rb +45 -0
  45. data/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/start_typing.rb +32 -0
  46. data/lib/slack-ruby-bot/rspec/support/slack_api_key.rb +10 -0
  47. data/lib/slack-ruby-bot/rspec/support/slack_ruby_bot_configure.rb +15 -0
  48. data/lib/slack-ruby-bot/rspec/support/spec_helpers.rb +14 -0
  49. data/lib/slack-ruby-bot/rspec.rb +14 -0
  50. data/lib/slack-ruby-bot/server.rb +88 -0
  51. data/lib/slack-ruby-bot/support/loggable.rb +25 -0
  52. data/lib/slack-ruby-bot/version.rb +5 -0
  53. data/lib/slack-ruby-bot.rb +29 -0
  54. data/lib/slack_ruby_bot.rb +3 -0
  55. data/screenshots/aliases.gif +0 -0
  56. data/screenshots/create-classic-app.png +0 -0
  57. data/screenshots/demo.gif +0 -0
  58. data/screenshots/dms.gif +0 -0
  59. data/screenshots/help.png +0 -0
  60. data/screenshots/market.gif +0 -0
  61. data/screenshots/weather.gif +0 -0
  62. data/slack-ruby-bot.gemspec +32 -0
  63. data/slack.png +0 -0
  64. metadata +244 -0
data/README.md ADDED
@@ -0,0 +1,766 @@
1
+ Slack-Ruby-Bot
2
+ ==============
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/slack-ruby-bot.svg)](http://badge.fury.io/rb/slack-ruby-bot)
5
+ [![Build Status](https://travis-ci.org/slack-ruby/slack-ruby-bot.svg)](https://travis-ci.org/slack-ruby/slack-ruby-bot)
6
+ [![Code Climate](https://codeclimate.com/github/slack-ruby/slack-ruby-bot/badges/gpa.svg)](https://codeclimate.com/github/slack-ruby/slack-ruby-bot)
7
+
8
+ ---
9
+
10
+ **Warning**: As of December 4th, 2020 Slack no longer accept resubmissions from apps that are not using granular permissions, or so-called "classic apps". On November 18, 2021 Slack will start delisting apps that have not migrated to use granular permissions. This library implements legacy, real-time support for classic apps. You should not be building a new bot with it and use [slack-ruby-bot-server-events](https://github.com/slack-ruby/slack-ruby-bot-server-events) instead. For a rudimentary bot you can even start with [slack-ruby-bot-server-events-app-mentions](https://github.com/slack-ruby/slack-ruby-bot-server-events-app-mentions). See [MIGRATION](MIGRATION.md) for migration help.
11
+
12
+ ---
13
+
14
+ The slack-ruby-bot library is a generic Slack bot framework written in Ruby on top of [slack-ruby-client](https://github.com/slack-ruby/slack-ruby-client). This library does all the heavy lifting, such as message parsing, so you can focus on implementing slack bot commands. It also attempts to introduce the bare minimum number of requirements or any sorts of limitations. It's a Slack bot boilerplate.
15
+
16
+ If you are not familiar with Slack bots or Slack API concepts, you might want to watch [this video](http://code.dblock.org/2016/03/11/your-first-slack-bot-service-video.html).
17
+
18
+ ![](slack.png)
19
+
20
+ # Table of Contents
21
+
22
+ - [Useful to Me?](#useful-to-me)
23
+ - [Stable Release](#stable-release)
24
+ - [Usage](#usage)
25
+ - [A Minimal Bot](#a-minimal-bot)
26
+ - [Gemfile](#gemfile)
27
+ - [pongbot.rb](#pongbotrb)
28
+ - [A Production Bot](#a-production-bot)
29
+ - [More Involved Examples](#more-involved-examples)
30
+ - [Commands and Operators](#commands-and-operators)
31
+ - [Threaded Messages](#threaded-messages)
32
+ - [Bot Aliases](#bot-aliases)
33
+ - [Generic Routing](#generic-routing)
34
+ - [Matching text in message attachments](#matching-text-in-message-attachments)
35
+ - [Providing description for your bot and commands](#providing-description-for-your-bot-and-commands)
36
+ - [Customize your command help output](#customize-your-command-help-output)
37
+ - [SlackRubyBot::Commands::Base](#slackrubybotcommandsbase)
38
+ - [Authorization](#authorization)
39
+ - [Built-In Commands](#built-in-commands)
40
+ - [[bot name]](#bot-name)
41
+ - [[bot name] hi](#bot-name-hi)
42
+ - [[bot name] help](#bot-name-help)
43
+ - [Hooks](#hooks)
44
+ - [Implementing and registering a Hook Handler](#implementing-and-registering-a-hook-handler)
45
+ - [Hooks registration on SlackRubyBot::Server initialization](#hooks-registration-on-slackrubybotserver-initialization)
46
+ - [Hooks registration on a SlackRubyBot::Server instance](#hooks-registration-on-a-slackrubybotserver-instance)
47
+ - [Hooks registration on SlackRubyBot::Server class](#hooks-registration-on-slackrubybotserver-class)
48
+ - [Bot Message Protection](#bot-message-protection)
49
+ - [Message Loop Protection](#message-loop-protection)
50
+ - [Logging](#logging)
51
+ - [Advanced Integration](#advanced-integration)
52
+ - [Proxy Configuration](#proxy-configuration)
53
+ - [Model-View-Controller Design](#model-view-controller-design)
54
+ - [Controller](#controller)
55
+ - [Model](#model)
56
+ - [View](#view)
57
+ - [Testing](#testing)
58
+ - [RSpec Shared Behaviors](#rspec-shared-behaviors)
59
+ - [Testing Lower Level Messages](#testing-lower-level-messages)
60
+ - [Useful Libraries](#useful-libraries)
61
+ - [Contributing](#contributing)
62
+ - [Upgrading](#upgrading)
63
+ - [Copyright and License](#copyright-and-license)
64
+
65
+ ## Useful to Me?
66
+
67
+ * If you are just trying to send messages to Slack, use [slack-ruby-client](https://github.com/slack-ruby/slack-ruby-client), which this library is built on top of.
68
+ * If you're trying to roll out a full service with Slack button integration, check out [slack-ruby-bot-server](https://github.com/slack-ruby/slack-ruby-bot-server), which uses this library.
69
+ * Otherwise, this piece of the puzzle will help you create a single bot instance for one team.
70
+
71
+ ## Stable Release
72
+
73
+ You're reading the documentation for the **next** release of slack-ruby-bot.
74
+ Please see the documentation for the [last stable release, v0.16.1](https://github.com/slack-ruby/slack-ruby-bot/tree/v0.16.1) unless you're integrating with HEAD.
75
+ See [CHANGELOG](CHANGELOG.md) for a history of changes and [UPGRADING](UPGRADING.md) for how to upgrade to more recent versions.
76
+
77
+ ## Usage
78
+
79
+ ### A Minimal Bot
80
+
81
+ #### Gemfile
82
+
83
+ ```ruby
84
+ source 'https://rubygems.org'
85
+
86
+ gem 'slackrb'
87
+ gem 'async-websocket', '~>0.8.0'
88
+ ```
89
+
90
+ #### pongbot.rb
91
+
92
+ ```ruby
93
+ require 'slackrb'
94
+
95
+ class PongBot < SlackRubyBot::Bot
96
+ command 'ping' do |client, data, match|
97
+ client.say(text: 'pong', channel: data.channel)
98
+ end
99
+ end
100
+
101
+ PongBot.run
102
+ ```
103
+
104
+ After [registering the bot](DEPLOYMENT.md), run with `SLACK_API_TOKEN=... bundle exec ruby pongbot.rb`. Have the bot join a channel and send it a ping.
105
+
106
+ ![](screenshots/demo.gif)
107
+
108
+ ### A Production Bot
109
+
110
+ A typical production Slack bot is a combination of a vanilla web server and a websocket application that talks to the Slack Real Time Messaging API. See our [Writing a Production Bot](TUTORIAL.md) tutorial for more information.
111
+
112
+ ### More Involved Examples
113
+
114
+ The following examples of bots based on slack-ruby-bot are listed in growing order of complexity.
115
+
116
+ * [slack-bot-on-rails](https://github.com/dblock/slack-bot-on-rails): A bot running on Rails and using React to display Slack messages on a website.
117
+ * [slack-mathbot](https://github.com/dblock/slack-mathbot): Slack integration with math.
118
+ * [slack-google-bot](https://github.com/dblock/slack-google-bot): A Slack bot that searches Google, including CSE.
119
+ * [slack-aws](https://github.com/dblock/slack-aws): Slack integration with Amazon Web Services.
120
+ * [slack-deploy-bot](https://github.com/accessd/slack-deploy-bot): A Slack bot that helps you to deploy your apps.
121
+ * [slack-gamebot](https://github.com/dblock/slack-gamebot): A game bot service for ping pong, chess, etc, hosted at [playplay.io](http://playplay.io).
122
+ * [slack-victorbot](https://github.com/uShip/victorbot): A Slack bot to talk to the Victorops service.
123
+
124
+ ### Commands and Operators
125
+
126
+ Bots are addressed by name, they respond to commands and operators. You can combine multiple commands.
127
+
128
+ ```ruby
129
+ class CallBot < SlackRubyBot::Bot
130
+ command 'call', '呼び出し' do |client, data, match|
131
+ client.say(channel: data.channel, text: 'called')
132
+ end
133
+ end
134
+ ```
135
+
136
+ Command match data includes `match['bot']`, `match['command']` and `match['expression']`. The `bot` match always checks against the `SlackRubyBot::Config.user` and `SlackRubyBot::Config.user_id` values obtained when the bot starts.
137
+
138
+ The `command` method can take strings, which will have to be escaped with `Regexp.escape`, and regular expressions.
139
+
140
+ ```ruby
141
+ class CallBot < SlackRubyBot::Bot
142
+ command 'string with spaces', /some\s*regex+\?*/ do |client, data, match|
143
+ client.say(channel: data.channel, text: match['command'])
144
+ end
145
+ end
146
+ ```
147
+
148
+ Operators are 1-letter long and are similar to commands. They don't require addressing a bot nor separating an operator from its arguments. The following class responds to `=2+2`.
149
+
150
+ ```ruby
151
+ class MathBot < SlackRubyBot::Bot
152
+ operator '=' do |client, data, match|
153
+ # implementation detail
154
+ end
155
+ end
156
+ ```
157
+
158
+ Operator match data includes `match['operator']` and `match['expression']`. The `bot` match always checks against the `SlackRubyBot::Config.user` setting.
159
+
160
+ ### Threaded Messages
161
+
162
+ To reply to a message in a thread you must provide a reference to the first message that initiated the thread, which is available as either `data.ts` if no threaded messages have been sent, or `data.thread_ts` if the message being replied to is already in a thread. See [message-threading](https://api.slack.com/docs/message-threading) for more information.
163
+
164
+ ```ruby
165
+ command 'reply in thread' do |client, data, match|
166
+ client.say(
167
+ channel: data.channel,
168
+ text: "let's avoid spamming everyone, I will tell you what you need in this thread",
169
+ thread_ts: data.thread_ts || data.ts
170
+ )
171
+ end
172
+ ```
173
+
174
+ _Note that sending a message using only `thread_ts: data.ts` can cause some permanent issues where Slack will keep reporting inaccessible messages as unread. At the time of writing the slack team is still having problems clearing those notifications. As recommended by the slack documentation ..._
175
+
176
+ > A true parent's thread_ts should be used when replying. Providing a child's message ID will result in a new, detached thread breaking all context and sense.
177
+
178
+ _... the replies to a thread should always be sent to the message `ts` that started the thread, available as `thread_ts` for subsequent messages. Hence `data.thread_ts || data.ts`._
179
+
180
+ For additional options, including broadcasting, see [slack-ruby-client#chat_postMessage](https://github.com/slack-ruby/slack-ruby-client/blob/41539c647ac877400f20aa338aa42d2ebfd2866b/lib/slack/web/api/endpoints/chat.rb#L105).
181
+
182
+ ### Bot Aliases
183
+
184
+ A bot will always respond to its name (eg. `rubybot`) and Slack ID (eg. `@rubybot`), but you can specify multiple aliases via the `SLACK_RUBY_BOT_ALIASES` environment variable or via an explicit configuration.
185
+
186
+ ```
187
+ SLACK_RUBY_BOT_ALIASES=:pp: table-tennis
188
+ ```
189
+
190
+ ```ruby
191
+ SlackRubyBot.configure do |config|
192
+ config.aliases = [':pong:', 'pongbot']
193
+ end
194
+ ```
195
+
196
+ ![](screenshots/aliases.gif)
197
+
198
+ Bots will also respond to a direct message, with or without the bot name in the message itself.
199
+
200
+ ![](screenshots/dms.gif)
201
+
202
+ ### Generic Routing
203
+
204
+ Commands and operators are generic versions of bot routes. You can respond to just about anything by defining a custom route.
205
+
206
+ ```ruby
207
+ class Weather < SlackRubyBot::Bot
208
+ match /^How is the weather in (?<location>\w*)\?$/ do |client, data, match|
209
+ client.say(channel: data.channel, text: "The weather in #{match[:location]} is nice.")
210
+ end
211
+ end
212
+ ```
213
+
214
+ ![](screenshots/weather.gif)
215
+
216
+ You can also capture multiple matchers with `scan`.
217
+
218
+ ```ruby
219
+ class Market < SlackRubyBot::Bot
220
+ scan(/([A-Z]{2,5})/) do |client, data, stocks|
221
+ # lookup stock market price
222
+ end
223
+ end
224
+ ```
225
+
226
+ ![](screenshots/market.gif)
227
+
228
+ See [examples/market](examples/market/marketbot.rb) for a working example.
229
+
230
+ ### Matching text in message attachments
231
+
232
+ You can respond to text in [attachments](https://api.slack.com/docs/message-attachments) with
233
+ `attachment`. It will scan `text`, `pretext` and `title` fields in each attachment until a first
234
+ match is found.
235
+
236
+ For example you can match [this example attachment](http://goo.gl/K0cLkH)
237
+ by its `title` with the following bot:
238
+
239
+ ```ruby
240
+ class Attachment < SlackRubyBot::Bot
241
+ attachment 'Slack API Documentation' do |client, data, match|
242
+ client.say(channel: data.channel, text: "Matched by #{match.attachment_field}.")
243
+ client.say(channel: data.channel, text: "The attachment's text: #{match.attachment.text}.")
244
+ end
245
+ end
246
+ ```
247
+
248
+ You can also define which fields in attachment object should be scanned.
249
+
250
+ Scan only a single field:
251
+
252
+ ```ruby
253
+ class Attachment < SlackRubyBot::Bot
254
+ attachment 'Slack API Documentation', :title do |client, data, match|
255
+ # implementation details
256
+ end
257
+ end
258
+ ```
259
+
260
+ Scan multiple fields:
261
+
262
+ ```ruby
263
+ class Attachment < SlackRubyBot::Bot
264
+ attachment 'Slack API Documentation', %i[text pretext author_name] do |client, data, match|
265
+ # implementation details
266
+ end
267
+ end
268
+ ```
269
+
270
+ ### Providing description for your bot and commands
271
+
272
+ You can specify help information for bot or commands with `help` block, for example:
273
+
274
+ in case of bot:
275
+
276
+ ```ruby
277
+ class WeatherBot < SlackRubyBot::Bot
278
+ help do
279
+ title 'Weather Bot'
280
+ desc 'This bot tells you the weather.'
281
+
282
+ command 'clouds' do
283
+ desc 'Tells you how many clouds there\'re above you.'
284
+ end
285
+
286
+ command 'What\'s the weather in <city>?' do
287
+ desc 'Tells you the weather in a <city>.'
288
+ long_desc "Accurate 10 Day Weather Forecasts for thousands of places around the World.\n" \
289
+ 'Bot provides detailed Weather Forecasts over a 10 day period updated four times a day.'
290
+ end
291
+ end
292
+
293
+ # commands implementation
294
+ end
295
+ ```
296
+
297
+ ![](screenshots/help.png)
298
+
299
+ in case of your own command:
300
+
301
+ ```ruby
302
+ class Deploy < SlackRubyBot::Commands::Base
303
+ help do
304
+ title 'deploy'
305
+ desc 'deploys your app'
306
+ long_desc 'command format: *deploy <branch> to <env>* where <env> is production or staging'
307
+ end
308
+ end
309
+ ```
310
+
311
+ ### Customize your command help output
312
+
313
+ If you've used the `help` block described above to document your
314
+ commands, you can provide your own implementation of outputting help
315
+ for commands like so:
316
+
317
+ ```ruby
318
+ class Market < SlackRubyBot::Bot
319
+ command 'help' do |client, data, match|
320
+ user_command = match[:expression]
321
+ help_attrs = SlackRubyBot::Commands::Support::Help.instance.find_command_help_attrs(user_command)
322
+ client.say(channel: data.channel, text: "#{help_attrs.command_desc}\n\n#{help_attrs.command_long_desc}")
323
+ end
324
+ end
325
+ ```
326
+
327
+ ### SlackRubyBot::Commands::Base
328
+
329
+ The `SlackRubyBot::Bot` class is DSL sugar deriving from `SlackRubyBot::Commands::Base`. For more involved bots you can organize the bot implementation into subclasses of `SlackRubyBot::Commands::Base` manually. By default a command class responds, case-insensitively, to its name. A class called `Phone` that inherits from `SlackRubyBot::Commands::Base` responds to `phone` and `Phone` and calls the `call` method when implemented.
330
+
331
+ ```ruby
332
+ class Phone < SlackRubyBot::Commands::Base
333
+ command 'call'
334
+
335
+ def self.call(client, data, match)
336
+ client.say(channel: data.channel, text: 'called')
337
+ end
338
+ end
339
+ ```
340
+
341
+ To respond to custom commands and to disable automatic class name matching, use the `command` keyword. The following command responds to `call` and `呼び出し` (call in Japanese).
342
+
343
+ ```ruby
344
+ class Phone < SlackRubyBot::Commands::Base
345
+ command 'call'
346
+ command '呼び出し'
347
+
348
+ def self.call(client, data, match)
349
+ client.say(channel: data.channel, text: 'called')
350
+ end
351
+ end
352
+ ```
353
+
354
+ ### Authorization
355
+
356
+ The framework does not provide any user authentication or command authorization capability out of the box. However, the `SlackRubyBot::Commands::Base` class does check every command invocation for permission prior to executing the command. The default method always returns true.
357
+
358
+ Therefore, subclasses of `SlackRubyBot::Commands::Base` can override the `permitted?` private method to provide its own authorization logic. This method is intended to be exploited by user code or external gems that want to provide custom authorization logic for command execution.
359
+
360
+ ```ruby
361
+ class AuthorizedBot < SlackRubyBot::Commands::Base
362
+ command 'phone home' do |client, data, match|
363
+ client.say(channel: data.channel, text: 'Elliot!')
364
+ end
365
+
366
+ # Only allow user 'Uxyzabc' to run this command
367
+ def self.permitted?(client, data, match)
368
+ data && data.user && data.user == 'Uxyzabc'
369
+ end
370
+ end
371
+ ```
372
+
373
+ ### Built-In Commands
374
+
375
+ Slack-ruby-bot comes with several built-in commands. You can re-define built-in commands, normally, as described above.
376
+
377
+ #### [bot name]
378
+
379
+ This is also known as the `default` command. Shows bot version and links.
380
+
381
+ #### [bot name] hi
382
+
383
+ Politely says 'hi' back.
384
+
385
+ #### [bot name] help
386
+
387
+ Get help.
388
+
389
+ ### Hooks
390
+
391
+ Hooks are event handlers and respond to Slack RTM API [events](https://api.slack.com/events), such as [hello](lib/slack-ruby-bot/hooks/hello.rb) or [message](lib/slack-ruby-bot/hooks/message.rb). You can implement your own in a couple of ways:
392
+
393
+ #### Implementing and registering a Hook Handler
394
+
395
+ A Hook Handler is any object that respond to a `call` message, like a proc, instance of an object, class with a `call` class method, etc.
396
+
397
+ Hooks can be registered using different methods based on user preference / use case.
398
+ Currently someone can use one of the following methods:
399
+
400
+ * Pass `hooks` in `SlackRubyBot::Server` initialization.
401
+ * Register `hooks` on `SlackRubyBot::Server` using `on` class method.
402
+ * Register `hooks` on `SlackRubyBot::Server` using `on` instance method.
403
+
404
+
405
+ ##### Hooks registration on `SlackRubyBot::Server` initialization
406
+
407
+ ```ruby
408
+ SlackRubyBot::Server.new(hook_handlers: {
409
+ hello: MyBot::Hooks::UserChange.new,
410
+ user_change: [->(client, data) { }, ->(client, data) {}]
411
+ })
412
+ ```
413
+
414
+ ##### Hooks registration on a `SlackRubyBot::Server` instance
415
+
416
+ ```ruby
417
+ # Register an object that implements `call` method
418
+ class MyBot::Hooks::Hello
419
+ def call(client, data)
420
+ puts "Hello"
421
+ end
422
+ end
423
+
424
+ server.on(:hello, MyBot::Hooks::Hello.new)
425
+
426
+ # or register a lambda function to handle the event
427
+ server.on(:hello, ->(client, data) { puts "Hello!" })
428
+ ```
429
+
430
+ For example, the following hook handles [user_change](https://api.slack.com/events/user_change), an event sent when a team member updates their profile or data. This can be useful to update the local user cache when a user is renamed.
431
+
432
+ ```ruby
433
+ module MyBot
434
+ module Hooks
435
+ class UserChange
436
+ def call(client, data)
437
+ # data['user']['id'] contains the user ID
438
+ # data['user']['name'] contains the new user name
439
+ # ...
440
+ end
441
+ end
442
+ end
443
+ end
444
+ ```
445
+
446
+ ##### Hooks registration on `SlackRubyBot::Server` class
447
+
448
+ Example:
449
+
450
+ ```ruby
451
+ module MyBot
452
+ class MyServer < SlackRubyBot::Server
453
+ on 'hello' do |client, data|
454
+ # data['user']['id'] contains the user ID
455
+ # data['user']['name'] contains the new user name
456
+ end
457
+
458
+ on 'user_change', ->(client, data) {
459
+ # data['user']['id'] contains the user ID
460
+ # data['user']['name'] contains the new user name
461
+ }
462
+ end
463
+ end
464
+ ```
465
+
466
+ These will get pushed into the hook set on initialization.
467
+
468
+ Either by configuration, explicit assignment or hook blocks, multiple handlers can exist for the same event type.
469
+
470
+ ### Bot Message Protection
471
+
472
+ By default bots do not respond to self or other bots. If you wish to change that behavior globally, set `allow_bot_messages` to `true`.
473
+
474
+ ```ruby
475
+ SlackRubyBot.configure do |config|
476
+ config.allow_bot_messages = true
477
+ end
478
+ ```
479
+
480
+ ### Message Loop Protection
481
+
482
+ By default bots do not respond to their own messages. If you wish to change that behavior globally, set `allow_message_loops` to `true`.
483
+
484
+ ```ruby
485
+ SlackRubyBot.configure do |config|
486
+ config.allow_message_loops = true
487
+ end
488
+ ```
489
+
490
+ ### Logging
491
+
492
+ By default bots set a logger to `$stdout` with `DEBUG` level. The logger is used in both the RealTime and Web clients.
493
+
494
+ Silence logger as follows.
495
+
496
+ ```ruby
497
+ SlackRubyBot::Client.logger.level = Logger::WARN
498
+ ```
499
+
500
+ If you wish to customize logger, set `logger` to your logger.
501
+
502
+ ```ruby
503
+ SlackRubyBot.configure do |config|
504
+ config.logger = Logger.new("slack-ruby-bot.log", "daily")
505
+ end
506
+ ```
507
+
508
+ ### Advanced Integration
509
+
510
+ You may want to integrate a bot or multiple bots into other systems, in which case a globally configured bot may not work for you. You may create instances of [SlackRubyBot::Server](lib/slack-ruby-bot/server.rb) which accepts `token` and `aliases`.
511
+
512
+ ```ruby
513
+ EM.run do
514
+ bot1 = SlackRubyBot::Server.new(token: token1, aliases: ['bot1'])
515
+ bot1.start_async
516
+
517
+ bot2 = SlackRubyBot::Server.new(token: token2, aliases: ['bot2'])
518
+ bot2.start_async
519
+ end
520
+ ```
521
+
522
+ For an example of advanced integration that supports multiple teams, see [slack-gamebot](https://github.com/dblock/slack-gamebot) and [playplay.io](http://playplay.io) that is built on top of it.
523
+
524
+ ### Proxy Configuration
525
+
526
+ There are [several proxy options](https://github.com/slack-ruby/slack-ruby-client#web-client-options) that can be configured on `Slack::Web::Client`. You can also control what proxy options are used by modifying the `http_proxy` environment variable per [Net::HTTP's documentation](https://docs.ruby-lang.org/en/2.0.0/Net/HTTP.html#class-Net::HTTP-label-Proxies).
527
+
528
+ Note that Docker on OSX seems to incorrectly set the proxy, [causing `Faraday::ConnectionFailed`](https://github.com/slack-ruby/slack-ruby-bot/issues/155), `ERROR -- : Failed to open TCP connection to : (getaddrinfo: Name or service not known)`. You might need to manually unset `http_proxy` in that case, eg. `http_proxy="" bundle exec ruby ./my_bot.rb`.
529
+
530
+ ### Model-View-Controller Design
531
+
532
+ The `command` method is essentially a controller method that receives input from the outside and acts upon it. Complex behaviors could lead to a long and difficult-to-understand `command` block. A complex `command` block is a candidate for separation into classes conforming to the Model-View-Controller pattern popularized by Rails.
533
+
534
+ The library provides three helpful base classes named `SlackRubyBot::MVC::Model::Base`, `SlackRubyBot::MVC::View::Base`, and `SlackRubyBot::MVC::Controller::Base`.
535
+
536
+ Testing a `command` block is difficult. As separate classes, the Model/View/Controller's behavior can be tested via `rspec` or a similar tool.
537
+
538
+ #### Controller
539
+
540
+ The Controller is the focal point of the bot behavior. Typically the code that would go into the `command` block will now go into an instance method in a Controller subclass. The instance method name should match the command name exactly (case sensitive).
541
+
542
+ As an example, these two classes are functionally equivalent.
543
+
544
+ Consider the following `Agent` class which is the simplest default approach to take.
545
+
546
+ ```ruby
547
+ class Agent < SlackRubyBot::Bot
548
+ command 'sayhello', 'alternate way to call hello' do |client, data, match|
549
+ client.say(channel: data.channel, text: "Received command #{match[:command]} with args #{match[:expression]}")
550
+ end
551
+ end
552
+ ```
553
+
554
+ Using the MVC functionality, we would create a controller instead to encapsulate this function.
555
+ ```ruby
556
+ class MyController < SlackRubyBot::MVC::Controller::Base
557
+ def sayhello
558
+ client.say(channel: data.channel, text: "Received command #{match[:command]} with args #{match[:expression]}")
559
+ end
560
+ alternate_name :sayhello, :alternate_way_to_call_hello
561
+ end
562
+ MyController.new(MyModel.new, MyView.new)
563
+ ```
564
+ Note in the above example that the Controller instance method `sayhello` does not receive any arguments. When the instance method is called, the Controller class sets up some accessor methods to provide the normal `client`, `data`, and `match` objects. These are the same objects passed to the `command` block.
565
+
566
+ However, the Controller anticipates that the model and view objects should contain business logic that will also operate on the `client`, `data`, and `match` objects. The controller provides access to the model and view via the `model` and `view` accessor methods. The [inventory example](examples/inventory/inventorybot.rb) provides a full example of a Model, View, and Controller working together.
567
+
568
+ A Controller may need helper methods for certain work. To prevent the helper method from creating a route that the bot will respond to directly, the instance method name should begin with an underscore (e.g. `_my_helper_method`). When building the bot routes, these methods will be skipped.
569
+
570
+ Calling `alternate_name` after the method definition allows for method aliases similar to the regular `command` structure. When commands can be triggered by multiple text strings it's useful to have that ability map to the controller methods too.
571
+
572
+ Lastly, the Controller class includes `ActiveSupport::Callbacks` which allows for full flexibility in creating `before`, `after`, and `around` hooks for all methods. Again, see the [inventory example](examples/inventory/inventorybot.rb) for more information.
573
+
574
+ #### Model
575
+
576
+ A complex bot may need to read or write data from a database or other network resource. Setting up and tearing down these connections can be costly, so the model can do it once upon instantiation.
577
+
578
+ The Model also includes `ActiveSupport::Callbacks`.
579
+
580
+ ```ruby
581
+ class MyModel < SlackRubyBot::MVC::Model::Base
582
+ define_callbacks :sanitize
583
+ set_callback :sanitize, :around, :sanitize_resource
584
+ attr_accessor :_resource
585
+
586
+ def initialize
587
+ @db = setup_database_connection
588
+ end
589
+
590
+ def read(resource)
591
+ self._resource = resource
592
+ run_callbacks :sanitize do
593
+ @db.select(:column1 => resource)
594
+ # ... do some expensive work
595
+ end
596
+ end
597
+
598
+ private
599
+
600
+ def sanitize_resource
601
+ self._resource.downcase
602
+ result = yield
603
+ puts "After read, result is #{result.inspect}"
604
+ end
605
+ end
606
+ ```
607
+
608
+ Like Controllers, the Model is automatically loaded with the latest version of the `client`, `data`, and `match` objects each time the controller method is called. Therefore the model will always have access to the latest objects when doing its work. It will typically only use the `data` and `match` objects.
609
+
610
+ Model methods are not matched to routes, so there is no restriction on how to name methods as there is in Controllers.
611
+
612
+ #### View
613
+
614
+ A typical bot just writes to a channel or uses the web client to react/unreact to a message. More complex bots will probably require more complex behaviors. These should be stored in a `SlackRubyBot::MVC::View::Base` subclass.
615
+
616
+ ```ruby
617
+ class MyView < SlackRubyBot::MVC::View::Base
618
+ define_callbacks :logit
619
+ set_callbacks :logit, :around, :audit_trail
620
+
621
+ def initialize
622
+ @mailer = setup_mailer
623
+ @ftp = setup_ftp_handler
624
+ end
625
+
626
+ def email_admin(message)
627
+ run_callbacks :logit do
628
+ @mailer.send(:administrator, message)
629
+ end
630
+ end
631
+
632
+ def react_thumbsup
633
+ client.web_client.reactions_add(
634
+ name: :thumbsup,
635
+ channel: data.channel,
636
+ timestamp: data.ts,
637
+ as_user: true)
638
+ end
639
+
640
+ def react_thumbsdown
641
+ client.web_client.reactions_remove(
642
+ name: :thumbsup,
643
+ channel: data.channel,
644
+ timestamp: data.ts,
645
+ as_user: true)
646
+ end
647
+
648
+ private
649
+
650
+ def audit_trail
651
+ Logger.audit("Sending email at [#{Time.now}]")
652
+ yield
653
+ Logger.audit("Email sent by [#{Time.now}]")
654
+ end
655
+ end
656
+ ```
657
+ Again, the View will have access to the most up to date `client`, `data`, and `match` objects. It will typically only use the `client` and `data` objects.
658
+
659
+ View methods are not matched to routes, so there is no restriction on how to name methods as there is in Controllers.
660
+
661
+ ### Testing
662
+
663
+ #### RSpec Shared Behaviors
664
+
665
+ Slack-ruby-bot comes with a number of shared RSpec behaviors that can be used in your RSpec tests.
666
+
667
+ * [behaves like a slack bot](lib/slack-ruby-bot/rspec/support/slack-ruby-bot/it_behaves_like_a_slack_bot.rb): A bot quacks like a Slack Ruby bot.
668
+ * [respond with slack message](lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_message.rb): The bot responds with a message.
669
+ * [respond with slack messages](lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_messages.rb): The bot responds with a multiple messages.
670
+ * [respond with error](lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_error.rb): An exception is raised inside a bot command.
671
+ * [start typing](lib/slack-ruby-bot/rspec/support/slack-ruby-bot/start_typing.rb): The bot calls `client.start_typing`.
672
+
673
+ Require `slack-ruby-bot/rspec` in your `spec_helper.rb` along with the following dependencies in Gemfile.
674
+
675
+ ```ruby
676
+ group :development, :test do
677
+ gem 'rack-test'
678
+ gem 'rspec'
679
+ gem 'vcr'
680
+ gem 'webmock'
681
+ end
682
+ ```
683
+
684
+ Use the `respond_with_slack_message` matcher.
685
+
686
+ ```ruby
687
+ describe SlackRubyBot::Commands do
688
+ it 'responds with any message' do
689
+ expect(message: "#{SlackRubyBot.config.user} hi").to respond_with_slack_message
690
+ end
691
+ it 'says hi' do
692
+ expect(message: "#{SlackRubyBot.config.user} hi").to respond_with_slack_message('hi')
693
+ end
694
+ end
695
+ ```
696
+
697
+ Use the `respond_with_slack_messages` matcher for multiple messages.
698
+
699
+ ```ruby
700
+ describe SlackRubyBot::Commands do
701
+ it 'responds with more than one message' do
702
+ expect(message: "#{SlackRubyBot.config.user} count").to respond_with_slack_messages
703
+ end
704
+ it 'says one and two' do
705
+ expect(message: "#{SlackRubyBot.config.user} count").to respond_with_slack_messages(['one', 'two'])
706
+ end
707
+ end
708
+ ```
709
+
710
+ Message matchers support regular expressions.
711
+
712
+ ```ruby
713
+ describe SlackRubyBot::Commands do
714
+ it 'says hi' do
715
+ expect(message: "#{SlackRubyBot.config.user} hi").to respond_with_slack_message(/hi/)
716
+ end
717
+ end
718
+ ```
719
+
720
+ Check that the bot called `client.start_typing(channel: 'channel')`.
721
+
722
+ ```ruby
723
+ describe SlackRubyBot::Commands do
724
+ it 'starts typing on channel' do
725
+ expect(message: "#{SlackRubyBot.config.user} hi").to start_typing(channel: 'channel')
726
+ end
727
+ end
728
+ ```
729
+
730
+ #### Testing Lower Level Messages
731
+
732
+ You can test client behavior at a lower level by fetching the message hook. The following example expects a bot command to call `client.typing(channel: data.channel)`.
733
+
734
+ ```ruby
735
+ describe SlackRubyBot::Commands do
736
+ let(:app) { Server.new }
737
+ let(:client) { app.send(:client) }
738
+ let(:message_hook) { SlackRubyBot::Hooks::Message.new }
739
+ it 'receives a typing event' do
740
+ expect(client).to receive(:typing)
741
+ message_hook.call(
742
+ client,
743
+ Hashie::Mash.new(text: "#{SlackRubyBot.config.user} type something", channel: 'channel')
744
+ )
745
+ end
746
+ end
747
+ end
748
+ ```
749
+
750
+ ### Useful Libraries
751
+
752
+ * [newrelic-slack-ruby-bot](https://github.com/dblock/newrelic-slack-ruby-bot): NewRelic instrumentation for slack-ruby-bot.
753
+
754
+ ## Contributing
755
+
756
+ See [CONTRIBUTING](CONTRIBUTING.md).
757
+
758
+ ## Upgrading
759
+
760
+ See [CHANGELOG](CHANGELOG.md) for a history of changes and [UPGRADING](UPGRADING.md) for how to upgrade to more recent versions.
761
+
762
+ ## Copyright and License
763
+
764
+ Copyright (c) 2015-2020, [Daniel Doubrovkine](https://twitter.com/dblockdotorg), [Artsy](https://www.artsy.net) and [Contributors](CHANGELOG.md).
765
+
766
+ This project is licensed under the [MIT License](LICENSE.md).