slackify 0.2.0 → 0.3.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: ca58d1a63e649165df6caccc7092ee6ab04ee2fa9edaa5b3225a156cb66d4566
4
- data.tar.gz: 3d3ae1cdd0f6f78ba32361fa61d858402968643819359fa363b15f4ee1547b49
3
+ metadata.gz: e158dfddb3b113177499b3247dbc9a4a7a4f9fff55f87ecafb21064aa4549325
4
+ data.tar.gz: cb03ce87203fc97ca0f4d7a728f25b6f729288e539b1530b39e6d51f59f6020a
5
5
  SHA512:
6
- metadata.gz: 51f0928b97da9b9839317b550ae616299ed4fb6692ed020f6ebfd6e0f14e0e222cc34aa7d225bfb0dc197441d6bb7988e3b2318369efe44814a0a60c4837770c
7
- data.tar.gz: 3c7a7e4d3e394c0024df2523716f92a6e556f43e6cb00e620257d0148c2f889daabb4dbd67db3a977e3524e8b0fc05f5f3acaa2f4bb78e734fa062f3a73c9f8e
6
+ metadata.gz: '093e9a1f4ab6e37c0020fb699d45ed392922477a195d72ba415ce702b3c2096663b6beba81364e61aae513ce0e3693f5463bd6a36a5ba48d2bc9888e4a48c856'
7
+ data.tar.gz: a0f3caca091ceef0e50e84e5d1a6b6c139dcf7de5982babec1b43e1f21d1376b1bdb76f223b4e43b4b9ddf925c66fb7906bf80aa5f9d3dc9df8e85e1631f5954
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## V0.3.0
2
+
3
+ * Add code documentation and improve exception message
4
+ * Add whitelisting of bot ids in the configuration through `whitelisted_bot_ids=`
5
+ * Refactored Handler configuration into `Slackify::Router` and `Slackify::Handlers::Factory`
6
+ * Improved testing
7
+ * Remove the need to perform `Slackify.load_handler`
8
+ * **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.
9
+
1
10
  ## V0.2.0
2
11
 
3
12
  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.
data/README.md CHANGED
@@ -1,4 +1,4 @@
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 [![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) ![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
 
@@ -218,29 +218,45 @@ First, you'll need to create a new app on slack. Head over to [slack api](https:
218
218
  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.
219
219
 
220
220
  6. **Configure Slackify**
221
+ Add a new initializer with the following code
222
+
221
223
  ```ruby
224
+ # config/initializers/slackify.rb
222
225
  Slackify.configure do |config|
223
226
  config.slack_bot_token = "xoxb-sdkjlkjsdflsd..."
224
227
  config.slack_secret_token = "1234dummysecret"
225
228
  end
226
229
  ```
227
230
 
228
- 7. **Add an initializer**
231
+ 7. **Define handlers specific subtypes** (Optional)
232
+ 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
233
+
229
234
  ```ruby
230
235
  # config/initializers/slackify.rb
231
- Slackify.load_handlers
236
+ Slackify.configure do |config|
237
+ config.slack_bot_token = "xoxb-sdkjlkjsdflsd..."
238
+ config.slack_secret_token = "1234dummysecret"
239
+ config.custom_message_subtype_handlers = {
240
+ file_share: ImageHandler,
241
+ channel_join: JoinHandler,
242
+ ...
243
+ }
244
+ config.custom_event_type_handlers = {
245
+ app_mention: ...
246
+ }
247
+ end
232
248
  ```
233
249
 
234
- 8. **Define handlers specific subtypes** (Optional)
250
+ 8. **Handle bot messages** (Highly optional)
251
+ 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 whitelisted bot ids.
235
252
 
236
253
  ```ruby
237
254
  # config/initializers/slackify.rb
238
- Slackify.load_handlers
239
- Slackify.configuration.custom_message_subtype_handlers = {
240
- file_share: ImageHandler,
241
- channel_join: JoinHandler,
242
- ...
243
- }
255
+ Slackify.configure do |config|
256
+ ...
257
+ config.whitelisted_bot_ids = ['abc123', 'def456']
258
+ ...
259
+ end
244
260
  ```
245
261
 
246
262
  **At this point, you are ready to go 😄**
@@ -59,10 +59,13 @@ module Slackify
59
59
  return
60
60
  end
61
61
 
62
- return if params[:event][:subtype] == "bot_message" || params[:event].key?(:bot_id) || params[:event][:hidden]
62
+ return if (params[:event][:subtype] == "bot_message" ||
63
+ params[:event].key?(:bot_id) ||
64
+ params[:event][:hidden]) &&
65
+ Slackify.configuration.whitelisted_bot_ids.exclude?(params.dig(:event, :bot_id))
63
66
 
64
67
  command = params[:event][:text]
65
- Slackify.configuration.handlers.call_command(command, params[:slack])
68
+ Slackify::Router.call_command(command, params[:slack])
66
69
  rescue RuntimeError => e
67
70
  raise e unless e.message == "Component not found for a command message"
68
71
  end
@@ -76,7 +79,7 @@ module Slackify
76
79
  class_name, method_name = callback_id.split('#')
77
80
  class_name = class_name.camelize
78
81
 
79
- raise Exceptions::HandlerNotSupported, class_name unless
82
+ raise Exceptions::HandlerNotSupported, "#{class_name} is not a subclass of Slackify::Handlers::Base" unless
80
83
  Handlers::Base.supported_handlers.include?(class_name)
81
84
 
82
85
  class_name.constantize.method(method_name)
@@ -85,7 +88,7 @@ module Slackify
85
88
  def verify_handler_slash_permission(handler_class, handler_method)
86
89
  handler_class = handler_class.camelize
87
90
 
88
- raise Exceptions::HandlerNotSupported, handler_class unless
91
+ raise Exceptions::HandlerNotSupported, "#{handler_class} is not a subclass of Slackify::Handlers::Base" unless
89
92
  Handlers::Base.supported_handlers.include?(handler_class)
90
93
 
91
94
  handler = handler_class.constantize
data/lib/slackify.rb CHANGED
@@ -4,6 +4,7 @@ require 'slackify/configuration'
4
4
  require 'slackify/engine'
5
5
  require 'slackify/exceptions'
6
6
  require 'slackify/handlers'
7
+ require 'slackify/router'
7
8
 
8
9
  module Slackify
9
10
  class << self
@@ -19,9 +20,5 @@ module Slackify
19
20
  def configure
20
21
  yield(configuration)
21
22
  end
22
-
23
- def load_handlers
24
- @configuration.handlers = Handlers::Configuration.new
25
- end
26
23
  end
27
24
  end
@@ -3,42 +3,75 @@
3
3
  require 'slack'
4
4
 
5
5
  module Slackify
6
+ # Where the configuration for Slackify lives
6
7
  class Configuration
7
8
  attr_reader :custom_message_subtype_handlers, :slack_bot_token, :unhandled_handler, :custom_event_type_handlers
8
- attr_accessor :handlers, :slack_secret_token, :slack_client
9
+ attr_accessor :handlers, :slack_secret_token, :slack_client, :whitelisted_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
16
  @custom_message_subtype_handlers = {}
16
17
  @custom_event_type_handlers = {}
17
18
  @unhandled_handler = Handlers::UnhandledHandler
19
+ @whitelisted_bot_ids = []
18
20
  end
19
21
 
22
+ # Set your own unhandled handler
20
23
  def unhandled_handler=(handler)
21
- 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
22
25
  handler < Handlers::Base
23
26
 
24
27
  @unhandled_handler = handler
25
28
  end
26
29
 
30
+ # Remove unhandled handler. The bot will not reply if the message doesn't
31
+ # match any regex
27
32
  def remove_unhandled_handler
28
33
  @unhandled_handler = nil
29
34
  end
30
35
 
36
+ # Set the token that we will use to connect to slack
31
37
  def slack_bot_token=(token)
32
38
  @slack_bot_token = token
33
39
  @slack_client = Slack::Web::Client.new(token: token).freeze
34
40
  end
35
41
 
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
36
45
  def custom_message_subtype_handlers=(event_subtype_hash)
37
46
  @custom_message_subtype_handlers = event_subtype_hash.with_indifferent_access
38
47
  end
39
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
40
52
  def custom_event_type_handlers=(event_type_hash)
41
53
  @custom_event_type_handlers = event_type_hash.with_indifferent_access
42
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") || []
75
+ end
43
76
  end
44
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 whitelisted 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,16 +2,25 @@
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
25
  if @allowed_slash_methods
17
26
  @allowed_slash_methods.push(*element)
@@ -20,10 +29,14 @@ module Slackify
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,27 @@
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
+ handler.commands << built_command.freeze
20
+ end
21
+
22
+ handler.commands.freeze
23
+ handler.freeze
24
+ end
25
+ end
26
+ end
27
+ 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,7 +2,10 @@
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
8
+ # Checks if your handler hash is valid. It's pass or raise 🧨💥
6
9
  def self.verify_handler_integrity(handler)
7
10
  handler_name = handler.keys.first
8
11
  handler_class = handler_name.camelize.constantize
@@ -0,0 +1,32 @@
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 { |command| return command if command.regex.match? message }
15
+ nil
16
+ end
17
+
18
+ # Call command based on message string
19
+ def call_command(message, params)
20
+ command = matching_command(message)
21
+ if command.nil?
22
+ return unless Slackify.configuration.unhandled_handler
23
+
24
+ Slackify.configuration.unhandled_handler.unhandled(params)
25
+ else
26
+ new_params = params.merge(command_arguments: command.regex.match(message).named_captures)
27
+ command.handler.call(new_params)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ 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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Leger
@@ -114,9 +114,10 @@ files:
114
114
  - lib/slackify/exceptions.rb
115
115
  - lib/slackify/handlers.rb
116
116
  - lib/slackify/handlers/base.rb
117
- - lib/slackify/handlers/configuration.rb
117
+ - lib/slackify/handlers/factory.rb
118
118
  - lib/slackify/handlers/unhandled_handler.rb
119
119
  - lib/slackify/handlers/validator.rb
120
+ - lib/slackify/router.rb
120
121
  homepage: https://github.com/jusleg/slackify
121
122
  licenses:
122
123
  - MIT
@@ -1,94 +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
- def bot_auth_test
47
- @bot_id = Slackify.configuration.slack_client.auth_test.fetch('user_id')
48
- Rails.logger.debug("[Slackify] Bot successfully authenticated to Slack")
49
- true
50
- rescue KeyError
51
- raise "[Slackify] The bot did not successfully authenticate to Slack"
52
- end
53
-
54
- private
55
-
56
- def environment_configurations
57
- if %w(production staging development).exclude?(Rails.env) || skip_auth?
58
- @bot_id = 'dummy_bot_id'
59
- else
60
- bot_auth_test
61
- end
62
- end
63
-
64
- def skip_auth?
65
- ENV['SLACKIFY_AUTH_SKIP'] == '1'
66
- end
67
-
68
- def read_handlers_yaml
69
- raise 'config/handlers.yml does not exist' unless File.exist?("#{Rails.root}/config/handlers.yml")
70
-
71
- YAML.load_file("#{Rails.root}/config/handlers.yml") || []
72
- end
73
-
74
- def generate_handler_from_yaml(handler_yaml)
75
- Validator.verify_handler_integrity(handler_yaml)
76
-
77
- handler = OpenStruct.new
78
- handler.name = handler_yaml.keys.first
79
- handler.commands = []
80
-
81
- handler_yaml[handler.name]['commands']&.each do |command|
82
- built_command = OpenStruct.new
83
- built_command.regex = command['regex']
84
- built_command.handler = handler.name.camelize.constantize.method(command['action'])
85
- built_command.description = command['description']
86
- handler.commands << built_command.freeze
87
- end
88
-
89
- handler.commands.freeze
90
- handler.freeze
91
- end
92
- end
93
- end
94
- end