slackify 0.1.4 โ†’ 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 481a11f7bd67bbd0dade00117995381beb8719b0aba7f4b886372be0fb169d57
4
- data.tar.gz: 6d2fad59ffe6938d3bf0f3ba9bf754218faf1cfdeb7f64ed3fd5c586535f465f
3
+ metadata.gz: e3b94c8d8ee9aadee9d6c12cfa3e84ebd938e3b34908315e6add11a60d4ddca8
4
+ data.tar.gz: 5d04503efbf517d256d55bdb49977ef0e13b79d01615b34c7dd6df44c3fd6806
5
5
  SHA512:
6
- metadata.gz: eec68d6c75d8537cc74036921025d68d14475d5f7f84b3d50d918d33a4f5dec96e2b2080956da0bf52717a24ee882282fd9eaced48f00abfefc30df843d7c0af
7
- data.tar.gz: 6e9888657a4a3ec71b86ed7714f21cfc275b9b1e90d0d1136ecba01dfc41853e9cc8b2a0dc58c15f8d8cbc35c3df77d6ea183190add7a2149b2bd759852ba3c2
6
+ metadata.gz: a5cc3b8a43401712f68b2b818f7d5f8c3339a8c0cee57aed7bb2907d2553d8066ca7f8c27cd63744468b2b4c659d71719b2bf9ecba816f0947fcf00c8c7d95e0
7
+ data.tar.gz: d5c1bc32e1ec8c054d059418e7137b83420b0117835ba629b21f325943c8dafed174215922ea11c4d430ebdaa078340b3e327f62a7c370c98689e7bbe700205a
@@ -1,15 +1,45 @@
1
+ ## V0.4.0
2
+
3
+ - **BREAKING CHANGE:** Use `approved_bot_ids` instead of `whitelisted_bot_ids` ([6243c11](https://github.com/jusleg/slackify/commit'6243c11c3b5e49fa31feb2ebfd9394c362509524)) by [@DougEdey](https://github.com/DougEdey)
4
+ - Add support for named parameters in commands ([PR #12](https://github.com/jusleg/slackify/pull/12)) by [@DougEdey](https://github.com/DougEdey)
5
+ - Add support for custom parameters in commands ([PR #17](https://github.com/jusleg/slackify/pull/17)) by [@DougEdey](https://github.com/DougEdey)
6
+ - Tidy up some tests
7
+ - Force slack-ruby-client to be version 0.15.1 or above to allow for pagination of Conversations
8
+
9
+ ## V0.3.2
10
+
11
+ - Add support for interactive block payloads ([b6cf1db](https://github.com/jusleg/slackify/commit/b6cf1dbb47b832037ebff56054efa27c9e3251dc)) by [@drose-shopify](https://github.com/drose-shopify)
12
+
13
+ ## V0.3.0
14
+
15
+ - Add code documentation and improve exception message
16
+ - Add approval of bot ids in the configuration through `allowed_bot_ids=`
17
+ - Refactored Handler configuration into `Slackify::Router` and `Slackify::Handlers::Factory`
18
+ - Improved testing
19
+ - Remove the need to perform `Slackify.load_handler`
20
+ - **Breaking change:** Given that we now load the handlers on `Slack.configure`, the configuration step done in `config/application.rb` will have to be done in an initializer to have all the handler class loaded.
21
+
22
+ ## V0.2.0
23
+
24
+ Update `custom_event_subtype_handlers` to `custom_message_subtype_handlers` and add support for `custom_event_type_handlers`. This is a breaking change since we rename the field that was previously used. To fix, update any calls from `custom_event_subtype_handlers` to `custom_message_subtype_handlers` and you should be good to go.
25
+
26
+ ## V0.1.5
27
+
28
+ Update how the `bot_id` is set in the handler configuration. You can disable the slack auth test (which is used to obtain the bot_id) by setting `SLACK_AUTH_SKIP=1` in your environment variables. If you are running in a Rails environment other than production, development or staging and would like to use the bot for real requests, you can trigger a manual auth test by calling `Slackify.configuration.handlers.bot_auth_test`. Gemfile.lock was removed.
29
+
1
30
  ## V0.1.4
2
31
 
3
32
  Custom unhandled_handler configuration fix. It wouldn't let you set a custom one as the validation was checking for `is_a?` instead of `<`
4
33
 
5
34
  ## V0.1.3
35
+
6
36
  Added `remove_unhandled_handler` as a configuration option to disable the unhandled handler.
7
37
 
8
38
  ## V0.1.2
9
39
 
10
- * Renaming the gem from toddlerbot to slackify.
11
- * Cleanup of `lib/slackify` folder.
12
- * Added a new configuration: unhandled_handler. You can specify a subclass of `Slackify::Handlers::Base` in the config. This class will be called with `#unhandled` when a message has no regex match.
40
+ - Renaming the gem from toddlerbot to slackify.
41
+ - Cleanup of `lib/slackify` folder.
42
+ - Added a new configuration: unhandled_handler. You can specify a subclass of `Slackify::Handlers::Base` in the config. This class will be called with `#unhandled` when a message has no regex match.
13
43
 
14
44
  ## V0.1.1
15
45
 
data/README.md CHANGED
@@ -1,24 +1,27 @@
1
- # Slackify [![Build Status](https://travis-ci.org/jusleg/slackify.svg?branch=master)](https://travis-ci.org/jusleg/slackify) [![Gem Version](https://badge.fury.io/rb/slackify.svg)](https://badge.fury.io/rb/slackify)
1
+ # Slackify [![Tests ๐Ÿงช](https://github.com/jusleg/slackify/workflows/Tests%20%F0%9F%A7%AA/badge.svg)](https://github.com/jusleg/slackify/actions?query=workflow%3A%22Tests+%F0%9F%A7%AA%22) [![Lint ๐Ÿ’Ž](https://github.com/jusleg/slackify/workflows/Lint%20%F0%9F%92%8E/badge.svg)](https://github.com/jusleg/slackify/actions?query=workflow%3A%22Lint+%F0%9F%92%8E%22) [![Gem Version](https://badge.fury.io/rb/slackify.svg)](https://badge.fury.io/rb/slackify) ![Gem downloads](https://img.shields.io/gem/dt/slackify)
2
2
 
3
3
  Slackify is a gem that allows to build slackbots on Rails using the [Event API](https://api.slack.com/events-api) from Slack.
4
4
 
5
5
  ## Table of Contents
6
- * [How does it work](#how-does-it-work)
7
- * [Handlers](#handlers)
8
- * [Plain messages](#handling-plain-messages)
9
- * [Interactive messages](#handling-interactive-messages)
10
- * [Slash Command](#handling-slash-commands)
11
- * [Custom handler for event subtypes](#custom-handler-for-event-subtypes)
12
- * [Custom unhandled handler](#custom-unhandled-handler)
13
- * [Slack client](#slack-client)
14
- * [Sending a simple message](#sending-a-simple-message)
15
- * [Sending an interactive message](#sending-an-interactive-message)
16
- * [Slack 3 second reply window](#slack-3-seconds-reply-window)
17
- * [How to run your own slackify](#how-to-run-your-own-slackify)
18
- * [Initial Setup](#initial-setup)
19
- * [Slack Setup](#slack-setup)
6
+
7
+ - [How does it work](#how-does-it-work)
8
+ - [Handlers](#handlers)
9
+ - [Plain messages](#handling-plain-messages)
10
+ - [Interactive messages](#handling-interactive-messages)
11
+ - [Slash Command](#handling-slash-commands)
12
+ - [Custom handler for message subtypes](#custom-handler-for-message-subtypes)
13
+ - [Custom handler for event types](#custom-handler-for-event-types)
14
+ - [Custom unhandled handler](#custom-unhandled-handler)
15
+ - [Slack client](#slack-client)
16
+ - [Sending a simple message](#sending-a-simple-message)
17
+ - [Sending an interactive message](#sending-an-interactive-message)
18
+ - [Slack 3 second reply window](#slack-3-seconds-reply-window)
19
+ - [How to run your own slackify](#how-to-run-your-own-slackify)
20
+ - [Initial Setup](#initial-setup)
21
+ - [Slack Setup](#slack-setup)
20
22
 
21
23
  # How does it work
24
+
22
25
  The core logic of the bot resides in its handlers. When the app starts, a list of handler gets initialized from a config file (`config/handlers.yml`). This initializes all the plain message handlers. Out of the box, the application supports three types of events
23
26
 
24
27
  1. [Plain messages](#handling-plain-messages)
@@ -26,19 +29,19 @@ The core logic of the bot resides in its handlers. When the app starts, a list o
26
29
  3. [Slash Command](#handling-slash-commands)
27
30
 
28
31
  ## Handlers
32
+
29
33
  ### Handling plain messages
34
+
30
35
  These are the basic handlers. They use a regex to identify if they should be called. When a message event gets sent to the bots, the slack controller sends the message to the list of handlers. The message will be checked against the regex of every handler until there is a match. When there is a match, the handler will get called with all the parameters provided by slack. If no handler matches the command, the unhandled handler will be called instead.
31
36
 
32
37
  Those handlers are configured via the `config/handlers.yml` configuration file. Let's dissect the configuration of a handler.
33
38
 
34
39
  ```yaml
35
- -
36
- repeat_handler:
40
+ - repeat_handler:
37
41
  commands:
38
- -
39
- name: Repeat
42
+ - name: Repeat
40
43
  description: "`repeat [sentence]`: Repeats the sentence you wrote"
41
- regex: !ruby/regexp '/^repeat (?<sentence>.+)/i'
44
+ regex: !ruby/regexp "/^repeat (?<sentence>.+)/i"
42
45
  action: repeat
43
46
  ```
44
47
 
@@ -64,20 +67,48 @@ To add a new handler, you can add a new file under `app/handlers/` and start add
64
67
 
65
68
  **Note:** The regex supports [named capture](https://www.regular-expressions.info/named.html). In this example, we have a name example of `sentence`. When the handler command will be called, a key in the parameter hash will be added: `command_arguments`. This key will point to a hash of the capture name and value. In this case, `command_arguments => {sentence: "the sentence you wrote"}`
66
69
 
70
+ ### Handling messages with defined parameters
71
+
72
+ The regular expression matching in the previous example provides a mechanism to supply named parameters to a method. However, many handlers may want to use named & typed parameters. This can be done using the `base_command` and `parameters` options.
73
+
74
+ We can rewrite the command above to
75
+
76
+ ```yaml
77
+ - repeat_handler:
78
+ commands:
79
+ - name: Repeat
80
+ description: "`repeat [sentence]`: Repeats the sentence you wrote"
81
+ base_command: "repeat"
82
+ parameters:
83
+ - sentence: string
84
+ - times: 10
85
+ action: repeat
86
+ ```
87
+
88
+ And call the command with
89
+
90
+ `slackify repeat sentence="Why hullo there" times=10`
91
+
92
+ This will supply a command arguments that are typed and coerced to the defined types:
93
+
94
+ `command_arguments => {sentence: "Why hullo there", time: 10}`
95
+
67
96
  ### Handling interactive messages
97
+
68
98
  When sending an interactive message to a user, slack let's you define the `callback_id`. The app uses the callback id to select the proper handler for the message that was sent. The callback id must follow the given format: `class_name#method_name`. For instance if you set the callback id to `repeat_handler#repeat`, then `RepeatHandler#repeat` will be called. Adding new handlers does not require to update the `config/handlers.yml` configuration. You only need to update the callback id to define the proper handler to be used when you send an interactive message.
69
99
 
70
100
  ### Handling slash commands
101
+
71
102
  The code also has an example of a slash command and its handler (`slash_handler.rb`). To add a command on the bot, head to you app configuration on https://api.slack.com/apps and navigate to Slack Commands using the sidebar. Create a new one. The important part is to set the path properly. To bind with the demo handler, you would need to setup the path like this: `/slackify/slash/slash_handler/example_slash`. The format is `/slackify/slash/[handler_name]/[action_name]`. An app shouldn't have many slash commands. Keep in mind that adding a slash command means that the whole organization will see it.
72
103
 
73
- You will need to whitelist the method in the handler to indicate it can be used as a slash command using `allow_slash_method`
104
+ You will need to allow the method in the handler to indicate it can be used as a slash command using `allow_slash_method`
74
105
 
75
106
  ```ruby
76
107
  class DummyHandler < Slackify::Handlers::Base
77
108
  allow_slash_method :slash_command
78
109
 
79
110
  class << self
80
-
111
+
81
112
  def slash_command(_params)
82
113
  "dummy_handler slash_command() was called"
83
114
  end
@@ -85,17 +116,29 @@ class DummyHandler < Slackify::Handlers::Base
85
116
  end
86
117
  ```
87
118
 
88
- ### Custom handler for event subtypes
119
+ ### Custom handler for message subtypes
89
120
 
90
- If you wish to add more functionalities to your bot, you can specify define new behaviours for different event subtypes. You can specify a hash with the event subtype as a key and the handler class as the value. Slackify will call `.handle_event` on your class and pass the controller params as parameters.
121
+ If you wish to add more functionalities to your bot, you can specify define new behaviours for different message subtypes. You can specify a hash with the event subtype as a key and the handler class as the value. Slackify will call `.handle_event` on your class and pass the controller params as parameters.
91
122
 
92
123
  ```ruby
93
- Slackify.configuration.custom_event_subtype_handlers = {
124
+ Slackify.configuration.custom_message_subtype_handlers = {
125
+ file_share: MessageImageHandler
126
+ }
127
+ ```
128
+
129
+ In this example, all message of subtype `file_share` will be sent to the `MessageImageHandler` class.
130
+
131
+ ### Custom handler for event types
132
+
133
+ If you wish to add more functionalities to your bot, you can specify define new behaviours for different event types. You can specify a hash with the event type as a key and the handler class as the value. Slackify will call `.handle_event` on your class and pass the controller params as parameters.
134
+
135
+ ```ruby
136
+ Slackify.configuration.custom_event_type_handlers = {
94
137
  file_share: ImageHandler
95
138
  }
96
139
  ```
97
140
 
98
- In this example, all events of subtype `file_share` will be sent to the `ImageHandler` class.
141
+ In this example, all events of type `file_share` will be sent to the `ImageHandler` class.
99
142
 
100
143
  ### Custom unhandled handler
101
144
 
@@ -121,18 +164,21 @@ end
121
164
  ```
122
165
 
123
166
  ## Slack client
167
+
124
168
  In order to send messages, the [slack ruby client gem](https://github.com/slack-ruby/slack-ruby-client) was used. You can send plain text messages, images and interactive messages. Since the bot was envisioned being more repsonsive than proactive, the client was made available for handlers to call using the `slack_client` method. If you wish to send messages outside of handlers, you can get the slack client by calling `Slackify.configuration.slack_client`
125
169
 
126
170
  ### Sending a simple message
171
+
127
172
  ```ruby
128
173
  slack_client.chat_postMessage(channel: 'MEMBER ID OR CHANNEL ID', text: 'Hello World', as_user: true)
129
174
  ```
130
175
 
131
176
  ### Sending an interactive message
177
+
132
178
  ```ruby
133
179
  slack_client.chat_postMessage(
134
- channel: 'MEMBER ID OR CHANNEL ID',
135
- as_user: true,
180
+ channel: 'MEMBER ID OR CHANNEL ID',
181
+ as_user: true,
136
182
  attachments: [{
137
183
  "fallback": "Would you recommend it to customers?",
138
184
  "title": "Would you recommend it to customers?",
@@ -158,10 +204,13 @@ slack_client.chat_postMessage(
158
204
  ```
159
205
 
160
206
  ## Slack 3 seconds reply window
207
+
161
208
  Slack introduced a [3 seconds reply window](https://api.slack.com/messaging/interactivity#response) for interactive messages. That means that if you reply to an interactive message or slash command event with a json, slack will show either update the attachment or send a new one without having to use `chat_postMessage`. If you wish to use this feature with Slackify, you only need to return either a json of an attachment or a plain text string when you handler method is called. **Your method should always return `nil` otherwise**.
162
209
 
163
210
  # How to run your own slackify
211
+
164
212
  ## Initial Setup
213
+
165
214
  1. Install slackify in your app by adding the following line in your `Gemfile`:
166
215
 
167
216
  ```ruby
@@ -180,58 +229,74 @@ bundle install
180
229
 
181
230
  5. [Proceed to connect your bot to slack](#slack-setup)
182
231
 
183
-
184
232
  ## Slack Setup
233
+
185
234
  First, you'll need to create a new app on slack. Head over to [slack api](https://api.slack.com/apps) and create a new app.
186
235
 
187
236
  1. **Set Slack Secret Token**
188
237
 
189
- In order to verify that the requets are coming from slack, we'll need to set the slack secret token in slackify. This value can be found as the signing secret in the app credentials section of the basic information page.
238
+ In order to verify that the requets are coming from slack, we'll need to set the slack secret token in slackify. This value can be found as the signing secret in the app credentials section of the basic information page.
190
239
 
191
240
  2. **Add a bot user**
192
-
193
- Under the feature section, click on "bot users". Pick a name for you slack bot and toggle on "Always Show My Bot as Online". Save the setting.
241
+
242
+ Under the feature section, click on "bot users". Pick a name for you slack bot and toggle on "Always Show My Bot as Online". Save the setting.
194
243
 
195
244
  3. **Enable events subscription**
196
245
 
197
- Under the feature section, click "Events subscription". Turn the feature on and use your app url followed by `/slackify/event`. [Ngrok](https://ngrok.com/) can easily get you a public url if you are developing locally. The app needs to be running when you configure this url. After the url is configured, under the section "Subscribe to Bot Events", add the bot user event `message.im`.
246
+ Under the feature section, click "Events subscription". Turn the feature on and use your app url followed by `/slackify/event`. [Ngrok](https://ngrok.com/) can easily get you a public url if you are developing locally. The app needs to be running when you configure this url. After the url is configured, under the section "Subscribe to Bot Events", add the bot user event `message.im`.
198
247
 
199
248
  4. **Activate the interactive components**
200
-
201
- Under the feature section, click "interactive components". Turn the feature on and use your ngrok url followed by `/slackify/interactive`. Save the setting.
249
+
250
+ Under the feature section, click "interactive components". Turn the feature on and use your ngrok url followed by `/slackify/interactive`. Save the setting.
202
251
 
203
252
  5. **Install the App**
204
253
 
205
- Under the setting section, click "install app" and proceed to install the app to the workspace. Once the app is installed, go back to the "install app" page and copy the Bot User OAuth Access Token.
254
+ Under the setting section, click "install app" and proceed to install the app to the workspace. Once the app is installed, go back to the "install app" page and copy the Bot User OAuth Access Token.
206
255
 
207
256
  6. **Configure Slackify**
257
+ Add a new initializer with the following code
258
+
208
259
  ```ruby
260
+ # config/initializers/slackify.rb
209
261
  Slackify.configure do |config|
210
262
  config.slack_bot_token = "xoxb-sdkjlkjsdflsd..."
211
263
  config.slack_secret_token = "1234dummysecret"
212
264
  end
213
265
  ```
214
266
 
215
- 7. **Add an initializer**
267
+ 7. **Define handlers specific subtypes** (Optional)
268
+ You can set custom [message subtype](https://api.slack.com/events/message) handlers or custom [event type](https://api.slack.com/events) handlers inside your configuration
269
+
216
270
  ```ruby
217
271
  # config/initializers/slackify.rb
218
- Slackify.load_handlers
272
+ Slackify.configure do |config|
273
+ config.slack_bot_token = "xoxb-sdkjlkjsdflsd..."
274
+ config.slack_secret_token = "1234dummysecret"
275
+ config.custom_message_subtype_handlers = {
276
+ file_share: ImageHandler,
277
+ channel_join: JoinHandler,
278
+ ...
279
+ }
280
+ config.custom_event_type_handlers = {
281
+ app_mention: ...
282
+ }
283
+ end
219
284
  ```
220
285
 
221
- 8. **Define handlers specific subtypes** (Optional)
286
+ 8. **Handle bot messages** (Highly optional)
287
+ If you want your bot to accept other bot messages (which you probably should not do), you can. In the configuration step, you can set an array of approved bot ids.
222
288
 
223
289
  ```ruby
224
290
  # config/initializers/slackify.rb
225
- Slackify.load_handlers
226
- Slackify.configuration.custom_event_subtype_handlers = {
227
- file_share: ImageHandler,
228
- channel_join: JoinHandler,
229
- ...
230
- }
291
+ Slackify.configure do |config|
292
+ ...
293
+ config.approved_bot_ids = ['abc123', 'def456']
294
+ ...
295
+ end
231
296
  ```
232
297
 
233
298
  **At this point, you are ready to go ๐Ÿ˜„**
234
299
 
235
-
236
300
  # LICENSE
301
+
237
302
  Copyright (c) 2019 Justin Lรฉger, Michel Chatmajian. See [LICENSE](https://github.com/jusleg/slackify/blob/master/LICENSE) for further details.
@@ -9,6 +9,8 @@ module Slackify
9
9
  def event_callback
10
10
  if params[:type] == "url_verification"
11
11
  render plain: params["challenge"]
12
+ elsif Slackify.configuration.custom_event_type_handlers[params[:event][:type]]
13
+ handle_custom_event_type
12
14
  elsif params[:event][:type] == "message"
13
15
  handle_direct_message_event
14
16
  head :ok
@@ -19,7 +21,14 @@ module Slackify
19
21
 
20
22
  def interactive_callback
21
23
  parsed_payload = JSON.parse(params[:payload])
22
- response = handler_from_callback_id(parsed_payload["callback_id"]).call(parsed_payload)
24
+
25
+ callback_id = if parsed_payload.key?('view')
26
+ parsed_payload.dig('view', 'callback_id')
27
+ else
28
+ parsed_payload['callback_id']
29
+ end
30
+
31
+ response = handler_from_callback_id(callback_id).call(parsed_payload)
23
32
  if !response.nil?
24
33
  Timeout.timeout(SLACK_TIMEOUT_SECONDS) do
25
34
  render json: response
@@ -28,7 +37,7 @@ module Slackify
28
37
  head :ok
29
38
  end
30
39
  rescue Timeout::Error
31
- raise Timeout::Error, "Slack interactive callback timed out for #{parsed_payload['callback_id']}"
40
+ raise Timeout::Error, "Slack interactive callback timed out for #{callback_id}"
32
41
  end
33
42
 
34
43
  def slash_command_callback
@@ -51,25 +60,33 @@ module Slackify
51
60
  private
52
61
 
53
62
  def handle_direct_message_event
54
- if handler = Slackify.configuration.custom_event_subtype_handlers[params[:event][:subtype]]
63
+ if handler = Slackify.configuration.custom_message_subtype_handlers[params[:event][:subtype]]
55
64
  handler.handle_event(params[:slack])
56
65
  head :ok
57
66
  return
58
67
  end
59
68
 
60
- return if params[:event][:subtype] == "bot_message" || params[:event].key?(:bot_id) || params[:event][:hidden]
69
+ return if (params[:event][:subtype] == "bot_message" ||
70
+ params[:event].key?(:bot_id) ||
71
+ params[:event][:hidden]) &&
72
+ Slackify.configuration.approved_bot_ids.exclude?(params.dig(:event, :bot_id))
61
73
 
62
74
  command = params[:event][:text]
63
- Slackify.configuration.handlers.call_command(command, params[:slack])
75
+ Slackify::Router.call_command(command, params[:slack])
64
76
  rescue RuntimeError => e
65
77
  raise e unless e.message == "Component not found for a command message"
66
78
  end
67
79
 
80
+ def handle_custom_event_type
81
+ Slackify.configuration.custom_event_type_handlers[params[:event][:type]].handle_event(params[:slack])
82
+ head :ok
83
+ end
84
+
68
85
  def handler_from_callback_id(callback_id)
69
86
  class_name, method_name = callback_id.split('#')
70
87
  class_name = class_name.camelize
71
88
 
72
- raise Exceptions::HandlerNotSupported, class_name unless
89
+ raise Exceptions::HandlerNotSupported, "#{class_name} is not a subclass of Slackify::Handlers::Base" unless
73
90
  Handlers::Base.supported_handlers.include?(class_name)
74
91
 
75
92
  class_name.constantize.method(method_name)
@@ -78,7 +95,7 @@ module Slackify
78
95
  def verify_handler_slash_permission(handler_class, handler_method)
79
96
  handler_class = handler_class.camelize
80
97
 
81
- raise Exceptions::HandlerNotSupported, handler_class unless
98
+ raise Exceptions::HandlerNotSupported, "#{handler_class} is not a subclass of Slackify::Handlers::Base" unless
82
99
  Handlers::Base.supported_handlers.include?(handler_class)
83
100
 
84
101
  handler = handler_class.constantize
@@ -4,6 +4,8 @@ require 'slackify/configuration'
4
4
  require 'slackify/engine'
5
5
  require 'slackify/exceptions'
6
6
  require 'slackify/handlers'
7
+ require 'slackify/parameter'
8
+ require 'slackify/router'
7
9
 
8
10
  module Slackify
9
11
  class << self
@@ -19,9 +21,5 @@ module Slackify
19
21
  def configure
20
22
  yield(configuration)
21
23
  end
22
-
23
- def load_handlers
24
- @configuration.handlers = Handlers::Configuration.new
25
- end
26
24
  end
27
25
  end
@@ -3,37 +3,75 @@
3
3
  require 'slack'
4
4
 
5
5
  module Slackify
6
+ # Where the configuration for Slackify lives
6
7
  class Configuration
7
- attr_reader :custom_event_subtype_handlers, :slack_bot_token, :unhandled_handler
8
- attr_accessor :handlers, :slack_secret_token, :slack_client
8
+ attr_reader :custom_message_subtype_handlers, :slack_bot_token, :unhandled_handler, :custom_event_type_handlers
9
+ attr_accessor :handlers, :slack_secret_token, :slack_client, :approved_bot_ids
9
10
 
10
11
  def initialize
11
12
  @slack_bot_token = nil
12
13
  @slack_secret_token = nil
13
- @handlers = nil
14
+ @handlers = generate_handlers
14
15
  @slack_client = nil
15
- @custom_event_subtype_handlers = {}
16
+ @custom_message_subtype_handlers = {}
17
+ @custom_event_type_handlers = {}
16
18
  @unhandled_handler = Handlers::UnhandledHandler
19
+ @approved_bot_ids = []
17
20
  end
18
21
 
22
+ # Set your own unhandled handler
19
23
  def unhandled_handler=(handler)
20
- raise Exceptions::InvalidHandler, "#{handler.class} is not a subclass of Slackify::Handlers::Base" unless
24
+ raise HandlerNotSupported, "#{handler.class} is not a subclass of Slackify::Handlers::Base" unless
21
25
  handler < Handlers::Base
22
26
 
23
27
  @unhandled_handler = handler
24
28
  end
25
29
 
30
+ # Remove unhandled handler. The bot will not reply if the message doesn't
31
+ # match any regex
26
32
  def remove_unhandled_handler
27
33
  @unhandled_handler = nil
28
34
  end
29
35
 
36
+ # Set the token that we will use to connect to slack
30
37
  def slack_bot_token=(token)
31
38
  @slack_bot_token = token
32
39
  @slack_client = Slack::Web::Client.new(token: token).freeze
33
40
  end
34
41
 
35
- def custom_event_subtype_handlers=(event_subtype_hash)
36
- @custom_event_subtype_handlers = event_subtype_hash.with_indifferent_access
42
+ # Set a handler for a specific message subtype
43
+ # That handler will have to implement `self.handle_event(params)`
44
+ # see https://api.slack.com/events/message
45
+ def custom_message_subtype_handlers=(event_subtype_hash)
46
+ @custom_message_subtype_handlers = event_subtype_hash.with_indifferent_access
47
+ end
48
+
49
+ # Set a handler for a event type
50
+ # That handler will have to implement `self.handle_event(params)`
51
+ # see https://api.slack.com/events
52
+ def custom_event_type_handlers=(event_type_hash)
53
+ @custom_event_type_handlers = event_type_hash.with_indifferent_access
54
+ end
55
+
56
+ private
57
+
58
+ # Convert a hash to a list of lambda functions that will be called to handle
59
+ # the user messages
60
+ def generate_handlers
61
+ generated_handlers = []
62
+ read_handlers_yaml.each do |handler_hash|
63
+ handler = Handlers::Factory.for(handler_hash)
64
+ generated_handlers << handler
65
+ end
66
+
67
+ generated_handlers
68
+ end
69
+
70
+ # Reads the config/handlers.yml configuration
71
+ def read_handlers_yaml
72
+ raise 'config/handlers.yml does not exist' unless File.exist?("#{Rails.root}/config/handlers.yml")
73
+
74
+ YAML.load_file("#{Rails.root}/config/handlers.yml") || []
37
75
  end
38
76
  end
39
77
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Slackify
4
+ # Makes the whole thing work. Adds the routes for slackify.
4
5
  class Engine < Rails::Engine
5
6
  isolate_namespace Slackify
6
7
  end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Slackify
4
+ # When things go boom, we need these.
4
5
  module Exceptions
6
+ # You tried to call a class that was not extending the base handler
5
7
  class HandlerNotSupported < StandardError; end
8
+ # You handler is failing validations
6
9
  class InvalidHandler < StandardError; end
10
+ # The handler method was not approved to be a slash command
7
11
  class MissingSlashPermission < StandardError; end
8
12
  end
9
13
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'handlers/base'
4
- require_relative 'handlers/configuration'
4
+ require_relative 'handlers/factory'
5
5
  require_relative 'handlers/unhandled_handler'
6
6
  require_relative 'handlers/validator'
7
7
 
@@ -2,28 +2,41 @@
2
2
 
3
3
  module Slackify
4
4
  module Handlers
5
+ # Base handler class that any user defined handlers must inherit from
5
6
  class Base
6
7
  @@supported_handlers = []
7
8
 
8
9
  class << self
9
10
  attr_reader :allowed_slash_methods
10
11
 
12
+ # Get the slack client that you can use to perform slack api calls
13
+ # @see https://github.com/slack-ruby/slack-ruby-client
11
14
  def slack_client
12
15
  Slackify.configuration.slack_client
13
16
  end
14
17
 
18
+ # Enables a method to be called for a slash command.
19
+ #
20
+ # More context:
21
+ # Slash commands have extra validations. To call a slash command, it
22
+ # needs to call a method on a supported handler and that handler needs
23
+ # to explicitly specify which methods are for slash command.
15
24
  def allow_slash_method(element)
16
- if @allowed_slash_methods
25
+ if defined?(@allowed_slash_methods) && @allowed_slash_methods
17
26
  @allowed_slash_methods.push(*element)
18
27
  else
19
28
  @allowed_slash_methods = Array(element)
20
29
  end
21
30
  end
22
31
 
32
+ # Any class inheriting from Slackify::Handler::Base will be added to
33
+ # the list of supported handlers
23
34
  def inherited(subclass)
24
35
  @@supported_handlers.push(subclass.to_s)
25
36
  end
26
37
 
38
+ # Show a list of the handlers supported by the app. Since we do
39
+ # metaprogramming, we want to ensure we can only call defined handlers
27
40
  def supported_handlers
28
41
  @@supported_handlers
29
42
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slackify
4
+ module Handlers
5
+ # Creates the handler structs
6
+ class Factory
7
+ def self.for(configuration)
8
+ Validator.verify_handler_integrity(configuration)
9
+
10
+ handler = OpenStruct.new
11
+ handler.name = configuration.keys.first
12
+ handler.commands = []
13
+
14
+ configuration[handler.name]['commands']&.each do |command|
15
+ built_command = OpenStruct.new
16
+ built_command.regex = command['regex']
17
+ built_command.handler = handler.name.camelize.constantize.method(command['action'])
18
+ built_command.description = command['description']
19
+ built_command.parameters = command['parameters']
20
+ built_command.base_command = command['base_command']
21
+ handler.commands << built_command.freeze
22
+ end
23
+
24
+ handler.commands.freeze
25
+ handler.freeze
26
+ end
27
+ end
28
+ end
29
+ end
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Slackify
4
4
  module Handlers
5
+ # Default handler for any text message that is not handler by other handlers
6
+ # This can easily be replaced by setting you own unhandled handler when
7
+ # configuring slackify. Use `#unhandled_handler=` to set it. Your handler
8
+ # must implement a `self.unhandled` method.
5
9
  class UnhandledHandler < Base
6
10
  def self.unhandled(params)
7
11
  slack_client.chat_postMessage(
@@ -2,36 +2,86 @@
2
2
 
3
3
  module Slackify
4
4
  module Handlers
5
+ # Simple validator for handlers. It will blow your app up on the
6
+ # configuration step instead of crashing when handling production requests.
5
7
  class Validator
6
- def self.verify_handler_integrity(handler)
7
- handler_name = handler.keys.first
8
- handler_class = handler_name.camelize.constantize
8
+ VALID_PARAMETER_TYPES = [:string, :int, :boolean, :float].freeze
9
9
 
10
- unless handler[handler_name].key?('commands') && handler.dig(handler_name, 'commands')&.any?
11
- raise Exceptions::InvalidHandler, "#{handler_name} doesn't have any command specified"
12
- end
10
+ class << self
11
+ # Checks if your handler hash is valid. It's pass or raise ๐Ÿงจ๐Ÿ’ฅ
12
+ def verify_handler_integrity(handler)
13
+ handler_name = handler.keys.first
14
+ handler_class = handler_name.camelize.constantize
15
+
16
+ unless handler[handler_name].key?('commands') && handler.dig(handler_name, 'commands')&.any?
17
+ raise Exceptions::InvalidHandler, "#{handler_name} doesn't have any command specified"
18
+ end
13
19
 
14
- handler_errors = []
20
+ handler_errors = []
15
21
 
16
- handler.dig(handler_name, 'commands').each do |command|
17
- command_errors = []
22
+ handler.dig(handler_name, 'commands').each do |command|
23
+ command_errors = []
18
24
 
19
- unless command['regex'].is_a?(Regexp)
20
- command_errors.append('No regex was provided.')
21
- end
25
+ unless command['regex'] || command['base_command']
26
+ command_errors.append('No regex or base command was provided.')
27
+ end
28
+
29
+ if command['regex'].present?
30
+ if command['base_command'].present?
31
+ command_errors.append('Regex and base_command cannot be used in the same handler.')
32
+ end
33
+
34
+ if command['parameters'].present?
35
+ command_errors.append('Regex and parameters cannot be used in the same handler.')
36
+ end
37
+
38
+ unless command['regex'].is_a?(Regexp)
39
+ command_errors.append('No regex was provided.')
40
+ end
41
+ end
22
42
 
23
- unless !command['action'].to_s.strip.empty? && handler_class.respond_to?(command['action'])
24
- command_errors.append('No valid action was provided.')
43
+ if command['base_command']
44
+ unless command['base_command'].is_a?(String)
45
+ command_errors.append('Invalid base command provided, it must be a string.')
46
+ end
47
+
48
+ if command['parameters'].present?
49
+ command_errors << validate_parameters(command['parameters'])
50
+ end
51
+ end
52
+
53
+ unless !command['action'].to_s.strip.empty? && handler_class.respond_to?(command['action'])
54
+ command_errors.append('No valid action was provided.')
55
+ end
56
+ command_errors = command_errors.flatten.compact
57
+ handler_errors.append("[#{command['name']}]: #{command_errors.join(' ')}") unless command_errors.empty?
25
58
  end
26
59
 
27
- handler_errors.append("[#{command['name']}]: #{command_errors.join(' ')}") unless command_errors.empty?
60
+ unless handler_errors.empty?
61
+ raise Exceptions::InvalidHandler, "#{handler_name} is not valid: #{handler_errors.join(' ')}"
62
+ end
63
+ rescue NameError
64
+ raise Exceptions::InvalidHandler, "#{handler_name} is not defined"
28
65
  end
29
66
 
30
- unless handler_errors.empty?
31
- raise Exceptions::InvalidHandler, "#{handler_name} is not valid: #{handler_errors.join(' ')}"
67
+ def validate_parameters(parameters)
68
+ errors = []
69
+ parameters.each do |parameter|
70
+ key = parameter.keys[0]
71
+ type = parameter.values[0]
72
+
73
+ next if VALID_PARAMETER_TYPES.include?(type.to_sym)
74
+
75
+ type.constantize
76
+ next if Slackify::Parameter.supported_parameters.include?(type)
77
+
78
+ errors << "Invalid parameter type for: #{key}, '#{type}'.\n"\
79
+ "If this is a custom parameter, make sure it inherits from Slackify::Parameter"
80
+ rescue NameError
81
+ errors << "Failed to find the custom class for: #{key}, '#{type}'."
82
+ end
83
+ errors
32
84
  end
33
- rescue NameError
34
- raise Exceptions::InvalidHandler, "#{handler_name} is not defined"
35
85
  end
36
86
  end
37
87
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slackify
4
+ # Base parameter class that any user defined parameters must inherit from
5
+ class Parameter
6
+ @@supported_parameters = []
7
+
8
+ class << self
9
+ # Any class inheriting from Slackify::Parameter will be added to
10
+ # the list of supported parameters
11
+ def inherited(subclass)
12
+ @@supported_parameters.push(subclass.to_s)
13
+ end
14
+
15
+ # Show a list of the parameters supported by the app. Since we do
16
+ # metaprogramming, we want to ensure we can only call defined parameter
17
+ def supported_parameters
18
+ @@supported_parameters
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slackify
4
+ # In charge of routing a message to its proper handler
5
+ class Router
6
+ class << self
7
+ # List all available commands
8
+ def all_commands
9
+ Slackify.configuration.handlers.collect(&:commands).flatten
10
+ end
11
+
12
+ # Find the matching command based on the message string
13
+ def matching_command(message)
14
+ all_commands.each do |command|
15
+ return command if command.regex.present? && command.regex.match?(message)
16
+ return command if command.base_command.present? && message.start_with?(command.base_command)
17
+ end
18
+ nil
19
+ end
20
+
21
+ # Call command based on message string
22
+ def call_command(message, params)
23
+ command = matching_command(message)
24
+ if command.nil?
25
+ return unless Slackify.configuration.unhandled_handler
26
+
27
+ Slackify.configuration.unhandled_handler.unhandled(params)
28
+ else
29
+ new_params = params.merge( command_arguments: extract_arguments(message, command) )
30
+
31
+ command.handler.call(new_params)
32
+ end
33
+ end
34
+
35
+ def extract_arguments(message, command)
36
+ if command.regex
37
+ command.regex.match(message).named_captures
38
+ else
39
+ raw_arguments = message.sub(/^#{command.base_command}/, '').strip
40
+ spec = {}
41
+ command.parameters.each do |parameter|
42
+ spec[parameter.keys[0].to_sym] = { type: parameter.values[0].to_sym }
43
+ end
44
+ parse_by_spec(spec, raw_arguments)
45
+ end
46
+ end
47
+
48
+ def parse_by_spec(spec, raw_arguments)
49
+ processed_args = {}
50
+
51
+ s = StringScanner.new(raw_arguments)
52
+ until s.eos?
53
+ # get the key, remove '=' and extra whitespace
54
+ current_key = s.scan_until(/=/)
55
+ break if current_key.nil?
56
+
57
+ current_key = current_key[0..-2].strip
58
+
59
+ # grab value accounting for any quotes
60
+ terminating_string = case s.peek(1)
61
+ when "'"
62
+ s.skip(/'/)
63
+ /'/
64
+ when '"'
65
+ s.skip(/"/)
66
+ /"/
67
+ else
68
+ / /
69
+ end
70
+ processed_args[current_key.to_sym] = if s.exist?(terminating_string)
71
+ # grab everything before the next instance of the terminating character
72
+ s.scan_until(terminating_string)[0..-2]
73
+ else
74
+ # this is probably wrong unless we were expecting a space, but hit eos
75
+ s.rest
76
+ end
77
+ end
78
+
79
+ # only pass on expected parameters for now.
80
+ processed_spec = {}
81
+ spec.each do |key, value|
82
+ # coerce to the expected type
83
+ type = value.fetch(:type, 'string')
84
+ processed_spec[key] = case type
85
+ when :int
86
+ processed_args[key].to_i
87
+ when :float
88
+ processed_args[key].to_f
89
+ when :boolean
90
+ ActiveModel::Type::Boolean.new.cast(processed_args[key])
91
+ when :string
92
+ processed_args[key]
93
+ else
94
+ Object.const_get(type).new(processed_args[key]).parse
95
+ end
96
+ end
97
+
98
+ processed_spec
99
+ end
100
+ end
101
+ end
102
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slackify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Leger
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-03-17 00:00:00.000000000 Z
12
+ date: 2019-12-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -27,6 +27,20 @@ dependencies:
27
27
  version: '0'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: slack-ruby-client
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 0.15.1
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 0.15.1
42
+ - !ruby/object:Gem::Dependency
43
+ name: strscan
30
44
  requirement: !ruby/object:Gem::Requirement
31
45
  requirements:
32
46
  - - ">="
@@ -114,9 +128,11 @@ files:
114
128
  - lib/slackify/exceptions.rb
115
129
  - lib/slackify/handlers.rb
116
130
  - lib/slackify/handlers/base.rb
117
- - lib/slackify/handlers/configuration.rb
131
+ - lib/slackify/handlers/factory.rb
118
132
  - lib/slackify/handlers/unhandled_handler.rb
119
133
  - lib/slackify/handlers/validator.rb
134
+ - lib/slackify/parameter.rb
135
+ - lib/slackify/router.rb
120
136
  homepage: https://github.com/jusleg/slackify
121
137
  licenses:
122
138
  - MIT
@@ -138,8 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
154
  - !ruby/object:Gem::Version
139
155
  version: '0'
140
156
  requirements: []
141
- rubyforge_project:
142
- rubygems_version: 2.7.3
157
+ rubygems_version: 3.0.3
143
158
  signing_key:
144
159
  specification_version: 4
145
160
  summary: Slackbot framework for Rails using the Events API
@@ -1,84 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'ostruct'
4
-
5
- module Slackify
6
- module Handlers
7
- class Configuration
8
- attr_reader :bot_id
9
- attr_accessor :handlers
10
- delegate :each, to: :@handlers
11
-
12
- def initialize
13
- @handlers = []
14
-
15
- read_handlers_yaml.each do |handler_yaml|
16
- handler = generate_handler_from_yaml(handler_yaml)
17
- @handlers << handler
18
- end
19
-
20
- environment_configurations
21
- end
22
-
23
- def all_commands
24
- @handlers.collect(&:commands).flatten
25
- end
26
-
27
- def matching_command(message)
28
- all_commands.each { |command| return command if command.regex.match? message }
29
- nil
30
- end
31
-
32
- def call_command(message, params)
33
- return if params.dig(:event, :user) == @bot_id || params.dig(:event, :message, :user) == @bot_id
34
-
35
- command = matching_command(message)
36
- if command.nil?
37
- return unless Slackify.configuration.unhandled_handler
38
-
39
- Slackify.configuration.unhandled_handler.unhandled(params)
40
- else
41
- new_params = params.merge(command_arguments: command.regex.match(message).named_captures)
42
- command.handler.call(new_params)
43
- end
44
- end
45
-
46
- private
47
-
48
- def environment_configurations
49
- @bot_id =
50
- case Rails.env
51
- when 'production', 'staging', 'development'
52
- Slackify.configuration.slack_client.auth_test['user_id']
53
- when 'test'
54
- ''
55
- end
56
- end
57
-
58
- def read_handlers_yaml
59
- raise 'config/handlers.yml does not exist' unless File.exist?("#{Rails.root}/config/handlers.yml")
60
-
61
- YAML.load_file("#{Rails.root}/config/handlers.yml") || []
62
- end
63
-
64
- def generate_handler_from_yaml(handler_yaml)
65
- Validator.verify_handler_integrity(handler_yaml)
66
-
67
- handler = OpenStruct.new
68
- handler.name = handler_yaml.keys.first
69
- handler.commands = []
70
-
71
- handler_yaml[handler.name]['commands']&.each do |command|
72
- built_command = OpenStruct.new
73
- built_command.regex = command['regex']
74
- built_command.handler = handler.name.camelize.constantize.method(command['action'])
75
- built_command.description = command['description']
76
- handler.commands << built_command.freeze
77
- end
78
-
79
- handler.commands.freeze
80
- handler.freeze
81
- end
82
- end
83
- end
84
- end