slack-ruby-bot 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|