grape-slack-bot 1.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 +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.md +21 -0
- data/README.md +498 -0
- data/grape-slack-bot.gemspec +45 -0
- data/lib/slack_bot/api_client.rb +63 -0
- data/lib/slack_bot/args.rb +58 -0
- data/lib/slack_bot/callback.rb +109 -0
- data/lib/slack_bot/callback_storage.rb +15 -0
- data/lib/slack_bot/command.rb +72 -0
- data/lib/slack_bot/config.rb +168 -0
- data/lib/slack_bot/dev_console.rb +33 -0
- data/lib/slack_bot/errors.rb +39 -0
- data/lib/slack_bot/event.rb +41 -0
- data/lib/slack_bot/grape_extension.rb +198 -0
- data/lib/slack_bot/interaction.rb +139 -0
- data/lib/slack_bot/menu_options.rb +10 -0
- data/lib/slack_bot/pager.rb +30 -0
- data/lib/slack_bot/view.rb +53 -0
- data/lib/slack_bot.rb +24 -0
- data/spec/slack_bot/api_client_spec.rb +479 -0
- data/spec/slack_bot/args_spec.rb +34 -0
- data/spec/slack_bot/callback_spec.rb +104 -0
- data/spec/slack_bot/command_spec.rb +5 -0
- data/spec/slack_bot/config_spec.rb +57 -0
- data/spec/slack_bot/event_spec.rb +5 -0
- data/spec/slack_bot/grape_extension_spec.rb +5 -0
- data/spec/slack_bot/interaction_spec.rb +5 -0
- data/spec/slack_bot/menu_options_spec.rb +5 -0
- data/spec/slack_bot/pager_spec.rb +5 -0
- data/spec/slack_bot/view_spec.rb +5 -0
- data/spec/slack_bot_spec.rb +7 -0
- metadata +217 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3aed9857efe4c5d25d1028d27e2011dfb9465c95a55fbffc1c494fc3d7052625
|
4
|
+
data.tar.gz: d7b42e0134acc14f0d0efaeea30900688c35e38aeff5a6dfd9e898b6f343131f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3bb6da8f98fc125e615216325a2779361ad2dc8444f14f98dc24240185e26049802cbe357ddee2d0d5eca8e1172d9e45fc0f447973f176a8f791450ed9eb378f
|
7
|
+
data.tar.gz: 1ed2fdaf78f7f71e4eb454fe7fa98559f6e59ea296cd6c5b9c17042115a7297a33e2e16012352539e6f835fac93c02ec87bdcb04a901a1f782f2ed773193b77c
|
data/CHANGELOG.md
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 amkisko
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,498 @@
|
|
1
|
+
# grape-slack-bot.rb
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/grape-slack-bot) [](https://github.com/amkisko/grape-slack-bot.rb/actions/workflows/test.yml)
|
4
|
+
|
5
|
+
Extensible Slack bot implementation gem for [ruby-grape](https://github.com/ruby-grape/grape)
|
6
|
+
|
7
|
+
Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
8
|
+
|
9
|
+
## Install
|
10
|
+
|
11
|
+
Using Bundler:
|
12
|
+
```sh
|
13
|
+
bundle add grape-slack-bot
|
14
|
+
```
|
15
|
+
|
16
|
+
Using RubyGems:
|
17
|
+
```sh
|
18
|
+
gem install grape-slack-bot
|
19
|
+
```
|
20
|
+
|
21
|
+
## Gemfile
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'grape-slack-bot'
|
25
|
+
```
|
26
|
+
|
27
|
+
## Gem modules and classes
|
28
|
+
|
29
|
+
`SlackBot` is the main module that contains all the classes and modules.
|
30
|
+
|
31
|
+
## Concepts
|
32
|
+
|
33
|
+
### Slash command
|
34
|
+
|
35
|
+
Slash command is a command that is triggered by user in Slack chat using `/` prefix.
|
36
|
+
|
37
|
+
Characteristics:
|
38
|
+
- Can have multiple URL endpoints (later called `url_token`, e.g. `/api/slack/commands/game`)
|
39
|
+
- Starts with `/` and is followed by command name (e.g. `/game`, called `token`)
|
40
|
+
- Can have multiple argument commands (e.g. `/game start`, called `token`)
|
41
|
+
- Can have multiple arguments (e.g. `/game start password=P@5sW0Rd`, called `args`)
|
42
|
+
- Can send message to chat
|
43
|
+
- Can open interactive component with callback identifier
|
44
|
+
- Can trigger event in background
|
45
|
+
|
46
|
+
References:
|
47
|
+
- [slash_command.rb](lib/slack_bot/slash_command.rb)
|
48
|
+
- [Slash command documentation](https://api.slack.com/interactivity/slash-commands)
|
49
|
+
|
50
|
+
### Interactive component
|
51
|
+
|
52
|
+
Interactive component is a component that is requested to be opened by bot app for the user in Slack application.
|
53
|
+
|
54
|
+
Characteristics:
|
55
|
+
- Can be associated with slash command
|
56
|
+
|
57
|
+
References:
|
58
|
+
- [interaction.rb](lib/slack_bot/interaction.rb)
|
59
|
+
- [Interactive components documentation](https://api.slack.com/interactivity/handling)
|
60
|
+
|
61
|
+
### Event
|
62
|
+
|
63
|
+
Event is a notification that is sent to bot app when something happens in Slack.
|
64
|
+
|
65
|
+
References:
|
66
|
+
- [event.rb](lib/slack_bot/event.rb)
|
67
|
+
- [Event documentation](https://api.slack.com/events-api)
|
68
|
+
|
69
|
+
### View
|
70
|
+
|
71
|
+
View is a class that has logic for rendering internals of message or modal or any other user interface component.
|
72
|
+
|
73
|
+
Characteristics:
|
74
|
+
- Can be associated with slash command, interactive component or event for using ready-made methods like `open_modal`, `update_modal` or `publish_view`
|
75
|
+
|
76
|
+
References:
|
77
|
+
- [view.rb](lib/slack_bot/view.rb)
|
78
|
+
- [App home documentation](https://api.slack.com/surfaces/app-home)
|
79
|
+
- [Messages documentation](https://api.slack.com/messaging)
|
80
|
+
- [Modals documentation](https://api.slack.com/surfaces/modals)
|
81
|
+
|
82
|
+
### Block
|
83
|
+
|
84
|
+
Block is an object that is used to render user interface elements in Slack.
|
85
|
+
|
86
|
+
References:
|
87
|
+
- [Block kit documentation](https://api.slack.com/block-kit)
|
88
|
+
|
89
|
+
### Callback
|
90
|
+
|
91
|
+
Callback is a class for managing interactive component state and handling interactive component actions.
|
92
|
+
|
93
|
+
Example uses `Rails.cache` for storing interactive component state, use `CallbackStorage` for building custom storage class as a base.
|
94
|
+
|
95
|
+
References:
|
96
|
+
- [callback.rb](lib/slack_bot/callback.rb)
|
97
|
+
- [callback_storage.rb](lib/slack_bot/callback_storage.rb)
|
98
|
+
|
99
|
+
### Arguments
|
100
|
+
|
101
|
+
Class for handling slash command and interactive element values as queries.
|
102
|
+
|
103
|
+
Gem implementation uses `Rack::Utils` for parsing and building query strings.
|
104
|
+
|
105
|
+
References:
|
106
|
+
- [args.rb](lib/slack_bot/args.rb)
|
107
|
+
|
108
|
+
### Pager
|
109
|
+
|
110
|
+
Own implementation of pagination that is relying on [Arguments](#arguments) and [ActiveRecord](https://guides.rubyonrails.org/active_record_querying.html).
|
111
|
+
|
112
|
+
References:
|
113
|
+
- [pager.rb](lib/slack_bot/pager.rb)
|
114
|
+
|
115
|
+
## Specification
|
116
|
+
|
117
|
+
[x] Create any amount of endpoints that will handle Slack calls
|
118
|
+
[x] Create multiple instances of bots and configure them separately or use the same configuration for all bots
|
119
|
+
[x] Define and reuse slash command handlers for Slack slash commands
|
120
|
+
[x] Define interactive component handlers for Slack interactive components
|
121
|
+
[x] Define and reuse views for slash commands, interactive components and events
|
122
|
+
[x] Define event handlers for Slack events
|
123
|
+
[x] Define menu options handlers for Slack menu options
|
124
|
+
[x] Store interactive component state in cache for usage in other handlers
|
125
|
+
[x] Access current user session and user from any handler
|
126
|
+
[x] Extend API endpoint with custom hooks and helpers within [grape specification](https://github.com/ruby-grape/grape)
|
127
|
+
[x] Supports Slack signature verification
|
128
|
+
[ ] Supports Slack socket mode (?)
|
129
|
+
[ ] Supports Slack token rotation
|
130
|
+
|
131
|
+
## Usage with grape
|
132
|
+
|
133
|
+
Create `app/api/slack_bot_api.rb`, it will contain bot configuration and endpoints setup:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
SlackBot::DevConsole.enabled = Rails.env.development?
|
137
|
+
SlackBot::Config.configure do
|
138
|
+
callback_storage Rails.cache
|
139
|
+
callback_user_finder ->(id) { User.active.find_by(id: id) }
|
140
|
+
|
141
|
+
# TODO: Register event handlers
|
142
|
+
event :app_home_opened, MySlackBot::AppHomeOpenedEvent
|
143
|
+
|
144
|
+
# TODO: Register slash command handlers
|
145
|
+
slash_command_endpoint :game, MySlackBot::Game::MenuCommand do
|
146
|
+
command :start, MySlackBot::Game::StartCommand
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class SlackBotApi < Grape::API
|
151
|
+
include SlackBot::GrapeExtension
|
152
|
+
|
153
|
+
helpers do
|
154
|
+
def config
|
155
|
+
SlackBot::Config.current_instance
|
156
|
+
end
|
157
|
+
|
158
|
+
def resolve_user_session(team_id, user_id)
|
159
|
+
uid = OmniAuth::Strategies::SlackOpenid.generate_uid(team_id, user_id)
|
160
|
+
UserSession.find_by(uid: uid, provider: UserSession.slack_openid_provider)
|
161
|
+
end
|
162
|
+
|
163
|
+
def current_user_session
|
164
|
+
# NOTE: fetch_team_id and fetch_user_id are provided by SlackBot::Grape::ApiExtension
|
165
|
+
@current_user_session ||=
|
166
|
+
resolve_user_session(fetch_team_id, fetch_user_id)
|
167
|
+
end
|
168
|
+
|
169
|
+
def current_user_ip
|
170
|
+
request.env["action_dispatch.remote_ip"].to_s
|
171
|
+
end
|
172
|
+
|
173
|
+
def current_user
|
174
|
+
@current_user ||= current_user_session&.user
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
In routes file `config/routes.rb` mount the API:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
mount SlackBotApi => "/api/slack"
|
184
|
+
```
|
185
|
+
|
186
|
+
## Slack bot manifest
|
187
|
+
|
188
|
+
You can use this manifest as a template for your Slack app configuration:
|
189
|
+
|
190
|
+
```yaml
|
191
|
+
display_information:
|
192
|
+
name: Example
|
193
|
+
description: Example bot
|
194
|
+
background_color: "#000000"
|
195
|
+
features:
|
196
|
+
bot_user:
|
197
|
+
display_name: Example
|
198
|
+
always_online: true
|
199
|
+
slash_commands:
|
200
|
+
- command: /game
|
201
|
+
url: https://example.com/api/slack/commands/game
|
202
|
+
description: The game
|
203
|
+
should_escape: false
|
204
|
+
oauth_config:
|
205
|
+
redirect_urls:
|
206
|
+
- https://example.com/user/auth/slack_openid/callback
|
207
|
+
scopes:
|
208
|
+
bot:
|
209
|
+
- incoming-webhook
|
210
|
+
- app_mentions:read
|
211
|
+
- chat:write
|
212
|
+
- users:read
|
213
|
+
- users:read.email
|
214
|
+
- im:read
|
215
|
+
- im:write
|
216
|
+
- im:history
|
217
|
+
- channels:read
|
218
|
+
- groups:read
|
219
|
+
- mpim:read
|
220
|
+
- reactions:read
|
221
|
+
- commands
|
222
|
+
settings:
|
223
|
+
event_subscriptions:
|
224
|
+
request_url: https://example.com/api/slack/events
|
225
|
+
bot_events:
|
226
|
+
- app_home_opened
|
227
|
+
- app_mention
|
228
|
+
- im_history_changed
|
229
|
+
- member_joined_channel
|
230
|
+
- member_left_channel
|
231
|
+
- message.im
|
232
|
+
- profile_opened
|
233
|
+
- reaction_added
|
234
|
+
- reaction_removed
|
235
|
+
interactivity:
|
236
|
+
is_enabled: true
|
237
|
+
request_url: https://example.com/api/slack/interactions
|
238
|
+
message_menu_options_url: https://example.com/api/slack/menu_options
|
239
|
+
org_deploy_enabled: false
|
240
|
+
socket_mode_enabled: false
|
241
|
+
token_rotation_enabled: false
|
242
|
+
```
|
243
|
+
|
244
|
+
## Command example
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
module MySlackBot::Game
|
248
|
+
class MenuCommand < SlackBot::Command
|
249
|
+
interaction MySlackBot::Game::MenuInteraction
|
250
|
+
view MySlackBot::Game::MenuView
|
251
|
+
def call
|
252
|
+
open_modal :index_modal
|
253
|
+
end
|
254
|
+
end
|
255
|
+
class StartCommand < SlackBot::Command
|
256
|
+
interaction MySlackBot::Game::StartInteraction
|
257
|
+
view MySlackBot::Game::StartView
|
258
|
+
def call
|
259
|
+
open_modal :index_modal
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
```
|
265
|
+
|
266
|
+
## Interaction example
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
module MySlackBot::Game
|
270
|
+
class StartInteraction < SlackBot::Interaction
|
271
|
+
view MySlackBot::Game::StartView
|
272
|
+
def call
|
273
|
+
return if interaction_type != "block_actions"
|
274
|
+
|
275
|
+
update_callback_args do |action|
|
276
|
+
action_id = action["action_id"]
|
277
|
+
action_type = action["type"]
|
278
|
+
case action_type
|
279
|
+
when "static_select"
|
280
|
+
if action_id == "games_users_list_select_user"
|
281
|
+
callback.args[:user_id] = action["selected_option"]["value"]
|
282
|
+
end
|
283
|
+
else
|
284
|
+
callback.args.raw_args = action["value"]
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
update_modal :index_modal
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
## View example
|
295
|
+
|
296
|
+
Modal view example:
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
module MySlackBot::Game
|
300
|
+
class MenuView < SlackBot::View
|
301
|
+
def index_modal
|
302
|
+
blocks = []
|
303
|
+
|
304
|
+
blocks << {
|
305
|
+
type: "section",
|
306
|
+
block_id: "section_help_list",
|
307
|
+
text: {
|
308
|
+
type: "mrkdwn",
|
309
|
+
text: "#{command} start - Start the game"
|
310
|
+
}
|
311
|
+
}
|
312
|
+
|
313
|
+
cursor = Game.active
|
314
|
+
pager = paginate(cursor)
|
315
|
+
|
316
|
+
blocks << {
|
317
|
+
type: "section",
|
318
|
+
block_id: "section_games_list",
|
319
|
+
text: {
|
320
|
+
type: "mrkdwn",
|
321
|
+
text: "*Games*"
|
322
|
+
}
|
323
|
+
}
|
324
|
+
|
325
|
+
if pager.cursor.present?
|
326
|
+
pager.cursor.find_each do |game|
|
327
|
+
blocks << {
|
328
|
+
type: "section",
|
329
|
+
block_id: "section_game_#{game.id}",
|
330
|
+
text: {
|
331
|
+
type: "mrkdwn",
|
332
|
+
text: "#{game.name}"
|
333
|
+
},
|
334
|
+
accessory: {
|
335
|
+
type: "button",
|
336
|
+
action_id: "games_users_list_join_game",
|
337
|
+
text: {
|
338
|
+
type: "plain_text",
|
339
|
+
text: "Join"
|
340
|
+
},
|
341
|
+
value: args.merge(game_id: game.id).to_s
|
342
|
+
}
|
343
|
+
}
|
344
|
+
end
|
345
|
+
else
|
346
|
+
blocks << {
|
347
|
+
type: "section",
|
348
|
+
block_id: "section_games_list_empty",
|
349
|
+
text: {
|
350
|
+
type: "mrkdwn",
|
351
|
+
text: "No active games"
|
352
|
+
}
|
353
|
+
}
|
354
|
+
end
|
355
|
+
|
356
|
+
if pager.pages_count > 1
|
357
|
+
pager_elements = []
|
358
|
+
if pager.page > 1
|
359
|
+
pager_elements << {
|
360
|
+
type: "button",
|
361
|
+
action_id: "games_list_previous_page",
|
362
|
+
text: {
|
363
|
+
type: "plain_text",
|
364
|
+
text: ":arrow_left: Previous page"
|
365
|
+
},
|
366
|
+
value: args.merge(page: pager.page - 1).to_s
|
367
|
+
}
|
368
|
+
end
|
369
|
+
if pager.page < pager.pages_count
|
370
|
+
pager_elements << {
|
371
|
+
type: "button",
|
372
|
+
action_id: "games_list_next_page",
|
373
|
+
text: {
|
374
|
+
type: "plain_text",
|
375
|
+
text: "Next page :arrow_right:"
|
376
|
+
},
|
377
|
+
value: args.merge(page: pager.page + 1).to_s
|
378
|
+
}
|
379
|
+
end
|
380
|
+
if pager_elements.present?
|
381
|
+
blocks << {
|
382
|
+
type: "actions",
|
383
|
+
elements: pager_elements
|
384
|
+
}
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
{
|
389
|
+
title: {
|
390
|
+
type: "plain_text",
|
391
|
+
text: "Example help"
|
392
|
+
},
|
393
|
+
blocks: blocks
|
394
|
+
}
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
```
|
399
|
+
|
400
|
+
App home view example:
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
module MySlackBot
|
404
|
+
class AppHomeView < SlackBot::View
|
405
|
+
def index_view
|
406
|
+
blocks = []
|
407
|
+
if current_user.present?
|
408
|
+
blocks += {
|
409
|
+
type: "section",
|
410
|
+
text: {
|
411
|
+
type: "mrkdwn",
|
412
|
+
text:
|
413
|
+
"*Hello, #{current_user.name}!*"
|
414
|
+
}
|
415
|
+
}
|
416
|
+
else
|
417
|
+
blocks << {
|
418
|
+
type: "section",
|
419
|
+
text: {
|
420
|
+
type: "mrkdwn",
|
421
|
+
text:
|
422
|
+
"*Please login at https://example.com using Slack*"
|
423
|
+
}
|
424
|
+
}
|
425
|
+
end
|
426
|
+
blocks << {
|
427
|
+
type: "context",
|
428
|
+
elements: [
|
429
|
+
{
|
430
|
+
type: "mrkdwn",
|
431
|
+
text: "Last updated at #{Time.current.strftime("%H:%M:%S %d.%m.%Y")}"
|
432
|
+
}
|
433
|
+
]
|
434
|
+
}
|
435
|
+
{ type: "home", blocks: blocks }
|
436
|
+
end
|
437
|
+
|
438
|
+
private
|
439
|
+
|
440
|
+
def format_date(date)
|
441
|
+
date.strftime("%d.%m.%Y")
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
```
|
446
|
+
|
447
|
+
## Event example
|
448
|
+
|
449
|
+
```ruby
|
450
|
+
module MySlackBot
|
451
|
+
class AppHomeOpenedEvent < SlackBot::Event
|
452
|
+
view MySlackBot::AppHomeView
|
453
|
+
def call
|
454
|
+
publish_view :index_view
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
```
|
459
|
+
|
460
|
+
## Extensibility
|
461
|
+
|
462
|
+
You can patch any class or module in this gem to extend its functionality, most of parts are not hardly attached to each other.
|
463
|
+
|
464
|
+
## Development and testing
|
465
|
+
|
466
|
+
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.
|
467
|
+
|
468
|
+
```sh
|
469
|
+
brew install cloudflare/cloudflare/cloudflared
|
470
|
+
cloudflared login
|
471
|
+
sudo cloudflared tunnel run --token <LONG_TOKEN_FROM_TUNNEL_PAGE>
|
472
|
+
```
|
473
|
+
|
474
|
+
For easiness of getting information, most of endpoints have `SlackBot::DevConsole.log` calls that will print out information to the console.
|
475
|
+
|
476
|
+
## Contributing
|
477
|
+
|
478
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/grape-slack-bot.rb
|
479
|
+
|
480
|
+
Contribution policy:
|
481
|
+
- It might take up to 2 calendar weeks to review and merge critical fixes
|
482
|
+
- It might take up to 6 calendar months to review and merge pull request
|
483
|
+
- It might take up to 1 calendar year to review an issue
|
484
|
+
- New Slack features are not nessessarily added to the gem
|
485
|
+
- Pull request should have test coverage for affected parts
|
486
|
+
- Pull request should have changelog entry
|
487
|
+
|
488
|
+
## Publishing
|
489
|
+
|
490
|
+
```sh
|
491
|
+
rm grape-slack-bot-*.gem
|
492
|
+
gem build grape-slack-bot.gemspec
|
493
|
+
gem push grape-slack-bot-*.gem
|
494
|
+
```
|
495
|
+
|
496
|
+
## License
|
497
|
+
|
498
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = "grape-slack-bot"
|
5
|
+
gem.version = File.read(File.expand_path('../lib/slack_bot.rb', __FILE__)).match(/VERSION\s*=\s*'(.*?)'/)[1]
|
6
|
+
|
7
|
+
repository_url = "https://github.com/amkisko/grape-slack-bot.rb"
|
8
|
+
root_files = %w(CHANGELOG.md LICENSE.md README.md)
|
9
|
+
root_files << "#{gem.name}.gemspec"
|
10
|
+
|
11
|
+
gem.license = "MIT"
|
12
|
+
|
13
|
+
gem.platform = Gem::Platform::RUBY
|
14
|
+
|
15
|
+
gem.authors = ["Andrei Makarov"]
|
16
|
+
gem.email = ["andrei@kiskolabs.com"]
|
17
|
+
gem.homepage = repository_url
|
18
|
+
gem.summary = %q{Slack bot implementation for ruby-grape}
|
19
|
+
gem.description = gem.summary
|
20
|
+
gem.metadata = {
|
21
|
+
"homepage" => repository_url,
|
22
|
+
"source_code_uri" => repository_url,
|
23
|
+
"bug_tracker_uri" => "#{repository_url}/issues",
|
24
|
+
"changelog_uri" => "#{repository_url}/blob/main/CHANGELOG.md",
|
25
|
+
"rubygems_mfa_required" => "true"
|
26
|
+
}
|
27
|
+
|
28
|
+
gem.executables = Dir.glob("bin/*").map{ |f| File.basename(f) }
|
29
|
+
gem.files = Dir.glob("lib/**/*.rb") + Dir.glob("bin/**/*") + root_files
|
30
|
+
gem.test_files = Dir.glob("spec/**/*_spec.rb")
|
31
|
+
|
32
|
+
gem.required_ruby_version = ">= 1.9.3"
|
33
|
+
gem.require_paths = ["lib"]
|
34
|
+
|
35
|
+
gem.add_runtime_dependency 'rack', '~> 2'
|
36
|
+
gem.add_runtime_dependency 'grape', '~> 1'
|
37
|
+
gem.add_runtime_dependency 'faraday', '~> 1'
|
38
|
+
gem.add_runtime_dependency 'activesupport', '~> 7'
|
39
|
+
|
40
|
+
gem.add_development_dependency 'bundler', '~> 2'
|
41
|
+
gem.add_development_dependency 'rake', '~> 13'
|
42
|
+
gem.add_development_dependency 'pry-byebug', '~> 3'
|
43
|
+
gem.add_development_dependency 'rspec', '~> 3'
|
44
|
+
gem.add_development_dependency 'activesupport', '~> 7'
|
45
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
module SlackBot
|
4
|
+
class ApiResponse
|
5
|
+
attr_reader :response
|
6
|
+
def initialize(&block)
|
7
|
+
@response = block.call
|
8
|
+
SlackBot::DevConsole.log_output "#{self.class.name}: #{response.body}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def ok?
|
12
|
+
response.status == 200 && data["ok"]
|
13
|
+
end
|
14
|
+
|
15
|
+
def error
|
16
|
+
data["error"]
|
17
|
+
end
|
18
|
+
|
19
|
+
def data
|
20
|
+
JSON.parse(response.body)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
class ApiClient
|
24
|
+
attr_reader :client
|
25
|
+
def initialize(authorization_token: ENV["SLACK_BOT_API_TOKEN"])
|
26
|
+
authorization_token_available = !authorization_token.nil? && authorization_token.is_a?(String) && !authorization_token.empty?
|
27
|
+
raise "Slack bot API token is not set" if !authorization_token_available
|
28
|
+
|
29
|
+
@client =
|
30
|
+
Faraday.new do |conn|
|
31
|
+
conn.request :url_encoded
|
32
|
+
conn.adapter Faraday.default_adapter
|
33
|
+
conn.url_prefix = "https://slack.com/api/"
|
34
|
+
conn.headers["Content-Type"] = "application/json; charset=utf-8"
|
35
|
+
conn.headers["Authorization"] = "Bearer #{authorization_token}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def views_open(trigger_id:, view:)
|
40
|
+
ApiResponse.new { client.post("views.open", { trigger_id: trigger_id, view: view }.to_json) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def views_update(view_id:, view:)
|
44
|
+
ApiResponse.new { client.post("views.update", { view_id: view_id, view: view }.to_json) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def chat_post_message(channel:, text:, blocks:)
|
48
|
+
ApiResponse.new { client.post("chat.postMessage", { channel: channel, text: text, blocks: blocks }.to_json) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def chat_update(channel:, ts:, text:, blocks:)
|
52
|
+
ApiResponse.new { client.post("chat.update", { channel: channel, ts: ts, text: text, blocks: blocks }.to_json) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def users_info(user_id:)
|
56
|
+
ApiResponse.new { client.post("users.info", { user: user_id }.to_json) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def views_publish(user_id:, view:)
|
60
|
+
ApiResponse.new { client.post("views.publish", { user_id: user_id, view: view }.to_json) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
3
|
+
|
4
|
+
module SlackBot
|
5
|
+
class ArgsParser
|
6
|
+
def initialize(args)
|
7
|
+
@args = args
|
8
|
+
end
|
9
|
+
def call
|
10
|
+
Rack::Utils.parse_query(@args)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
class ArgsBuilder
|
14
|
+
def initialize(args)
|
15
|
+
@args = args
|
16
|
+
end
|
17
|
+
def call
|
18
|
+
Rack::Utils.build_query(@args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
class Args
|
22
|
+
attr_accessor :args
|
23
|
+
def initialize(builder: ArgsBuilder, parser: ArgsParser)
|
24
|
+
@args = {}
|
25
|
+
@builder = builder
|
26
|
+
@parser = parser
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
args[key]
|
31
|
+
end
|
32
|
+
|
33
|
+
def []=(key, value)
|
34
|
+
args[key] = value
|
35
|
+
end
|
36
|
+
|
37
|
+
def raw_args=(raw_args)
|
38
|
+
@raw_args = raw_args
|
39
|
+
self.args = @parser.new(raw_args).call&.with_indifferent_access || {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
@builder.new(args).call
|
44
|
+
end
|
45
|
+
|
46
|
+
def merge(**other_args)
|
47
|
+
self.class.new.tap do |new_args|
|
48
|
+
new_args.args = args.merge(other_args)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def except(*keys)
|
53
|
+
self.class.new.tap do |new_args|
|
54
|
+
new_args.args = args.except(*keys)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|