Over the past year, Slack has changed the way businesses communicate. With a very well documented and intuitive API, Slack gives developers the ability to easily create custom integrations to get data out of or into Slack.
Need to get data out of Slack? Use their:
Need to get data into Slack? Use their:
At QuickLeft, we recently built a tool called Partyline, which allows us to create project tickets right from within our Slack channels.
We’re able to leverage the communication layer and infrastructure of Slack and turn it into a command interface to Sprintly, our project management software. All for free!
To make this work, we needed Partyline to be able to associate one Slack channel to one Sprintly product. This 1–1 relationship means that /sprintly
commands in a chosen channel cause an effect in one Sprintly product. Simple.
We implemented Partyline in 5 easy steps:
- Slack authentication
- Slash Command configuration
- Local development setup
- Receiving messages
- Processing Messages
Our tech stack:
Along the way we will use our nifty new slash command sytnax so you can get a feel for how Partyline works :)
Authentication
/sprintly create:story As a user I want to associate a slack channel to a sprintly channel so that my '/sprintly' command relates to my chosen sprintly product
Before writing any code, we need to setup a Slack App so that we can get API keys for authenticated requests. Set one up within the Slack developer API documentation. Create a new app and give it:
- A name
- The URL of your app
- A callback URL where Slack would redirect to after our users have authenticated
- A description
- The team which the Slash Command is for
For our server authentication we used 2 things:
Slack follows standard OAuth2 and documentation about that can be found here.
When authenticated, we fetch the user’s slack channels so that they can choose one to associate with a Sprintly product.
To do this we use Slack’s channels.list endpoint, making sure to make an authenticated request using our recently received oauth token. For a full description of this endpoint, visit Slack’s documentation.
ProTip: Make sure to exclude any Slack channels which you may have archived by passing an exclude_archived: 1
param.
In our request callback we make sure to:
- check for error states and
- Slack’s
body.ok
attribute to ensure that we have made a successful request
var request = require('request');
request.get('https://slack.com/api/channels.list', {
json: true,
qs: { token: #{YOUR_OAUTH_TOKEN}, exclude_archived: 1 }
}, function(err, resp, body) {
if (err) {
return next(err);
}
if (body.ok) {
next(null, body.channels);
} else {
next(new Error(body.error));
}
}
});
The body will contain a channels
key which contains the array of your team’s channels. We fetch our team’s Sprintly products in a separate request. We use these two collections to populate the select components in the Partyline UI. Users can now create an Integration
between the channel and product, which will be referenced when requests come from a Slack channel.
Slash Command Integrations
/sprintly create:task create slash command integration for my team
The Slash Command is the real communication enabler. It will add a custom named slash command for your team to use throughout your Slack app. When this command is submitted, Slack will POST a payload containing your message and other data to the URL you have specified during setup.
The payload looks similar to this:
token=12345SDFDFGS678
team_id=T0001
team_domain=example
channel_id=C123456488
channel_name=test
user_id=U12346589898
user_name=Steve
command=/commandName
text=something
The URL is the public endpoint of our app that will receive incoming requests. We determine whether the request is coming from an authentic and configured Slack source by inspecting the payload’s token
.
Head over to the Slack Slash Command integration page to create a new one for your team.
- They will be listed under the
Configured Integrations
tab on the services page. - Click on
Slash Commands
- Click on
Add
- Give your command a name!
- Enter the production URL which Slack will POST messages to
- Add the remaining non-essential description meta fields
The Token
In our case we require our users to save their slash command token via the client UI so that we can validate incoming requests from different teams.(We do this before we have an official Slack integration for Partyline. Currently there are over 300 integrations awaiting approval!)
In your case, if you are just validating incoming requests for your team, then you can store this token in an environment variable. If you are unsure which option is most suitable, go with the environment variable route for now while you play around with things :)
Save your slash command integration and you are all set.
Local Development
/sprintly create:task setup a secure tunnel from Slack to my local development server
Slack’s outgoing slash command requests need to be sent to a public facing url, which is a problem if we want to receive these messages to our local development server.
How do we solve this?
One way is with the use of a secure tunnel which acts as a public HTTPS URL for our local development server. Problem solved!
Who provides this service?
ForwardHQ provide the best user experience, including a browser extension for setting up a local tunnel in one click. They have a free 7 day trial.
My preferred option is ngrok. It’s free for one concurrent tunnel client, with no time restriction. Woop! Its a little harder to use but it does the job.
Using ngrok
- Download ngrok
- Unzip it wherever you like:
$ unzip /path/to/ngrok.zip
- Spin up a tunnel with the port number your local server is running on:
$ ./ngrok http 3700
- The output will look something like this:
Forwarding https://${UNIQUE_ID}.ngrok.io -> localhost:3700
- IMPORTANT: This secure url
https://${UNIQUE_ID}.ngrok.io
needs to be set as the url in your Slack slash command configuration - Once that is done, test out your slash command in a slack channel!
- If you see a
{"statusCode":404,"error":"Not Found"}
then your url is incorrect / your server is not running :)
Receiving messages
/sprintly create:task validate and ingest Slack commands
With a slash command url setup, your requests should now be received by your public endpoint.
For example we receive requests at ${APP_ROOT}/api/slack
.
With our Hapi server, the request
object will be available in the handler of the configured route. With it, we do 3 things.
- Check if the incoming payload’s a token matches your configured Slack command token
- Perform some sort of validation on the message
- Process the message
{
method: ['POST'],
path: '/api/slack',
config: {
handler: function(request, reply) {
// Does the incoming token match our token?
if (request.payload.token === config.slash_token) {
// Is the message format valid?
var command = validate(request.payload.text);
if (command.isValid) {
// Digest the message and take the reply fn as a callback
internals.processCommand(server, request, command, reply);
} else {
reply(command.error);
}
} else {
reply('Incorrect slash token')
}
}
}
ProTip:
The reply
function can be used to send an activity indicator message back to the user in Slack after their initial request. This message is plain text and private to that user by default when received within Slack. Privacy is important because we don’t want to clog up the channel with unnecessary activity
content. This message lets the user know that Partyline is busy with their request. However if the processed response is intended to be private also, (e.g. /sprintly help
), we should avoid using the reply
function for activity as a second reply
function call won’t work.
Process Message
/sprintly create:task validate and ingest Slack commands
Processing the message involves two steps:
- Interpretation
- Response
Interpretation
Depending on your use case, you may want certain words to trigger different actions. In the case of Partyline, it was important for CRUD keywords like create, delete and update to trigger those actions within the user’s Sprintly product. In order to understand the message intention we use a RegExp to parse out the action. Remember that the text of the message comes across in the POST payload received from Slack.
A sample case only dealing with the keyword ‘create’ can be seen below. Once we determine the action type, we can pass the message to a Create
service object to be parsed for relevant attributes like ticket score, description, etc. which can then be used in our request payload to Sprintly.
// The incoming message is passed into a parse function parse(message.text);
var parse = function(message) {
var action = {};
if (messageBeginsWithPattern(message, 'create')) {
action = Create.parse(message, action);
}
return action;
}
var messageBeginsWithPattern = function(message, pattern) {
return new RegExp('^'+buildPattern(pattern)).test(message);
}
var buildPattern = function(pattern) {
if (pattern === 'create') {
pattern += ':(story|defect|task|test)';
}
return pattern;
}
var Create = {
parse: function(message, action) {
// Use a RegExp to interpret the message after the keyword `create` command
}
}
Response
Once we have ingested the Slash command and made the appropriate request to Sprintly we have to post a public success message back into the Slack channel. To do this we use two Slack features:
- Web API
- Rich Message formatting with Attachments
We have already seen the web API, super simple. All you need to do is:
- Make a request to the the correct endpoint –
https://slack.com/api/chat.postMessage
- Include the correct query string params:
- A slack auth token
- Slack channel id
Check out our sendToSlack
example function below:
sendToSlack: function(attachment, integration, message) {
request.post({
json: true,
url: 'https://slack.com/api/chat.postMessage',
qs: {
"token": integration.get('slack_token'),
"channel": integration.get('channel_id'),
"username": 'Partyline',
"attachments": JSON.stringify(attachment),
"icon_url": SLACK_BOT_ICON,
},
}, function(err, resp, body) {
if (body.ok) {
console.log(body);
} else {
console.log(err);
}
});
}
We want our message posted back into the Slack channel to look nice and include valuable metadata such as:
- A Link to the recently created ticket
- Attribute values
- A message icon
- A color bar
To do this, we used Slack’s message attachment formatting. A Slack message can have zero or more attachments, defined as an array, with each hash in that array containing multiple properties. Check out the example props below:
fallback
color
pretext
author_name
author_link
author_icon
title
title_link
text
fields: [{ title:,value:,short: }],
image_url
thumb_url
Below is an example of the Partyline attachment object:
var attachment = [{
'fallback': 'Required plain-text summary of the attachment',
'author_name': 'Success! You created an item!',
'title': 'Item: #1234',
'title_link': 'http://sprint.ly/1/item/1234',
'fields': internals.buildFieldsForItem(item),
'color': '#36a64f'
}]
var buildFieldsForItem = function(item) {
// Relevant Sprintly ticket attrs
var allFields = ['issue_id', 'score', 'tags', 'assigned_to'];
var fields = _.map(allFields, function(name) {
var value = internals[name](item);
return {
'title': helpers.stripUnderscoreAndTitleCase(name),
'value': value,
'short': true
}
}, this);
return fields;
}
For more examples of what you can do with Slack message formatting look here.
And that’s it! What did we just do?
- Authenticated with our Slack App
- Configured a Slash Command
- Setup local development to build out our integration
- Exposed an endpoint to receive messages
- Processed that message and responded with the outcome to the user
Slack has made communication between team members so easy. In doing so, they have created a huge opportunity for us as developers to leverage that user input for interaction with our 3rd-party APIs. Partyline for Sprintly is the beginning of our integration with Slack. Over the coming months we will deepen this integration and build others.
Want us to build an integration for your favorite piece of project management software? Let us know!
Think something could have been explained better? Let me know and I will update it.
Want another topic to be covered, just let me know @shanedjrogers.
Happy hacking!
Party on Wayne. Party on Garth.