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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +12 -7
- data/grape-slack-bot.gemspec +14 -11
- data/lib/slack_bot/api_client.rb +48 -41
- data/lib/slack_bot/grape_extension.rb +118 -112
- data/lib/slack_bot/interaction.rb +33 -43
- data/lib/slack_bot.rb +1 -1
- metadata +69 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d79cd991f4efaa9ae3319b25d73e0addde71aa04d59369236225f1bb97e0b4e
|
|
4
|
+
data.tar.gz: 402ede62474860b3009c44107f7e54fba9de531c992315eba021bc84a3a2c21e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
[](https://badge.fury.io/rb/grape-slack-bot) [](https://github.com/amkisko/grape-slack-bot.rb/actions/workflows/test.yml)
|
|
3
|
+
[](https://badge.fury.io/rb/grape-slack-bot) [](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
|
-
```
|
|
512
|
+
```sh
|
|
513
513
|
bundle install
|
|
514
|
-
bundle exec
|
|
515
|
-
bundle exec
|
|
516
|
-
|
|
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
|
|
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.
|
|
565
|
+
usr/bin/release.rb
|
|
561
566
|
```
|
|
562
567
|
|
|
563
568
|
## License
|
data/grape-slack-bot.gemspec
CHANGED
|
@@ -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", "<
|
|
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
|
|
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
|
|
40
|
-
gem.add_development_dependency "
|
|
41
|
-
gem.add_development_dependency "
|
|
42
|
-
gem.add_development_dependency "
|
|
43
|
-
gem.add_development_dependency "standard", "~>
|
|
44
|
-
gem.add_development_dependency "
|
|
45
|
-
gem.add_development_dependency "
|
|
46
|
-
gem.add_development_dependency "
|
|
47
|
-
gem.add_development_dependency "
|
|
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
|
data/lib/slack_bot/api_client.rb
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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 =
|
|
23
|
-
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
+
callback
|
|
139
|
+
end
|
|
121
140
|
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
147
|
+
handler_class_obj&.interaction_klass if handler_class_obj&.respond_to?(:interaction_klass)
|
|
148
|
+
end
|
|
127
149
|
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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 =
|
|
14
|
-
response =
|
|
15
|
-
|
|
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 =
|
|
31
|
-
response =
|
|
32
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
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.
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
135
|
+
version: '13'
|
|
122
136
|
- !ruby/object:Gem::Dependency
|
|
123
|
-
name:
|
|
137
|
+
name: standard
|
|
124
138
|
requirement: !ruby/object:Gem::Requirement
|
|
125
139
|
requirements:
|
|
126
140
|
- - "~>"
|
|
127
141
|
- !ruby/object:Gem::Version
|
|
128
|
-
version: '
|
|
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: '
|
|
149
|
+
version: '1.52'
|
|
136
150
|
- !ruby/object:Gem::Dependency
|
|
137
|
-
name:
|
|
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
|
|
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
|
|
163
|
+
version: '1.0'
|
|
150
164
|
- !ruby/object:Gem::Dependency
|
|
151
|
-
name:
|
|
165
|
+
name: standard-performance
|
|
152
166
|
requirement: !ruby/object:Gem::Requirement
|
|
153
167
|
requirements:
|
|
154
168
|
- - "~>"
|
|
155
169
|
- !ruby/object:Gem::Version
|
|
156
|
-
version: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
275
|
+
version: '2'
|
|
234
276
|
description: Slack bot implementation for ruby-grape
|
|
235
277
|
email:
|
|
236
278
|
- contact@kiskolabs.com
|