grape-slack-bot 1.8.2 → 2.0.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 +4 -4
- data/CHANGELOG.md +42 -23
- data/README.md +147 -120
- data/grape-slack-bot.gemspec +15 -10
- data/lib/slack_bot/api_client.rb +127 -33
- data/lib/slack_bot/args.rb +2 -0
- data/lib/slack_bot/callback.rb +29 -4
- data/lib/slack_bot/callback_storage.rb +3 -3
- data/lib/slack_bot/config.rb +4 -0
- data/lib/slack_bot/errors.rb +20 -0
- data/lib/slack_bot/grape_extension.rb +76 -21
- data/lib/slack_bot/interaction.rb +5 -1
- data/lib/slack_bot/logger.rb +15 -0
- data/lib/slack_bot/menu_options.rb +4 -0
- data/lib/slack_bot/view.rb +6 -2
- data/lib/slack_bot.rb +4 -1
- data/sig/slack_bot.rbs +379 -0
- metadata +114 -34
data/lib/slack_bot/api_client.rb
CHANGED
|
@@ -16,90 +16,176 @@ module SlackBot
|
|
|
16
16
|
data["error"]
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def rate_limited?
|
|
20
|
+
response.status == 429 || (data["ok"] == false && data["error"] == "rate_limited")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def retry_after
|
|
24
|
+
response.headers["Retry-After"]&.to_i || 60
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def slack_error?
|
|
28
|
+
!ok? && error.present?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def authentication_error?
|
|
32
|
+
slack_error? && %w[invalid_auth account_inactive].include?(error)
|
|
33
|
+
end
|
|
34
|
+
|
|
19
35
|
def data
|
|
20
36
|
JSON.parse(response.body)
|
|
37
|
+
rescue JSON::ParserError => e
|
|
38
|
+
SlackBot::Logger.error("Failed to parse Slack API response: #{e.message}")
|
|
39
|
+
{"ok" => false, "error" => "invalid_json_response"}
|
|
21
40
|
end
|
|
22
41
|
end
|
|
23
42
|
|
|
24
43
|
class ApiClient
|
|
44
|
+
# Slack API base URL
|
|
45
|
+
SLACK_API_BASE_URL = "https://slack.com/api/"
|
|
46
|
+
# Request timeout in seconds
|
|
47
|
+
REQUEST_TIMEOUT = 30
|
|
48
|
+
# Connection timeout in seconds
|
|
49
|
+
CONNECTION_TIMEOUT = 10
|
|
50
|
+
|
|
25
51
|
attr_reader :client
|
|
26
52
|
def initialize(authorization_token: ENV["SLACK_BOT_API_TOKEN"])
|
|
27
53
|
authorization_token_available = !authorization_token.nil? && authorization_token.is_a?(String) && !authorization_token.empty?
|
|
28
|
-
|
|
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
|
|
29
65
|
|
|
30
66
|
@client =
|
|
31
67
|
Faraday.new do |conn|
|
|
32
68
|
conn.request :url_encoded
|
|
33
69
|
conn.adapter Faraday.default_adapter
|
|
34
|
-
conn.url_prefix =
|
|
70
|
+
conn.url_prefix = SLACK_API_BASE_URL
|
|
35
71
|
conn.headers["Content-Type"] = "application/json; charset=utf-8"
|
|
36
72
|
conn.headers["Authorization"] = "Bearer #{authorization_token}"
|
|
73
|
+
conn.options.timeout = REQUEST_TIMEOUT
|
|
74
|
+
conn.options.open_timeout = CONNECTION_TIMEOUT
|
|
37
75
|
end
|
|
38
76
|
end
|
|
39
77
|
|
|
40
78
|
def views_open(trigger_id:, view:)
|
|
41
|
-
ApiResponse.new
|
|
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
|
|
42
84
|
end
|
|
43
85
|
|
|
44
86
|
def views_update(view_id:, view:)
|
|
45
|
-
ApiResponse.new
|
|
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
|
|
46
92
|
end
|
|
47
93
|
|
|
48
94
|
def chat_post_message(channel:, text:, blocks:)
|
|
49
|
-
ApiResponse.new
|
|
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
|
|
50
100
|
end
|
|
51
101
|
|
|
52
102
|
def chat_update(channel:, ts:, text:, blocks:)
|
|
53
|
-
ApiResponse.new
|
|
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
|
|
54
108
|
end
|
|
55
109
|
|
|
56
110
|
def chat_delete(channel:, ts:)
|
|
57
|
-
ApiResponse.new
|
|
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
|
|
58
116
|
end
|
|
59
117
|
|
|
60
118
|
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)
|
|
61
|
-
ApiResponse.new
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
119
|
+
ApiResponse.new do
|
|
120
|
+
client.post("chat.unfurl", {
|
|
121
|
+
channel: channel,
|
|
122
|
+
ts: ts,
|
|
123
|
+
unfurls: unfurls,
|
|
124
|
+
source: source,
|
|
125
|
+
unfurl_id: unfurl_id,
|
|
126
|
+
user_auth_blocks: user_auth_blocks,
|
|
127
|
+
user_auth_message: user_auth_message,
|
|
128
|
+
user_auth_required: user_auth_required,
|
|
129
|
+
user_auth_url: user_auth_url
|
|
130
|
+
}.to_json)
|
|
131
|
+
rescue Faraday::Error => e
|
|
132
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
133
|
+
end
|
|
72
134
|
end
|
|
73
135
|
|
|
74
136
|
def chat_schedule_message(channel:, text:, post_at:, blocks: nil)
|
|
75
|
-
ApiResponse.new
|
|
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
|
|
76
142
|
end
|
|
77
143
|
|
|
78
144
|
def scheduled_messages_list(channel: nil, cursor: nil, latest: nil, limit: nil, oldest: nil, team_id: nil)
|
|
79
|
-
ApiResponse.new
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
87
157
|
end
|
|
88
158
|
|
|
89
159
|
def chat_delete_scheduled_message(channel:, scheduled_message_id:)
|
|
90
|
-
ApiResponse.new
|
|
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
|
|
91
165
|
end
|
|
92
166
|
|
|
93
167
|
def chat_get_permalink(channel:, message_ts:)
|
|
94
|
-
ApiResponse.new
|
|
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
|
|
95
173
|
end
|
|
96
174
|
|
|
97
175
|
def users_info(user_id:)
|
|
98
|
-
ApiResponse.new
|
|
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
|
|
99
181
|
end
|
|
100
182
|
|
|
101
183
|
def views_publish(user_id:, view:)
|
|
102
|
-
ApiResponse.new
|
|
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
|
|
103
189
|
end
|
|
104
190
|
|
|
105
191
|
def users_list(cursor: nil, limit: 200, include_locale: nil, team_id: nil)
|
|
@@ -108,7 +194,11 @@ module SlackBot
|
|
|
108
194
|
args[:limit] = limit if limit
|
|
109
195
|
args[:include_locale] = include_locale if include_locale
|
|
110
196
|
args[:team_id] = team_id if team_id
|
|
111
|
-
ApiResponse.new
|
|
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
|
|
112
202
|
end
|
|
113
203
|
|
|
114
204
|
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)
|
|
@@ -125,7 +215,11 @@ module SlackBot
|
|
|
125
215
|
args[:parse] = parse if parse
|
|
126
216
|
args[:thread_ts] = thread_ts if thread_ts
|
|
127
217
|
args[:username] = username if username
|
|
128
|
-
ApiResponse.new
|
|
218
|
+
ApiResponse.new do
|
|
219
|
+
client.post("chat.postEphemeral", args.to_json)
|
|
220
|
+
rescue Faraday::Error => e
|
|
221
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
222
|
+
end
|
|
129
223
|
end
|
|
130
224
|
end
|
|
131
225
|
end
|
data/lib/slack_bot/args.rb
CHANGED
data/lib/slack_bot/callback.rb
CHANGED
|
@@ -37,7 +37,8 @@ module SlackBot
|
|
|
37
37
|
create(id: id, class_name: class_name, user: user, channel_id: channel_id, payload: payload, config: config, expires_in: expires_in, user_scope: user_scope)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
attr_reader :
|
|
40
|
+
attr_reader :data, :args, :config, :expires_in, :user_scope
|
|
41
|
+
attr_accessor :id
|
|
41
42
|
def initialize(id: nil, class_name: nil, user: nil, channel_id: nil, payload: nil, config: nil, expires_in: nil, user_scope: nil, view_id: nil)
|
|
42
43
|
@id = id
|
|
43
44
|
@data = {
|
|
@@ -50,7 +51,7 @@ module SlackBot
|
|
|
50
51
|
@args = SlackBot::Args.new
|
|
51
52
|
@config = config || SlackBot::Config.current_instance
|
|
52
53
|
@expires_in = expires_in || CALLBACK_RECORD_EXPIRES_IN
|
|
53
|
-
@user_scope = user_scope.nil?
|
|
54
|
+
@user_scope = user_scope.nil? || user_scope
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
def reload
|
|
@@ -102,18 +103,34 @@ module SlackBot
|
|
|
102
103
|
@data[:user_id] = user&.id
|
|
103
104
|
end
|
|
104
105
|
|
|
106
|
+
def class_name
|
|
107
|
+
data[:class_name]
|
|
108
|
+
end
|
|
109
|
+
|
|
105
110
|
def class_name=(class_name)
|
|
106
111
|
@data[:class_name] = class_name
|
|
107
112
|
end
|
|
108
113
|
|
|
114
|
+
def channel_id
|
|
115
|
+
data[:channel_id]
|
|
116
|
+
end
|
|
117
|
+
|
|
109
118
|
def channel_id=(channel_id)
|
|
110
119
|
@data[:channel_id] = channel_id
|
|
111
120
|
end
|
|
112
121
|
|
|
122
|
+
def payload
|
|
123
|
+
data[:payload]
|
|
124
|
+
end
|
|
125
|
+
|
|
113
126
|
def payload=(payload)
|
|
114
127
|
@data[:payload] = payload
|
|
115
128
|
end
|
|
116
129
|
|
|
130
|
+
def view_id
|
|
131
|
+
data[:view_id]
|
|
132
|
+
end
|
|
133
|
+
|
|
117
134
|
def view_id=(view_id)
|
|
118
135
|
@data[:view_id] = view_id
|
|
119
136
|
end
|
|
@@ -138,6 +155,10 @@ module SlackBot
|
|
|
138
155
|
super
|
|
139
156
|
end
|
|
140
157
|
|
|
158
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
159
|
+
data.key?(method_name.to_sym) || (data[:payload].is_a?(Hash) && data[:payload].key?(method_name.to_s)) || super
|
|
160
|
+
end
|
|
161
|
+
|
|
141
162
|
def read_view_callback_id
|
|
142
163
|
return if view_id.blank?
|
|
143
164
|
|
|
@@ -159,13 +180,17 @@ module SlackBot
|
|
|
159
180
|
end
|
|
160
181
|
|
|
161
182
|
def storage_key
|
|
162
|
-
|
|
183
|
+
if user.blank?
|
|
184
|
+
raise SlackBot::Errors::SlackApiError.new("User is required for scoped callback")
|
|
185
|
+
end
|
|
163
186
|
|
|
164
187
|
"#{CALLBACK_KEY_PREFIX}:u#{user.id}:#{id}"
|
|
165
188
|
end
|
|
166
189
|
|
|
167
190
|
def view_storage_key
|
|
168
|
-
|
|
191
|
+
if user.blank?
|
|
192
|
+
raise SlackBot::Errors::SlackApiError.new("User is required for scoped callback")
|
|
193
|
+
end
|
|
169
194
|
|
|
170
195
|
"#{CALLBACK_KEY_PREFIX}:u#{user.id}:#{view_id}"
|
|
171
196
|
end
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
module SlackBot
|
|
2
2
|
class CallbackStorage
|
|
3
3
|
def read(*_args, **_kwargs)
|
|
4
|
-
raise "
|
|
4
|
+
raise SlackBot::Errors::NotImplementedError.new("CallbackStorage#read must be implemented by subclass")
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def write(*_args, **_kwargs)
|
|
8
|
-
raise "
|
|
8
|
+
raise SlackBot::Errors::NotImplementedError.new("CallbackStorage#write must be implemented by subclass")
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def delete(*_args, **_kwargs)
|
|
12
|
-
raise "
|
|
12
|
+
raise SlackBot::Errors::NotImplementedError.new("CallbackStorage#delete must be implemented by subclass")
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
end
|
data/lib/slack_bot/config.rb
CHANGED
|
@@ -80,11 +80,15 @@ module SlackBot
|
|
|
80
80
|
|
|
81
81
|
def handler_class(class_name, klass)
|
|
82
82
|
@handler_classes ||= {}
|
|
83
|
+
return if class_name.nil?
|
|
84
|
+
|
|
83
85
|
@handler_classes[class_name.to_sym] = klass
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
def find_handler_class(class_name)
|
|
87
89
|
@handler_classes ||= {}
|
|
90
|
+
return if class_name.nil?
|
|
91
|
+
|
|
88
92
|
@handler_classes.fetch(class_name.to_sym)
|
|
89
93
|
rescue KeyError
|
|
90
94
|
raise SlackBot::Errors::HandlerClassNotFound.new(class_name, handler_classes: @handler_classes)
|
data/lib/slack_bot/errors.rb
CHANGED
|
@@ -62,5 +62,25 @@ module SlackBot
|
|
|
62
62
|
|
|
63
63
|
class PublishViewError < SlackResponseError
|
|
64
64
|
end
|
|
65
|
+
|
|
66
|
+
class CallbackUserMismatchError < SlackBot::Error
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class InvalidPayloadError < SlackBot::Error
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class SlackApiError < SlackBot::Error
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class UnknownActionTypeError < SlackBot::Error
|
|
76
|
+
attr_reader :action_type
|
|
77
|
+
def initialize(action_type)
|
|
78
|
+
@action_type = action_type
|
|
79
|
+
super("Unknown action type: #{action_type}")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class NotImplementedError < SlackBot::Error
|
|
84
|
+
end
|
|
65
85
|
end
|
|
66
86
|
end
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
require "active_support"
|
|
2
2
|
require "active_support/core_ext/object"
|
|
3
|
+
require "active_support/security_utils"
|
|
3
4
|
|
|
4
5
|
module SlackBot
|
|
5
6
|
module GrapeHelpers
|
|
7
|
+
# Slack recommends rejecting requests older than 5 minutes
|
|
8
|
+
TIMESTAMP_TOLERANCE_SECONDS = 300
|
|
9
|
+
# Minimum length for Slack signing secret (Slack's requirement)
|
|
10
|
+
MIN_SIGNING_SECRET_LENGTH = 32
|
|
11
|
+
|
|
6
12
|
def fetch_team_id
|
|
7
13
|
params.dig("team_id") || params.dig("team", "id")
|
|
8
14
|
end
|
|
@@ -19,9 +25,26 @@ module SlackBot
|
|
|
19
25
|
raise SlackBot::Errors::SignatureAuthenticationError.new("Missing signature headers")
|
|
20
26
|
end
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
32
|
+
|
|
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
|
|
25
48
|
|
|
26
49
|
sig_basestring = "v0:#{timestamp}:#{request_body}"
|
|
27
50
|
my_signature =
|
|
@@ -61,11 +84,9 @@ module SlackBot
|
|
|
61
84
|
end
|
|
62
85
|
|
|
63
86
|
def verify_current_user!
|
|
64
|
-
if current_user
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
raise SlackBot::Errors::UserAuthenticationError.new("User is not authorized")
|
|
68
|
-
end
|
|
87
|
+
return true if current_user
|
|
88
|
+
|
|
89
|
+
raise SlackBot::Errors::UserAuthenticationError.new("User is not authorized")
|
|
69
90
|
end
|
|
70
91
|
|
|
71
92
|
def events_callback(params)
|
|
@@ -73,7 +94,7 @@ module SlackBot
|
|
|
73
94
|
|
|
74
95
|
SlackBot::DevConsole.log_input "SlackApi::Events#events_callback: #{params.inspect}"
|
|
75
96
|
handler = config.find_event_handler(params[:event][:type].to_sym)
|
|
76
|
-
return if handler.blank?
|
|
97
|
+
return false if handler.blank?
|
|
77
98
|
|
|
78
99
|
event = handler.new(params: params, current_user: current_user)
|
|
79
100
|
event.call
|
|
@@ -84,6 +105,12 @@ module SlackBot
|
|
|
84
105
|
{challenge: params[:challenge]}
|
|
85
106
|
end
|
|
86
107
|
|
|
108
|
+
def validate_callback_user!(callback, user)
|
|
109
|
+
if callback.user_id != user.id
|
|
110
|
+
raise SlackBot::Errors::CallbackUserMismatchError.new("Callback user is not equal to action user")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
87
114
|
def handle_block_actions_view(view:, user:, params:)
|
|
88
115
|
callback_id = view&.dig("callback_id")
|
|
89
116
|
|
|
@@ -92,12 +119,11 @@ module SlackBot
|
|
|
92
119
|
|
|
93
120
|
SlackBot::DevConsole.log_check "SlackApi::Interactions##{__method__}: #{callback.id} #{callback.payload} #{callback.user_id} #{user&.id}"
|
|
94
121
|
|
|
95
|
-
|
|
96
|
-
raise "Callback user is not equal to action user"
|
|
97
|
-
end
|
|
122
|
+
validate_callback_user!(callback, user)
|
|
98
123
|
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
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?
|
|
101
127
|
|
|
102
128
|
interaction_klass.new(current_user: user, params: params, callback: callback, config: config).call
|
|
103
129
|
end
|
|
@@ -107,9 +133,16 @@ module SlackBot
|
|
|
107
133
|
def self.included(base)
|
|
108
134
|
base.format :json
|
|
109
135
|
base.content_type :json, "application/json"
|
|
110
|
-
base.use ActionDispatch::RemoteIp
|
|
136
|
+
base.use ActionDispatch::RemoteIp if defined?(ActionDispatch::RemoteIp)
|
|
111
137
|
base.helpers SlackBot::GrapeHelpers
|
|
112
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
|
+
base.rescue_from SlackBot::Error do |e|
|
|
143
|
+
error!({error: e.message}, 200)
|
|
144
|
+
end
|
|
145
|
+
|
|
113
146
|
base.before do
|
|
114
147
|
verify_slack_signature!
|
|
115
148
|
end
|
|
@@ -135,7 +168,11 @@ module SlackBot
|
|
|
135
168
|
verify_current_user! if action.only_user?
|
|
136
169
|
|
|
137
170
|
result = action.call
|
|
138
|
-
|
|
171
|
+
if !result
|
|
172
|
+
body false
|
|
173
|
+
status 200
|
|
174
|
+
return
|
|
175
|
+
end
|
|
139
176
|
|
|
140
177
|
result
|
|
141
178
|
end
|
|
@@ -143,7 +180,11 @@ module SlackBot
|
|
|
143
180
|
|
|
144
181
|
base.resource :interactions do
|
|
145
182
|
post do
|
|
146
|
-
|
|
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
|
|
147
188
|
|
|
148
189
|
action_user_session =
|
|
149
190
|
resolve_user_session(
|
|
@@ -161,10 +202,14 @@ module SlackBot
|
|
|
161
202
|
params: params
|
|
162
203
|
)
|
|
163
204
|
else
|
|
164
|
-
raise
|
|
205
|
+
raise SlackBot::Errors::UnknownActionTypeError.new(action_type)
|
|
165
206
|
end
|
|
166
207
|
|
|
167
|
-
|
|
208
|
+
if result.blank? || result == false
|
|
209
|
+
body false
|
|
210
|
+
status 200
|
|
211
|
+
return
|
|
212
|
+
end
|
|
168
213
|
|
|
169
214
|
result
|
|
170
215
|
end
|
|
@@ -178,9 +223,15 @@ module SlackBot
|
|
|
178
223
|
url_verification(params)
|
|
179
224
|
when "event_callback"
|
|
180
225
|
events_callback(params)
|
|
226
|
+
else
|
|
227
|
+
raise SlackBot::Errors::UnknownActionTypeError.new(params[:type])
|
|
181
228
|
end
|
|
182
229
|
|
|
183
|
-
|
|
230
|
+
if result.blank? || result == false
|
|
231
|
+
body false
|
|
232
|
+
status 200
|
|
233
|
+
return
|
|
234
|
+
end
|
|
184
235
|
|
|
185
236
|
result
|
|
186
237
|
end
|
|
@@ -195,7 +246,11 @@ module SlackBot
|
|
|
195
246
|
raise SlackBot::Errors::MenuOptionsNotImplemented.new if menu_options_klass.blank?
|
|
196
247
|
|
|
197
248
|
menu_options = menu_options_klass.new(current_user: current_user, params: params, config: config).call
|
|
198
|
-
|
|
249
|
+
if menu_options.blank?
|
|
250
|
+
body false
|
|
251
|
+
status 200
|
|
252
|
+
return
|
|
253
|
+
end
|
|
199
254
|
|
|
200
255
|
menu_options
|
|
201
256
|
end
|
|
@@ -149,7 +149,11 @@ module SlackBot
|
|
|
149
149
|
end
|
|
150
150
|
|
|
151
151
|
def payload
|
|
152
|
-
@payload ||=
|
|
152
|
+
@payload ||= begin
|
|
153
|
+
JSON.parse(params[:payload])
|
|
154
|
+
rescue JSON::ParserError => e
|
|
155
|
+
raise SlackBot::Errors::InvalidPayloadError.new("Invalid JSON payload: #{e.message}")
|
|
156
|
+
end
|
|
153
157
|
end
|
|
154
158
|
end
|
|
155
159
|
end
|
data/lib/slack_bot/logger.rb
CHANGED
|
@@ -4,5 +4,20 @@ module SlackBot
|
|
|
4
4
|
puts args.inspect if args.any?
|
|
5
5
|
puts kwargs.inspect if kwargs.any?
|
|
6
6
|
end
|
|
7
|
+
|
|
8
|
+
def error(*args, **kwargs)
|
|
9
|
+
puts args.inspect if args.any?
|
|
10
|
+
puts kwargs.inspect if kwargs.any?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def warn(*args, **kwargs)
|
|
14
|
+
puts args.inspect if args.any?
|
|
15
|
+
puts kwargs.inspect if kwargs.any?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def debug(*args, **kwargs)
|
|
19
|
+
puts args.inspect if args.any?
|
|
20
|
+
puts kwargs.inspect if kwargs.any?
|
|
21
|
+
end
|
|
7
22
|
end
|
|
8
23
|
end
|
data/lib/slack_bot/view.rb
CHANGED
|
@@ -22,14 +22,18 @@ module SlackBot
|
|
|
22
22
|
super
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
26
|
+
(@context.is_a?(Hash) && @context.key?(method_name.to_sym)) || super
|
|
27
|
+
end
|
|
28
|
+
|
|
25
29
|
def text_modal
|
|
26
30
|
{
|
|
27
31
|
title: {
|
|
28
32
|
type: "plain_text",
|
|
29
|
-
text: context
|
|
33
|
+
text: context&.dig(:title) || ""
|
|
30
34
|
},
|
|
31
35
|
blocks: [
|
|
32
|
-
{type: "section", text: {type: "mrkdwn", text: context
|
|
36
|
+
{type: "section", text: {type: "mrkdwn", text: context&.dig(:text) || ""}}
|
|
33
37
|
]
|
|
34
38
|
}
|
|
35
39
|
end
|
data/lib/slack_bot.rb
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "openssl"
|
|
3
|
+
|
|
1
4
|
require "slack_bot/logger"
|
|
2
5
|
require "slack_bot/dev_console"
|
|
3
6
|
|
|
@@ -22,5 +25,5 @@ require "slack_bot/pager"
|
|
|
22
25
|
require "slack_bot/grape_extension"
|
|
23
26
|
|
|
24
27
|
module SlackBot
|
|
25
|
-
VERSION = "
|
|
28
|
+
VERSION = "2.0.0".freeze
|
|
26
29
|
end
|