Skip to content

Commit 45da8fe

Browse files
authored
Merge pull request #29 from ombulabs/feature/slack-bot
Add slack bot to allow for puzzle suggestions
2 parents d046f21 + feff8a7 commit 45da8fe

16 files changed

Lines changed: 257 additions & 2 deletions

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ gem "thruster", require: false
4444
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
4545
# gem "image_processing", "~> 1.2"
4646

47+
gem "slack-ruby-block-kit", ">= 0.24.0"
48+
gem "slack-ruby-client", ">= 2.4.0"
49+
4750
# auth gems
4851
gem "omniauth"
4952
gem "omniauth-google-oauth2"

Gemfile.lock

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ GEM
7777
ast (2.4.2)
7878
base64 (0.3.0)
7979
bcrypt_pbkdf (1.1.1)
80+
bcrypt_pbkdf (1.1.1-arm64-darwin)
8081
benchmark (0.4.1)
8182
bigdecimal (3.2.1)
8283
bindex (0.8.1)
@@ -125,6 +126,11 @@ GEM
125126
faraday-net_http (>= 2.0, < 3.5)
126127
json
127128
logger
129+
faraday-mashify (1.0.0)
130+
faraday (~> 2.0)
131+
hashie
132+
faraday-multipart (1.1.0)
133+
multipart-post (~> 2.0)
128134
faraday-net_http (3.4.0)
129135
net-http (>= 0.5.0)
130136
ffi (1.17.1-aarch64-linux-gnu)
@@ -137,6 +143,8 @@ GEM
137143
fugit (1.11.1)
138144
et-orbi (~> 1, >= 1.2.11)
139145
raabro (~> 1.4)
146+
gli (2.22.2)
147+
ostruct
140148
globalid (1.2.1)
141149
activesupport (>= 6.1)
142150
hashie (5.0.0)
@@ -193,6 +201,7 @@ GEM
193201
msgpack (1.8.0)
194202
multi_xml (0.7.2)
195203
bigdecimal (~> 3.1)
204+
multipart-post (2.4.1)
196205
mutex_m (0.3.0)
197206
net-http (0.6.0)
198207
uri
@@ -375,6 +384,15 @@ GEM
375384
rufus-scheduler (~> 3.2)
376385
sidekiq (>= 6, < 8)
377386
tilt (>= 1.4.0, < 3)
387+
slack-ruby-block-kit (0.26.0)
388+
zeitwerk (~> 2.6)
389+
slack-ruby-client (2.6.0)
390+
faraday (>= 2.0)
391+
faraday-mashify
392+
faraday-multipart
393+
gli
394+
hashie
395+
logger
378396
snaky_hash (2.0.3)
379397
hashie (>= 0.1.0, < 6)
380398
version_gem (>= 1.1.8, < 3)
@@ -473,6 +491,8 @@ DEPENDENCIES
473491
selenium-webdriver
474492
sidekiq
475493
sidekiq-scheduler
494+
slack-ruby-block-kit (>= 0.24.0)
495+
slack-ruby-client (>= 2.4.0)
476496
solid_cable
477497
solid_cache
478498
solid_queue

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Get Daily Puzzles delivered to your preferred server channel with a "Is it Ruby
1414
# Set Up
1515
```
1616
git clone git@github.com:ombulabs/ruby-or-rails.git
17-
cd reads
17+
cd ruby-or-rails
1818
./bin/setup
1919
```
2020
Check the .env.sample file for information on what environment variables you will need.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
class Slack::ApplicationController < ApplicationController
2+
skip_before_action :verify_authenticity_token
3+
before_action :valid_slack_request?
4+
5+
private
6+
7+
def valid_slack_request?
8+
@verified ||= verify_slack_signature
9+
end
10+
11+
def verify_slack_signature
12+
timestamp = request.headers["X-Slack-Request-Timestamp"]
13+
signature = request.headers["X-Slack-Signature"]
14+
15+
if Time.now.to_i - timestamp.to_i > 300
16+
@verified = false
17+
return
18+
end
19+
20+
base_string = "v0:#{timestamp}:#{request.raw_post}"
21+
my_signature = "v0=" + OpenSSL::HMAC.hexdigest(
22+
"SHA256",
23+
ENV["SLACK_SIGNING_SECRET"],
24+
base_string
25+
)
26+
27+
ActiveSupport::SecurityUtils.secure_compare(signature, my_signature)
28+
end
29+
30+
def slack_client
31+
@slack_client ||= SlackClient::Client.instance
32+
end
33+
34+
def open_view(view, trigger_id:)
35+
slack_client = SlackClient::Client.instance
36+
slack_client.views_open view: view, trigger_id: trigger_id
37+
rescue Slack::Web::Api::Errors::SlackError => e
38+
Rails.logger.error "Failed to open Slack modal: #{e.message} #{e.response_metadata}"
39+
head :unprocessable_entity
40+
end
41+
42+
def send_message(message, channel_id:)
43+
SlackClient::Client.instance.chat_postMessage(channel: channel_id, blocks: message)
44+
rescue Slack::Web::Api::Errors::SlackError
45+
head :unprocessable_entity
46+
end
47+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class Slack::CommandsController < Slack::ApplicationController
2+
def new_puzzle
3+
view = SlackClient::Views::PuzzleForm.new.create
4+
open_view(view, trigger_id: params[:trigger_id])
5+
end
6+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class Slack::PuzzlesController < Slack::ApplicationController
2+
def create
3+
payload = JSON.parse(params[:payload])
4+
user_id = payload["user"]["id"]
5+
values = payload["view"]["state"]["values"]
6+
question = values.dig("question", "question", "value")
7+
answer = values.dig("answer", "answer", "selected_option", "value")
8+
explanation = values.dig("explanation", "explanation", "value")
9+
link = values.dig("link", "link", "value")
10+
puzzle = Puzzle.new(question:, answer:, explanation:, link:, status: :pending, suggested_by: user_id)
11+
12+
if puzzle.save
13+
view = SlackClient::Views::Success.new.create
14+
notification_message = SlackClient::Messages::NewPuzzleNotification.new(puzzle).create
15+
send_message(notification_message, channel_id: ENV.fetch("SHIELD_NOTIFICATIONS_CHANNEL", nil))
16+
else
17+
view = SlackClient::Views::Failure.new.create
18+
end
19+
20+
render json: { response_action: "update", view: view }, status: :ok
21+
end
22+
end

app/lib/slack_client/client.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
require "singleton"
2+
3+
module SlackClient
4+
# This class is a wrapper around the Slack::Web::Client class that allows us to use the
5+
# singleton pattern to create a single instance of the Slack client.
6+
# This is useful because we can reuse the same client instance across the application,
7+
# which can help reduce the number of connections to the Slack API.
8+
#
9+
# Example:
10+
# slack_client = SlackClient::Client.instance
11+
#
12+
class Client
13+
include Singleton
14+
15+
def initialize
16+
@slack_client = Slack::Web::Client.new(token: ENV["SLACK_TOKEN"])
17+
end
18+
19+
private
20+
21+
def method_missing(method, *args, &block)
22+
if @slack_client.respond_to?(method)
23+
@slack_client.send(method, *args, &block)
24+
else
25+
super
26+
end
27+
rescue Slack::Web::Api::Errors::SlackError => e
28+
Rails.logger.error "Failed to complete Slack request: #{e.message} #{e.response_metadata}"
29+
raise
30+
end
31+
32+
def respond_to_missing?(method, include_private = false)
33+
@slack_client.respond_to?(method) || super
34+
end
35+
end
36+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module SlackClient
2+
module Messages
3+
class NewPuzzleNotification
4+
def initialize(puzzle, shield_group_id: ENV["SHIELD_GROUP_ID"])
5+
@puzzle = puzzle
6+
@shield_group_id = shield_group_id
7+
end
8+
9+
def create
10+
Slack::BlockKit.blocks do |block|
11+
block.section do |section|
12+
section.mrkdwn text: "*New Puzzle Suggestion!*\n\n"
13+
end
14+
block.section do |section|
15+
section.mrkdwn text: body_text
16+
end
17+
end.as_json
18+
end
19+
20+
private
21+
22+
def body_text
23+
<<~TEXT
24+
Hey <!subteam^#{@shield_group_id}>!
25+
26+
<@#{@puzzle.suggested_by}> has submitted a new suggestion for a puzzle:
27+
28+
Question:
29+
#{@puzzle.question}
30+
31+
Check out the <#{ENV.fetch('APP_URL', nil)}|admin panel> for more details.
32+
TEXT
33+
end
34+
end
35+
end
36+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module SlackClient
2+
module Views
3+
class Failure
4+
def create
5+
blocks = Slack::BlockKit.blocks do |block|
6+
block.section do |section|
7+
section.plain_text text: ":blob-no: Sorry, I could not register the puzzle suggestion."
8+
end
9+
end
10+
Slack::BlockKit.modal blocks: blocks, title: "Uh oh..." do |modal|
11+
modal.close text: "Close"
12+
end.as_json
13+
end
14+
end
15+
end
16+
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module SlackClient
2+
module Views
3+
class PuzzleForm
4+
def create
5+
blocks = Slack::BlockKit.blocks do |blocks|
6+
blocks.input label: "What is the Puzzle question?", block_id: "question" do |input|
7+
input.plain_text_input action_id: "question", multiline: true
8+
end
9+
10+
blocks.input label: "Answer", block_id: "answer" do |input|
11+
input.radio_buttons action_id: "answer" do |radio_button|
12+
radio_button.option text: "Ruby", value: "ruby"
13+
radio_button.option text: "Rails", value: "rails"
14+
end
15+
end
16+
17+
blocks.input label: "Explanation", block_id: "explanation" do |input|
18+
input.plain_text_input action_id: "explanation", multiline: true
19+
end
20+
21+
blocks.input label: "Link to documentation", block_id: "link", optional: true do |input|
22+
input.plain_text_input action_id: "link"
23+
end
24+
end
25+
26+
Slack::BlockKit.modal blocks: blocks, external_id: "puzzle_form", title: "Suggest a Puzzle" do |modal|
27+
modal.submit text: "Submit"
28+
modal.close text: "Close"
29+
end.as_json
30+
end
31+
end
32+
end
33+
end

0 commit comments

Comments
 (0)