Skip to content

Build a Slackbot with Node.js to Monitor your Applications

Building Slackbot Cover

Slack is a really useful communication tool when working in teams. Many developers find themselves using it almost constantly when working on projects.

One of the stand out features of Slack is the rich API it exposes, to allow developers to integrate with it.

In this tutorial, we'll use the Slack API to give our apps a voice. We'll be able to talk to our apps running on Code Capsules, to ask their status and see if they are up and running. They will also be able to alert us when they boot up, so we know if they have been successfully deployed or restarted.

Overview and Requirements

As we're building a Slackbot, you'll need to sign up for an account on Slack, if you haven't already got one. Ideally, for this tutorial you should use a Slack workspace that you can safely send many test messages to while we are creating this bot, without disturbing people.

We'll also need the following:

  • Git set up and installed, and a registered GitHub account.
  • Node.js installed.
  • A registered Code Capsules account.
  • An IDE or text editor to create the project in. This tutorial was made using Visual Studio Code, but feel free to use any tool you like.

Setting Up the Project

With our requirements in place, we can get started on setting them up to work as needed for our Slackbot project.

Create a new repo on GitHub

We need a place to store our code from which Code Capsules can deploy to a capsule. A repository on GitHub is just what we need.

Head over to GitHub, and create a new repo. We're calling it slackbot here, but you can call it whatever you like.

Note: You can also add this code to an existing backend project if you would like to monitor it; perhaps something you built in an earlier tutorial.

Initialise the base project

Now we can get some base code set up. Let's start by cloning the new GitHub repo onto our local computer.

Now, go into the directory of the repo you've just cloned.

We can initialise a new Node.js project by typing the following at the terminal (or command prompt, if you're on Windows):

npm init

We can just press enter for each of the questions it asks; the defaults are good to start with.

Install our packages

Now that we have our project initialised, we can add the packages we need to create our bot. These are:

  • Express: This acts as our web server and HTTP request router. We'll use this to route requests from Slack to the correct logic.
  • body-parser: This interprets and parses payload data from HTTP requests. We'll need this to parse the URL-encoded data Slack sends to us with a request.
  • superagent: This package allows us to make outgoing HTTP requests. We'll need this to send a message to Slack.

Let's type in the following at the terminal to install the packages:

npm install express body-parser superagent 

Now let's create an index.js file, which will be the main file for our app. A simple way to do this is to open up your project folder in an editor, like Visual Studio Code. Now you can create a new index.js file.

create indexjs in visual studio

Save this blank file.

Great, it's time to push this boilerplate project up to Git. We can do it from the terminal with the following:

git add . 
git commit -am 'added base files for project'
git push origin

Create a new Code Capsule

We'll need a place to host our app.

  1. Log in to Code Capsules, and create a Team and Space as necessary.
  2. Link Code Capsules to the GitHub repository you created earlier. You can do this by clicking your username at the top right, and choosing "Edit Profile". Now click the "GitHub" button to link to a repo.
  3. Create a new Capsule, selecting the "Backend" capsule type.
  4. Select the GitHub repository you created above. If you are only using the repo for this project, you can leave the "Repo Subpath" field empty. You may need to add your repo to the team repo if you haven't already. Click the "Modify Team Repos" to do so.
  5. Click "Next", then on the following page, click "Create Capsule".

create capsule

Register an app on Slack

After you've created a workspace on Slack, or logged into an existing one, head over to https://api.slack.com and click on "Create a custom app".

On the dialog that comes up, we can give our app a name, and choose which workspace we want to add it to. You can choose any name you wish – we've used Serverbot here. Now we can click "Create App".

Great! We've created our app. Now we can configure it.

For this tutorial, we would like the following two functions:

  1. Our Code Capsules app should automatically send us a notification whenever it starts up. This allows us to easily know when a new deployment is successful. It can also alert us to any potential crashes and restarts.
  2. We want to query our Code Capsules app from Slack at any time to see how it's doing.

Our first requirement can be configured on the Slack side by clicking "OAuth & Permissions" on the left panel. Scroll down to the Scopes section, and click "Add an OAuth Scope" under the Bot Token Scopes section, and choose "Chat:Write" from the options list. This now allows our bot to initiate and post messages to us when it starts up.

select scopes slack

Our second requirement can be configured by setting up a slash command. Click on the "Slash Commands" menu item on the left, under Features.

slash command menu

Then click "Create a new Command". We'll give the command the name /stats. For the Request URL, copy the Domain name from your Code Capsules Overview page.

code capsules domain

Paste your domain into the Request URL box on Slack, and add /slack/command/stats to the end of it. We can fill in a description as well, something like 'Returns key stats from the app'.

create new command settings

Great, now we can click "Save" at the bottom of the page to finish setting up our slash command.

Writing the Slackbot Code

Now that we have all our systems set up, we can get onto the coding part.

Adding the base code

Let's add the boilerplate code to startup a new Express server. Open up the index.js file and add the following:

const express = require('express'); 

const app = express();

let port = process.env.PORT || 3000;
app.listen(port, ()=>{
  console.log(`App listening on port ${port}`);
});

Sending a startup message to Slack

Ok, cool, we've got the base code to create an Express app, and start it up to begin listening for requests. Now we can add some code to send a message to Slack when it boots up, not just locally to the console. If we look at the docs on Slack, we see that we can POST to the endpoint https://slack.com/api/chat.postMessage to send a message. In their example, they specify that we need: 1. An access token. 2. The channel ID of the channel to post the message to. 3. The message we want to post as the requirements.

To get the access token, head over to your app dashboard on Slack, and click on the "OAuth & Permissions" menu item on the left-hand side. Then click the "Install to Workspace" button, and then the "Allow" button. After this, you should see a newly generated "Bot User OAuth Token". Copy this token – this is our access token.

We could just put this token in our code. However, this is not really considered best practice for sensitive secrets and credentials. Rather, let's add this secret as an Environment Variable, and access it from the Node.js process object, on the .env property.

To add the access token to the environment in Code Capsules, head over to the capsule we created earlier, and click on the "Config" tab. Now we can fill in our environment variable for the access token. Add a new environment variable with name SLACK_BOT_TOKEN and set the value to the token copied from Slack.

add token environment variable

Now that we've added our access token, we need to find the ID of the channel we want to post to. Find a channel on your Slack workspace that you want to send to, or create a new channel. Now we can get the channel ID by right-clicking on the channel name to bring up a context menu. Now, we can choose "Copy Link" from that menu:

copy channel link from Slack

If we paste that link, we get something like https://<workspace-name>.slack.com/archives/C01SZ6Z3TCY. The last part of that URL is the channel ID; in this example case, C01SZ6Z3TCY.

Let's add this to our environment variables as well, as it keeps all the configurations in one place. Head back over to your Capsule, and add in an environment variable with the name SLACK_CHANNEL_ID and set the value to the channel ID we extracted above. Click the "Update & Start Build" button to save the changes to the environment variables.

add slack channel ID environment variable

We also need to invite our bot to the chosen channel, so that it will be able to post there. Go to the channel, and @ mention the name you gave the bot to add it. Click "Invite Them" when Slack prompts you.

invite bot to channel

Now let's add the code to call Slack on startup, and write a message to our channel. We can modify our boilerplate code above to make the HTTP POST to the endpoint https://slack.com/api/chat.postMessage. We'll use Superagent to make the call.

const express = require('express');
const superagent = require('superagent');

const app = express();

let port = process.env.PORT || 3000;
app.listen(port, ()=>{
  console.log(`App listening on port ${port}`);
  sendStartupMessageToSlack(); 
});

function sendStartupMessageToSlack(){
    superagent
      .post('https://slack.com/api/chat.postMessage')
      .send({
        channel:process.env.SLACK_CHANNEL_ID, 
        text:"I'm alive and running"
      })
      .set('accept', 'json')
      .set('Authorization', 'Bearer '+ process.env.SLACK_BOT_TOKEN)
      .end((err, result) => {
      });
}
We've added in a function sendStartupMessageToSlack which makes the call out to Slack. Notice that we send the auth token in a header, using .set('Authorization', 'Bearer '+ process.env.SLACK_BOT_TOKEN). The Authorization header is a standard HTTP header.

The channel and the message are sent in the body. Feel free to modify the startup message from I'm alive and running to whatever you'd like.

Deploying to Code Capsules

This seems like a great time to test out our app on Code Capsules. But before we do that, there is one thing we have to do to make it work. We need to tell Code Capsules how to run our app. By default, Code Capsules will call npm start after deploying the code. Therefore, we just need to add a start script to our package.json file in order for our code to be run on Code Capsules.

Open the package.json file. Under the scripts section, add the line "start": "node index.js". The package.json file should look like this now:

{
  "name": "slackbot",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.0",
    "express": "^4.17.1",
    "superagent": "^6.1.0"
  }
}

Ok, let's save all the files we've created, add and commit, and then push to our repo. When Code Capsules sees that there is a new commit, it will automatically deploy our code.

git add . 
git commit -am 'added code to call Slack on startup'
git push origin

If all goes well, in a few minutes you should get a message on your Slack channel from your code!

startup message

Adding a slash command

Now that our app can send us messages, can we send messages back to it? Let's implement the slash command, which will allow us to ask our app for some of its important stats and info. This time, Slack will send an HTTP POST to our app. If we take a look at the Slack docs again, we notice that Slack will send the slash command instruction to the URL we specified in the command set up earlier. We can also see that the POST payload is in the format application/x-www-form-urlencoded. We can set up a body-parser to interpret this data.

Let's extend our code with the snippet below to implement the slash command receiver as specified in the Slack docs. First add a require statement for body-parser at the top.

const bodyParser = require("body-parser");

Then add the code below:

app.use(bodyParser.urlencoded());

app.post('/slack/command/stats', [function(req,res){
  const slackReqObj = req.body;
  const packageJson = require('./package.json');

  const current_time = new Date(); 
  const stats = {
    name: packageJson.name,
    version: packageJson.version,
    environment: process.env.NODE_ENV,
    platform: process.platform,
    architecture: process.arch,
    node_version: process.version,
    pid: process.pid,
    current_server_time: current_time.toString(),
    uptime: process.uptime(),
    memory_usage: process.memoryUsage()
  };

  const response = {
    response_type: 'in_channel',
    channel: slackReqObj.channel_id,
    text: JSON.stringify(stats, null, '\t')
  };

  return res.json(response); 
}]);
This code listens for incoming POST calls on the line app.post('/slack/command/stats', [function(req,res){. If we receive one, we build up a return object, consisting of various interesting stats and info. This includes the current time on the server (in case it is in a different time zone to us), the name and version of our app as set in the package.json file, and various environment and process info.

Then it replies to the request in the format specified by Slack in their docs. We use the line text: JSON.stringify(stats, null, '\t') to turn our info and stats object into a nicely formatted text string, in the style of a JSON object.

Then, in the line return res.json(response);, we return all the info back to Slack to display as the response to a matching slash command.

Great, now we can commit and push this code.

git commit -am 'added handler for slash command'
git push origin 

After the code has finished deploying on Code Capsules (it should send a startup message again when it's ready), we can test the slash command.

Type /stats in the channel we chose earlier. After a second or two, the app should respond with its current vital stats and information.

testing the slash command

Adding verification

We can ask our app via Slack (which we use constantly!) how it's doing; pretty cool, huh? There is a problem though. If we call our slash command endpoint from anywhere else, for instance if we just call it using Postman, it also returns all the information and stats! This would not be good for a production system, as sensitive information will be easily found by attackers.

insecure reply from server to any request

So how can we ensure that the request comes from our Slack workspace? Luckily, Slack has thought about this, and sends a message signature with its requests. From the guide in Slack's docs, we can put together some code to check that the request is legitimately from Slack. The main parts of the check, copied from the docs, looks like this:

  • Retrieve the X-Slack-Request-Timestamp header on the HTTP request, and the body of the request.
  • Concatenate the version number, the timestamp, and the body of the request to form a basestring. Use a colon as the delimiter between the three elements. For example, v0:123456789:command=/weather&text=94070. The version number right now is always v0.
  • With the help of HMAC SHA256 implemented in your favorite programming language, hash the above basestring, using the Slack Signing Secret as the key.
  • Compare this computed signature to the X-Slack-Signature header on the request.

We can also check the timestamp to ensure that it is not a replay attack of a message from long ago.

Ok, let's implement this in our project. First, we somehow need to access the raw body of the request, before it has been parsed by body-parser. This is to ensure that the signing hash we calculate is using the same data that Slack did. After parsing, there could be extra characters and formatting etc. Luckily, the body parser package has a verify option, which passes a binary buffer of the raw body request to a user defined function. Let's make a function that conforms to the specs given by body-parser. Add this code to your index.js file:

var rawBodySaver = function (req, res, buf, encoding) {
  if (buf && buf.length) {
    req.rawBody = buf.toString(encoding || 'utf8');
  }
}

In this function, we grab the bit stream buffer buf, and check that it is not null and that it is not empty (by checking that it has a length). Then we tack it onto the request req as a new property rawBody. We also convert the buffer to a string, using the encoding supplied, or fall back to utf8 as a default. Now that the rawBody is added to the request, it will be available for use by subsequent middleware. We can add it to the body parser by modifying the code where we add the body parser to the app.

app.use(bodyParser.urlencoded({ verify: rawBodySaver}));

In the code above, we added options to our body parser initialisation. We set the verify option to the method we added above.

Now, let's make a new middleware function to calculate the signature and compare it. We'll be able to call this middleware before our current code for responding to our Slack slash command. Making it a middleware function will also allow us to easily re-use it on other routes, if we want to add more slash commands, or other commands from Slack in the future. We'll make a new file to hold this code. We'll call it signing.js.

In the new file, let's add this code:

const crypto = require('crypto');

function checkSlackMessageSignature(req, res, next){
    const timestamp = req.headers['x-slack-request-timestamp']; 
    const fiveMinutesAgo = Math.floor(Date.now() / 1000) - (60 * 5);

    if (timestamp < fiveMinutesAgo) {
        return res.sendFail(401, "mismatched timestamp");
    }

    const signing_secret = process.env.SLACK_SIGNING_SECRET; 

    const slack_signature = req.headers['x-slack-signature']; 
    const [version, slack_hash] = slack_signature.split('=');

    const sig_basestring = version + ':' + timestamp + ':' + req.rawBody;
    const hmac = crypto.createHmac('sha256', signing_secret); 
    hmac.update(sig_basestring); 
    const our_hash = hmac.digest('hex');    

    if (crypto.timingSafeEqual(Buffer.from(slack_hash), Buffer.from(our_hash))) {
        return next(); 
    }
    else {
        return res.send(401, "Invalid request signature");
    }
}

module.exports = checkSlackMessageSignature; 

Let's take a look at this code. Firstly, we import the crypto (cryptography) library. We don't need to install this as a package, as it is built into Node.js. This library will allow us to perform the hash of the basestring to compare with the signature.

Next, we create a function, with the standard Express middleware parameters: - req, representing the request data. - res, representing an output object that we return results to the user via. - next, representing a function to call if we want to hand control to the next middleware function in the chain. It can also be used to pass an error object back up if something goes wrong processing the request.

Then, on the first few lines of the function, we get the timestamp Slack sends from the request headers, and check that it is within the last few minutes. Note the name of the header is all in lowercase, even though Slack specifies that the header is capitalised. This is because Express converts all header keys to lowercase when serving a request.

After that, we retrieve the Slack Signing Secret from our environment variables. Let's get our Signing Secret from Slack and add it to the Code Capsules environment now. Head over to your Slack app dashboard, and click on "Basic Information" in the left-hand sidebar. Then scroll down to App Credentials, and look for the Signing Secret. Click "Show", and copy the secret.

copying signing secret from slack

Now head over to your Capsule on Code Capsules, and click on the Config tab. Add a new environment variable with Name SLACK_SIGNING_SECRET and paste in the value of the Signing Secret we copied above. Click "Update & Start Build" to save the changes.

setting signing env variable on Code Capsules

Ok, back to the function. After we retrieve the signing secret from the environment variables, we read out the hash calculated and sent by Slack from the headers using const slack_signature = req.headers['x-slack-signature']. This will be a string that looks something like v0=xxxxxxxxxxxxxxxxxxxxxxx, where the xxxx represents the actual hash value. We need to split the version identifier v0 from the beginning of the string though, as this is not part of the hash value. We do this in the next line, const [version, slack_hash] = slack_signature.split('='). Now we have both the version, and the hash string in variables that we can access.

After this, we construct our basestring, made from the version we extracted above, the timestamp of the request, and the rawBody (which we extracted in our body parser verify function earlier).

The next two lines are where we actually calculate the hash. First, we set up the crypto module with our crypto algorithm type SHA256, and with our unique Signing Secret. This allows us to then create an HMAC – or Hash Based Message Authentication code, which is the fancy name for the message signature. We then use the update method on our newly created HMAC to load in our basestring that we constructed above.

Now that the crypto HMAC is primed with all the info it needs, we can call the digest function to actually calculate the hash. We pass in as a parameter hex to indicate that we want the result back in hexadecimal format, as this is the same format that Slack sends their calculated hash value in.

Great, so now we have Slack's signature hash, and our hash. We need to check that they are the same, which will prove that the message was legitimately sent by Slack. We could just use a normal string compare, i.e. if (slack_hash === our_hash), but there is a slight security issue with this, known as a timing attack. This type of attack is based on the knowledge that a normal string compare function takes a different amount of time to compare two strings, depending on how close the strings are to each other. An attacker can take advantage of this timing difference to repeatedly send messages and, based on the time for our server to respond, can guess at how close their hash is to what we are expecting. With much patience and many thousands of messages, an attacker could eventually guess our Signing Secret, compromising all our checks.

Luckily, there is a simple way to protect from this, and it's built right into the crypto library. This is where we call crypto.timingSafeEqual. This compare always returns in the same amount of time, regardless of how close the hashes are to each other. Therefore, we don't give any extra information away to would-be attackers.

Now, if the hashes are equal, from our timingSafeEqual test, we just call return next() which exits our function and passes control to the next middleware function (which will be our slash command handler).

If the hashes are not equal, then we know this request is not genuinely from Slack, so we can end early and send a 401, which is a standard HTTP code for Unauthorized. Basically, we boot the imposter out.

Now, the last line in this file is module.exports = checkSlackMessageSignature. This allows our middleware function to be visible to other modules that import this file.

Ok, now that we've got this middleware created, let's link it to our slash command handler. Head on back to the index.js file, and import the middleware function by adding this line near the top of the file:

const checkSlackMessageSignature = require('./signing'); 

Now, we can navigate to our slack command handler, which started like this: app.post('/slack/command/stats'. Modify that to include a call to the message signature check before the actual handler, like this:

app.post('/slack/command/stats', [checkSlackMessageSignature, function(req,res){

Fantastic, now our app is secure. You can commit all the changes, and push it up to Git, which will kick off our final deploy to Code Capsules:

git add . 
git commit -am 'added message signature checking'
git push origin

Once the code is up and running on Code Capsules, test it out to see that it still responds to the Slack slash command. Then you can try again from Postman or other similar apps, and see that it will not send any info without a valid signature (you can use v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503 as an example x-slack-signature parameter):

returning 401 for invalid signature

Things to Try Next

What else can we do? It's almost endless!

  • Add this code to an existing app you have built to get easy info straight from Slack!
  • Add in more slash commands for more info – for example, you could get current user count on your app, number of database records etc. Basically, any information you could need for dev ops.
  • Look at some of the other functionality Slack offers for integration; for example, using modals, or listening in for keywords in messages.