grape-slack-bot 2.0.0 → 2.0.1

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: 4d79cd991f4efaa9ae3319b25d73e0addde71aa04d59369236225f1bb97e0b4e
4
+ data.tar.gz: 402ede62474860b3009c44107f7e54fba9de531c992315eba021bc84a3a2c21e
5
5
  SHA512:
6
- metadata.gz: 3217a763efe6f6b101067c422029450e59efee9ea47d611bef1bd06a166a99404c1243955a3e42a5e9f31a78e9c3cbdd051d324fcd3b0b2414812da3d84f7336
7
- data.tar.gz: 3faca964608cb59b26ab6712c4eec82356f0ce761adfb4811d282e7d0051f9cdc58ac6958634feef2e24563b800a38259756a5847e0c6b0d8a3b2b6c62cb74b5
6
+ metadata.gz: c1bf71eedfc65e15481d6f06ed038bfd637f1ef0784c8040a482386f20087c98c53c435bd43f3b62eed9ac82482c36a35c70f87920e40372eefca45d2f783243
7
+ data.tar.gz: daaded041bc84bac15c0c20bc04709df914ab895b222244d05d1aef9f568e7b63ed1ce6475a0a027a8601bcf597a91cb1a97ef1a7e840456f49c3e5104e4549c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.0.1 (2026-05-31)
4
+
5
+ - Allow `grape` 3.x by expanding the runtime dependency to `< 4.0`
6
+ - Replace SimpleCov and JUnit formatter test wiring with Polyrun coverage and failure reporting
7
+ - Move lint and type-check execution into Polyrun `before_suite` hooks
8
+ - Refresh development and release tooling, CI, and repository maintenance configuration
9
+
3
10
  ## 2.0.0 (2025-11-06)
4
11
 
5
12
  - 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
 
@@ -509,11 +509,16 @@ The gem implements Slack's signature verification with the following security fe
509
509
 
510
510
  ## Development
511
511
 
512
- ```bash
512
+ ```sh
513
513
  bundle install
514
- bundle exec rspec
515
- bundle exec rbs validate
516
- bundle exec standardrb --fix
514
+ bundle exec polyrun hook run before_suite
515
+ bundle exec polyrun parallel-rspec --workers 1 --merge-failures
516
+ ```
517
+
518
+ To generate a local coverage report with Polyrun:
519
+
520
+ ```sh
521
+ POLYRUN_COVERAGE=1 bundle exec polyrun parallel-rspec --workers 1 --merge-failures
517
522
  ```
518
523
 
519
524
  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 +533,7 @@ For easiness of getting information, most of endpoints have `SlackBot::DevConsol
528
533
 
529
534
  ### Code Quality
530
535
 
531
- The gem uses [StandardRB](https://github.com/standardrb/standard) for consistent code style. Run `bundle exec standardrb --fix` to automatically fix style issues.
536
+ 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
537
 
533
538
  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
539
 
@@ -557,7 +562,7 @@ gem push grape-slack-bot-*.gem
557
562
  Or use the release script:
558
563
 
559
564
  ```sh
560
- usr/bin/release.sh
565
+ usr/bin/release.rb
561
566
  ```
562
567
 
563
568
  ## License
@@ -30,19 +30,22 @@ Gem::Specification.new do |gem|
30
30
  gem.require_paths = ["lib"]
31
31
 
32
32
  gem.add_runtime_dependency "rack", "~> 3.0"
33
- gem.add_runtime_dependency "grape", ">= 1.6", "< 3.0"
33
+ gem.add_runtime_dependency "grape", ">= 1.6", "< 4.0"
34
34
  gem.add_runtime_dependency "faraday", "~> 2.0"
35
35
  gem.add_runtime_dependency "activesupport", ">= 6.1", "< 9.0"
36
36
 
37
- gem.add_development_dependency "rspec", "~> 3.12"
37
+ gem.add_development_dependency "rspec", "~> 3"
38
+ gem.add_development_dependency "polyrun", "~> 1.4.2"
38
39
  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"
40
+ gem.add_development_dependency "rake", "~> 13"
41
+ gem.add_development_dependency "standard", "~> 1.52"
42
+ gem.add_development_dependency "standard-custom", "~> 1.0"
43
+ gem.add_development_dependency "standard-performance", "~> 1.8"
44
+ gem.add_development_dependency "standard-rspec", "~> 0.3"
45
+ gem.add_development_dependency "rubocop-rspec", "~> 3.8"
46
+ gem.add_development_dependency "rubocop-thread_safety", "~> 0.7"
47
+ gem.add_development_dependency "appraisal", "~> 2"
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
@@ -47,32 +47,13 @@ module SlackBot
47
47
  REQUEST_TIMEOUT = 30
48
48
  # Connection timeout in seconds
49
49
  CONNECTION_TIMEOUT = 10
50
+ TOKEN_FORMAT = /\Axox[bpa]-/
50
51
 
51
52
  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
53
 
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
54
+ def initialize(authorization_token: ENV["SLACK_BOT_API_TOKEN"])
55
+ validate_authorization_token!(authorization_token)
56
+ @client = build_client(authorization_token)
76
57
  end
77
58
 
78
59
  def views_open(trigger_id:, view:)
@@ -189,11 +170,7 @@ module SlackBot
189
170
  end
190
171
 
191
172
  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
173
+ args = compact_payload(cursor: cursor, limit: limit, include_locale: include_locale, team_id: team_id)
197
174
  ApiResponse.new do
198
175
  client.post("users.list", args.to_json)
199
176
  rescue Faraday::Error => e
@@ -202,24 +179,54 @@ module SlackBot
202
179
  end
203
180
 
204
181
  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
182
+ args = compact_payload(
183
+ channel: channel,
184
+ user: user,
185
+ text: text,
186
+ as_user: as_user,
187
+ attachments: attachments,
188
+ blocks: blocks,
189
+ icon_emoji: icon_emoji,
190
+ icon_url: icon_url,
191
+ link_names: link_names,
192
+ parse: parse,
193
+ thread_ts: thread_ts,
194
+ username: username
195
+ )
218
196
  ApiResponse.new do
219
197
  client.post("chat.postEphemeral", args.to_json)
220
198
  rescue Faraday::Error => e
221
199
  raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
222
200
  end
223
201
  end
202
+
203
+ private
204
+
205
+ def validate_authorization_token!(authorization_token)
206
+ raise SlackBot::Errors::SlackApiError.new("Slack bot API token is not set") unless valid_authorization_token?(authorization_token)
207
+ return if authorization_token.start_with?("test_") || authorization_token.match?(TOKEN_FORMAT)
208
+
209
+ raise SlackBot::Errors::SlackApiError.new("Invalid Slack API token format")
210
+ end
211
+
212
+ def valid_authorization_token?(authorization_token)
213
+ authorization_token.is_a?(String) && !authorization_token.empty?
214
+ end
215
+
216
+ def build_client(authorization_token)
217
+ Faraday.new do |conn|
218
+ conn.request :url_encoded
219
+ conn.adapter Faraday.default_adapter
220
+ conn.url_prefix = SLACK_API_BASE_URL
221
+ conn.headers["Content-Type"] = "application/json; charset=utf-8"
222
+ conn.headers["Authorization"] = "Bearer #{authorization_token}"
223
+ conn.options.timeout = REQUEST_TIMEOUT
224
+ conn.options.open_timeout = CONNECTION_TIMEOUT
225
+ end
226
+ end
227
+
228
+ def compact_payload(**payload)
229
+ payload.compact
230
+ end
224
231
  end
225
232
  end
@@ -19,49 +19,12 @@ module SlackBot
19
19
 
20
20
  def verify_slack_signature!
21
21
  slack_signing_secret = ENV["SLACK_SIGNING_SECRET"]
22
- timestamp = request.headers["x-slack-request-timestamp"] || request.headers["X-Slack-Request-Timestamp"]
23
- slack_signature = request.headers["x-slack-signature"] || request.headers["X-Slack-Signature"]
24
- if slack_signing_secret.blank? || timestamp.blank? || slack_signature.blank?
25
- raise SlackBot::Errors::SignatureAuthenticationError.new("Missing signature headers")
26
- end
27
-
28
- # Validate signing secret format (allow test secrets for testing)
29
- unless slack_signing_secret.start_with?("test_") || slack_signing_secret.length >= MIN_SIGNING_SECRET_LENGTH
30
- raise SlackBot::Errors::SignatureAuthenticationError.new("Invalid signing secret format")
31
- end
22
+ timestamp = slack_request_header("x-slack-request-timestamp", "X-Slack-Request-Timestamp")
23
+ slack_signature = slack_request_header("x-slack-signature", "X-Slack-Signature")
32
24
 
33
- # Validate timestamp to prevent replay attacks (Slack recommends 5 minutes)
34
- request_timestamp = timestamp.to_i
35
- current_timestamp = Time.now.to_i
36
- if (current_timestamp - request_timestamp).abs > TIMESTAMP_TOLERANCE_SECONDS
37
- raise SlackBot::Errors::SignatureAuthenticationError.new("Request timestamp too old")
38
- end
39
-
40
- request_body = if request.body
41
- request.body.rewind if request.body.respond_to?(:rewind)
42
- body_content = request.body.read
43
- request.body.rewind if request.body.respond_to?(:rewind)
44
- body_content
45
- else
46
- ""
47
- end
48
-
49
- sig_basestring = "v0:#{timestamp}:#{request_body}"
50
- my_signature =
51
- "v0=" +
52
- OpenSSL::HMAC.hexdigest(
53
- OpenSSL::Digest.new("sha256"),
54
- slack_signing_secret,
55
- sig_basestring
56
- )
57
- if ActiveSupport::SecurityUtils.secure_compare(
58
- my_signature,
59
- slack_signature
60
- )
61
- true
62
- else
63
- raise SlackBot::Errors::SignatureAuthenticationError.new("Signature mismatch")
64
- end
25
+ validate_signature_headers!(slack_signing_secret, timestamp, slack_signature)
26
+ validate_request_timestamp!(timestamp)
27
+ verify_signature_match!(slack_signing_secret, timestamp, slack_signature)
65
28
  end
66
29
 
67
30
  def verify_slack_team!
@@ -112,41 +75,117 @@ module SlackBot
112
75
  end
113
76
 
114
77
  def handle_block_actions_view(view:, user:, params:)
115
- callback_id = view&.dig("callback_id")
78
+ callback = find_callback!(view: view, user: user)
79
+ log_callback_check(callback, user)
80
+ validate_callback_user!(callback, user)
81
+
82
+ interaction_klass = callback_interaction_klass(callback)
83
+ return false if interaction_klass.blank?
84
+
85
+ interaction_klass.new(current_user: user, params: params, callback: callback, config: config).call
86
+ end
87
+
88
+ private
89
+
90
+ def slack_request_header(*names)
91
+ names.each do |name|
92
+ header = request.headers[name]
93
+ return header if header
94
+ end
95
+
96
+ nil
97
+ end
98
+
99
+ def validate_signature_headers!(slack_signing_secret, timestamp, slack_signature)
100
+ raise SlackBot::Errors::SignatureAuthenticationError.new("Missing signature headers") if slack_signing_secret.blank? || timestamp.blank? || slack_signature.blank?
101
+ return if slack_signing_secret.start_with?("test_") || slack_signing_secret.length >= MIN_SIGNING_SECRET_LENGTH
102
+
103
+ raise SlackBot::Errors::SignatureAuthenticationError.new("Invalid signing secret format")
104
+ end
105
+
106
+ def validate_request_timestamp!(timestamp)
107
+ request_timestamp = timestamp.to_i
108
+ current_timestamp = Time.now.to_i
109
+ return if (current_timestamp - request_timestamp).abs <= TIMESTAMP_TOLERANCE_SECONDS
110
+
111
+ raise SlackBot::Errors::SignatureAuthenticationError.new("Request timestamp too old")
112
+ end
113
+
114
+ def verify_signature_match!(slack_signing_secret, timestamp, slack_signature)
115
+ return true if ActiveSupport::SecurityUtils.secure_compare(computed_signature(slack_signing_secret, timestamp), slack_signature)
116
116
 
117
- callback = SlackBot::Callback.find(callback_id, user: user, config: config)
117
+ raise SlackBot::Errors::SignatureAuthenticationError.new("Signature mismatch")
118
+ end
119
+
120
+ def computed_signature(slack_signing_secret, timestamp)
121
+ sig_basestring = "v0:#{timestamp}:#{request_body_content}"
122
+ "v0=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), slack_signing_secret, sig_basestring)
123
+ end
124
+
125
+ def request_body_content
126
+ return "" unless request.body
127
+
128
+ request.body.rewind if request.body.respond_to?(:rewind)
129
+ body_content = request.body.read
130
+ request.body.rewind if request.body.respond_to?(:rewind)
131
+ body_content
132
+ end
133
+
134
+ def find_callback!(view:, user:)
135
+ callback = SlackBot::Callback.find(view&.dig("callback_id"), user: user, config: config)
118
136
  raise SlackBot::Errors::CallbackNotFound.new if callback.blank?
119
137
 
120
- SlackBot::DevConsole.log_check "SlackApi::Interactions##{__method__}: #{callback.id} #{callback.payload} #{callback.user_id} #{user&.id}"
138
+ callback
139
+ end
121
140
 
122
- validate_callback_user!(callback, user)
141
+ def log_callback_check(callback, user)
142
+ SlackBot::DevConsole.log_check "SlackApi::Interactions##{__method__}: #{callback.id} #{callback.payload} #{callback.user_id} #{user&.id}"
143
+ end
123
144
 
145
+ def callback_interaction_klass(callback)
124
146
  handler_class_obj = callback.handler_class
125
- interaction_klass = handler_class_obj&.interaction_klass if handler_class_obj&.respond_to?(:interaction_klass)
126
- return false if interaction_klass.blank?
147
+ handler_class_obj&.interaction_klass if handler_class_obj&.respond_to?(:interaction_klass)
148
+ end
127
149
 
128
- interaction_klass.new(current_user: user, params: params, callback: callback, config: config).call
150
+ def parse_interaction_payload!(raw_payload)
151
+ JSON.parse(raw_payload)
152
+ rescue JSON::ParserError => e
153
+ raise SlackBot::Errors::InvalidPayloadError.new("Invalid JSON payload: #{e.message}")
154
+ end
155
+
156
+ def resolve_action_user(payload)
157
+ resolve_user_session(payload.dig("user", "team_id"), payload.dig("user", "id"))
158
+ end
159
+
160
+ def blank_slack_response!
161
+ body false
162
+ status 200
129
163
  end
130
164
  end
131
165
 
132
166
  module GrapeExtension
133
167
  def self.included(base)
168
+ configure_base!(base)
169
+ add_commands_resource!(base)
170
+ add_interactions_resource!(base)
171
+ add_events_resource!(base)
172
+ add_menu_options_resource!(base)
173
+ end
174
+
175
+ def self.configure_base!(base)
134
176
  base.format :json
135
177
  base.content_type :json, "application/json"
136
178
  base.use ActionDispatch::RemoteIp if defined?(ActionDispatch::RemoteIp)
137
179
  base.helpers SlackBot::GrapeHelpers
138
-
139
- # Handle custom errors
140
- # Slack API requires 200 OK responses to avoid retries
141
- # Errors should be returned as 200 OK with error information in the response body
142
180
  base.rescue_from SlackBot::Error do |e|
143
181
  error!({error: e.message}, 200)
144
182
  end
145
-
146
183
  base.before do
147
184
  verify_slack_signature!
148
185
  end
186
+ end
149
187
 
188
+ def self.add_commands_resource!(base)
150
189
  base.resource :commands do
151
190
  post ":url_token" do
152
191
  command_config = config.find_slash_command_config(params[:url_token], params[:command], params[:text])
@@ -156,101 +195,68 @@ module SlackBot
156
195
  args = params[:text].gsub(/^#{command_config.full_token}\s?/, "")
157
196
  SlackBot::DevConsole.log_input "SlackApi::SlashCommands#post: #{command_config.url_token} | #{command_config.full_token} | #{args}"
158
197
 
159
- action =
160
- command_klass.new(
161
- current_user: current_user,
162
- params: params,
163
- args: args,
164
- config: config
165
- )
198
+ action = command_klass.new(current_user: current_user, params: params, args: args, config: config)
166
199
  verify_slack_team! if action.only_slack_team?
167
200
  verify_direct_message_channel! if action.only_direct_message?
168
201
  verify_current_user! if action.only_user?
169
202
 
170
203
  result = action.call
171
- if !result
172
- body false
173
- status 200
174
- return
175
- end
204
+ return blank_slack_response! unless result
176
205
 
177
206
  result
178
207
  end
179
208
  end
209
+ end
180
210
 
211
+ def self.add_interactions_resource!(base)
181
212
  base.resource :interactions do
182
213
  post do
183
- begin
184
- payload = JSON.parse(params[:payload])
185
- rescue JSON::ParserError => e
186
- raise SlackBot::Errors::InvalidPayloadError.new("Invalid JSON payload: #{e.message}")
187
- end
188
-
189
- action_user_session =
190
- resolve_user_session(
191
- payload.dig("user", "team_id"),
192
- payload.dig("user", "id")
193
- )
194
- action_user = action_user_session&.user
214
+ payload = parse_interaction_payload!(params[:payload])
215
+ action_user = resolve_action_user(payload)&.user
195
216
 
196
- action_type = payload["type"]
197
- result = case action_type
217
+ result = case payload["type"]
198
218
  when "block_actions", "view_submission"
199
- handle_block_actions_view(
200
- view: payload["view"],
201
- user: action_user,
202
- params: params
203
- )
219
+ handle_block_actions_view(view: payload["view"], user: action_user, params: params)
204
220
  else
205
- raise SlackBot::Errors::UnknownActionTypeError.new(action_type)
221
+ raise SlackBot::Errors::UnknownActionTypeError.new(payload["type"])
206
222
  end
207
223
 
208
- if result.blank? || result == false
209
- body false
210
- status 200
211
- return
212
- end
224
+ return blank_slack_response! if result.blank? || result == false
213
225
 
214
226
  result
215
227
  end
216
228
  end
229
+ end
217
230
 
231
+ def self.add_events_resource!(base)
218
232
  base.resource :events do
219
233
  post do
220
- result =
221
- case params[:type]
222
- when "url_verification"
223
- url_verification(params)
224
- when "event_callback"
225
- events_callback(params)
226
- else
227
- raise SlackBot::Errors::UnknownActionTypeError.new(params[:type])
228
- end
229
-
230
- if result.blank? || result == false
231
- body false
232
- status 200
233
- return
234
+ result = case params[:type]
235
+ when "url_verification"
236
+ url_verification(params)
237
+ when "event_callback"
238
+ events_callback(params)
239
+ else
240
+ raise SlackBot::Errors::UnknownActionTypeError.new(params[:type])
234
241
  end
235
242
 
243
+ return blank_slack_response! if result.blank? || result == false
244
+
236
245
  result
237
246
  end
238
247
  end
248
+ end
239
249
 
250
+ def self.add_menu_options_resource!(base)
240
251
  base.resource :menu_options do
241
252
  get do
242
253
  SlackBot::DevConsole.log_input "SlackApi::MenuOptions#get: #{params.inspect}"
243
254
 
244
- action_id = params[:action_id]
245
- menu_options_klass = config.find_menu_options(action_id)
255
+ menu_options_klass = config.find_menu_options(params[:action_id])
246
256
  raise SlackBot::Errors::MenuOptionsNotImplemented.new if menu_options_klass.blank?
247
257
 
248
258
  menu_options = menu_options_klass.new(current_user: current_user, params: params, config: config).call
249
- if menu_options.blank?
250
- body false
251
- status 200
252
- return
253
- end
259
+ return blank_slack_response! if menu_options.blank?
254
260
 
255
261
  menu_options
256
262
  end
@@ -10,55 +10,20 @@ module SlackBot
10
10
  include SlackBot::Concerns::ViewKlass
11
11
 
12
12
  def self.open_modal(callback:, trigger_id:, view:)
13
- view = view.merge({type: "modal", callback_id: callback&.id})
14
- response =
15
- SlackBot::ApiClient.new.views_open(trigger_id: trigger_id, view: view)
16
-
17
- if !response.ok?
18
- raise SlackBot::Errors::OpenModalError.new(response.error, data: response.data, payload: view)
19
- end
20
-
21
- view_id = response.data.dig("view", "id")
22
- if callback.present? && view_id.present?
23
- callback.view_id = view_id
24
- callback.save
25
- end
26
- SlackViewsReply.new(callback&.id, view_id)
13
+ view = modal_payload(callback, view)
14
+ response = SlackBot::ApiClient.new.views_open(trigger_id: trigger_id, view: view)
15
+ build_view_reply(response: response, callback: callback, payload: view, error_class: SlackBot::Errors::OpenModalError)
27
16
  end
28
17
 
29
18
  def self.update_modal(callback:, view_id:, view:)
30
- view = view.merge({type: "modal", callback_id: callback&.id})
31
- response =
32
- SlackBot::ApiClient.new.views_update(view_id: view_id, view: view)
33
-
34
- if !response.ok?
35
- raise SlackBot::Errors::UpdateModalError.new(response.error, data: response.data, payload: view)
36
- end
37
-
38
- view_id = response.data.dig("view", "id")
39
- if callback.present? && view_id.present?
40
- callback.view_id = view_id
41
- callback.save
42
- end
43
- SlackViewsReply.new(callback&.id, view_id)
19
+ view = modal_payload(callback, view)
20
+ response = SlackBot::ApiClient.new.views_update(view_id: view_id, view: view)
21
+ build_view_reply(response: response, callback: callback, payload: view, error_class: SlackBot::Errors::UpdateModalError)
44
22
  end
45
23
 
46
24
  def self.publish_view(user_id:, view:, callback: nil, metadata: nil)
47
- view = view.merge(callback_id: callback.id) if callback.present?
48
- view = view.merge(private_metadata: metadata) if metadata.present?
49
- response =
50
- SlackBot::ApiClient.new.views_publish(user_id: user_id, view: view)
51
-
52
- if !response.ok?
53
- raise SlackBot::Errors::PublishViewError.new(response.error, data: response.data, payload: view)
54
- end
55
-
56
- view_id = response.data.dig("view", "id")
57
- if callback.present? && view_id.present?
58
- callback.view_id = view_id
59
- callback.save
60
- end
61
- SlackViewsReply.new(callback&.id, view_id)
25
+ response = SlackBot::ApiClient.new.views_publish(user_id: user_id, view: publish_payload(callback, metadata, view))
26
+ build_view_reply(response: response, callback: callback, payload: view, error_class: SlackBot::Errors::PublishViewError)
62
27
  end
63
28
 
64
29
  attr_reader :current_user, :params, :callback, :config
@@ -73,6 +38,31 @@ module SlackBot
73
38
  nil
74
39
  end
75
40
 
41
+ def self.modal_payload(callback, view)
42
+ view.merge(type: "modal", callback_id: callback&.id)
43
+ end
44
+
45
+ def self.publish_payload(callback, metadata, view)
46
+ view = view.merge(callback_id: callback.id) if callback.present?
47
+ view = view.merge(private_metadata: metadata) if metadata.present?
48
+ view
49
+ end
50
+
51
+ def self.build_view_reply(response:, callback:, payload:, error_class:)
52
+ raise error_class.new(response.error, data: response.data, payload: payload) unless response.ok?
53
+
54
+ view_id = response.data.dig("view", "id")
55
+ persist_view_id(callback, view_id)
56
+ SlackViewsReply.new(callback&.id, view_id)
57
+ end
58
+
59
+ def self.persist_view_id(callback, view_id)
60
+ return unless callback.present? && view_id.present?
61
+
62
+ callback.view_id = view_id
63
+ callback.save
64
+ end
65
+
76
66
  private
77
67
 
78
68
  def interaction_type
data/lib/slack_bot.rb CHANGED
@@ -25,5 +25,5 @@ require "slack_bot/pager"
25
25
  require "slack_bot/grape_extension"
26
26
 
27
27
  module SlackBot
28
- VERSION = "2.0.0".freeze
28
+ VERSION = "2.0.1".freeze
29
29
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape-slack-bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '1.6'
33
33
  - - "<"
34
34
  - !ruby/object:Gem::Version
35
- version: '3.0'
35
+ version: '4.0'
36
36
  type: :runtime
37
37
  prerelease: false
38
38
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,7 +42,7 @@ dependencies:
42
42
  version: '1.6'
43
43
  - - "<"
44
44
  - !ruby/object:Gem::Version
45
- version: '3.0'
45
+ version: '4.0'
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: faraday
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -83,14 +83,28 @@ dependencies:
83
83
  requirements:
84
84
  - - "~>"
85
85
  - !ruby/object:Gem::Version
86
- version: '3.12'
86
+ version: '3'
87
87
  type: :development
88
88
  prerelease: false
89
89
  version_requirements: !ruby/object:Gem::Requirement
90
90
  requirements:
91
91
  - - "~>"
92
92
  - !ruby/object:Gem::Version
93
- version: '3.12'
93
+ version: '3'
94
+ - !ruby/object:Gem::Dependency
95
+ name: polyrun
96
+ requirement: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: 1.4.2
101
+ type: :development
102
+ prerelease: false
103
+ version_requirements: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: 1.4.2
94
108
  - !ruby/object:Gem::Dependency
95
109
  name: webmock
96
110
  requirement: !ruby/object:Gem::Requirement
@@ -111,126 +125,154 @@ dependencies:
111
125
  requirements:
112
126
  - - "~>"
113
127
  - !ruby/object:Gem::Version
114
- version: '13.0'
128
+ version: '13'
115
129
  type: :development
116
130
  prerelease: false
117
131
  version_requirements: !ruby/object:Gem::Requirement
118
132
  requirements:
119
133
  - - "~>"
120
134
  - !ruby/object:Gem::Version
121
- version: '13.0'
135
+ version: '13'
122
136
  - !ruby/object:Gem::Dependency
123
- name: simplecov
137
+ name: standard
124
138
  requirement: !ruby/object:Gem::Requirement
125
139
  requirements:
126
140
  - - "~>"
127
141
  - !ruby/object:Gem::Version
128
- version: '0.21'
142
+ version: '1.52'
129
143
  type: :development
130
144
  prerelease: false
131
145
  version_requirements: !ruby/object:Gem::Requirement
132
146
  requirements:
133
147
  - - "~>"
134
148
  - !ruby/object:Gem::Version
135
- version: '0.21'
149
+ version: '1.52'
136
150
  - !ruby/object:Gem::Dependency
137
- name: rspec_junit_formatter
151
+ name: standard-custom
138
152
  requirement: !ruby/object:Gem::Requirement
139
153
  requirements:
140
154
  - - "~>"
141
155
  - !ruby/object:Gem::Version
142
- version: '0.6'
156
+ version: '1.0'
143
157
  type: :development
144
158
  prerelease: false
145
159
  version_requirements: !ruby/object:Gem::Requirement
146
160
  requirements:
147
161
  - - "~>"
148
162
  - !ruby/object:Gem::Version
149
- version: '0.6'
163
+ version: '1.0'
150
164
  - !ruby/object:Gem::Dependency
151
- name: simplecov-cobertura
165
+ name: standard-performance
152
166
  requirement: !ruby/object:Gem::Requirement
153
167
  requirements:
154
168
  - - "~>"
155
169
  - !ruby/object:Gem::Version
156
- version: '3'
170
+ version: '1.8'
157
171
  type: :development
158
172
  prerelease: false
159
173
  version_requirements: !ruby/object:Gem::Requirement
160
174
  requirements:
161
175
  - - "~>"
162
176
  - !ruby/object:Gem::Version
163
- version: '3'
177
+ version: '1.8'
164
178
  - !ruby/object:Gem::Dependency
165
- name: standard
179
+ name: standard-rspec
166
180
  requirement: !ruby/object:Gem::Requirement
167
181
  requirements:
168
182
  - - "~>"
169
183
  - !ruby/object:Gem::Version
170
- version: '1.0'
184
+ version: '0.3'
171
185
  type: :development
172
186
  prerelease: false
173
187
  version_requirements: !ruby/object:Gem::Requirement
174
188
  requirements:
175
189
  - - "~>"
176
190
  - !ruby/object:Gem::Version
177
- version: '1.0'
191
+ version: '0.3'
192
+ - !ruby/object:Gem::Dependency
193
+ name: rubocop-rspec
194
+ requirement: !ruby/object:Gem::Requirement
195
+ requirements:
196
+ - - "~>"
197
+ - !ruby/object:Gem::Version
198
+ version: '3.8'
199
+ type: :development
200
+ prerelease: false
201
+ version_requirements: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - "~>"
204
+ - !ruby/object:Gem::Version
205
+ version: '3.8'
206
+ - !ruby/object:Gem::Dependency
207
+ name: rubocop-thread_safety
208
+ requirement: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - "~>"
211
+ - !ruby/object:Gem::Version
212
+ version: '0.7'
213
+ type: :development
214
+ prerelease: false
215
+ version_requirements: !ruby/object:Gem::Requirement
216
+ requirements:
217
+ - - "~>"
218
+ - !ruby/object:Gem::Version
219
+ version: '0.7'
178
220
  - !ruby/object:Gem::Dependency
179
221
  name: appraisal
180
222
  requirement: !ruby/object:Gem::Requirement
181
223
  requirements:
182
224
  - - "~>"
183
225
  - !ruby/object:Gem::Version
184
- version: '2.4'
226
+ version: '2'
185
227
  type: :development
186
228
  prerelease: false
187
229
  version_requirements: !ruby/object:Gem::Requirement
188
230
  requirements:
189
231
  - - "~>"
190
232
  - !ruby/object:Gem::Version
191
- version: '2.4'
233
+ version: '2'
192
234
  - !ruby/object:Gem::Dependency
193
235
  name: memory_profiler
194
236
  requirement: !ruby/object:Gem::Requirement
195
237
  requirements:
196
238
  - - "~>"
197
239
  - !ruby/object:Gem::Version
198
- version: '1.0'
240
+ version: '1'
199
241
  type: :development
200
242
  prerelease: false
201
243
  version_requirements: !ruby/object:Gem::Requirement
202
244
  requirements:
203
245
  - - "~>"
204
246
  - !ruby/object:Gem::Version
205
- version: '1.0'
247
+ version: '1'
206
248
  - !ruby/object:Gem::Dependency
207
249
  name: rbs
208
250
  requirement: !ruby/object:Gem::Requirement
209
251
  requirements:
210
252
  - - "~>"
211
253
  - !ruby/object:Gem::Version
212
- version: '3.0'
254
+ version: '3'
213
255
  type: :development
214
256
  prerelease: false
215
257
  version_requirements: !ruby/object:Gem::Requirement
216
258
  requirements:
217
259
  - - "~>"
218
260
  - !ruby/object:Gem::Version
219
- version: '3.0'
261
+ version: '3'
220
262
  - !ruby/object:Gem::Dependency
221
263
  name: rack-test
222
264
  requirement: !ruby/object:Gem::Requirement
223
265
  requirements:
224
266
  - - "~>"
225
267
  - !ruby/object:Gem::Version
226
- version: '2.0'
268
+ version: '2'
227
269
  type: :development
228
270
  prerelease: false
229
271
  version_requirements: !ruby/object:Gem::Requirement
230
272
  requirements:
231
273
  - - "~>"
232
274
  - !ruby/object:Gem::Version
233
- version: '2.0'
275
+ version: '2'
234
276
  description: Slack bot implementation for ruby-grape
235
277
  email:
236
278
  - contact@kiskolabs.com