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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/CHANGELOG.md +5 -0
- data/README.md +131 -2
- data/TUTORIAL.md +3 -1
- data/examples/inventory/Gemfile +5 -0
- data/examples/inventory/Procfile +1 -0
- data/examples/inventory/inventorybot.rb +248 -0
- data/lib/slack-ruby-bot.rb +1 -0
- data/lib/slack-ruby-bot/commands/base.rb +1 -1
- data/lib/slack-ruby-bot/mvc.rb +1 -0
- data/lib/slack-ruby-bot/mvc/controller/base.rb +111 -0
- data/lib/slack-ruby-bot/mvc/model/base.rb +25 -0
- data/lib/slack-ruby-bot/mvc/mvc.rb +5 -0
- data/lib/slack-ruby-bot/mvc/view/base.rb +28 -0
- data/lib/slack-ruby-bot/version.rb +1 -1
- data/spec/slack-ruby-bot/app_spec.rb +0 -2
- data/spec/slack-ruby-bot/client_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/about_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/aliases_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/bot_message_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/bot_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/commands_command_classes_spec.rb +7 -2
- data/spec/slack-ruby-bot/commands/commands_precedence_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/commands_regexp_escape_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/commands_spaces_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/commands_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/commands_with_block_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/commands_with_expression_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/direct_messages_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/empty_text_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/help/attrs_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/help_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/hi_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/match_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/message_loop_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/nil_message_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/not_implemented_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/operators_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/operators_with_block_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/scan_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/send_gif_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/send_message_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/send_message_with_gif_spec.rb +0 -2
- data/spec/slack-ruby-bot/commands/unknown_spec.rb +0 -2
- data/spec/slack-ruby-bot/config_spec.rb +0 -2
- data/spec/slack-ruby-bot/hooks/hook_support_spec.rb +0 -2
- data/spec/slack-ruby-bot/hooks/message_spec.rb +0 -2
- data/spec/slack-ruby-bot/hooks/set_spec.rb +0 -2
- data/spec/slack-ruby-bot/mvc/controller/controller_to_command_spec.rb +97 -0
- data/spec/slack-ruby-bot/rspec/respond_with_error_spec.rb +0 -2
- data/spec/slack-ruby-bot/server_spec.rb +0 -2
- data/spec/slack-ruby-bot/support/commands_helper_spec.rb +0 -2
- data/spec/slack-ruby-bot/support/loggable_spec.rb +0 -2
- data/spec/slack-ruby-bot/version_spec.rb +0 -2
- metadata +13 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15ac681a340b2b17bc5a4148c0b55dd7295fc1f2
|
4
|
+
data.tar.gz: 04d0018c8acf732715f5992af917a69fcaa6d337
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f04ac47c686f1acd671d691826e7192d24da2b73c30fcdfda5a553f0906fa37439b5d4b4c2690148fd8fee7dd58cb9c6b2074e141b4d9670dbe5ceaa05e2e75
|
7
|
+
data.tar.gz: 9e65685bc89a791ac36000bfd955daef01ef230f1010e31ff71b2133d82ee241c6f81dd6660ff81488d7d20a5e0b3f6eab5b79ba22323e5a3794e7e93b22349f
|
data/.rspec
CHANGED
data/CHANGELOG.md
CHANGED
@@ -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 **
|
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 `
|
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.
|
data/TUTORIAL.md
CHANGED
@@ -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 '
|
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 @@
|
|
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
|
data/lib/slack-ruby-bot.rb
CHANGED
@@ -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,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,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
|
@@ -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
|
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.
|
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-
|
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.
|
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
|