How to build a Slack Bot in Go
Tags: go, programming, tutorial
Published: 2023-09-22 09:15:39

🚀 **Need a Slack bot but don't want to waste time? You've come to the right place!**

In this tutorial you will create a Slack bot that brings the greatness of the cowsay utility to Slack, using Go and [Encore](https://encore.dev) for the backend.

Encore is a development platform that comes built-in with the tooling you need to get a scalable Go backend up and running in the cloud for free, in just a few minutes.

You can see the end result code on GitHub [here](https://github.com/encoredev/examples/tree/main/slack-bot).

_– Let's get started!_

## 1. Install Encore

Start by installing Encore using the appropriate command for your system.

**MacOS**
```
brew install encoredev/tap/encore
```

**Windows**
```
iwr https://encore.dev/install.ps1 | iex
```

**Linux**
```
curl -L https://encore.dev/install.sh | bash
```

## 2. Create your Encore app

Create a new Encore application by running `encore app create` and selecting `Empty app` as the template.

Once created you will get an app ID, which we'll use in the next step.

## 3. Create a Slack app

The next step is to create a new Slack app.

Head over to [Slack's API site](https://api.slack.com/apps) and create a new app.

When prompted, choose to create the app from an app manifest.
Choose a workspace to install the app in.

Enter the following manifest (replace `$APP_ID` in the URL below with your app ID from step 2):

```go
_metadata:
  major_version: 1
display_information:
  name: Encore Bot
  description: Cowsay for the cloud age.
features:
  slash_commands:
    - command: /cowsay
      # Replace $APP_ID below
      url: https://staging-$APP_ID.encr.app/cowsay
      description: Say things with a flair!
      usage_hint: your message here
      should_escape: false
  bot_user:
    display_name: encore-bot
    always_online: true
oauth_config:
  scopes:
    bot:
      - commands
      - chat:write
      - chat:write.public
settings:
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false
```

Once created, we're ready to move on to implementing our Encore API endpoint!

## 4. Implement the Slack endpoint

Since Slack sends custom HTTP headers that we need to pay attention to, we're going to use a **raw endpoint** in Encore. 

For more information on this check out [Slack's documentation on Enabling interactivity with Slash Commands](https://api.slack.com/interactivity/slash-commands).

In your Encore app, create a new directory named `slack` and create a file `slack/slack.go` with the following contents:

```go
package slack

import (
	"encoding/json"
	"fmt"
	"net/http"
)

// cowart is the formatting string for printing the cow art.
const cowart = "Moo! %s"

//encore:api public raw path=/cowsay
func Cowsay(w http.ResponseWriter, req *http.Request) {
	text := req.FormValue("text")
	data, _ := json.Marshal(map[string]string{
		"response_type": "in_channel",
		"text":          fmt.Sprintf(cowart, text),
	})
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(200)
	w.Write(data)
}
```

Now, let's try it out locally.

Start your app with `encore run` and then call it in another terminal:

```
curl http://localhost:4000/cowsay -d 'text=Eat your greens!'
```

You should get this response:

```
{"response_type":"in_channel","text":"Moo! Eat your greens!"}
```

Great job, you've got a working API running locally!

## 5. Deploy to the cloud

Let's deploy our Slack bot backend to Encore's free development cloud so it's available on the Internet.

All you need to do is `git push encore` and Encore provision a cloud environment that you can view and manage in Encore's [Cloud Dashboard](https://app.encore.dev).

```
git add -A .
git commit -m 'Initial commit'
git push encore
```

Once the deploy is finished, we're ready to try our Slack command!

Head over to the Slack workspace, where you installed the app, and run `/cowsay Hello` there. You should see something like this:

![Cowsay Slack bot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ri5ixtnyitke1dc8ng2i.png)

And just like that, we have a fully working Slack integration. Great job!

## Bonus: Secure the webhook endpoint

In order to get up and running quickly we ignored one important aspect of a production-ready Slack app: verifying that the webhook requests are actually coming from Slack. 
Let's do that now!

The Slack documentation covers this really well on the [Verifying requests from Slack page](https://api.slack.com/authentication/verifying-requests-from-slack).

In short, what we need to do is:

1. Save a shared secret that Slack provides us.
2. Use the secret to verify that the request comes from Slack, using HMAC (Hash-based Message Authentication Code).

### Save the shared secret

Let's define a secret using Encore's built-in secrets management functionality.

Add this to your `slack.go` file:

```go
var secrets struct {
	SlackSigningSecret string
}
```

Next, head over to the configuration section for your Slack app: Open your app in **[Your Apps](https://api.slack.com/apps)** → **Select your app** → **Basic Information**.

Copy the Signing Secret and then run `encore secret set --prod SlackSigningSecret` and paste the secret.

For local development, you will also want to set the secret using `encore secret set --dev SlackSigningSecret`. (You can use the same secret value or a placeholder value.)

### Compute the HMAC

Go makes computing HMAC very straightforward, but it's still a fair amount of code.

Add a few more imports to your file, so that it reads:

```go
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"encore.dev/beta/errs"
	"encore.dev/rlog"
)

```

Next, we'll add the `verifyRequest` function:

```go
// verifyRequest verifies that a request is coming from Slack.
func verifyRequest(req *http.Request) (body []byte, err error) {
	eb := errs.B().Code(errs.InvalidArgument)
	body, err = ioutil.ReadAll(req.Body)
	if err != nil {
		return nil, eb.Cause(err).Err()
	}

	// Compare timestamps to prevent replay attack
	ts := req.Header.Get("X-Slack-Request-Timestamp")
	threshold := int64(5 * 60)
	n, _ := strconv.ParseInt(ts, 10, 64)
	if diff := time.Now().Unix() - n; diff > threshold || diff < -threshold {
		return body, eb.Msg("message not recent").Err()
	}

	// Compare HMAC signature
	sig := req.Header.Get("X-Slack-Signature")
	prefix := "v0="
	if !strings.HasPrefix(sig, prefix) {
		return body, eb.Msg("invalid signature").Err()
	}
	gotMac, _ := hex.DecodeString(sig[len(prefix):])

	mac := hmac.New(sha256.New, []byte(secrets.SlackSigningSecret))
	fmt.Fprintf(mac, "v0:%s:", ts)
	mac.Write(body)
	expectedMac := mac.Sum(nil)
	if !hmac.Equal(gotMac, expectedMac) {
		return body, eb.Msg("bad mac").Err()
	}
	return body, nil
}

```

> As you can see, this function needs to consume the whole HTTP body in order to compute the HMAC. This breaks the use of req.FormValue("text") that we used earlier, since it relies on reading the HTTP body. That's the reason we're returning the body from verifyRequest, so that we can parse the form values from that directly instead.

We're now ready to verify the signature!

Update the `Cowsay` function to look like this:

```go
//encore:api public raw path=/cowsay
func Cowsay(w http.ResponseWriter, req *http.Request) {
	body, err := verifyRequest(req)
	if err != nil {
		errs.HTTPError(w, err)
		return
	}
	q, _ := url.ParseQuery(string(body))
	text := q.Get("text")
	data, _ := json.Marshal(map[string]string{
		"response_type": "in_channel",
		"text":          fmt.Sprintf(cowart, text),
	})
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(200)
	w.Write(data)
}

```

Finally, we're ready to put it all together.

Update the `cowart` like so:

```
const cowart = `
 ________________________________________
< %- 38s >
 ----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
`

```

Finally, let's commit our changes and deploy it:

```
git add -A .
git commit -m 'Verify webhook requests and improve art'
git push encore
```

Once deployed, head back to Slack and run `/cowsay Hello` there.

If everything is set up correctly, you should see:

![Slack bot cowsay art](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/39wppumyqeigbw4kp8vo.png)

And there we go, a production-ready Slack bot in less than 100 lines of code.

✨ Awesome work, well done you!

## Next project

If you want to build more Go backend systems without the pain of setting up infrastructure, [check out more Encore tutorials here](https://encore.dev/docs/tutorials).

## About Encore

Encore is a backend development platform that automates infrastructure in local, preview, and cloud environments.

It comes with built-in tools like tracing, test automation, generated API documentation, and much more.

Create a free account at [Encore.dev](https://encore.dev) to start building.

- Have questions? Join our developer community on [Discord](https://encore.dev/discord).