Building a Slack bot

… and using it to send status messages or other notifications to Slack from any browser app

Photo by Loic Leray on Unsplash

TL;DR

I’ll be showcasing Node. It’s an Express like framework that gives you the full power of Slacks web-api and app SDK. Using it to message channels from client apps is as simple as hitting an endpoint. You can also create slash commands for slack-to-bot communication. I’ll show you how to deploy the bot using Google cloud functions. See the link in references for other cloud provider examples.

This blog takes a look at why you shouldn’t use webhooks in a client, even though you can. How to setup and build a Node slack bot that receives requests from a client app and posts to any Slack channel. Along with Slack’s Bolt.js SDK, I’ll be using Typescript, Express, and Node. I’ll also show you how we deploy it to Google cloud functions.

What are we solving and why bother

We looked at three options for notifications, Sentry, Slack and Google Analytics. We settled with Slack for three reasons;

  • Notifications are for non-technical people,
  • Implementation looked straight forward using webhooks… (a false assumption at best), and
  • We wanted the results to be accessible to anyone in the company.

Sentry is for technical people and we don’t use Google Analytics much. So it needed to be Slack.

Webhooks for messaging a Slack channel

Slack has more than four ways to interface with its services. One of the simplest is channel scoped webhooks. All you need is a Slack app installed in the channel you want to message (creating one is well documented, see the resources). Generate the incoming webhook and post to it with a JSON payload. Done! All I had to do was use fetch from my uploader app and post to the channel on each event. Not so easy…

Access to XMLHttpRequest at'WEBHOOK_ADDRESS'from origin 'https://my.uploaderapp.com' has been blocked byCORS policy: Response to preflight request doesn't pass access control check:It does not have HTTP ok status.

This stumped me for an embarrassingly long time. I thought up numerous reasons why it wouldn’t work:

  • was it https in my local dev env — nope!
  • maybe it was my local env? Deployed it ad-hoc and still no fix — damn!
  • Let me try Slacks @slack/web-api Node package… again no! It only works on the server…

The penny slowly started to drop. But I didn’t want to give up yet, I’d invested so much time trying to get my request to work…! Some deep Googling eventually surfaced a Content-type header that does not need a preflight request. The cause of my CORS error.

'Content-type': 'application/x-www-form-urlencoded'

Further research indicates it’s common practice for API’s to accept this content type. But, we didn’t like the idea of using it to get around a CORs issue for a couple of reasons.

  • We don’t have control over Slacks services and there’s no guarantee it will be supported in the future.
  • Using the webhook this way is not idiomatic. There’s no official documentation. And I couldn’t find any tutorials or blogs about how it could be done either.

It was clear that we should build our own Slack bot and use it as middleware for notifications.

Building a Slack bot using Typescript, Node and Bolt

1. Scaffolding a Node app and setting up a development environment

yarn install -D typescript nodemon tsc-watch @types/node @types/node-fetch

Typescript for compiling code to ES5. nodemon for restarting the Node server when a file changes (think HMR for node apps). tsc-watch for running both together while using Typescript in watch mode.

I then initialised a Typescript project with npx tsc --init. I like doing it this way because the .tsconfig.json file it produces has most of the properties with comments included (all commented out except a handful). I then whittled down the config options to these:

The compiled files will go into the dist folder, both for development and then when building the deployable code. We need commonjs modules to satisfy a Google cloud functions requirement. The output code can be whatever you like. I’m choosing to use a node>=14.1 runtime so using a target of ES2020 will be fine. Using a more modern version will increase the speed of build too.

To start my dev environment I’ll use tsc-watch, running Typescript in watch mode, and auto re-compiling each time I make a change. It also allows me to run a second command on the same process. I need to run an Express server which I’ll walk through in a bit. My Node server needs to re-start each time a file is changed and for that I use nodemon. The script I use in my package.json looks like this:

// package.json.scripts  "start": "NODE_ENV=development tsc-watch --onSuccess \"nodemon     dist/index.js\""

2. Setting up a Bolt app and running a local server

yarn install @slack/bolt @slack/web-api body-parser cors dotenv express

I also need a Slack app. I recommend the getting started steps in the Bolt docs. If you’re following along come back once you have a token and signing secret. Their guide covers many of the steps I show here but I’ll be adding extra pieces for setting up in-coming requests and making it deployable to GCP functions.

The heart of the app looks like this:

The code speaks for itself, but there are a few things to point out. The receiver is an instance of ExpressReciever. It’s a built in class that gives us an Express app and router — handy for endpoints. In development we’ll need to start the app locally when running yarn start (line 31).

Running yarn start will spin up the dev server and we should see Started slack bot 🚀 — PORT: 3000 in the console. You’ll also see output from tsc-watch and nodemon.

3. Middleware for using Express

Using Slacks web-api for messaging a channel

1. Creating an Express router

You can use Express to create a Router instance which I can add multiple routes too. I’ll then pass this router into my primary catch all route back in the index.ts.

2. Adding an instance of the web-api to each request

I’d tried putting it directly onto the request object (because that made more sense to me) but the tsc compiler was not happy with me. I couldn’t find a way to extend the Request interface, so after some googling I settled on the result option. The middleware runs before my routes, so every request will have access to the webClient. This gives me a way to interact with my Slack workspace with over 100 different methods. For this exercise all I’ll need it one — webClient.chat.postMessage

3. Messaging a channel with a POST and postMessage

The naming convention I settled on matched the web-api method that it uses. postMessage becomes /post-message.

Tip: Add in some authentication middleware to the incoming web requests.

That’s it. Grab a channel id from the workspace you installed you bot in. Send request using your favourite tool. I’ll be following this post up with another showing how I used the endpoint in our Uploader web app.

Deploying to Google Cloud Functions

Cloud functions require commonjs export syntax even if you use a Node >14. 99% of this is solved when we compile using tsc but, we need to export or entry point from index.ts. I found the easiest was to write it explicitly. Take note of the exported property name, slackBot, we’ll need it below when we deploy.

To deploy to cloud functions you can run this command:

Give the function a name and point it at the correct project.

  • --source is the directly that we want to upload to the function. Out built app is in the dist directory so originally I thought I’d just upload that. This was a mistake. The function needs a package.json in the root so it knows how to find the entry point — the value of to main. Set it as dist/index.js and upload the whole root directory. To keep my deployment clean I use a .gcloudignore file. Here I add all my non-build directories and config files.
  • --entry-point is property name we used when from index.ts. This is the entry point to the whole app and is called by the function when it initialises.
  • Environment variables can be set in the deploy command using the --set-env-vars flag. But, you can also set them from the Google cloud console. I prefer using the console because when I set-up CI/CD with CircleCI I don’t have to manage adding them there too. Yes you’ll need to add them for each separate deployment but, that’s perfect, 12 Factor App for the win. To set variables in the console go to your function (you’ll need to have run the deploy script already). Click edit, then click the RUNTIME, BUILD AND CONNECTION SETTINGS drop down and add/edit any variables you need. Then click NEXT, then DEPLOY. This will re-deploy the function with the updated variables.

To run my deploy script I put it in a bash file called scripts/deploy.sh. Then from the terminal run bash ./scripts/deploy.sh. The terminal will put out some “in progress messaging” before show a success one. If you’re setting the environment variables using the console your first deployment will fail. This is because the missing variables will cause the app to crash when it’s initialised. Once you’ve set the variables you should be all set. Future deployments will work as expected!

Resources

Software engineer with a love of maps and anything geospatial. I enjoy techno, the outdoors, tequila & coffee. Currently working at Hummingbird Technologies

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store