grape-slack-bot 1.8.2 → 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 +49 -23
- data/README.md +153 -121
- data/grape-slack-bot.gemspec +18 -10
- data/lib/slack_bot/api_client.rb +161 -60
- 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 +138 -77
- data/lib/slack_bot/interaction.rb +38 -44
- 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 +159 -37
data/lib/slack_bot/api_client.rb
CHANGED
|
@@ -16,116 +16,217 @@ 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
|
+
TOKEN_FORMAT = /\Axox[bpa]-/
|
|
51
|
+
|
|
25
52
|
attr_reader :client
|
|
26
|
-
def initialize(authorization_token: ENV["SLACK_BOT_API_TOKEN"])
|
|
27
|
-
authorization_token_available = !authorization_token.nil? && authorization_token.is_a?(String) && !authorization_token.empty?
|
|
28
|
-
raise "Slack bot API token is not set" if !authorization_token_available
|
|
29
53
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
conn.adapter Faraday.default_adapter
|
|
34
|
-
conn.url_prefix = "https://slack.com/api/"
|
|
35
|
-
conn.headers["Content-Type"] = "application/json; charset=utf-8"
|
|
36
|
-
conn.headers["Authorization"] = "Bearer #{authorization_token}"
|
|
37
|
-
end
|
|
54
|
+
def initialize(authorization_token: ENV["SLACK_BOT_API_TOKEN"])
|
|
55
|
+
validate_authorization_token!(authorization_token)
|
|
56
|
+
@client = build_client(authorization_token)
|
|
38
57
|
end
|
|
39
58
|
|
|
40
59
|
def views_open(trigger_id:, view:)
|
|
41
|
-
ApiResponse.new
|
|
60
|
+
ApiResponse.new do
|
|
61
|
+
client.post("views.open", {trigger_id: trigger_id, view: view}.to_json)
|
|
62
|
+
rescue Faraday::Error => e
|
|
63
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
64
|
+
end
|
|
42
65
|
end
|
|
43
66
|
|
|
44
67
|
def views_update(view_id:, view:)
|
|
45
|
-
ApiResponse.new
|
|
68
|
+
ApiResponse.new do
|
|
69
|
+
client.post("views.update", {view_id: view_id, view: view}.to_json)
|
|
70
|
+
rescue Faraday::Error => e
|
|
71
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
72
|
+
end
|
|
46
73
|
end
|
|
47
74
|
|
|
48
75
|
def chat_post_message(channel:, text:, blocks:)
|
|
49
|
-
ApiResponse.new
|
|
76
|
+
ApiResponse.new do
|
|
77
|
+
client.post("chat.postMessage", {channel: channel, text: text, blocks: blocks}.to_json)
|
|
78
|
+
rescue Faraday::Error => e
|
|
79
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
80
|
+
end
|
|
50
81
|
end
|
|
51
82
|
|
|
52
83
|
def chat_update(channel:, ts:, text:, blocks:)
|
|
53
|
-
ApiResponse.new
|
|
84
|
+
ApiResponse.new do
|
|
85
|
+
client.post("chat.update", {channel: channel, ts: ts, text: text, blocks: blocks}.to_json)
|
|
86
|
+
rescue Faraday::Error => e
|
|
87
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
88
|
+
end
|
|
54
89
|
end
|
|
55
90
|
|
|
56
91
|
def chat_delete(channel:, ts:)
|
|
57
|
-
ApiResponse.new
|
|
92
|
+
ApiResponse.new do
|
|
93
|
+
client.post("chat.delete", {channel: channel, ts: ts}.to_json)
|
|
94
|
+
rescue Faraday::Error => e
|
|
95
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
96
|
+
end
|
|
58
97
|
end
|
|
59
98
|
|
|
60
99
|
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
|
-
|
|
100
|
+
ApiResponse.new do
|
|
101
|
+
client.post("chat.unfurl", {
|
|
102
|
+
channel: channel,
|
|
103
|
+
ts: ts,
|
|
104
|
+
unfurls: unfurls,
|
|
105
|
+
source: source,
|
|
106
|
+
unfurl_id: unfurl_id,
|
|
107
|
+
user_auth_blocks: user_auth_blocks,
|
|
108
|
+
user_auth_message: user_auth_message,
|
|
109
|
+
user_auth_required: user_auth_required,
|
|
110
|
+
user_auth_url: user_auth_url
|
|
111
|
+
}.to_json)
|
|
112
|
+
rescue Faraday::Error => e
|
|
113
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
114
|
+
end
|
|
72
115
|
end
|
|
73
116
|
|
|
74
117
|
def chat_schedule_message(channel:, text:, post_at:, blocks: nil)
|
|
75
|
-
ApiResponse.new
|
|
118
|
+
ApiResponse.new do
|
|
119
|
+
client.post("chat.scheduleMessage", {channel: channel, text: text, post_at: post_at, blocks: blocks}.to_json)
|
|
120
|
+
rescue Faraday::Error => e
|
|
121
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
122
|
+
end
|
|
76
123
|
end
|
|
77
124
|
|
|
78
125
|
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
|
-
|
|
126
|
+
ApiResponse.new do
|
|
127
|
+
client.post("scheduled_messages.list", {
|
|
128
|
+
channel: channel,
|
|
129
|
+
cursor: cursor,
|
|
130
|
+
latest: latest,
|
|
131
|
+
limit: limit,
|
|
132
|
+
oldest: oldest,
|
|
133
|
+
team_id: team_id
|
|
134
|
+
}.to_json)
|
|
135
|
+
rescue Faraday::Error => e
|
|
136
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
137
|
+
end
|
|
87
138
|
end
|
|
88
139
|
|
|
89
140
|
def chat_delete_scheduled_message(channel:, scheduled_message_id:)
|
|
90
|
-
ApiResponse.new
|
|
141
|
+
ApiResponse.new do
|
|
142
|
+
client.post("chat.deleteScheduledMessage", {channel: channel, scheduled_message_id: scheduled_message_id}.to_json)
|
|
143
|
+
rescue Faraday::Error => e
|
|
144
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
145
|
+
end
|
|
91
146
|
end
|
|
92
147
|
|
|
93
148
|
def chat_get_permalink(channel:, message_ts:)
|
|
94
|
-
ApiResponse.new
|
|
149
|
+
ApiResponse.new do
|
|
150
|
+
client.post("chat.getPermalink", {channel: channel, message_ts: message_ts}.to_json)
|
|
151
|
+
rescue Faraday::Error => e
|
|
152
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
153
|
+
end
|
|
95
154
|
end
|
|
96
155
|
|
|
97
156
|
def users_info(user_id:)
|
|
98
|
-
ApiResponse.new
|
|
157
|
+
ApiResponse.new do
|
|
158
|
+
client.post("users.info", {user: user_id}.to_json)
|
|
159
|
+
rescue Faraday::Error => e
|
|
160
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
161
|
+
end
|
|
99
162
|
end
|
|
100
163
|
|
|
101
164
|
def views_publish(user_id:, view:)
|
|
102
|
-
ApiResponse.new
|
|
165
|
+
ApiResponse.new do
|
|
166
|
+
client.post("views.publish", {user_id: user_id, view: view}.to_json)
|
|
167
|
+
rescue Faraday::Error => e
|
|
168
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
169
|
+
end
|
|
103
170
|
end
|
|
104
171
|
|
|
105
172
|
def users_list(cursor: nil, limit: 200, include_locale: nil, team_id: nil)
|
|
106
|
-
args =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
173
|
+
args = compact_payload(cursor: cursor, limit: limit, include_locale: include_locale, team_id: team_id)
|
|
174
|
+
ApiResponse.new do
|
|
175
|
+
client.post("users.list", args.to_json)
|
|
176
|
+
rescue Faraday::Error => e
|
|
177
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
178
|
+
end
|
|
112
179
|
end
|
|
113
180
|
|
|
114
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)
|
|
115
|
-
args =
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
)
|
|
196
|
+
ApiResponse.new do
|
|
197
|
+
client.post("chat.postEphemeral", args.to_json)
|
|
198
|
+
rescue Faraday::Error => e
|
|
199
|
+
raise SlackBot::Errors::SlackApiError.new("Network error: #{e.message}")
|
|
200
|
+
end
|
|
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
|
|
129
230
|
end
|
|
130
231
|
end
|
|
131
232
|
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
|