Making a Slack bot from scratch with embedded player (part 1) 
Tags: slack, bot, firebase, twitter
Published: 2020-08-15 17:07:12

---
title: Making a Slack bot from scratch with embedded player (part 1) 
published: true
description: This is a small story of how to make a Slack bot with an embed player within slack to be able to play sounds without going to another app. Part one is about making the Slack bot, part 2 about the bot embeded player. 
tags: slack, bot, firebase, twitter
---

Some time ago, I've found an [awesome soundboard](https://kaamelott-soundboard.2ec0b4.fr/) for french TV Show Kaamelott. I wanted to make it available on Slack like the Giphy app, and distribute it. Here is the story. 
In this part 1, I will explain how to make a Slack bot, and in part 2, I will explain the pitfall & specific details for an embedded player within Slack. 

# Demo

Here is the actual result
![Bot preview](https://dev-to-uploads.s3.amazonaws.com/i/5ui374sb7oyqzakqwci9.gif)

# Vocabulary
- Interactivity: when a user click or interact with a message a bot sent. For example, when Giphy propose you a gif, selecting it to be sent in the channel will send an Interactivity request. 
- Slash command: when a user submit a new `/mycommand myinput` within Slack, this will trigger an HTTP Request. 
- OAuth 2: authentification protocol used by Slack to authentificate a bot and link it to a Slack workplace
- Unfurling: the process that Slack does to add a preview (text/image/rich) below a link

**Disclaimer:** this is not an indepth tutorial but an overview on the steps that you'll need to do in order to make your first Slack command bot.

# Preparing the server
Let's pick [Firebase](https://firebase.google.com/) stack to make this project. Firebase offer all the services we need to setup this bot easily: an hosting, a serverless platform and a database + all the SDKs you need to communicate with them. 
Within Firebase, we'll need at least 3 serverless functions: 
- slackCommand
- slackInteractivity
- slackOAuth 

1. Create a new project on [firebase.google.com](firebase.google.com)
2. Install Firebase CLI: `npm install -g firebase-tools`
3. Login: `firebase login`
4. Init the local repo: `firebase init` and select Firestore, Functions, Hosting. I've picked Typescript for the functions language.  
5. Within `functions/` dir, create the [3 HTTP functions](https://firebase.google.com/docs/functions/http-events) listed above and implement them with a dumb answer:  `response.send()`. 
6. Export them in the `index.ts`: `export {slackOAuth} from './https/slackOAuth'`
6. Deploy them using `firebase deploy` 

An exemple of a dumb serverless functions on Firebase in TS: 
```
import * as functions from 'firebase-functions'

export const slackCommand = functions.https.onRequest(async (request, response) => {
    return response.send()
}
```

At this time, you'll have deployed three functions. For each function you have a unique URL that you can call from everywhere. Save those URLs for the following part. 

# Creating the Slack App

![useless screenshot of a Slack app dashboard](https://dev-to-uploads.s3.amazonaws.com/i/10te1tehfano6cq48bt7.png)

We now need to create a new Slack app [here](https://api.slack.com/apps). The app will be registered to a workplace so you can test it easily. You'll need to fill some information & images, and then configure it as bellow:
- Enable Interactivity: this will allow you to receive an HTTP Request when a user click on a button your Slack bot sent. The URL needs to be the one you've setup in the last parth for the `slackInteractivity` function. 
- Add a Slash command, use the URL for the `slackCommand` function.
- Configure OAuth: 
    - add a Bot Token Scope: `commands` that will allow the bot the receive the command
    - add a Redirect URLs: the URL for the `slackOAuth` function

This is almost all of what you'll need related to Slack.  At this time, a bot user will already be available within your Slack workplace, and the command too, though it will not do much. 

You can also find the "Add to Slack" button in "Manage distribution" of the app, the URL will be useful to test the OAuth proccess.  

# Add the Slack app credentials as environment variable for the functions

We'll need the following environement var in the serverless functions: 
- Slack `client_id`
- Slack `client_secret`
- Slack `signing_secret`

You can add them to the function using [Firebase CLI](https://github.com/firebase/firebase-tools/): 
```
firebase functions:config:set slack.client_id=XXXX
```
After deploying the functions, your code can access them with: 
```
import * as functions from 'firebase-functions'

functions.config().slack.client_id
```

To run the functions locally (not very useful for a Slack bot), you'll need to populate a `.runtimeconfig` within the `functions` dir: 

```
firebase functions:config:get > .runtimeconfig.json
```




# Implementing the OAuth

![slak install process](https://dev-to-uploads.s3.amazonaws.com/i/ph7ynwkihdna9fjm9k72.png)

The goal of this part is to handle any workplace when the bot is added using the "Add to Slack" button. 
After allowing the bot to a workplace with the "Add to Slack" button, Slack will redirect the user to the `slackOAuth` function. This request will allow the Slack app to add its configuration to the new workplace (slash command, command scope, bot profil) as well has having a valid token on this workplace. 
The function is composed of the following parts: 
1. Validating that the environment variables exist
2. Answering for the SSL Check Slack does regularly
3. Validating the request (code, method)
4. Exchange the temporary Slack OAuth verifier code for an access token (using `oauth.v2.access` Slack API)
5. Saving the token to Firestore database. 
6. Redirecting the user to a success or failure page. 

You can find the full code of this function [here](https://github.com/HugoGresse/kaamelott-bot/blob/master/functions/src/https/oauthRedirect.ts#L6), but here is some interesting part: 

Get environment variable in functions using `functions.config().my_object.my_key`

Call the Slack `oauth.v2.access` API
```
    import fetch from 'node-fetch'
    import {URLSearchParams} from 'url'

    const params = new URLSearchParams()
    params.append('code', `${request.query.code}`)
    params.append('client_id', `${slack.client_id}`)
    params.append('client_secret', `${slack.client_secret}`)
    params.append('redirect_uri', `https://us-central1-${process.env.GCLOUD_PROJECT}.cloudfunctions.net/slackOAuth`)

    const oauthV2AccessResult = await fetch("https://slack.com/api/oauth.v2.access", {
        method: "POST",
        body: params
    })
```

Save the token to Firestore: 
```
   import * as admin from "firebase-admin"
   admin.initializeApp()

   const slackResultData = await oauthV2AccessResult.json()

   admin.firestore()
        .collection("installations")
        .doc(slackResultData.team.id).set({
            token: slackResultData.access_token,
            teamId: slackResultData.team.id,
            teamName: slackResultData.team.name,
            createdAt: serverTimestamp()
        })
```

The not funny part is that Slack cannot call a local URL, so you'll need to deploy this each time to test it. Here is a shortcut to only deploy a function: 
```
firebase deploy --only functions:slackOAuth
```

At this time, if everything work, upon calling the app installation URL (within Slack Manage distribution app page), you should be asked to allow the app and then redirected to success page (in my case, it's [this one](https://kaamelott-bot.firebaseapp.com/slack/success)). 
The token will have been saved in Firestore: 
![Firestore database installations](https://dev-to-uploads.s3.amazonaws.com/i/ioan06j9tzna5x6wgycw.png)

[Slack OAuth documentation](https://api.slack.com/authentication/oauth-v2)



# Responding to a command request
When a new command is executed in Slack, the `slackCommand` function will be called. This is post of your bot logic will take place. I believe this was the hardest part for me. 

![slak command](https://dev-to-uploads.s3.amazonaws.com/i/h13dsom6kv7cvgjaitn7.png)

Implementation guidelines: 
1. Verify the request integrity:  
    - only POST request  
    - checking if `x-slack-signature` & `x-slack-request-timestamp` headers are present  
    - preventing request attack  
    - making a hash of the request body using hmac 256  
    - comparing the hash with the `x-slack-signature`  
2. Sending a ACK to Slack: `response.send()` to prevent a timeout after 3s on Slack side. 
2. Getting the app token, if any
3. [Your bot logic here]
4. Answering to the `response_url` with the message. 

Let's go a little deeper in the integrity check. 
1. First of all, you'll need to stringify the request body using `RFC1738` which is possible using the `qs` package in Node. 
   `const requestBody = qs.stringify(request.body, {format: 'RFC1738'})`
2. Using the Node crypto package, make the digest: 
    ``` 
import * as crypto from "crypto"
const digest = 'v0=' + crypto.createHmac('sha256', signingSecret)
        .update(`v0:${requestSlackTimestamp}:${requestBody}`)
        .digest('hex')
    ```
3. Checking the Slack signature and your new digest: 
```
crypto.timingSafeEqual(
        Buffer.from(digest, 'utf8'),
        Buffer.from(requestSlackSignature, 'utf8'))
```
We are using this safe equal method to prevent a timing attack on the string compare (more info [here](https://stackoverflow.com/questions/31095905/whats-the-difference-between-a-secure-compare-and-a-simple)) 

[Slack verifying documentation](https://api.slack.com/authentication/verifying-requests-from-slack)


# Sending some Block back to Slack

Assuming you now have the token (bot token usually start with `xoxb-`), you can send message in Slack using the `response_url` provided in the request body: 

```
    const responseUrl = request.body.response_url

    return fetch(responseUrl, {
        method: "POST",
        body: JSON.stringify({
            blocks: [
                {
                    type: "section",
                    text: {
                        type: "mrkdwn",
                        text: `An \n awesome _text_`
                    },
                    accessory: {
                        type: "button",
                        text: {
                            type: "plain_text",
                            text: "Send",
                            emoji: true
                        },
                        value: someValue
                    }
                }
            ]
        })
    })
```

There is two ways to [compose message](https://api.slack.com/messaging/composing) in Slack: 
- the [block format](https://api.slack.com/block-kit/building)
- the legacy format: a `text` and some optional `attachments`

Usually, you should go with the block format, as the [BlockKit Builder](https://api.slack.com/tools/block-kit-builder) is really great and the layout possibilities are all you need. 

The API is [here](https://api.slack.com/reference/messaging/payload)

Some messages are only visible to the user issuing the command (ephemeral) and other are public within a conversation (in_channel). This will be explained in part 2. 

![Ephemeral message from the bot](https://dev-to-uploads.s3.amazonaws.com/i/gmi2v20bsuku061oor5h.png)

  
  
# Interactivity implementation

When an user interact with a bot message (clicking on button from the message), `slackInteractivity` function is called. 

Here is some implementation steps: 
1. Send an empty response to prevent the 3s Slack timeout. (some functions may be longer to start if they are cold (not called for 10min) 
2. Verify the signature of the message
3. Get the payload type & check request data
   a. type === `block_actions`?
   b. action value present? (this will be the `accessory.value` of the block in example above)
4. [your bot logic]
5. Call the `response_url` with a new payload of your choosing. 
6. To convert a `ephemeral` message to a `in_channel` you'll need to wait for the part 2 soon. 

And  🎉

You'll then need to deploy everything, add your bot logic and fix the bugs. 

In part 2, I'll explain my own bot logic where I'm using Twitter as an embeded player for Slack as well as some other implementation details.  Stay tuned. 

Sources code of the bot is [here on github.com](https://github.com/HugoGresse/kaamelott-bot). 

Thanks [Roch Dardié](https://www.linkedin.com/in/rdardie/?originalSubdomain=fr) for proofreading me.