slack-ruby-bot 0.10.1 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/CHANGELOG.md +5 -0
  4. data/README.md +131 -2
  5. data/TUTORIAL.md +3 -1
  6. data/examples/inventory/Gemfile +5 -0
  7. data/examples/inventory/Procfile +1 -0
  8. data/examples/inventory/inventorybot.rb +248 -0
  9. data/lib/slack-ruby-bot.rb +1 -0
  10. data/lib/slack-ruby-bot/commands/base.rb +1 -1
  11. data/lib/slack-ruby-bot/mvc.rb +1 -0
  12. data/lib/slack-ruby-bot/mvc/controller/base.rb +111 -0
  13. data/lib/slack-ruby-bot/mvc/model/base.rb +25 -0
  14. data/lib/slack-ruby-bot/mvc/mvc.rb +5 -0
  15. data/lib/slack-ruby-bot/mvc/view/base.rb +28 -0
  16. data/lib/slack-ruby-bot/version.rb +1 -1
  17. data/spec/slack-ruby-bot/app_spec.rb +0 -2
  18. data/spec/slack-ruby-bot/client_spec.rb +0 -2
  19. data/spec/slack-ruby-bot/commands/about_spec.rb +0 -2
  20. data/spec/slack-ruby-bot/commands/aliases_spec.rb +0 -2
  21. data/spec/slack-ruby-bot/commands/bot_message_spec.rb +0 -2
  22. data/spec/slack-ruby-bot/commands/bot_spec.rb +0 -2
  23. data/spec/slack-ruby-bot/commands/commands_command_classes_spec.rb +7 -2
  24. data/spec/slack-ruby-bot/commands/commands_precedence_spec.rb +0 -2
  25. data/spec/slack-ruby-bot/commands/commands_regexp_escape_spec.rb +0 -2
  26. data/spec/slack-ruby-bot/commands/commands_spaces_spec.rb +0 -2
  27. data/spec/slack-ruby-bot/commands/commands_spec.rb +0 -2
  28. data/spec/slack-ruby-bot/commands/commands_with_block_spec.rb +0 -2
  29. data/spec/slack-ruby-bot/commands/commands_with_expression_spec.rb +0 -2
  30. data/spec/slack-ruby-bot/commands/direct_messages_spec.rb +0 -2
  31. data/spec/slack-ruby-bot/commands/empty_text_spec.rb +0 -2
  32. data/spec/slack-ruby-bot/commands/help/attrs_spec.rb +0 -2
  33. data/spec/slack-ruby-bot/commands/help_spec.rb +0 -2
  34. data/spec/slack-ruby-bot/commands/hi_spec.rb +0 -2
  35. data/spec/slack-ruby-bot/commands/match_spec.rb +0 -2
  36. data/spec/slack-ruby-bot/commands/message_loop_spec.rb +0 -2
  37. data/spec/slack-ruby-bot/commands/nil_message_spec.rb +0 -2
  38. data/spec/slack-ruby-bot/commands/not_implemented_spec.rb +0 -2
  39. data/spec/slack-ruby-bot/commands/operators_spec.rb +0 -2
  40. data/spec/slack-ruby-bot/commands/operators_with_block_spec.rb +0 -2
  41. data/spec/slack-ruby-bot/commands/scan_spec.rb +0 -2
  42. data/spec/slack-ruby-bot/commands/send_gif_spec.rb +0 -2
  43. data/spec/slack-ruby-bot/commands/send_message_spec.rb +0 -2
  44. data/spec/slack-ruby-bot/commands/send_message_with_gif_spec.rb +0 -2
  45. data/spec/slack-ruby-bot/commands/unknown_spec.rb +0 -2
  46. data/spec/slack-ruby-bot/config_spec.rb +0 -2
  47. data/spec/slack-ruby-bot/hooks/hook_support_spec.rb +0 -2
  48. data/spec/slack-ruby-bot/hooks/message_spec.rb +0 -2
  49. data/spec/slack-ruby-bot/hooks/set_spec.rb +0 -2
  50. data/spec/slack-ruby-bot/mvc/controller/controller_to_command_spec.rb +97 -0
  51. data/spec/slack-ruby-bot/rspec/respond_with_error_spec.rb +0 -2
  52. data/spec/slack-ruby-bot/server_spec.rb +0 -2
  53. data/spec/slack-ruby-bot/support/commands_helper_spec.rb +0 -2
  54. data/spec/slack-ruby-bot/support/loggable_spec.rb +0 -2
  55. data/spec/slack-ruby-bot/version_spec.rb +0 -2
  56. metadata +13 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 599b19f9020a13b61a9cda2f4bd570e4121a4208
4
- data.tar.gz: 885f72dffbb46d911fb7368309713393a684d35f
3
+ metadata.gz: 15ac681a340b2b17bc5a4148c0b55dd7295fc1f2
4
+ data.tar.gz: 04d0018c8acf732715f5992af917a69fcaa6d337
5
5
  SHA512:
6
- metadata.gz: 2f98b14588981310cafa3b95aa12b167551bdd32c7c0a78f77689d6d7ded74a10088f0b65a60a777f186f280a5e2ad1fd309e3166987d4f9c0e4fef83059a313
7
- data.tar.gz: 25c471d74bf5b09f4ebe5de10f74add3decf68c226fa8e57506a58a282c7736441604431bac0773afc08a169e36b4c0c0c848643d255fe5ed13e7bb245e7b2fb
6
+ metadata.gz: 9f04ac47c686f1acd671d691826e7192d24da2b73c30fcdfda5a553f0906fa37439b5d4b4c2690148fd8fee7dd58cb9c6b2074e141b4d9670dbe5ceaa05e2e75
7
+ data.tar.gz: 9e65685bc89a791ac36000bfd955daef01ef230f1010e31ff71b2133d82ee241c6f81dd6660ff81488d7d20a5e0b3f6eab5b79ba22323e5a3794e7e93b22349f
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --format documentation
2
2
  --color
3
+ --require spec_helper
@@ -1,3 +1,8 @@
1
+ ### 0.10.2 (06/03/2017)
2
+
3
+ * [#137](https://github.com/slack-ruby/slack-ruby-bot/pull/137): Add Model-View-Controller classes to allow for more explicit control over how `command`s are designed - [@chuckremes](https://github.com/chuckremes).
4
+ * [#130](https://github.com/slack-ruby/slack-ruby-bot/issues/130): Added test dependencies in TUTORIAL.md - [@jbristow](https://github.com/jbristow).
5
+
1
6
  ### 0.10.1 (2/12/2017)
2
7
 
3
8
  * [#113](https://github.com/slack-ruby/slack-ruby-bot/issues/113): Fixed commands in subclassed `SlackRubyBot::Bot` - [@dblock](https://github.com/dblock).
data/README.md CHANGED
@@ -19,7 +19,7 @@ If you are not familiar with Slack bots or Slack API concepts, you might want to
19
19
 
20
20
  ## Stable Release
21
21
 
22
- You're reading the documentation for the **stable** release of slack-ruby-bot, 0.10.1. See [CHANGELOG](CHANGELOG.md) for a history of changes and [UPGRADING](UPGRADING.md) for how to upgrade to more recent versions.
22
+ You're reading the documentation for the **next** release of slack-ruby-bot. Please see the documentation for the [last stable release, v0.10.1](https://github.com/slack-ruby/slack-ruby-bot/tree/v0.10.1) unless you're integrating with HEAD. See [CHANGELOG](CHANGELOG.md) for a history of changes and [UPGRADING](UPGRADING.md) for how to upgrade to more recent versions.
23
23
 
24
24
  ## Usage
25
25
 
@@ -64,7 +64,9 @@ The following examples of bots based on slack-ruby-bot are listed in growing ord
64
64
  * [slack-mathbot](https://github.com/dblock/slack-mathbot): Slack integration with math.
65
65
  * [slack-google-bot](https://github.com/dblock/slack-google-bot): A Slack bot that searches Google, including CSE.
66
66
  * [slack-aws](https://github.com/dblock/slack-aws): Slack integration with Amazon Web Services.
67
+ * [slack-deploy-bot](https://github.com/accessd/slack-deploy-bot): A Slack bot that helps you to deploy your apps.
67
68
  * [slack-gamebot](https://github.com/dblock/slack-gamebot): A game bot service for ping pong, chess, etc, hosted at [playplay.io](http://playplay.io).
69
+ * [slack-victorbot](https://github.com/uShip/victorbot): A Slack bot to talk to the Victorops service.
68
70
 
69
71
  ### Commands and Operators
70
72
 
@@ -293,7 +295,7 @@ module MyBot
293
295
  end
294
296
  ```
295
297
 
296
- Hooks can also be written as blocks inside the `SlackBotRuby::Server` class, for example
298
+ Hooks can also be written as blocks inside the `SlackRubyBot::Server` class, for example
297
299
 
298
300
  ```ruby
299
301
  module MyBot
@@ -344,6 +346,133 @@ end
344
346
 
345
347
  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.
346
348
 
349
+ ### Model-View-Controller Design
350
+
351
+ 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.
352
+
353
+ The library provides three helpful base classes named `SlackRubyBot::MVC::Model::Base`, `SlackRubyBot::MVC::View::Base`, and `SlackRubyBot::MVC::Controller::Base`.
354
+
355
+ Testing a `command` block is difficult. As separate classes, the Model/View/Controller's behavior can be tested via `rspec` or a similar tool.
356
+
357
+ #### Controller
358
+
359
+ 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).
360
+
361
+ As an example, these two classes are functionally equivalent.
362
+
363
+ Consider the following `Agent` class which is the simplest default approach to take.
364
+
365
+ ```ruby
366
+ class Agent < SlackRubyBot::Bot
367
+ command 'sayhello' do |client, data, match|
368
+ client.say(channel: data.channel, text: "Received command #{match[:command]} with args #{match[:expression]}")
369
+ end
370
+ end
371
+
372
+ Using the MVC functionality, we would create a controller instead to encapsulate this function.
373
+ ```ruby
374
+ class MyController < SlackRubyBot::MVC::Controller::Base
375
+ def sayhello
376
+ client.say(channel: data.channel, text: "Received command #{match[:command]} with args #{match[:expression]}")
377
+ end
378
+ end
379
+ MyController.new(MyModel.new, MyView.new)
380
+ ```
381
+ 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.
382
+
383
+ 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.
384
+
385
+ 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.
386
+
387
+ 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.
388
+
389
+ #### Model
390
+
391
+ 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.
392
+
393
+ The Model also includes `ActiveSupport::Callbacks`.
394
+
395
+ ```ruby
396
+ class MyModel < SlackRubyBot::MVC::Model::Base
397
+ define_callbacks :sanitize
398
+ set_callback :sanitize, :around, :sanitize_resource
399
+ attr_accessor :_resource
400
+
401
+ def initialize
402
+ @db = setup_database_connection
403
+ end
404
+
405
+ def read(resource)
406
+ self._resource = resource
407
+ run_callbacks :sanitize do
408
+ @db.select(:column1 => resource)
409
+ # ... do some expensive work
410
+ end
411
+ end
412
+
413
+ private
414
+
415
+ def sanitize_resource
416
+ self._resource.downcase
417
+ result = yield
418
+ puts "After read, result is #{result.inspect}"
419
+ end
420
+ end
421
+ ```
422
+
423
+ 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.
424
+
425
+ Model methods are not matched to routes, so there is no restriction on how to name methods as there is in Controllers.
426
+
427
+ #### View
428
+
429
+ 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.
430
+
431
+ ```ruby
432
+ class MyView < SlackRubyBot::MVC::View::Base
433
+ define_callbacks :logit
434
+ set_callbacks :logit, :around, :audit_trail
435
+
436
+ def initialize
437
+ @mailer = setup_mailer
438
+ @ftp = setup_ftp_handler
439
+ end
440
+
441
+ def email_admin(message)
442
+ run_callbacks :logit do
443
+ @mailer.send(:administrator, message)
444
+ end
445
+ end
446
+
447
+ def react_thumbsup
448
+ client.web_client.reactions_add(
449
+ name: :thumbs_up,
450
+ channel: data.channel,
451
+ timestamp: data.ts,
452
+ as_user: true)
453
+ end
454
+
455
+ def react_thumbsdown
456
+ client.web_client.reactions_remove(
457
+ name: :thumbs_up,
458
+ channel: data.channel,
459
+ timestamp: data.ts,
460
+ as_user: true)
461
+ end
462
+
463
+ private
464
+
465
+ def audit_trail
466
+ Logger.audit("Sending email at [#{Time.now}]")
467
+ yield
468
+ Logger.audit("Email sent by [#{Time.now}]")
469
+ end
470
+ end
471
+ ```
472
+ 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.
473
+
474
+ View methods are not matched to routes, so there is no restriction on how to name methods as there is in Controllers.
475
+
347
476
  ### RSpec Shared Behaviors
348
477
 
349
478
  Slack-ruby-bot ships with a number of shared RSpec behaviors that can be used in your RSpec tests.
@@ -13,7 +13,7 @@ A typical production Slack bot is a combination of a vanilla web server and a we
13
13
  Create a `Gemfile` that uses [slack-ruby-bot](https://github.com/slack-ruby/slack-ruby-bot), [sinatra](https://github.com/sinatra/sinatra) (a web framework) and [puma](https://github.com/puma/puma) (a web server). For development we'll also use [foreman](https://github.com/theforeman/foreman) and write tests with [rspec](https://github.com/rspec/rspec).
14
14
 
15
15
  ```ruby
16
- source 'http://rubygems.org'
16
+ source 'https://rubygems.org'
17
17
 
18
18
  gem 'slack-ruby-bot'
19
19
  gem 'puma'
@@ -29,6 +29,8 @@ end
29
29
  group :test do
30
30
  gem 'rspec'
31
31
  gem 'rack-test'
32
+ gem 'vcr'
33
+ gem 'webmock'
32
34
  end
33
35
  ```
34
36
 
@@ -0,0 +1,5 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'slack-ruby-bot', path: '../..'
4
+ gem 'celluloid-io'
5
+ gem 'sqlite3'
@@ -0,0 +1 @@
1
+ console: bundle exec ruby inventorybot.rb
@@ -0,0 +1,248 @@
1
+ require 'slack-ruby-bot'
2
+ require 'sqlite3'
3
+
4
+ # Demonstrate the usage of the Model, View, and Controller
5
+ # classes to build an inventory bot.
6
+
7
+ SlackRubyBot::Client.logger.level = Logger::WARN
8
+
9
+ # The Controller takes the place of the `command` block. It has access to the
10
+ # `client`, `data`, and `match` arguments passed to the `command` block. Each
11
+ # instance method generates a route for the bot to match against.
12
+ #
13
+ # The controller instance accesses the model and view via the `model` and
14
+ # `view` accessor methods.
15
+ #
16
+ # ActiveSupport::Callbacks support is included so every method can have
17
+ # before, after, or around hooks.
18
+ #
19
+ # Helper methods should begin with an underscore (e.g. _row) so that
20
+ # `command` routes are not auto-generated for the method.
21
+ #
22
+ class InventoryController < SlackRubyBot::MVC::Controller::Base
23
+ define_callbacks :react, :notify
24
+ set_callback :react, :around, :around_reaction
25
+ set_callback :notify, :after, :notify_admin
26
+ attr_accessor :_row
27
+
28
+ def create
29
+ model.add_item(match[:expression])
30
+ end
31
+
32
+ def read
33
+ run_callbacks :react do
34
+ row = model.read_item(match[:expression])
35
+ view.say(channel: data.channel, text: row.inspect)
36
+ end
37
+ end
38
+
39
+ def update
40
+ run_callbacks :notify do
41
+ self._row = model.update_item(match[:expression])
42
+ end
43
+ end
44
+
45
+ def delete
46
+ result = model.delete_item(match[:expression])
47
+ if result
48
+ view.delete_succeeded
49
+ else
50
+ view.delete_failed
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def notify_admin
57
+ row = _row.first
58
+ return if row[:quantity].to_i.zero?
59
+ view.email_admin("Inventory item #{row[:name]} needs to be refilled.")
60
+ view.say(channel: data.channel, text: "Administrator notified via email to refill #{row[:name]}.")
61
+ end
62
+
63
+ def around_reaction
64
+ view.react_wait
65
+ view.say(channel: data.channel, text: 'Please wait while long-running action completes...')
66
+ sleep 2.0
67
+ yield
68
+ view.say(channel: data.channel, text: 'Action has completed!')
69
+ view.unreact_wait
70
+ end
71
+ end
72
+
73
+ # The Model contains all business logic.
74
+ #
75
+ # ActiveSupport::Callbacks support is included for before, after,
76
+ # or around hooks.
77
+ #
78
+ # The Model has access to the `client`, `data`, and `match` objects.
79
+ #
80
+ class InventoryModel < SlackRubyBot::MVC::Model::Base
81
+ define_callbacks :fixup
82
+ set_callback :fixup, :before, :normalize_data
83
+ attr_accessor :_line
84
+
85
+ def initialize
86
+ @db = SQLite3::Database.new ':memory'
87
+ @db.results_as_hash = true
88
+ @db.execute 'CREATE TABLE IF NOT EXISTS Inventory(Id INTEGER PRIMARY KEY,
89
+ Name TEXT, Quantity INT, Price INT)'
90
+
91
+ s = @db.prepare 'SELECT * FROM Inventory'
92
+ results = s.execute
93
+ count = 0
94
+ count += 1 while results.next
95
+ return if count < 4
96
+ add_item "'Audi',3,52642"
97
+ add_item "'Mercedes',1,57127"
98
+ add_item "'Skoda',5,9000"
99
+ add_item "'Volvo',1,29000"
100
+ end
101
+
102
+ def add_item(line)
103
+ self._line = line # make line accessible to callback
104
+ run_callbacks :fixup do
105
+ name, quantity, price = parse(_line)
106
+ row = @db.prepare('SELECT MAX(Id) FROM Inventory').execute
107
+ max_id = row.next_hash['MAX(Id)']
108
+ @db.execute "INSERT INTO Inventory VALUES(#{max_id + 1},'#{name}',#{quantity.to_i},#{price.to_i})"
109
+ end
110
+ end
111
+
112
+ def read_item(line)
113
+ self._line = line
114
+ run_callbacks :fixup do
115
+ name, _other = parse(_line)
116
+ statement = if name == '*'
117
+ @db.prepare 'SELECT * FROM Inventory'
118
+ else
119
+ @db.prepare("SELECT * FROM Inventory WHERE Name='#{name}'")
120
+ end
121
+
122
+ results = statement.execute
123
+ a = []
124
+ results.each do |row|
125
+ a << { id: row['Id'], name: row['Name'], quantity: row['Quantity'], price: row['Price'] }
126
+ end
127
+ a
128
+ end
129
+ end
130
+
131
+ def update_item(line)
132
+ self._line = line
133
+ run_callbacks :fixup do
134
+ name, quantity, price = parse(_line)
135
+ statement = if price
136
+ @db.prepare "UPDATE Inventory SET Quantity=#{quantity}, Price=#{price} WHERE Name='#{name}'"
137
+ else
138
+ @db.prepare "UPDATE Inventory SET Quantity=#{quantity} WHERE Name='#{name}'"
139
+ end
140
+ statement.execute
141
+ read_item(_line)
142
+ end
143
+ end
144
+
145
+ def delete_item(line)
146
+ self._line = line
147
+ run_callbacks :fixup do
148
+ name, _other = parse(_line)
149
+ before_count = row_count
150
+ statement = @db.prepare "DELETE FROM Inventory WHERE Name='#{name}'"
151
+ statement.execute
152
+ before_count != row_count
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ def row_count
159
+ statement = @db.prepare 'SELECT COUNT(*) FROM Inventory'
160
+ result = statement.execute
161
+ result.next_hash['COUNT(*)']
162
+ end
163
+
164
+ def parse(line)
165
+ line.split(',')
166
+ end
167
+
168
+ def normalize_data
169
+ name, quantity, price = parse(_line)
170
+ self._line = [name.capitalize, quantity, price].join(',')
171
+ end
172
+ end
173
+
174
+ # All interactivity logic should live in this class.
175
+ #
176
+ # ActiveSupport::Callbacks support is included for before, after,
177
+ # or around hooks.
178
+ #
179
+ # The Model has access to the `client`, `data`, and `match` objects.
180
+ #
181
+ class InventoryView < SlackRubyBot::MVC::View::Base
182
+ def react_wait
183
+ client.web_client.reactions_add(
184
+ name: :hourglass_flowing_sand,
185
+ channel: data.channel,
186
+ timestamp: data.ts,
187
+ as_user: true
188
+ )
189
+ end
190
+
191
+ def unreact_wait
192
+ client.web_client.reactions_remove(
193
+ name: :hourglass_flowing_sand,
194
+ channel: data.channel,
195
+ timestamp: data.ts,
196
+ as_user: true
197
+ )
198
+ end
199
+
200
+ def email_admin(message)
201
+ # send email to administrator with +message+
202
+ # ...
203
+ puts "Sent email to administrator: #{message}"
204
+ end
205
+
206
+ def delete_succeeded
207
+ say(channel: data.channel, text: 'Item was successfully deleted.')
208
+ end
209
+
210
+ def delete_failed
211
+ say(channel: data.channel, text: 'Item failed to be deleted.')
212
+ end
213
+ end
214
+
215
+ class InventoryBot < SlackRubyBot::Bot
216
+ help do
217
+ title 'Inventory Bot'
218
+ desc 'This bot lets you create, read, update, and delete items from an inventory.'
219
+
220
+ command 'create' do
221
+ desc 'Add an item to the inventory.'
222
+ end
223
+
224
+ command 'read' do
225
+ desc 'Get inventory status for an item.'
226
+ end
227
+
228
+ command 'update' do
229
+ desc 'Update inventory levels for an item.'
230
+ end
231
+
232
+ command 'delete' do
233
+ desc 'Remove an item from inventory.'
234
+ end
235
+ end
236
+
237
+ # Instantiate the Model, View, and Controller objects within the +Bot+ subclass
238
+ # or within a SlackRubyBot::Commands::Base subclass.
239
+ #
240
+ model = InventoryModel.new
241
+ view = InventoryView.new
242
+ @controller = InventoryController.new(model, view)
243
+ @controller.class.command_class.routes.each do |route|
244
+ STDERR.puts route.inspect
245
+ end
246
+ end
247
+
248
+ InventoryBot.run
@@ -25,3 +25,4 @@ require 'slack-ruby-bot/client'
25
25
  require 'slack-ruby-bot/server'
26
26
  require 'slack-ruby-bot/app'
27
27
  require 'slack-ruby-bot/bot'
28
+ require 'slack-ruby-bot/mvc'
@@ -36,7 +36,7 @@ module SlackRubyBot
36
36
  end
37
37
 
38
38
  def default_command_name
39
- name && name.split(':').last.downcase
39
+ name ? name.split(':').last.downcase : object_id.to_s
40
40
  end
41
41
 
42
42
  def operator(*values, &block)
@@ -0,0 +1 @@
1
+ require 'slack-ruby-bot/mvc/mvc'
@@ -0,0 +1,111 @@
1
+ module SlackRubyBot
2
+ module MVC
3
+ module Controller
4
+ class Base
5
+ include ActiveSupport::Callbacks
6
+
7
+ class << self
8
+ attr_reader :abstract
9
+ alias abstract? abstract
10
+
11
+ def controllers
12
+ Base.instance_variable_get(:@controllers)
13
+ end
14
+
15
+ def command_class
16
+ Base.instance_variable_get(:@command_class)
17
+ end
18
+
19
+ def reset!
20
+ # Remove any earlier anonymous classes from prior calls so we don't leak them
21
+ Commands::Base.command_classes.delete(Controller::Base.command_class) if Base.command_class
22
+
23
+ # Create anonymous class to hold our #commands; required by SlackRubyBot::Commands::Base
24
+ Base.instance_variable_set(:@command_class, Class.new(SlackRubyBot::Commands::Base))
25
+ Base.instance_variable_set(:@controllers, [])
26
+ end
27
+
28
+ # Define a controller as abstract. See internal_methods for more
29
+ # details.
30
+ def abstract!
31
+ @abstract = true
32
+ end
33
+
34
+ def inherited(klass) # :nodoc:
35
+ # Define the abstract ivar on subclasses so that we don't get
36
+ # uninitialized ivar warnings
37
+ unless klass.instance_variable_defined?(:@abstract)
38
+ klass.instance_variable_set(:@abstract, false)
39
+ end
40
+ super
41
+ end
42
+
43
+ def register_controller(controller)
44
+ # Only used to keep a reference around so the instance object doesn't get garbage collected
45
+ Base.instance_variable_set(:@controllers, []) unless controllers
46
+ controller_ary = Base.instance_variable_get(:@controllers)
47
+ controller_ary << controller
48
+ klass = controller.class
49
+
50
+ methods = (klass.public_instance_methods(true) -
51
+ # Except for public instance methods of Base and its ancestors
52
+ internal_methods(klass) +
53
+ # Be sure to include shadowed public instance methods of this class
54
+ klass.public_instance_methods(false)).uniq.map(&:to_s)
55
+
56
+ methods.each do |meth_name|
57
+ next if meth_name[0] == '_'
58
+
59
+ # sprinkle a little syntactic sugar on top of existing `command` infrastructure
60
+ command_class.class_eval do
61
+ command meth_name.to_s do |client, data, match|
62
+ controller.use_args(client, data, match)
63
+ controller.call_command
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # A list of all internal methods for a controller. This finds the first
70
+ # abstract superclass of a controller, and gets a list of all public
71
+ # instance methods on that abstract class. Public instance methods of
72
+ # a controller would normally be considered action methods, so methods
73
+ # declared on abstract classes are being removed.
74
+ # (Controller::Base is defined as abstract)
75
+ def internal_methods(controller)
76
+ controller = controller.superclass until controller.abstract?
77
+ controller.public_instance_methods(true)
78
+ end
79
+ end
80
+
81
+ abstract!
82
+ reset!
83
+
84
+ attr_reader :model, :view, :client, :data, :match
85
+
86
+ def initialize(model, view)
87
+ @model = model
88
+ @view = view
89
+ self.class.register_controller(self)
90
+ end
91
+
92
+ # Hand off the latest updated objects to the +model+ and +view+ and
93
+ # update our +client+, +data+, and +match+ accessors.
94
+ def use_args(client, data, match)
95
+ @client = client
96
+ @data = data
97
+ @match = match
98
+ model.use_args(client, data, match)
99
+ view.use_args(client, data, match)
100
+ end
101
+
102
+ # Determine the command issued and call the corresponding instance method
103
+ def call_command
104
+ verb = match.captures[match.names.index('command')]
105
+ verb = verb.downcase if verb
106
+ public_send(verb)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,25 @@
1
+ module SlackRubyBot
2
+ module MVC
3
+ module Model
4
+ class Base
5
+ include ActiveSupport::Callbacks
6
+
7
+ class << self
8
+ def inherited(klass) # :nodoc:
9
+ super
10
+ end
11
+ end
12
+
13
+ attr_reader :client, :data, :match
14
+
15
+ # Hand off the latest updated objects to the +model+ and +view+ and
16
+ # update our +client+, +data+, and +match+ accessors.
17
+ def use_args(client, data, match)
18
+ @client = client
19
+ @data = data
20
+ @match = match
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ require_relative '../mvc/controller/base'
2
+
3
+ require_relative '../mvc/model/base'
4
+
5
+ require_relative '../mvc/view/base'
@@ -0,0 +1,28 @@
1
+ module SlackRubyBot
2
+ module MVC
3
+ module View
4
+ class Base
5
+ include ActiveSupport::Callbacks
6
+ extend Forwardable
7
+
8
+ class << self
9
+ def inherited(klass) # :nodoc:
10
+ super
11
+ end
12
+ end
13
+
14
+ attr_reader :client, :data, :match
15
+
16
+ def_delegators :@client, :say
17
+
18
+ # Hand off the latest updated objects to the +model+ and +view+ and
19
+ # update our +client+, +data+, and +match+ accessors.
20
+ def use_args(client, data, match)
21
+ @client = client
22
+ @data = data
23
+ @match = match
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module SlackRubyBot
2
- VERSION = '0.10.1'.freeze
2
+ VERSION = '0.10.2'.freeze
3
3
  end
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::App do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Client do
4
2
  describe '#send_gifs?' do
5
3
  context 'without giphy is false', unless: ENV.key?('WITH_GIPHY') do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Default do
4
2
  it 'lowercase' do
5
3
  expect(message: SlackRubyBot.config.user).to respond_with_slack_message(SlackRubyBot::ABOUT)
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot do
4
2
  def client
5
3
  SlackRubyBot::Client.new aliases: %w(:emoji: alias каспаров)
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Help do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Bot do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Bot) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Base do
4
2
  let! :command1 do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -35,5 +33,12 @@ describe SlackRubyBot::Commands::Base do
35
33
  command_classes = SlackRubyBot::Commands::Base.command_classes
36
34
  expect(command_classes.find_index(command1)).to be < command_classes.find_index(command2)
37
35
  end
36
+
37
+ it 'uses the object_id of an anonymous class as the default command name' do
38
+ command_classes = SlackRubyBot::Commands::Base.command_classes
39
+ anon_class = Class.new(SlackRubyBot::Commands::Base)
40
+ expect(command_classes).to include anon_class
41
+ expect(command_classes.find { |obj| obj == anon_class }.object_id.to_s).to eq anon_class.default_command_name
42
+ end
38
43
  end
39
44
  end
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Help::Attrs do
4
2
  let(:help_attrs) { described_class.new('WeatherBot') }
5
3
 
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Help do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Hi do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::App do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Base do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands, if: ENV.key?('WITH_GIPHY') do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands, if: ENV.key?('WITH_GIPHY') do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Unknown do
4
2
  it 'invalid command' do
5
3
  expect(message: "#{SlackRubyBot.config.user} foobar").to respond_with_slack_message("Sorry <@user>, I don't understand that command!")
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Config do
4
2
  describe '.send_gifs?' do
5
3
  after { ENV.delete 'SLACK_RUBY_BOT_SEND_GIFS' }
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Hooks::HookSupport do
4
2
  subject do
5
3
  Class.new do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Hooks::Message do
4
2
  let(:message_hook) { described_class.new }
5
3
 
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Hooks::Set do
4
2
  let(:client) { Slack::RealTime::Client.new }
5
3
 
@@ -0,0 +1,97 @@
1
+ describe SlackRubyBot::MVC::Controller::Base, 'initialization' do
2
+ let(:model) { double('model') }
3
+ let(:view) { double('view') }
4
+ let(:controller) do
5
+ Class.new(SlackRubyBot::MVC::Controller::Base) do
6
+ def quxo() end
7
+ end
8
+ end
9
+
10
+ after(:each) { controller.reset! }
11
+
12
+ it 'sets :model accessor to passed in model' do
13
+ expect(controller.new(model, view).model).to eq(model)
14
+ end
15
+
16
+ it 'sets :view accessor to passed in view' do
17
+ expect(controller.new(model, view).view).to eq(view)
18
+ end
19
+
20
+ it 'saves the controller instance to the class' do
21
+ instance = controller.new(model, view)
22
+ expect(SlackRubyBot::MVC::Controller::Base.controllers).to include(instance)
23
+ SlackRubyBot::MVC::Controller::Base.controllers.clear
24
+ end
25
+
26
+ it 'creates a command route on SlackRubyBot::Commands::Base' do
27
+ controller.reset!
28
+ route_count = 0
29
+ controller.new(model, view)
30
+ expect(controller.command_class.routes.size).to eq(route_count + 1)
31
+ end
32
+
33
+ it 'does NOT create a command route on SlackRubyBot::Commands::Base when method starts with a single "_"' do
34
+ controller.reset!
35
+ route_count = 0
36
+ controller.class_eval do
37
+ def _no_route_to_me() end
38
+ end
39
+ controller.new(model, view)
40
+ expect(controller.command_class.routes.size).to eq(route_count + 1) # instead of +2
41
+ end
42
+ end
43
+
44
+ describe SlackRubyBot::MVC::Controller::Base, 'setup' do
45
+ let(:controller) do
46
+ Class.new(SlackRubyBot::MVC::Controller::Base) do
47
+ def quxo
48
+ client.say(channel: data.channel, text: "#{match[:command]}: #{match[:expression]}")
49
+ end
50
+ end
51
+ end
52
+
53
+ after(:each) { controller.reset! }
54
+
55
+ it 'passes client, data, and match args to the model' do
56
+ model = SlackRubyBot::MVC::Model::Base.new
57
+ view = SlackRubyBot::MVC::View::Base.new
58
+ controller.new(model, view)
59
+ expect(message: " #{SlackRubyBot.config.user} quxo red").to respond_with_slack_message('quxo: red')
60
+ expect(model.client).to be_kind_of(SlackRubyBot::Client)
61
+ expect(model.data).to be_kind_of(Hash)
62
+ expect(model.match).to be_kind_of(MatchData)
63
+ end
64
+
65
+ it 'passes client, data, and match args to the view' do
66
+ model = SlackRubyBot::MVC::Model::Base.new
67
+ view = SlackRubyBot::MVC::View::Base.new
68
+ controller.new(model, view)
69
+ expect(message: " #{SlackRubyBot.config.user} quxo red").to respond_with_slack_message('quxo: red')
70
+ expect(view.client).to be_kind_of(SlackRubyBot::Client)
71
+ expect(view.data).to be_kind_of(Hash)
72
+ expect(view.match).to be_kind_of(MatchData)
73
+ end
74
+ end
75
+
76
+ describe SlackRubyBot::MVC::Controller::Base, 'execution' do
77
+ let(:controller) do
78
+ Class.new(SlackRubyBot::MVC::Controller::Base) do
79
+ attr_accessor :__flag
80
+ def quxo
81
+ @__flag = true
82
+ client.say(channel: data.channel, text: "#{match[:command]}: #{match[:expression]}")
83
+ end
84
+ end
85
+ end
86
+
87
+ after(:each) { controller.reset! }
88
+
89
+ it 'runs the controller method' do
90
+ model = SlackRubyBot::MVC::Model::Base.new
91
+ view = SlackRubyBot::MVC::View::Base.new
92
+ instance = controller.new(model, view)
93
+ instance.__flag = false
94
+ expect(message: " #{SlackRubyBot.config.user} quxo red").to respond_with_slack_message('quxo: red')
95
+ expect(instance.__flag).to be_truthy
96
+ end
97
+ end
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe RSpec do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Server do
4
2
  let(:logger) { subject.send :logger }
5
3
  let(:client) { Slack::RealTime::Client.new }
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::CommandsHelper do
4
2
  let(:bot_class) { Testing::WeatherBot }
5
3
  let(:command_class) { Testing::HelloCommand }
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Loggable do
4
2
  let! :class_with_logger do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot do
4
2
  it 'has a version' do
5
3
  expect(SlackRubyBot::VERSION).to_not be nil
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slack-ruby-bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Doubrovkine
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-12 00:00:00.000000000 Z
11
+ date: 2017-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashie
@@ -144,6 +144,9 @@ files:
144
144
  - Rakefile
145
145
  - TUTORIAL.md
146
146
  - UPGRADING.md
147
+ - examples/inventory/Gemfile
148
+ - examples/inventory/Procfile
149
+ - examples/inventory/inventorybot.rb
147
150
  - examples/market/Gemfile
148
151
  - examples/market/Procfile
149
152
  - examples/market/marketbot.rb
@@ -175,6 +178,11 @@ files:
175
178
  - lib/slack-ruby-bot/hooks/hook_support.rb
176
179
  - lib/slack-ruby-bot/hooks/message.rb
177
180
  - lib/slack-ruby-bot/hooks/set.rb
181
+ - lib/slack-ruby-bot/mvc.rb
182
+ - lib/slack-ruby-bot/mvc/controller/base.rb
183
+ - lib/slack-ruby-bot/mvc/model/base.rb
184
+ - lib/slack-ruby-bot/mvc/mvc.rb
185
+ - lib/slack-ruby-bot/mvc/view/base.rb
178
186
  - lib/slack-ruby-bot/rspec.rb
179
187
  - lib/slack-ruby-bot/rspec/support/bots_for_tests.rb
180
188
  - lib/slack-ruby-bot/rspec/support/fixtures/slack/migration_in_progress.yml
@@ -231,6 +239,7 @@ files:
231
239
  - spec/slack-ruby-bot/hooks/hook_support_spec.rb
232
240
  - spec/slack-ruby-bot/hooks/message_spec.rb
233
241
  - spec/slack-ruby-bot/hooks/set_spec.rb
242
+ - spec/slack-ruby-bot/mvc/controller/controller_to_command_spec.rb
234
243
  - spec/slack-ruby-bot/rspec/respond_with_error_spec.rb
235
244
  - spec/slack-ruby-bot/server_spec.rb
236
245
  - spec/slack-ruby-bot/support/commands_helper_spec.rb
@@ -257,7 +266,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
257
266
  version: 1.3.6
258
267
  requirements: []
259
268
  rubyforge_project:
260
- rubygems_version: 2.5.1
269
+ rubygems_version: 2.6.11
261
270
  signing_key:
262
271
  specification_version: 4
263
272
  summary: The easiest way to write a Slack bot in Ruby.
@@ -295,6 +304,7 @@ test_files:
295
304
  - spec/slack-ruby-bot/hooks/hook_support_spec.rb
296
305
  - spec/slack-ruby-bot/hooks/message_spec.rb
297
306
  - spec/slack-ruby-bot/hooks/set_spec.rb
307
+ - spec/slack-ruby-bot/mvc/controller/controller_to_command_spec.rb
298
308
  - spec/slack-ruby-bot/rspec/respond_with_error_spec.rb
299
309
  - spec/slack-ruby-bot/server_spec.rb
300
310
  - spec/slack-ruby-bot/support/commands_helper_spec.rb