-
Notifications
You must be signed in to change notification settings - Fork 5
Implement slash command to list channels #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0837bd9
d1f1070
6fef2a0
cc010d1
33d66f0
269011a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,4 +8,13 @@ def verify_slack_hmac | |
| rescue Slack::Events::Request::MissingSigningSecret, Slack::Events::Request::InvalidSignature, Slack::Events::Request::TimestampExpired | ||
| render plain: "Nope.", status: :unauthorized | ||
| end | ||
|
|
||
| def subcommand | ||
| Utils::ParsesCliStyleCommandArgs.new.call(text: params[:text]).subcommand | ||
| end | ||
|
|
||
| def command_params | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The intent for this method is for arguments passed to a slash command to feel like typical rails |
||
| parsed = Utils::ParsesCliStyleCommandArgs.new.call(text: params[:text]) | ||
| ActionController::Parameters.new(parsed.args) | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| module Chatops | ||
| class SlackSlashCommandListController < ::ApplicationChatopsController | ||
| def handle | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be treated as a minimal first pass to be iterated on. Slack does not turn channels into links when the channel name is used. Instead we need to use the channel name to look up the channel ID and use that, like |
||
| config = Matchmaking.config | ||
| message = config.to_h.keys.sort.each_with_object([]) do |key, result| | ||
| grouping = config[key] | ||
| next unless include_grouping_in_list?(grouping) | ||
| result << "*#{key.to_s.titleize}*: Meets #{grouping.schedule} in groups of #{grouping.size} (Join: ##{grouping.channel})" | ||
| end.join("\n") | ||
|
|
||
| if config.to_h.keys.any? | ||
| render plain: message | ||
| else | ||
| render plain: "Sorry! There are no configured channels for groupings." | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def include_grouping_in_list?(grouping) | ||
| grouping.active || command_params.key?(:all) | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| module Matchmaking | ||
| def config | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Despite my growing distaste for Rails' |
||
| Rails.application.config.x.matchmaking | ||
| end | ||
| module_function :config | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,20 @@ | ||
| class SlackSlashSubcommandConstraint | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Constraint allows us to route different subcommands to different controllers instead of building up some big case statement based on subcommands. There are no specific tests for this but it should be covered by the Request specs that make sure the |
||
| def initialize(matches_subcommand:) | ||
| @subcommand = matches_subcommand | ||
| end | ||
|
|
||
| def matches?(request) | ||
| parsed = Utils::ParsesCliStyleCommandArgs.new.call(text: request.params["text"]) | ||
| parsed.subcommand == @subcommand | ||
| end | ||
| end | ||
|
|
||
| Rails.application.routes.draw do | ||
| if Rails.env.test? | ||
| TestOnlyRoutes = ActionDispatch::Routing::RouteSet.new unless defined?(::TestOnlyRoutes) | ||
| mount TestOnlyRoutes, at: "/" | ||
| end | ||
|
|
||
| post "/command/doubleup", to: "chatops/slack_slash_command_list#handle", constraints: SlackSlashSubcommandConstraint.new(matches_subcommand: "channels:list") | ||
| post "/command/doubleup", to: "chatops/slack_slash_command#handle" | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| require "rails_helper" | ||
|
|
||
| RSpec.describe "SlackSlashCommandListController", type: :request do | ||
| scenario "responds with default message when no channels configured" do | ||
| allow(Matchmaking).to receive(:config).and_return({}) | ||
|
|
||
| signed = signed_request_body(params: { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Proposing this as a simple helper to manage the params in tests (see private method in this file below). If this patterns works then we can extract this into a test helper method and update the other tests. |
||
| "team_id" => "T02PF6RHYSY", | ||
| "team_domain" => "testdouble-hq", | ||
| "channel_id" => "C02NYBB3VPH", | ||
| "channel_name" => "some-channel", | ||
| "user_id" => "U02PRHH0XEV", | ||
| "user_name" => "cliff.pruitt", | ||
| "command" => "/doubleup", | ||
| "text" => "channels:list", | ||
| "api_app_id" => "A02PD0DUE03", | ||
| "is_enterprise_install" => "false", | ||
| "response_url" => | ||
| "https://hooks.slack.com/commands/T02PF6RHYSY/2823421496992/0WC0HfWeGJpHetxmF8yUmawo", | ||
| "trigger_id" => "2801968477828.2797229610916.883001cf5008d6d02fd58a3cf70f449e" | ||
| }) | ||
|
|
||
| request_headers = { | ||
| "X-Slack-Signature" => signed.signature, | ||
| "X-Slack-Request-Timestamp" => signed.timestamp | ||
| } | ||
|
|
||
| post "/command/doubleup", | ||
| params: signed.body, | ||
| headers: request_headers | ||
|
|
||
| expect(response).to have_http_status(:ok) | ||
| expect(response.body).to eq("Sorry! There are no configured channels for groupings.") | ||
| end | ||
|
|
||
| scenario "responds with list of only active channels" do | ||
| allow(Matchmaking).to receive(:config).and_return(OpenStruct.new({ | ||
| active_one: OpenStruct.new({ | ||
| active: true, | ||
| schedule: "weekly", | ||
| size: 2, | ||
| channel: "active-1" | ||
| }), | ||
| not_active_one: OpenStruct.new({ | ||
| active: false, | ||
| schedule: "weekly", | ||
| size: 2, | ||
| channel: "not-active-1" | ||
| }), | ||
| active_two: OpenStruct.new({ | ||
| active: true, | ||
| schedule: "weekly", | ||
| size: 2, | ||
| channel: "active-2" | ||
| }) | ||
| })) | ||
|
|
||
| signed = signed_request_body(params: { | ||
| "team_id" => "T02PF6RHYSY", | ||
| "team_domain" => "testdouble-hq", | ||
| "channel_id" => "C02NYBB3VPH", | ||
| "channel_name" => "some-channel", | ||
| "user_id" => "U02PRHH0XEV", | ||
| "user_name" => "cliff.pruitt", | ||
| "command" => "/doubleup", | ||
| "text" => "channels:list", | ||
| "api_app_id" => "A02PD0DUE03", | ||
| "is_enterprise_install" => "false", | ||
| "response_url" => | ||
| "https://hooks.slack.com/commands/T02PF6RHYSY/2823421496992/0WC0HfWeGJpHetxmF8yUmawo", | ||
| "trigger_id" => "2801968477828.2797229610916.883001cf5008d6d02fd58a3cf70f449e" | ||
| }) | ||
|
|
||
| request_headers = { | ||
| "X-Slack-Signature" => signed.signature, | ||
| "X-Slack-Request-Timestamp" => signed.timestamp | ||
| } | ||
|
|
||
| post "/command/doubleup", | ||
| params: signed.body, | ||
| headers: request_headers | ||
|
|
||
| expect(response).to have_http_status(:ok) | ||
| expected_response = <<~MSG.chomp | ||
| *Active One*: Meets weekly in groups of 2 (Join: #active-1) | ||
| *Active Two*: Meets weekly in groups of 2 (Join: #active-2) | ||
| MSG | ||
| expect(response.body).to eq(expected_response) | ||
| end | ||
|
|
||
| scenario "responds with list of active and inactive channels with --all" do | ||
| allow(Matchmaking).to receive(:config).and_return(OpenStruct.new({ | ||
| active_one: OpenStruct.new({ | ||
| active: true, | ||
| schedule: "weekly", | ||
| size: 2, | ||
| channel: "active-1" | ||
| }), | ||
| not_active_one: OpenStruct.new({ | ||
| active: false, | ||
| schedule: "weekly", | ||
| size: 2, | ||
| channel: "not-active-1" | ||
| }), | ||
| active_two: OpenStruct.new({ | ||
| active: true, | ||
| schedule: "weekly", | ||
| size: 2, | ||
| channel: "active-2" | ||
| }) | ||
| })) | ||
|
|
||
| signed = signed_request_body(params: { | ||
| "team_id" => "T02PF6RHYSY", | ||
| "team_domain" => "testdouble-hq", | ||
| "channel_id" => "C02NYBB3VPH", | ||
| "channel_name" => "some-channel", | ||
| "user_id" => "U02PRHH0XEV", | ||
| "user_name" => "cliff.pruitt", | ||
| "command" => "/doubleup", | ||
| "text" => "channels:list --all", | ||
| "api_app_id" => "A02PD0DUE03", | ||
| "is_enterprise_install" => "false", | ||
| "response_url" => | ||
| "https://hooks.slack.com/commands/T02PF6RHYSY/2823421496992/0WC0HfWeGJpHetxmF8yUmawo", | ||
| "trigger_id" => "2801968477828.2797229610916.883001cf5008d6d02fd58a3cf70f449e" | ||
| }) | ||
|
|
||
| request_headers = { | ||
| "X-Slack-Signature" => signed.signature, | ||
| "X-Slack-Request-Timestamp" => signed.timestamp | ||
| } | ||
|
|
||
| post "/command/doubleup", | ||
| params: signed.body, | ||
| headers: request_headers | ||
|
|
||
| expect(response).to have_http_status(:ok) | ||
| expected_response = <<~MSG.chomp | ||
| *Active One*: Meets weekly in groups of 2 (Join: #active-1) | ||
| *Active Two*: Meets weekly in groups of 2 (Join: #active-2) | ||
| *Not Active One*: Meets weekly in groups of 2 (Join: #not-active-1) | ||
| MSG | ||
| expect(response.body).to eq(expected_response) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def signed_request_body(params: {}) | ||
| slack_signing_secret = Slack::Events.config.signing_secret | ||
| timestamp = Time.zone.now.to_i | ||
| request_body = params.to_param | ||
| data = ["v0", timestamp, request_body].join(":") | ||
| mac = OpenSSL::HMAC.hexdigest("SHA256", slack_signing_secret, data) | ||
| OpenStruct.new({ | ||
| signature: "v0=#{mac}", | ||
| timestamp: timestamp, | ||
| body: request_body | ||
| }) | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| require "rails_helper" | ||
|
|
||
| class CommandParamsChatopsController < ApplicationChatopsController | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Testing slash command |
||
| def index | ||
| render plain: "Got command <#{subcommand}> with name <#{command_params[:name]}> and group size <#{command_params[:group_size]}>" | ||
| end | ||
| end | ||
|
|
||
| RSpec.describe "ApplicationChatopsController command params", type: :request do | ||
| before :all do | ||
| TestOnlyRoutes.draw do | ||
| post "/chatops/test-params", to: "command_params_chatops#index" | ||
| end | ||
| end | ||
|
|
||
| after :all do | ||
| TestOnlyRoutes.clear! | ||
| end | ||
|
|
||
| scenario "provides subcommand and command_params to actions" do | ||
| slack_signing_secret = Slack::Events.config.signing_secret | ||
| timestamp = Time.zone.now.to_i | ||
| request_body = "token=#{slack_signing_secret}&team_id=T02PF6RHYSY&team_domain=testdouble-hq&channel_id=C02NYBB3VPH&channel_name=some-channel&user_id=U02PRHH0XEV&user_name=cliff.pruitt&command=%2Fdoubleup&text=snazzy%3Acommand+--name%3Dblargh+--group-size%3D4&api_app_id=A02PD0DUE03&is_enterprise_install=false&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT02PF6RHYSY%2F2823421496992%2F0WC0HfWeGJpHetxmF8yUmawo&trigger_id=2801968477828.2797229610916.883001cf5008d6d02fd58a3cf70f449e" | ||
| data = ["v0", timestamp, request_body].join(":") | ||
| mac = OpenSSL::HMAC.hexdigest("SHA256", slack_signing_secret, data) | ||
| signature = "v0=#{mac}" | ||
|
|
||
| request_headers = { | ||
| "X-Slack-Signature" => signature, | ||
| "X-Slack-Request-Timestamp" => timestamp | ||
| } | ||
|
|
||
| request_params = { | ||
| "token" => slack_signing_secret, | ||
| "team_id" => "T02PF6RHYSY", | ||
| "team_domain" => "testdouble-hq", | ||
| "channel_id" => "C02NYBB3VPH", | ||
| "channel_name" => "some-channel", | ||
| "user_id" => "U02PRHH0XEV", | ||
| "user_name" => "cliff.pruitt", | ||
| "command" => "/doubleup", | ||
| "text" => "snazzy:command --name=blargh --group-size=4", | ||
| "api_app_id" => "A02PD0DUE03", | ||
| "is_enterprise_install" => "false", | ||
| "response_url" => | ||
| "https://hooks.slack.com/commands/T02PF6RHYSY/2823421496992/0WC0HfWeGJpHetxmF8yUmawo", | ||
| "trigger_id" => "2801968477828.2797229610916.883001cf5008d6d02fd58a3cf70f449e" | ||
| } | ||
|
|
||
| post "/chatops/test-params", | ||
| params: request_params, | ||
| headers: request_headers | ||
|
|
||
| expect(response).to have_http_status(:ok) | ||
| expect(response.body).to eq("Got command <snazzy:command> with name <blargh> and group size <4>") | ||
| end | ||
|
|
||
| scenario "provides subcommand and command_params to actions for empty params" do | ||
| slack_signing_secret = Slack::Events.config.signing_secret | ||
| timestamp = Time.zone.now.to_i | ||
| request_body = "token=#{slack_signing_secret}&team_id=T02PF6RHYSY&team_domain=testdouble-hq&channel_id=C02NYBB3VPH&channel_name=some-channel&user_id=U02PRHH0XEV&user_name=cliff.pruitt&command=%2Fdoubleup&text=&api_app_id=A02PD0DUE03&is_enterprise_install=false&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT02PF6RHYSY%2F2823421496992%2F0WC0HfWeGJpHetxmF8yUmawo&trigger_id=2801968477828.2797229610916.883001cf5008d6d02fd58a3cf70f449e" | ||
| data = ["v0", timestamp, request_body].join(":") | ||
| mac = OpenSSL::HMAC.hexdigest("SHA256", slack_signing_secret, data) | ||
| signature = "v0=#{mac}" | ||
|
|
||
| request_headers = { | ||
| "X-Slack-Signature" => signature, | ||
| "X-Slack-Request-Timestamp" => timestamp | ||
| } | ||
|
|
||
| request_params = { | ||
| "token" => slack_signing_secret, | ||
| "team_id" => "T02PF6RHYSY", | ||
| "team_domain" => "testdouble-hq", | ||
| "channel_id" => "C02NYBB3VPH", | ||
| "channel_name" => "some-channel", | ||
| "user_id" => "U02PRHH0XEV", | ||
| "user_name" => "cliff.pruitt", | ||
| "command" => "/doubleup", | ||
| "text" => "", | ||
| "api_app_id" => "A02PD0DUE03", | ||
| "is_enterprise_install" => "false", | ||
| "response_url" => | ||
| "https://hooks.slack.com/commands/T02PF6RHYSY/2823421496992/0WC0HfWeGJpHetxmF8yUmawo", | ||
| "trigger_id" => "2801968477828.2797229610916.883001cf5008d6d02fd58a3cf70f449e" | ||
| } | ||
|
|
||
| post "/chatops/test-params", | ||
| params: request_params, | ||
| headers: request_headers | ||
|
|
||
| expect(response).to have_http_status(:ok) | ||
| expect(response.body).to eq("Got command <> with name <> and group size <>") | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This returns the subcommand for the given request. For
/doubleup listthis would be"list"