Back in my Campfire days, I remember being blown away when Github released Hubot. Our team tinkered around with our install for days – hoping to make Hubot do our bidding. It was a great little tool (and still is!), but I’ve recently fallen in love with Slack’s “Slash Commands”. They’re a handy way to send commands to a service and get a response. And the best part? You can write one in Ruby!
Slash commands work in a simple manner: you start a command with a forward slash, then the command name. For this basic example, we’ll ask for weather for a particular zip code:
/weather 83864
The entire command, along with any additional text, is sent as a webhook to your application.
token=gIkuvaNzQIHg97ATvDxqgjtO
team_id=T0001
team_domain=example
channel_id=C2147483705
channel_name=test
user_id=U2147483697
user_name=Steve
command=/weather
text=83864
response_url=https://hooks.slack.com/commands/1234/5678
The token
is a secret setup when you configure the command inside your Slack instance. You’ll be checking this to ensure the command is actually from Slack.
The response_url
is where we’ll send our data once it’s ready.
The webhook from Slack comes as a POST
request, so we’ll create a Rails controller with a create
method:
class CommandsController < ApplicationController
def create
return render json: {}, status: 403 unless valid_slack_token?
CommandWorker.perform_async(command_params.to_h)
render json: { response_type: "in_channel" }, status: :created
end
private
def valid_slack_token?
params[:token] == ENV["SLACK_SLASH_COMMAND_TOKEN"]
end
# Only allow a trusted parameter "white list" through.
def command_params
params.permit(:text, :token, :user_id, :response_url)
end
end
We’re doing a few simple things here:
valid_slack_token?
which compares the token in the params with the one Slack gave us (and is stored in an ENV var)..to_h
to do this for me) We’re using a background job here since we’ll be communicating with a 3rd party API. That way, we can return a response quickly to Slack and avoid a timeout.The { response_type: "in_channel" }
JSON tells slack to leave the original command (/weather 83864
) in the Slack chat history. If you’re sending something back that only the user will see privately, you’ll want to return { response_type: "ephemeral" }
at this step.
Next, we’ll have our Sidekiq worker fetch the data and send it all back using the response_url
that Slack sent us in the params.
To fetch the Weather data, I’m using Weather Underground. I’m going to create a basic class to encapsulate some of this behavior.
class Weather
attr_reader :zip
def initialize(zip)
@zip = zip
end
def city
data["full"]
end
def temperature
data["temperature_string"]
end
def icon_url
data["icon_url"]
end
private
def data
@data ||= HTTParty.get("https://api.wunderground.com/api/#{ENV['WUNDERGROUND_KEY']}/conditions/q/#{zip}.json")["current_observation"]["display_location"]
end
end
And then, the Sidekiq worker which uses this class:
class CommandWorker
include Sidekiq::Worker
sidekiq_options :retry => false
def perform(params)
weather = Weather.new(params[:text])
message = {
text: "The temperature in #{weather.city} is #{weather.temperature}",
response_type: "in_channel"
}
HTTParty.post(params[:response_url], { body: message.to_json, headers: {
"Content-Type" => "application/json"
}
})
end
end
Here’s what’s happening:
CommandWorker
to be used in generating the text.response_url
value it sent us in the first step.To make our message a little prettier, we can send along the image URL as an “attachment” to Slack. This requires simply modifying our JSON a bit like this:
{
"attachments": [
{
"title": "Weather for Sandpoint, ID",
"title_link": "https://www.wunderground.com/US/ID/Sandpoint.html",
"text": "Optional text that appears within the attachment",
"fields": [
{
"title": "Temp",
"value": "68F",
"short": false
},
{
"title": "Wind",
"value": "16mph",
"short": false
}
],
"image_url": "https://icons.wxug.com/i/c/k/clear.gif"
}
]
}
Slack provides a nice demo tool of message formatting. It’s very handy when getting your messages to look just right.
Next, we may want to send different types of commands to the same app. For instance, I could specify what I want from the weather app, like this:
/weather forecast:tomorrow 83864
/weather temp:f 83864
To do so, you may choose to separate out pieces of the app into separate modules or sub-classes, then dispatch them from one place. In an app I’m working on at Heroku, we pass commands like this:
/slack-app-name task:subtask command
…where slack-app-name
is one of a few different Slash Commands apps. In order to properly parse the task, subtask, and command, we have a Processor module:
module Commands
module Processor
extend self
def init(params)
task = task(params).titleize
"Commands::#{task}".constantize.new(params)
end
# Given `task:sub message`, return task
def task(params)
params[:text].scan(/\w+/).first
end
# Given `task:sub message`, return subtask
def subtask(params)
params[:text].scan(/\w+/)[1]
end
# Given `task:sub message`, return message
def message(params)
params[:text][/\w+:\w+\s(.*)/, 1]
end
end
end
We use this to initialize the correct class to handle the command:
class CommandWorker
include Sidekiq::Worker
sidekiq_options :retry => false # job will be discarded immediately if failed
def perform(params)
Commands::Processor.init(params).run
end
end
Now we can send something like /weather forecast:tomorrow 83864
and have a class Commands::Forecast
process it by looking at subtask
(tomorrow
) and message
(83864
). By doing so we can extend and expand our app to process multiple different tasks.
I hope this gives you a glimpse into what’s possible with Slash Commands! Happy coding, Rubyist!
Writer. Musician. Adventurer. Nerd.
Purveyor of GIFs and dad jokes.