Slack on Rails

Back in the days of Campfire, Hubot was an essential companion to the company chatroom. Hubot was great for the automation of some simple tasks, but the performance of more complex tasks involving external services often seemed more complicated than necessary. And the performance of private tasks was completely impossible. Campfire simply doesn't allow for a better solution.

Now it's 2015 and Slack fever is in full tilt. Slack offers numerous advantages over Campfire, but without question, the most powerful is a greatly enhanced ability to use the chatroom as an interface to other services through its powerful integrations.

Writing custom service integrations for your chatroom is easier than ever before. At Viget, we use Slack as an interface to a few of our internal apps. In this post you'll discover exactly how easy it is to build these integrations into an existing Ruby on Rails app.

Creating a Rails Slack Service

Choosing Your Integration Methods

Slack offers a number of ways to interact with your company's chat data.

integration types

For the purposes of a simple command-and-response service (an app that listens to your coworkers' commands, performs some action, and replies with handy info), Slash Commands and Outgoing WebHooks are perfect.

Let's take a closer look at each of these types of integrations.

Outgoing WebHooks

Outgoing WebHooks allow your app to receive Slack messages, and respond publicly to the channel (the responses are visible to all users in the channel).

Example Outgoing WebHook Interaction

outgoing webhook at work

Slack Configuration

Choose to receive either A) messages beginning with a trigger word from any channel, or B) all messages from a particular channel.

configuration

Receiving

Slack will make a POST request with a payload similar to the following:

 {
 token<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>a1b23c4d5e6f7g<span class="pl-pds">"</span></span>
 team_id<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>T1234<span class="pl-pds">"</span></span>
 channel_id<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>C1234567890<span class="pl-pds">"</span></span>
 channel_name<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>dev<span class="pl-pds">"</span></span>
 timestamp<span class="pl-k">:</span> <span class="pl-c1">1355517523.000005</span>
 user_id<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>U1234567890<span class="pl-pds">"</span></span>
 user_name<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>ltk<span class="pl-pds">"</span></span>
 text<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>graba who's on first?<span class="pl-pds">"</span></span>
 trigger_word<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>graba<span class="pl-pds">"</span></span>
}

Responding

Respond as JSON, with your message at the key text.

 {
 <span class="pl-s"><span class="pl-pds">"</span>text<span class="pl-pds">"</span></span><span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>This is my response.<span class="pl-pds">"</span></span>
}

Slash Commands

Slash Commands allow your service to receive command messages (invisible to other users) and respond privately to the user issuing the command.

Example Slash Command Interaction

slash command at work

Slack Configuration

configuration

Receiving

Slack will make a POST request with a payload similar to the following:

 {
 token<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>a1b23c4d5e6f7g<span class="pl-pds">"</span></span>
 team_id<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>T1234<span class="pl-pds">"</span></span>
 channel_id<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>C1234567890<span class="pl-pds">"</span></span>
 channel_name<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>dev<span class="pl-pds">"</span></span>
 user_id<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>U1234567890<span class="pl-pds">"</span></span>
 user_name<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>ltk<span class="pl-pds">"</span></span>
 command<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>/graba<span class="pl-pds">"</span></span>
 text<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">"</span>book me something slick<span class="pl-pds">"</span></span>
}

Responding

Text response will be relayed to the user as the command response. (No JSON.)

Updating Your Rails App for Slack

Once the Slack configuration is complete, the easy part begins. Adding Slack integration endpoints to an existing Rails app is a trivial affair.

config/initializers/slack.rb

First we'll add our Slack integration tokens to our app so that we'll be able to authenticate requests made to our service's Slack endpoint.

You could also put this in config/secrets.yml if you'd prefer. It's your world; I'm just blogging in it.

We're reading the tokens from ENV, so be sure to actually set that environment variable (e.g. SLACK_TOKENS=a1b23c4d5e6f7g,a9b8c7d6e5f4g3).

 <span class="pl-k">module</span> <span class="pl-en">Slack</span>
 <span class="pl-c1">TOKENS</span> <span class="pl-k">=</span> <span class="pl-c1">ENV</span>.fetch(<span class="pl-s"><span class="pl-pds">'</span>SLACK_TOKENS<span class="pl-pds">'</span></span>).split(<span class="pl-s"><span class="pl-pds">'</span>,<span class="pl-pds">'</span></span>)
<span class="pl-k">end</span>

config/routes.rb

 resources <span class="pl-c1">:slack_responses</span>, <span class="pl-c1">only:</span> <span class="pl-c1">:create</span>

app/controllers/slack_responses_controller.rb

Now Slack message and command POSTs are flowing into our SlackResponsesController. In this controller we only need to account for the required response difference between Slash Commands and normal Slack messages. (We can do this by checking for a :command parameter in the POST. We delegate all responsibility for creating the response to a Responder.

 <span class="pl-k">class</span> <span class="pl-en">SlackResponsesController<span class="pl-e"> < ApplicationController</span></span>

 skip_before_filter <span class="pl-c1">:verify_authenticity_token</span>
 before_filter <span class="pl-c1">:verify_slack_token</span>

 <span class="pl-k">def</span> <span class="pl-en">create</span>
 render <span class="pl-c1">nothing:</span> <span class="pl-c1">true</span>, <span class="pl-c1">status:</span> <span class="pl-c1">:ok</span> <span class="pl-k">and</span> <span class="pl-k">return</span> <span class="pl-k">unless</span> responder.respond?

 <span class="pl-c"># Respond differently to Slash Command vs Webhook POSTs</span>
 <span class="pl-c"># See `Responding` sections above for the require difference.</span>
 <span class="pl-k">if</span> params[<span class="pl-c1">:command</span>].present? 
 render <span class="pl-c1">text:</span> responder.response.to_s
 <span class="pl-k">else</span>
 render <span class="pl-c1">json:</span> { <span class="pl-c1">text:</span> responder.response.to_s }
 <span class="pl-k">end</span>
 <span class="pl-k">end</span>

 <span class="pl-k">private</span>

 <span class="pl-k">def</span> <span class="pl-en">responder</span>
 <span class="pl-smi">@responder</span> <span class="pl-k">||=</span> <span class="pl-c1">Slack</span>::<span class="pl-c1">Responder</span>.<span class="pl-k">new</span>(params[<span class="pl-c1">:text</span>])
 <span class="pl-k">end</span>

 <span class="pl-k">def</span> <span class="pl-en">verify_slack_token</span>
 render <span class="pl-c1">nothing:</span> <span class="pl-c1">true</span>, <span class="pl-c1">status:</span> <span class="pl-c1">:forbidden</span> <span class="pl-k">and</span> <span class="pl-k">return</span> <span class="pl-k">unless</span> <span class="pl-c1">Slack</span>::<span class="pl-c1">TOKENS</span>.include?(params[<span class="pl-c1">:token</span>])
 <span class="pl-k">end</span>

<span class="pl-k">end</span>

lib/slack/responder.rb

This is where the magic happens. It's up to you to turn the Slack message into a useful reply within #response. (If your service needs more information than the message, simply pass the additional info into the responder object during instantiation.)

 <span class="pl-k">class</span> <span class="pl-en">Slack::Responder</span>

 <span class="pl-k">def</span> <span class="pl-en">initialize</span>(<span class="pl-smi">message</span>)
 <span class="pl-smi">@message</span> <span class="pl-k">=</span> message
 <span class="pl-k">end</span>

 <span class="pl-k">def</span> <span class="pl-en">respond?</span>
 response.present?
 <span class="pl-k">end</span>

 <span class="pl-k">def</span> <span class="pl-en">response</span>
 <span class="pl-smi">@response</span> <span class="pl-k">||=</span> <span class="pl-s"><span class="pl-pds">"</span>You asked: <span class="pl-pse">#{</span><span class="pl-s1">message</span><span class="pl-pse"><span class="pl-s1">}</span></span><span class="pl-pds">"</span></span>
 <span class="pl-k">end</span>

 <span class="pl-k">private</span>

 <span class="pl-k">attr_reader</span> <span class="pl-c1">:message</span>

<span class="pl-k">end</span>

This is what our responder looks like for Viget's conference room management service Graba.


That's it! Adding Slack integrations to your existing Rails services is straightforward and makes everyone's life easier. So what are you waiting for? Happy Slacking.

Lawson Kurtz

,
Posted in Article Category: #Code
on