bolt_rb 0.1.0
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +218 -0
- data/lib/bolt_rb/app.rb +216 -0
- data/lib/bolt_rb/configuration.rb +44 -0
- data/lib/bolt_rb/context.rb +173 -0
- data/lib/bolt_rb/handlers/action_handler.rb +162 -0
- data/lib/bolt_rb/handlers/base.rb +194 -0
- data/lib/bolt_rb/handlers/command_handler.rb +119 -0
- data/lib/bolt_rb/handlers/event_handler.rb +113 -0
- data/lib/bolt_rb/handlers/shortcut_handler.rb +132 -0
- data/lib/bolt_rb/handlers/view_submission_handler.rb +159 -0
- data/lib/bolt_rb/middleware/base.rb +35 -0
- data/lib/bolt_rb/middleware/chain.rb +58 -0
- data/lib/bolt_rb/middleware/logging.rb +60 -0
- data/lib/bolt_rb/router.rb +75 -0
- data/lib/bolt_rb/socket_mode/client.rb +296 -0
- data/lib/bolt_rb/testing/payload_factory.rb +143 -0
- data/lib/bolt_rb/testing/rspec_helpers.rb +62 -0
- data/lib/bolt_rb/testing.rb +20 -0
- data/lib/bolt_rb/version.rb +4 -0
- data/lib/bolt_rb.rb +76 -0
- metadata +149 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BoltRb
|
|
4
|
+
module Handlers
|
|
5
|
+
# Handler for Slack interactive component actions (buttons, select menus, etc.)
|
|
6
|
+
#
|
|
7
|
+
# This handler provides the `action` DSL for matching block_actions payloads.
|
|
8
|
+
# Block actions are triggered when users interact with interactive components
|
|
9
|
+
# like buttons, overflow menus, date pickers, and select menus in messages
|
|
10
|
+
# or modals.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic button handler
|
|
13
|
+
# class ApproveHandler < BoltRb::ActionHandler
|
|
14
|
+
# action 'approve_button'
|
|
15
|
+
#
|
|
16
|
+
# def handle
|
|
17
|
+
# ack
|
|
18
|
+
# say("Approved by <@#{user}>!")
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example Handler with block_id filter
|
|
23
|
+
# class RequestApprovalHandler < BoltRb::ActionHandler
|
|
24
|
+
# action 'approve', block_id: 'approval_block'
|
|
25
|
+
#
|
|
26
|
+
# def handle
|
|
27
|
+
# ack
|
|
28
|
+
# # Handle approval request
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# @example Regex-based action matching
|
|
33
|
+
# class DynamicButtonHandler < BoltRb::ActionHandler
|
|
34
|
+
# action /^approve_request_/
|
|
35
|
+
#
|
|
36
|
+
# def handle
|
|
37
|
+
# ack
|
|
38
|
+
# request_id = action_id.gsub('approve_request_', '')
|
|
39
|
+
# # Process the request
|
|
40
|
+
# end
|
|
41
|
+
# end
|
|
42
|
+
class ActionHandler < Base
|
|
43
|
+
class << self
|
|
44
|
+
# Configures which action_id this handler responds to
|
|
45
|
+
#
|
|
46
|
+
# @param action_id [String, Regexp] The action_id to match (exact string or regex)
|
|
47
|
+
# @param block_id [String, Regexp, nil] Optional block_id filter for more specific matching
|
|
48
|
+
# @return [void]
|
|
49
|
+
#
|
|
50
|
+
# @example Match exact action_id
|
|
51
|
+
# action 'approve_button'
|
|
52
|
+
#
|
|
53
|
+
# @example Match with block_id
|
|
54
|
+
# action 'approve_button', block_id: 'approval_block'
|
|
55
|
+
#
|
|
56
|
+
# @example Match action_id pattern
|
|
57
|
+
# action /^approve_/
|
|
58
|
+
def action(action_id, block_id: nil)
|
|
59
|
+
@matcher_config = {
|
|
60
|
+
type: :action,
|
|
61
|
+
action_id: action_id,
|
|
62
|
+
block_id: block_id
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Determines if this handler matches the given payload
|
|
67
|
+
#
|
|
68
|
+
# Checks if the payload is a block_actions type and if any of the
|
|
69
|
+
# actions in the payload match the configured action_id and optional block_id.
|
|
70
|
+
#
|
|
71
|
+
# @param payload [Hash] The incoming Slack block_actions payload
|
|
72
|
+
# @return [Boolean] true if this handler should process the action
|
|
73
|
+
def matches?(payload)
|
|
74
|
+
return false unless matcher_config
|
|
75
|
+
return false unless payload['type'] == 'block_actions'
|
|
76
|
+
|
|
77
|
+
actions = payload['actions'] || []
|
|
78
|
+
actions.any? do |action|
|
|
79
|
+
action_matches?(action) && block_matches?(action)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Checks if the action's action_id matches the configured pattern
|
|
86
|
+
#
|
|
87
|
+
# @param action [Hash] A single action from the payload
|
|
88
|
+
# @return [Boolean] true if action_id matches
|
|
89
|
+
def action_matches?(action)
|
|
90
|
+
if matcher_config[:action_id].is_a?(Regexp)
|
|
91
|
+
matcher_config[:action_id].match?(action['action_id'])
|
|
92
|
+
else
|
|
93
|
+
action['action_id'] == matcher_config[:action_id]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Checks if the action's block_id matches the configured pattern
|
|
98
|
+
#
|
|
99
|
+
# If no block_id is configured, this always returns true (no filtering).
|
|
100
|
+
#
|
|
101
|
+
# @param action [Hash] A single action from the payload
|
|
102
|
+
# @return [Boolean] true if block_id matches or no block_id filter is set
|
|
103
|
+
def block_matches?(action)
|
|
104
|
+
return true if matcher_config[:block_id].nil?
|
|
105
|
+
|
|
106
|
+
if matcher_config[:block_id].is_a?(Regexp)
|
|
107
|
+
matcher_config[:block_id].match?(action['block_id'])
|
|
108
|
+
else
|
|
109
|
+
action['block_id'] == matcher_config[:block_id]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns the first action from the payload
|
|
115
|
+
#
|
|
116
|
+
# Block actions payloads can contain multiple actions, but typically
|
|
117
|
+
# only one action is triggered at a time. This returns the first action.
|
|
118
|
+
#
|
|
119
|
+
# @return [Hash, nil] The action hash containing action_id, value, etc.
|
|
120
|
+
def action
|
|
121
|
+
payload['actions']&.first
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Returns the action_id from the triggered action
|
|
125
|
+
#
|
|
126
|
+
# @return [String, nil] The action_id
|
|
127
|
+
def action_id
|
|
128
|
+
action&.dig('action_id')
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns the value from the triggered action
|
|
132
|
+
#
|
|
133
|
+
# For buttons this is the button's value. For select menus this is
|
|
134
|
+
# the selected option's value.
|
|
135
|
+
#
|
|
136
|
+
# @return [String, nil] The action value
|
|
137
|
+
def action_value
|
|
138
|
+
action&.dig('value')
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns the block_id from the triggered action
|
|
142
|
+
#
|
|
143
|
+
# @return [String, nil] The block_id
|
|
144
|
+
def block_id
|
|
145
|
+
action&.dig('block_id')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns the trigger_id for opening modals
|
|
149
|
+
#
|
|
150
|
+
# Slack provides a trigger_id with interactive actions that can be used
|
|
151
|
+
# to open modals within 3 seconds of receiving the action.
|
|
152
|
+
#
|
|
153
|
+
# @return [String, nil] The trigger_id for views.open
|
|
154
|
+
def trigger_id
|
|
155
|
+
payload['trigger_id']
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Top-level alias for convenience
|
|
161
|
+
ActionHandler = Handlers::ActionHandler
|
|
162
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BoltRb
|
|
4
|
+
module Handlers
|
|
5
|
+
# Base class for all Slack event handlers.
|
|
6
|
+
#
|
|
7
|
+
# This provides the common interface and middleware execution that all
|
|
8
|
+
# handler types (Event, Command, Action, Shortcut) inherit from.
|
|
9
|
+
#
|
|
10
|
+
# Subclasses should:
|
|
11
|
+
# - Override .matches?(payload) to define matching logic
|
|
12
|
+
# - Override #handle to implement the handler behavior
|
|
13
|
+
# - Optionally use .use(middleware) to add handler-specific middleware
|
|
14
|
+
#
|
|
15
|
+
# @example Creating a custom handler
|
|
16
|
+
# class MyHandler < BoltRb::Handlers::Base
|
|
17
|
+
# use MyCustomMiddleware
|
|
18
|
+
#
|
|
19
|
+
# def self.matches?(payload)
|
|
20
|
+
# payload.dig('event', 'type') == 'message'
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# def handle
|
|
24
|
+
# say("Received your message!")
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
class Base
|
|
28
|
+
class << self
|
|
29
|
+
# Configuration for matching this handler to payloads
|
|
30
|
+
# Subclasses should set this to define their matching criteria
|
|
31
|
+
#
|
|
32
|
+
# @return [Object, nil] The matcher configuration
|
|
33
|
+
attr_reader :matcher_config
|
|
34
|
+
|
|
35
|
+
# Returns the middleware stack for this handler class
|
|
36
|
+
#
|
|
37
|
+
# @return [Array<Class>] Array of middleware classes
|
|
38
|
+
def middleware_stack
|
|
39
|
+
@middleware_stack ||= []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Adds middleware to this handler's stack
|
|
43
|
+
#
|
|
44
|
+
# Middleware is executed in the order added, wrapping the #handle method.
|
|
45
|
+
# Each middleware should call yield to continue the chain.
|
|
46
|
+
#
|
|
47
|
+
# @param middleware_class [Class] The middleware class to add
|
|
48
|
+
# @return [void]
|
|
49
|
+
#
|
|
50
|
+
# @example Adding middleware
|
|
51
|
+
# class MyHandler < Base
|
|
52
|
+
# use LoggingMiddleware
|
|
53
|
+
# use AuthenticationMiddleware
|
|
54
|
+
# end
|
|
55
|
+
def use(middleware_class)
|
|
56
|
+
middleware_stack << middleware_class
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Determines if this handler matches the given payload
|
|
60
|
+
#
|
|
61
|
+
# Base implementation always returns false. Subclasses should override
|
|
62
|
+
# this to implement their matching logic.
|
|
63
|
+
#
|
|
64
|
+
# @param _payload [Hash] The incoming Slack payload
|
|
65
|
+
# @return [Boolean] true if this handler should process the payload
|
|
66
|
+
def matches?(_payload)
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Hook called when a class inherits from Base
|
|
71
|
+
#
|
|
72
|
+
# Ensures each subclass gets its own independent middleware stack
|
|
73
|
+
# and registers the handler with the global router.
|
|
74
|
+
#
|
|
75
|
+
# @param subclass [Class] The inheriting class
|
|
76
|
+
# @return [void]
|
|
77
|
+
def inherited(subclass)
|
|
78
|
+
super
|
|
79
|
+
subclass.instance_variable_set(:@middleware_stack, [])
|
|
80
|
+
# Auto-register with the global router
|
|
81
|
+
BoltRb.router.register(subclass)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Context] The context object for this handler invocation
|
|
86
|
+
attr_reader :context
|
|
87
|
+
|
|
88
|
+
# Creates a new handler instance
|
|
89
|
+
#
|
|
90
|
+
# @param context [Context] The context containing payload, client, and ack
|
|
91
|
+
def initialize(context)
|
|
92
|
+
@context = context
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns the raw payload from the context
|
|
96
|
+
#
|
|
97
|
+
# @return [Hash] The Slack event payload
|
|
98
|
+
def payload
|
|
99
|
+
context.payload
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns the Slack Web API client
|
|
103
|
+
#
|
|
104
|
+
# @return [Slack::Web::Client] The API client
|
|
105
|
+
def client
|
|
106
|
+
context.client
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns the user ID from the context
|
|
110
|
+
#
|
|
111
|
+
# @return [String, nil] The user ID
|
|
112
|
+
def user
|
|
113
|
+
context.user
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Returns the channel ID from the context
|
|
117
|
+
#
|
|
118
|
+
# @return [String, nil] The channel ID
|
|
119
|
+
def channel
|
|
120
|
+
context.channel
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Posts a message to the channel
|
|
124
|
+
#
|
|
125
|
+
# Delegates to Context#say
|
|
126
|
+
#
|
|
127
|
+
# @param message [String, Hash] The message to post
|
|
128
|
+
# @return [Hash] The Slack API response
|
|
129
|
+
def say(message)
|
|
130
|
+
context.say(message)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Acknowledges the event
|
|
134
|
+
#
|
|
135
|
+
# Delegates to Context#ack
|
|
136
|
+
#
|
|
137
|
+
# @param response [String, Hash, nil] Optional response to include
|
|
138
|
+
# @return [void]
|
|
139
|
+
def ack(response = nil)
|
|
140
|
+
context.ack(response)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Responds using the response_url
|
|
144
|
+
#
|
|
145
|
+
# Delegates to Context#respond
|
|
146
|
+
#
|
|
147
|
+
# @param message [String, Hash] The message to send
|
|
148
|
+
# @return [Net::HTTPResponse, nil] The HTTP response
|
|
149
|
+
def respond(message)
|
|
150
|
+
context.respond(message)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Executes this handler
|
|
154
|
+
#
|
|
155
|
+
# Runs the middleware stack, then calls #handle at the end of the chain.
|
|
156
|
+
#
|
|
157
|
+
# @return [void]
|
|
158
|
+
def call
|
|
159
|
+
run_middleware { handle }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The main handler logic
|
|
163
|
+
#
|
|
164
|
+
# Subclasses must override this method to implement their behavior.
|
|
165
|
+
#
|
|
166
|
+
# @raise [NotImplementedError] Always raises in the base class
|
|
167
|
+
def handle
|
|
168
|
+
raise NotImplementedError, 'Subclasses must implement #handle'
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
# Executes the middleware chain, then the given block
|
|
174
|
+
#
|
|
175
|
+
# Creates a recursive chain where each middleware can call yield
|
|
176
|
+
# to invoke the next middleware (or the final block).
|
|
177
|
+
#
|
|
178
|
+
# @yield The block to execute at the end of the chain
|
|
179
|
+
# @return [void]
|
|
180
|
+
def run_middleware(&block)
|
|
181
|
+
chain = self.class.middleware_stack.dup
|
|
182
|
+
run_next = proc do
|
|
183
|
+
if chain.empty?
|
|
184
|
+
block.call
|
|
185
|
+
else
|
|
186
|
+
middleware = chain.shift.new
|
|
187
|
+
middleware.call(context) { run_next.call }
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
run_next.call
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BoltRb
|
|
4
|
+
module Handlers
|
|
5
|
+
# Handler for Slack slash commands (/deploy, /help, etc.)
|
|
6
|
+
#
|
|
7
|
+
# This handler provides the `command` DSL for matching slash command invocations.
|
|
8
|
+
# Slash commands have a different payload structure than events, with fields like
|
|
9
|
+
# `user_id`, `channel_id`, and `trigger_id` at the top level.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic command handler
|
|
12
|
+
# class DeployHandler < BoltRb::CommandHandler
|
|
13
|
+
# command '/deploy'
|
|
14
|
+
#
|
|
15
|
+
# def handle
|
|
16
|
+
# ack
|
|
17
|
+
# say("Deploying: #{command_text}")
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example Command with modal
|
|
22
|
+
# class SettingsHandler < BoltRb::CommandHandler
|
|
23
|
+
# command '/settings'
|
|
24
|
+
#
|
|
25
|
+
# def handle
|
|
26
|
+
# ack
|
|
27
|
+
# client.views_open(
|
|
28
|
+
# trigger_id: trigger_id,
|
|
29
|
+
# view: { type: 'modal', ... }
|
|
30
|
+
# )
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
class CommandHandler < Base
|
|
34
|
+
class << self
|
|
35
|
+
# Configures which slash command this handler responds to
|
|
36
|
+
#
|
|
37
|
+
# @param command_name [String] The slash command to handle (e.g., '/deploy')
|
|
38
|
+
# @return [void]
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# command '/deploy'
|
|
42
|
+
def command(command_name)
|
|
43
|
+
@matcher_config = {
|
|
44
|
+
type: :command,
|
|
45
|
+
command: command_name
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Determines if this handler matches the given payload
|
|
50
|
+
#
|
|
51
|
+
# Checks if the payload's command field matches the configured command.
|
|
52
|
+
#
|
|
53
|
+
# @param payload [Hash] The incoming Slack command payload
|
|
54
|
+
# @return [Boolean] true if this handler should process the command
|
|
55
|
+
def matches?(payload)
|
|
56
|
+
return false unless matcher_config
|
|
57
|
+
|
|
58
|
+
payload['command'] == matcher_config[:command]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns the slash command that was invoked
|
|
63
|
+
#
|
|
64
|
+
# @return [String] The command name (e.g., '/deploy')
|
|
65
|
+
def command_name
|
|
66
|
+
payload['command']
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns the text provided after the command
|
|
70
|
+
#
|
|
71
|
+
# For `/deploy production --force`, this returns "production --force"
|
|
72
|
+
#
|
|
73
|
+
# @return [String, nil] The text argument or nil if none provided
|
|
74
|
+
def command_text
|
|
75
|
+
payload['text']
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns the command parameters as a hash
|
|
79
|
+
#
|
|
80
|
+
# @return [Hash] Hash containing :text key with command text
|
|
81
|
+
def params
|
|
82
|
+
{ text: command_text }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns the trigger_id for opening modals
|
|
86
|
+
#
|
|
87
|
+
# Slack provides a trigger_id with slash commands that can be used
|
|
88
|
+
# to open modals within 3 seconds of receiving the command.
|
|
89
|
+
#
|
|
90
|
+
# @return [String, nil] The trigger_id for views.open
|
|
91
|
+
def trigger_id
|
|
92
|
+
payload['trigger_id']
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns the user ID who invoked the command
|
|
96
|
+
#
|
|
97
|
+
# Overrides Base#user because slash command payloads use 'user_id'
|
|
98
|
+
# instead of nested event.user structure.
|
|
99
|
+
#
|
|
100
|
+
# @return [String, nil] The user ID
|
|
101
|
+
def user
|
|
102
|
+
payload['user_id'] || super
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the channel ID where the command was invoked
|
|
106
|
+
#
|
|
107
|
+
# Overrides Base#channel because slash command payloads use 'channel_id'
|
|
108
|
+
# instead of nested event.channel structure.
|
|
109
|
+
#
|
|
110
|
+
# @return [String, nil] The channel ID
|
|
111
|
+
def channel
|
|
112
|
+
payload['channel_id'] || super
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Top-level alias for convenience
|
|
118
|
+
CommandHandler = Handlers::CommandHandler
|
|
119
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BoltRb
|
|
4
|
+
module Handlers
|
|
5
|
+
# Handler for Slack events (message, app_mention, reaction_added, etc.)
|
|
6
|
+
#
|
|
7
|
+
# This handler provides the `listen_to` DSL for matching event types
|
|
8
|
+
# and optionally filtering by text patterns.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic message handler
|
|
11
|
+
# class GreetingHandler < BoltRb::EventHandler
|
|
12
|
+
# listen_to :message
|
|
13
|
+
#
|
|
14
|
+
# def handle
|
|
15
|
+
# say("Hello, #{user}!")
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Handler with pattern matching
|
|
20
|
+
# class HelloHandler < BoltRb::EventHandler
|
|
21
|
+
# listen_to :message, pattern: /hello/i
|
|
22
|
+
#
|
|
23
|
+
# def handle
|
|
24
|
+
# say("Hello to you too!")
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example App mention handler
|
|
29
|
+
# class MentionHandler < BoltRb::EventHandler
|
|
30
|
+
# listen_to :app_mention
|
|
31
|
+
#
|
|
32
|
+
# def handle
|
|
33
|
+
# say("You mentioned me in #{channel}!")
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
class EventHandler < Base
|
|
37
|
+
class << self
|
|
38
|
+
# Configures which event type this handler responds to
|
|
39
|
+
#
|
|
40
|
+
# @param event_type [Symbol, String] The Slack event type to listen for
|
|
41
|
+
# (e.g., :message, :app_mention, :reaction_added)
|
|
42
|
+
# @param pattern [Regexp, nil] Optional regex pattern to match against
|
|
43
|
+
# the event's text field
|
|
44
|
+
# @return [void]
|
|
45
|
+
#
|
|
46
|
+
# @example Listen to all messages
|
|
47
|
+
# listen_to :message
|
|
48
|
+
#
|
|
49
|
+
# @example Listen to messages matching a pattern
|
|
50
|
+
# listen_to :message, pattern: /help/i
|
|
51
|
+
def listen_to(event_type, pattern: nil)
|
|
52
|
+
@matcher_config = {
|
|
53
|
+
type: :event,
|
|
54
|
+
event_type: event_type,
|
|
55
|
+
pattern: pattern
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Determines if this handler matches the given payload
|
|
60
|
+
#
|
|
61
|
+
# Checks the event type and optionally the text pattern.
|
|
62
|
+
#
|
|
63
|
+
# @param payload [Hash] The incoming Slack event payload
|
|
64
|
+
# @return [Boolean] true if this handler should process the event
|
|
65
|
+
def matches?(payload)
|
|
66
|
+
return false unless matcher_config
|
|
67
|
+
|
|
68
|
+
event = payload['event']
|
|
69
|
+
return false unless event
|
|
70
|
+
return false unless event['type'].to_s == matcher_config[:event_type].to_s
|
|
71
|
+
|
|
72
|
+
if matcher_config[:pattern]
|
|
73
|
+
return false if event['text'].nil?
|
|
74
|
+
return matcher_config[:pattern].match?(event['text'])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns the event portion of the payload
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash, nil] The event data
|
|
84
|
+
def event
|
|
85
|
+
payload['event']
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns the text content of the event
|
|
89
|
+
#
|
|
90
|
+
# @return [String, nil] The message text
|
|
91
|
+
def text
|
|
92
|
+
event&.dig('text')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns the thread timestamp if this message is in a thread
|
|
96
|
+
#
|
|
97
|
+
# @return [String, nil] The thread_ts value
|
|
98
|
+
def thread_ts
|
|
99
|
+
event&.dig('thread_ts')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns the message timestamp
|
|
103
|
+
#
|
|
104
|
+
# @return [String, nil] The ts value
|
|
105
|
+
def ts
|
|
106
|
+
event&.dig('ts')
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Top-level alias for convenience
|
|
112
|
+
EventHandler = Handlers::EventHandler
|
|
113
|
+
end
|