grape-slack-bot 2.0.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a6e6b020caecc8ee5a8520f7f6f76107d006ca88070189e0cf23428bc10d2e7
4
- data.tar.gz: 02aa101bb61f5ad8fc96c9d295e75fe032e1d088d90d3d49b008e09551f53b8c
3
+ metadata.gz: 9361a3a86619e251da1df7649973cdd911619f2b55f18d5d622f8a1e6d5ee707
4
+ data.tar.gz: ebd8c29c0f6957bbaa0cf1fefbce369f7072b60d4b01cbae4037fa7a5fe3212b
5
5
  SHA512:
6
- metadata.gz: 3217a763efe6f6b101067c422029450e59efee9ea47d611bef1bd06a166a99404c1243955a3e42a5e9f31a78e9c3cbdd051d324fcd3b0b2414812da3d84f7336
7
- data.tar.gz: 3faca964608cb59b26ab6712c4eec82356f0ce761adfb4811d282e7d0051f9cdc58ac6958634feef2e24563b800a38259756a5847e0c6b0d8a3b2b6c62cb74b5
6
+ metadata.gz: 9f4bf3f86997214af675d7a21c40c4270fbb3e8c93093dbdacabf08da89a4ed054221e27345702e4e48da4c945ec12ef4291c7c46a412ee7a02d82fadd6e1db5
7
+ data.tar.gz: a2c0ea8a963da6d8aff07dca5bdb90c517e1ae44a64b34b5f1e70903279b33239f6182b1bd8ddef22de17d45c7eb0dda86f101d3ed0cfd4461ea080357e11db2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.1.0 (2026-07-10)
4
+
5
+ - Add `block_action` registration for message-level block action buttons outside modal callbacks
6
+ - Add `user_session_resolver` and `event_dispatcher` configuration hooks for session lookup and background event dispatch
7
+ - Acknowledge Slack event retries with an empty 200 response so duplicate deliveries do not run handlers twice
8
+ - Skip `bot_message` event payloads at ingress to avoid handler loops from bot echoes
9
+ - Retry Slack API requests once when rate limited, sleeping at most 3 seconds before the retry
10
+ - Fix `scheduled_messages_list` to send the `channel` argument to the Slack API
11
+ - Fix team verification when `SLACK_TEAM_ID` is unset so requests are rejected instead of matching any team
12
+ - Raise `SlackBot::Errors::ConfigurationError` when required configuration is missing
13
+ - Raise `SlackBot::Errors::BlockActionNotImplemented` when an unregistered block action is submitted
14
+ - Require `faraday` `>= 2.14.3`
15
+
16
+ ## 2.0.1 (2026-05-31)
17
+
18
+ - Allow `grape` 3.x by expanding the runtime dependency to `< 4.0`
19
+ - Replace SimpleCov and JUnit formatter test wiring with Polyrun coverage and failure reporting
20
+ - Move lint and type-check execution into Polyrun `before_suite` hooks
21
+ - Refresh development and release tooling, CI, and repository maintenance configuration
22
+
3
23
  ## 2.0.0 (2025-11-06)
4
24
 
5
25
  - Fix status code handling for empty/false responses - ensure 200 OK instead of 204 No Content
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # grape-slack-bot.rb
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/grape-slack-bot.svg)](https://badge.fury.io/rb/grape-slack-bot) [![Test Status](https://github.com/amkisko/grape-slack-bot.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/grape-slack-bot.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/grape-slack-bot.rb/graph/badge.svg?token=VIZ94XFOR3)](https://codecov.io/gh/amkisko/grape-slack-bot.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/grape-slack-bot.svg)](https://badge.fury.io/rb/grape-slack-bot) [![Test Status](https://github.com/amkisko/grape-slack-bot.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/grape-slack-bot.rb/actions/workflows/test.yml)
4
4
 
5
5
  Extensible Slack bot implementation gem for [ruby-grape](https://github.com/ruby-grape/grape) with support for slash commands, interactive components, events, and views.
6
6
 
@@ -55,10 +55,18 @@ SlackBot::DevConsole.enabled = Rails.env.development?
55
55
  SlackBot::Config.configure do
56
56
  callback_storage Rails.cache
57
57
  callback_user_finder ->(id) { User.active.find_by(id: id) }
58
+ user_session_resolver ->(team_id, user_id) do
59
+ uid = OmniAuth::Strategies::SlackOpenid.generate_uid(team_id, user_id)
60
+ UserSession.find_by(uid: uid, provider: UserSession.slack_openid_provider)
61
+ end
62
+ event_dispatcher ->(handler:, params:, current_user:) do
63
+ SlackEventJob.perform_later(handler.name, params.deep_stringify_keys, current_user&.id)
64
+ end
58
65
 
59
66
  # Register event handlers
60
67
  event :app_home_opened, MySlackBot::AppHomeOpenedEvent
61
68
  interaction MySlackBot::AppHomeInteraction
69
+ block_action :approve_request, MySlackBot::ApproveRequestInteraction
62
70
 
63
71
  # Register slash command handlers
64
72
  slash_command_endpoint :game, MySlackBot::Game::MenuCommand do
@@ -74,11 +82,6 @@ class SlackBotApi < Grape::API
74
82
  SlackBot::Config.current_instance
75
83
  end
76
84
 
77
- def resolve_user_session(team_id, user_id)
78
- uid = OmniAuth::Strategies::SlackOpenid.generate_uid(team_id, user_id)
79
- UserSession.find_by(uid: uid, provider: UserSession.slack_openid_provider)
80
- end
81
-
82
85
  def current_user_session
83
86
  # NOTE: fetch_team_id and fetch_user_id are provided by SlackBot::GrapeHelpers
84
87
  @current_user_session ||=
@@ -509,11 +512,16 @@ The gem implements Slack's signature verification with the following security fe
509
512
 
510
513
  ## Development
511
514
 
512
- ```bash
515
+ ```sh
513
516
  bundle install
514
- bundle exec rspec
515
- bundle exec rbs validate
516
- bundle exec standardrb --fix
517
+ bundle exec polyrun hook run before_suite
518
+ bundle exec polyrun parallel-rspec --workers 1 --merge-failures
519
+ ```
520
+
521
+ To generate a local coverage report with Polyrun:
522
+
523
+ ```sh
524
+ POLYRUN_COVERAGE=1 bundle exec polyrun parallel-rspec --workers 1 --merge-failures
517
525
  ```
518
526
 
519
527
  For development and testing purposes you can use [Cloudflare Argo Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps) to expose your local development environment to the internet.
@@ -528,7 +536,7 @@ For easiness of getting information, most of endpoints have `SlackBot::DevConsol
528
536
 
529
537
  ### Code Quality
530
538
 
531
- The gem uses [StandardRB](https://github.com/standardrb/standard) for consistent code style. Run `bundle exec standardrb --fix` to automatically fix style issues.
539
+ The gem uses a StandardRB-compatible RuboCop configuration for consistent code style, with Polyrun orchestrating the lint and type-check hooks. Run `bundle exec polyrun hook run before_suite` to execute the configured checks.
532
540
 
533
541
  The gem includes [RBS](https://github.com/ruby/rbs) type signatures in the `sig/` directory for better type checking and IDE support. Type signatures are included in the gem package.
534
542
 
@@ -557,7 +565,7 @@ gem push grape-slack-bot-*.gem
557
565
  Or use the release script:
558
566
 
559
567
  ```sh
560
- usr/bin/release.sh
568
+ usr/bin/release.rb
561
569
  ```
562
570
 
563
571
  ## License
@@ -23,26 +23,29 @@ Gem::Specification.new do |gem|
23
23
  "rubygems_mfa_required" => "true"
24
24
  }
25
25
 
26
- gem.executables = Dir.glob("bin/*").map { |f| File.basename(f) }
27
- gem.files = Dir.glob("lib/**/*.rb") + Dir.glob("bin/**/*") + Dir.glob("sig/**/*.rbs") + root_files
26
+ gem.files = Dir.glob("lib/**/*.rb") + Dir.glob("sig/**/*.rbs") + root_files
28
27
 
29
28
  gem.required_ruby_version = ">= 3"
30
29
  gem.require_paths = ["lib"]
31
30
 
32
31
  gem.add_runtime_dependency "rack", "~> 3.0"
33
- gem.add_runtime_dependency "grape", ">= 1.6", "< 3.0"
34
- gem.add_runtime_dependency "faraday", "~> 2.0"
32
+ gem.add_runtime_dependency "grape", ">= 1.6", "< 4.0"
33
+ gem.add_runtime_dependency "faraday", ">= 2.14.3", "< 3.0"
35
34
  gem.add_runtime_dependency "activesupport", ">= 6.1", "< 9.0"
36
35
 
37
- gem.add_development_dependency "rspec", "~> 3.12"
36
+ gem.add_development_dependency "rspec", "~> 3"
37
+ gem.add_development_dependency "polyrun", "~> 1.5.0"
38
38
  gem.add_development_dependency "webmock", "~> 3"
39
- gem.add_development_dependency "rake", "~> 13.0"
40
- gem.add_development_dependency "simplecov", "~> 0.21"
41
- gem.add_development_dependency "rspec_junit_formatter", "~> 0.6"
42
- gem.add_development_dependency "simplecov-cobertura", "~> 3"
43
- gem.add_development_dependency "standard", "~> 1.0"
44
- gem.add_development_dependency "appraisal", "~> 2.4"
45
- gem.add_development_dependency "memory_profiler", "~> 1.0"
46
- gem.add_development_dependency "rbs", "~> 3.0"
47
- gem.add_development_dependency "rack-test", "~> 2.0"
39
+ gem.add_development_dependency "rake", "~> 13"
40
+ gem.add_development_dependency "standard", "~> 1.52"
41
+ gem.add_development_dependency "standard-custom", "~> 1.0"
42
+ gem.add_development_dependency "standard-performance", "~> 1.8"
43
+ gem.add_development_dependency "standard-rspec", "~> 0.3"
44
+ gem.add_development_dependency "rubocop-rspec", "~> 3.8"
45
+ gem.add_development_dependency "rubocop-thread_safety", "~> 0.7"
46
+ gem.add_development_dependency "appraisal", "~> 2"
47
+ gem.add_development_dependency "bundler-audit", "~> 0.9"
48
+ gem.add_development_dependency "memory_profiler", "~> 1"
49
+ gem.add_development_dependency "rbs", "~> 3"
50
+ gem.add_development_dependency "rack-test", "~> 2"
48
51
  end
@@ -2,7 +2,23 @@ require "faraday"
2
2
 
3
3
  module SlackBot
4
4
  class ApiResponse
5
+ # Cap in-request sleep so rate-limit retries do not hold web workers for Slack's default Retry-After.
6
+ RATE_LIMIT_RETRY_SLEEP_SECONDS = 3
7
+
5
8
  attr_reader :response
9
+
10
+ def self.from_request(&block)
11
+ response = new(&block)
12
+ return response unless response.rate_limited?
13
+
14
+ Kernel.sleep(rate_limit_retry_sleep_seconds(response.retry_after))
15
+ new(&block)
16
+ end
17
+
18
+ def self.rate_limit_retry_sleep_seconds(retry_after)
19
+ [retry_after.to_i, RATE_LIMIT_RETRY_SLEEP_SECONDS].min
20
+ end
21
+
6
22
  def initialize(&block)
7
23
  @response = block.call
8
24
  SlackBot::DevConsole.log_output "#{self.class.name}: #{response.body}"
@@ -47,76 +63,37 @@ module SlackBot
47
63
  REQUEST_TIMEOUT = 30
48
64
  # Connection timeout in seconds
49
65
  CONNECTION_TIMEOUT = 10
66
+ TOKEN_FORMAT = /\Axox[bpa]-/
50
67
 
51
68
  attr_reader :client
52
- def initialize(authorization_token: ENV["SLACK_BOT_API_TOKEN"])
53
- authorization_token_available = !authorization_token.nil? && authorization_token.is_a?(String) && !authorization_token.empty?
54
- unless authorization_token_available
55
- raise SlackBot::Errors::SlackApiError.new("Slack bot API token is not set")
56
- end
57
-
58
- # Validate token format
59
- # Bot tokens: xoxb-, User tokens: xoxp-, App tokens: xoxa-
60
- # For this gem, we primarily expect bot tokens (xoxb-)
61
- # Allow test tokens (starting with "test_") for testing purposes
62
- unless authorization_token.start_with?("test_") || authorization_token.match?(/\Axox[bpa]-/)
63
- raise SlackBot::Errors::SlackApiError.new("Invalid Slack API token format")
64
- end
65
69
 
66
- @client =
67
- Faraday.new do |conn|
68
- conn.request :url_encoded
69
- conn.adapter Faraday.default_adapter
70
- conn.url_prefix = SLACK_API_BASE_URL
71
- conn.headers["Content-Type"] = "application/json; charset=utf-8"
72
- conn.headers["Authorization"] = "Bearer #{authorization_token}"
73
- conn.options.timeout = REQUEST_TIMEOUT
74
- conn.options.open_timeout = CONNECTION_TIMEOUT
75
- end
70
+ def initialize(authorization_token: ENV["SLACK_BOT_API_TOKEN"])
71
+ validate_authorization_token!(authorization_token)
72
+ @client = build_client(authorization_token)
76
73
  end
77
74
 
78
75
  def views_open(trigger_id:, view:)
79
- ApiResponse.new do
80
- client.post("views.open", {trigger_id: trigger_id, view: view}.to_json)
81
- rescue Faraday::Error => e
82
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
83
- end
76
+ perform_request { client.post("views.open", {trigger_id: trigger_id, view: view}.to_json) }
84
77
  end
85
78
 
86
79
  def views_update(view_id:, view:)
87
- ApiResponse.new do
88
- client.post("views.update", {view_id: view_id, view: view}.to_json)
89
- rescue Faraday::Error => e
90
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
91
- end
80
+ perform_request { client.post("views.update", {view_id: view_id, view: view}.to_json) }
92
81
  end
93
82
 
94
83
  def chat_post_message(channel:, text:, blocks:)
95
- ApiResponse.new do
96
- client.post("chat.postMessage", {channel: channel, text: text, blocks: blocks}.to_json)
97
- rescue Faraday::Error => e
98
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
99
- end
84
+ perform_request { client.post("chat.postMessage", {channel: channel, text: text, blocks: blocks}.to_json) }
100
85
  end
101
86
 
102
87
  def chat_update(channel:, ts:, text:, blocks:)
103
- ApiResponse.new do
104
- client.post("chat.update", {channel: channel, ts: ts, text: text, blocks: blocks}.to_json)
105
- rescue Faraday::Error => e
106
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
107
- end
88
+ perform_request { client.post("chat.update", {channel: channel, ts: ts, text: text, blocks: blocks}.to_json) }
108
89
  end
109
90
 
110
91
  def chat_delete(channel:, ts:)
111
- ApiResponse.new do
112
- client.post("chat.delete", {channel: channel, ts: ts}.to_json)
113
- rescue Faraday::Error => e
114
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
115
- end
92
+ perform_request { client.post("chat.delete", {channel: channel, ts: ts}.to_json) }
116
93
  end
117
94
 
118
95
  def chat_unfurl(channel:, ts:, unfurls:, source: nil, unfurl_id: nil, user_auth_blocks: nil, user_auth_message: nil, user_auth_required: nil, user_auth_url: nil)
119
- ApiResponse.new do
96
+ perform_request do
120
97
  client.post("chat.unfurl", {
121
98
  channel: channel,
122
99
  ts: ts,
@@ -128,98 +105,92 @@ module SlackBot
128
105
  user_auth_required: user_auth_required,
129
106
  user_auth_url: user_auth_url
130
107
  }.to_json)
131
- rescue Faraday::Error => e
132
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
133
108
  end
134
109
  end
135
110
 
136
111
  def chat_schedule_message(channel:, text:, post_at:, blocks: nil)
137
- ApiResponse.new do
138
- client.post("chat.scheduleMessage", {channel: channel, text: text, post_at: post_at, blocks: blocks}.to_json)
139
- rescue Faraday::Error => e
140
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
141
- end
112
+ perform_request { client.post("chat.scheduleMessage", {channel: channel, text: text, post_at: post_at, blocks: blocks}.to_json) }
142
113
  end
143
114
 
144
115
  def scheduled_messages_list(channel: nil, cursor: nil, latest: nil, limit: nil, oldest: nil, team_id: nil)
145
- ApiResponse.new do
146
- client.post("scheduled_messages.list", {
147
- channel: channel,
148
- cursor: cursor,
149
- latest: latest,
150
- limit: limit,
151
- oldest: oldest,
152
- team_id: team_id
153
- }.to_json)
154
- rescue Faraday::Error => e
155
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
156
- end
116
+ args = compact_payload(channel: channel, cursor: cursor, limit: limit, latest: latest, oldest: oldest, team_id: team_id)
117
+ perform_request { client.post("scheduled_messages.list", args.to_json) }
157
118
  end
158
119
 
159
120
  def chat_delete_scheduled_message(channel:, scheduled_message_id:)
160
- ApiResponse.new do
161
- client.post("chat.deleteScheduledMessage", {channel: channel, scheduled_message_id: scheduled_message_id}.to_json)
162
- rescue Faraday::Error => e
163
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
164
- end
121
+ perform_request { client.post("chat.deleteScheduledMessage", {channel: channel, scheduled_message_id: scheduled_message_id}.to_json) }
165
122
  end
166
123
 
167
124
  def chat_get_permalink(channel:, message_ts:)
168
- ApiResponse.new do
169
- client.post("chat.getPermalink", {channel: channel, message_ts: message_ts}.to_json)
170
- rescue Faraday::Error => e
171
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
172
- end
125
+ perform_request { client.post("chat.getPermalink", {channel: channel, message_ts: message_ts}.to_json) }
173
126
  end
174
127
 
175
128
  def users_info(user_id:)
176
- ApiResponse.new do
177
- client.post("users.info", {user: user_id}.to_json)
178
- rescue Faraday::Error => e
179
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
180
- end
129
+ perform_request { client.post("users.info", {user: user_id}.to_json) }
181
130
  end
182
131
 
183
132
  def views_publish(user_id:, view:)
184
- ApiResponse.new do
185
- client.post("views.publish", {user_id: user_id, view: view}.to_json)
186
- rescue Faraday::Error => e
187
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
188
- end
133
+ perform_request { client.post("views.publish", {user_id: user_id, view: view}.to_json) }
189
134
  end
190
135
 
191
136
  def users_list(cursor: nil, limit: 200, include_locale: nil, team_id: nil)
192
- args = {}
193
- args[:cursor] = cursor if cursor
194
- args[:limit] = limit if limit
195
- args[:include_locale] = include_locale if include_locale
196
- args[:team_id] = team_id if team_id
197
- ApiResponse.new do
198
- client.post("users.list", args.to_json)
199
- rescue Faraday::Error => e
200
- raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
201
- end
137
+ args = compact_payload(cursor: cursor, limit: limit, include_locale: include_locale, team_id: team_id)
138
+ perform_request { client.post("users.list", args.to_json) }
202
139
  end
203
140
 
204
141
  def chat_post_ephemeral(channel:, user:, text:, as_user: nil, attachments: nil, blocks: nil, icon_emoji: nil, icon_url: nil, link_names: nil, parse: nil, thread_ts: nil, username: nil)
205
- args = {}
206
- args[:channel] = channel
207
- args[:user] = user
208
- args[:text] = text if text
209
- args[:as_user] = as_user if as_user
210
- args[:attachments] = attachments if attachments
211
- args[:blocks] = blocks if blocks
212
- args[:icon_emoji] = icon_emoji if icon_emoji
213
- args[:icon_url] = icon_url if icon_url
214
- args[:link_names] = link_names if link_names
215
- args[:parse] = parse if parse
216
- args[:thread_ts] = thread_ts if thread_ts
217
- args[:username] = username if username
218
- ApiResponse.new do
219
- client.post("chat.postEphemeral", args.to_json)
142
+ args = compact_payload(
143
+ channel: channel,
144
+ user: user,
145
+ text: text,
146
+ as_user: as_user,
147
+ attachments: attachments,
148
+ blocks: blocks,
149
+ icon_emoji: icon_emoji,
150
+ icon_url: icon_url,
151
+ link_names: link_names,
152
+ parse: parse,
153
+ thread_ts: thread_ts,
154
+ username: username
155
+ )
156
+ perform_request { client.post("chat.postEphemeral", args.to_json) }
157
+ end
158
+
159
+ private
160
+
161
+ def perform_request(&block)
162
+ ApiResponse.from_request do
163
+ block.call
220
164
  rescue Faraday::Error => e
221
165
  raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
222
166
  end
223
167
  end
168
+
169
+ def validate_authorization_token!(authorization_token)
170
+ raise SlackBot::Errors::SlackApiError.new("Slack bot API token is not set") unless valid_authorization_token?(authorization_token)
171
+ return if authorization_token.start_with?("test_") || authorization_token.match?(TOKEN_FORMAT)
172
+
173
+ raise SlackBot::Errors::SlackApiError.new("Invalid Slack API token format")
174
+ end
175
+
176
+ def valid_authorization_token?(authorization_token)
177
+ authorization_token.is_a?(String) && !authorization_token.empty?
178
+ end
179
+
180
+ def build_client(authorization_token)
181
+ Faraday.new do |conn|
182
+ conn.request :url_encoded
183
+ conn.adapter Faraday.default_adapter
184
+ conn.url_prefix = SLACK_API_BASE_URL
185
+ conn.headers["Content-Type"] = "application/json; charset=utf-8"
186
+ conn.headers["Authorization"] = "Bearer #{authorization_token}"
187
+ conn.options.timeout = REQUEST_TIMEOUT
188
+ conn.options.open_timeout = CONNECTION_TIMEOUT
189
+ end
190
+ end
191
+
192
+ def compact_payload(**payload)
193
+ payload.compact
194
+ end
224
195
  end
225
196
  end
@@ -94,7 +94,7 @@ module SlackBot
94
94
  def user
95
95
  @user ||= begin
96
96
  user_id = data&.dig(:user_id)
97
- config.callback_user_finder_method.call(user_id) if user_id.present?
97
+ config.callback_user_finder!.call(user_id) if user_id.present?
98
98
  end
99
99
  end
100
100
 
@@ -137,7 +137,7 @@ module SlackBot
137
137
 
138
138
  def handler_class=(handler_class)
139
139
  new_class_name = handler_class&.name
140
- config.find_handler_class(class_name)
140
+ config.find_handler_class(new_class_name)
141
141
 
142
142
  self.class_name = new_class_name
143
143
  end
@@ -162,7 +162,7 @@ module SlackBot
162
162
  def read_view_callback_id
163
163
  return if view_id.blank?
164
164
 
165
- config.callback_storage_instance.read(view_storage_key)
165
+ config.callback_storage!.read(view_storage_key)
166
166
  end
167
167
 
168
168
  private
@@ -196,18 +196,18 @@ module SlackBot
196
196
  end
197
197
 
198
198
  def read_data
199
- config.callback_storage_instance.read(storage_key)
199
+ config.callback_storage!.read(storage_key)
200
200
  end
201
201
 
202
202
  def write_data(data, expires_in: nil)
203
203
  expires_in ||= CALLBACK_RECORD_EXPIRES_IN
204
- config.callback_storage_instance.write(view_storage_key, id, expires_in: expires_in) if view_id.present?
205
- config.callback_storage_instance.write(storage_key, data, expires_in: expires_in)
204
+ config.callback_storage!.write(view_storage_key, id, expires_in: expires_in) if view_id.present?
205
+ config.callback_storage!.write(storage_key, data, expires_in: expires_in)
206
206
  end
207
207
 
208
208
  def delete_data
209
- config.callback_storage_instance.delete(view_storage_key) if view_id.present?
210
- config.callback_storage_instance.delete(storage_key)
209
+ config.callback_storage!.delete(view_storage_key) if view_id.present?
210
+ config.callback_storage!.delete(storage_key)
211
211
  end
212
212
  end
213
213
  end
@@ -17,11 +17,45 @@ module SlackBot
17
17
  @callback_storage_instance = klass
18
18
  end
19
19
 
20
+ def callback_storage!
21
+ return @callback_storage_instance if @callback_storage_instance
22
+
23
+ raise SlackBot::Errors::ConfigurationError.new(
24
+ "callback_storage is not configured; call SlackBot::Config.configure { |config| config.callback_storage ... }"
25
+ )
26
+ end
27
+
20
28
  attr_reader :callback_user_finder_method
21
29
  def callback_user_finder(method_lambda)
22
30
  @callback_user_finder_method = method_lambda
23
31
  end
24
32
 
33
+ def callback_user_finder!
34
+ return @callback_user_finder_method if @callback_user_finder_method
35
+
36
+ raise SlackBot::Errors::ConfigurationError.new(
37
+ "callback_user_finder is not configured; call SlackBot::Config.configure { |config| config.callback_user_finder ... }"
38
+ )
39
+ end
40
+
41
+ attr_reader :user_session_resolver_method
42
+ def user_session_resolver(method_lambda)
43
+ @user_session_resolver_method = method_lambda
44
+ end
45
+
46
+ def user_session_resolver!
47
+ return @user_session_resolver_method if @user_session_resolver_method
48
+
49
+ raise SlackBot::Errors::ConfigurationError.new(
50
+ "user_session_resolver is not configured; call SlackBot::Config.configure { |config| config.user_session_resolver ... }"
51
+ )
52
+ end
53
+
54
+ attr_reader :event_dispatcher_method
55
+ def event_dispatcher(method_lambda)
56
+ @event_dispatcher_method = method_lambda
57
+ end
58
+
25
59
  def interaction(interaction_klass, handler_name: nil)
26
60
  handler_name ||= interaction_klass.name
27
61
  handler_class(handler_name, interaction_klass)
@@ -78,6 +112,17 @@ module SlackBot
78
112
  @menu_options[action_id.to_sym]
79
113
  end
80
114
 
115
+ def block_action(action_id, interaction_klass, handler_name: nil)
116
+ @block_actions ||= {}
117
+ @block_actions[action_id.to_sym] = interaction_klass
118
+ interaction(interaction_klass, handler_name: handler_name)
119
+ end
120
+
121
+ def find_block_action(action_id)
122
+ @block_actions ||= {}
123
+ @block_actions[action_id.to_sym]
124
+ end
125
+
81
126
  def handler_class(class_name, klass)
82
127
  @handler_classes ||= {}
83
128
  return if class_name.nil?
@@ -130,7 +175,10 @@ module SlackBot
130
175
  end
131
176
 
132
177
  def find_command_config(text)
133
- route_key = text.scan(/^(#{routes.keys.join("|")})(?:\s|$)/).flatten.first
178
+ return if routes.blank?
179
+
180
+ route_pattern = routes.keys.map { |route_key| Regexp.escape(route_key) }.join("|")
181
+ route_key = text.scan(/^(#{route_pattern})(?:\s|$)/).flatten.first
134
182
  return if route_key.blank?
135
183
 
136
184
  routes[route_key]
@@ -18,6 +18,12 @@ module SlackBot
18
18
  class MenuOptionsNotImplemented < SlackBot::Error
19
19
  end
20
20
 
21
+ class BlockActionNotImplemented < SlackBot::Error
22
+ def initialize
23
+ super("Block action is not implemented")
24
+ end
25
+ end
26
+
21
27
  class CallbackNotFound < SlackBot::Error
22
28
  end
23
29
 
@@ -82,5 +88,8 @@ module SlackBot
82
88
 
83
89
  class NotImplementedError < SlackBot::Error
84
90
  end
91
+
92
+ class ConfigurationError < SlackBot::Error
93
+ end
85
94
  end
86
95
  end