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.
@@ -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
- @client =
31
- Faraday.new do |conn|
32
- conn.request :url_encoded
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 { client.post("views.open", {trigger_id: trigger_id, view: view}.to_json) }
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 { client.post("views.update", {view_id: view_id, view: view}.to_json) }
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 { client.post("chat.postMessage", {channel: channel, text: text, blocks: blocks}.to_json) }
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 { client.post("chat.update", {channel: channel, ts: ts, text: text, blocks: blocks}.to_json) }
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 { client.post("chat.delete", {channel: channel, ts: ts}.to_json) }
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 { client.post("chat.unfurl", {
62
- channel: channel,
63
- ts: ts,
64
- unfurls: unfurls,
65
- source: source,
66
- unfurl_id: unfurl_id,
67
- user_auth_blocks: user_auth_blocks,
68
- user_auth_message: user_auth_message,
69
- user_auth_required: user_auth_required,
70
- user_auth_url: user_auth_url
71
- }.to_json) }
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 { client.post("chat.scheduleMessage", {channel: channel, text: text, post_at: post_at, blocks: blocks}.to_json) }
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 { client.post("scheduled_messages.list", {
80
- channel: channel,
81
- cursor: cursor,
82
- latest: latest,
83
- limit: limit,
84
- oldest: oldest,
85
- team_id: team_id
86
- }.to_json) }
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 { client.post("chat.deleteScheduledMessage", {channel: channel, scheduled_message_id: scheduled_message_id}.to_json) }
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 { client.post("chat.getPermalink", {channel: channel, message_ts: message_ts}.to_json) }
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 { client.post("users.info", {user: user_id}.to_json) }
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 { client.post("views.publish", {user_id: user_id, view: view}.to_json) }
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
- args[:cursor] = cursor if cursor
108
- args[:limit] = limit if limit
109
- args[:include_locale] = include_locale if include_locale
110
- args[:team_id] = team_id if team_id
111
- ApiResponse.new { client.post("users.list", args.to_json) }
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
- args[:channel] = channel
117
- args[:user] = user
118
- args[:text] = text if text
119
- args[:as_user] = as_user if as_user
120
- args[:attachments] = attachments if attachments
121
- args[:blocks] = blocks if blocks
122
- args[:icon_emoji] = icon_emoji if icon_emoji
123
- args[:icon_url] = icon_url if icon_url
124
- args[:link_names] = link_names if link_names
125
- args[:parse] = parse if parse
126
- args[:thread_ts] = thread_ts if thread_ts
127
- args[:username] = username if username
128
- ApiResponse.new { client.post("chat.postEphemeral", args.to_json) }
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
@@ -44,6 +44,8 @@ module SlackBot
44
44
  self.args = @parser.new(raw_args).call&.with_indifferent_access || {}
45
45
  end
46
46
 
47
+ attr_reader :raw_args
48
+
47
49
  def to_s
48
50
  @builder.new(args).call
49
51
  end
@@ -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 :id, :data, :args, :config, :expires_in, :user_scope
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? ? true : user_scope
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
- raise "User is required for scoped callback" if user.blank?
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
- raise "User is required for scoped callback" if user.blank?
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 "Not implemented"
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 "Not implemented"
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 "Not implemented"
12
+ raise SlackBot::Errors::NotImplementedError.new("CallbackStorage#delete must be implemented by subclass")
13
13
  end
14
14
  end
15
15
  end
@@ -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)
@@ -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