Building an interactive slack bot that makes excuses with Dark !

Dark log

What is Dark ?

So recently I have been playing around with Dark. It is

A holistic programming language, editor and infrastructure for building backends without accidental complexity.

So basically it's like an all in one language, but dark managed to do it seamlessly. Ballerina is another language that has a somewhat similar approach but I think that Dark takes it to a whole new level.

I find Dark quite intresting since it takes on a new approach to modern programming/practices by taking away most of the services/tools offered by Cloud Native platforms. Dark identifies most of these services/tools as stuff that just brings in accidental complexity and proposes a much simpler and productive approach to software development. This seems strange at first but it made a lot of sense once I went through their talks/documentations. If you want to learn more about Dark, I suggest that you go through the following two introductory videos Introduction to dark and Dark tutorial.

What am I going to build with it ?

So I decided to give it a try.To start things off I needed to request access to it as it is currently in private beta. I got access the following day. Just to test it out I decided to build an interactive slack bot that would make your life a bit easier. So the bot I am going to build is going to provide you with random excuses as to why you won't be coming to office today, which is quite helpful especially when you really can't think of an excuse.

Not interested in the technical implementation and just want to use the app?

Look no further and just press this button :

Share App

see demo of the bot here

Embracing the dark...

Just as soon as I got started I was impressed by one of the really neat features built into dark called Trace Driven Development. It revolves around sending requests to dark before writing any code or the implementation. The request itself gave me a neat little trace of what it was made of and then I was able to easily follow through with development with much confidence as I was able to trace the actual request throughout the development phase. I will explain more about this later as I walk you through the development process.

Getting started

So with tracing in mind, I got started by creating a new slack app. I also created a new workspace for testing this out initially and created the app on that workspace. I would advise you to do the same. Then I went into the Basic Information tab and selected the features and functionality I wanted out of this app.

Slack app basic info

I started with the permissions. Essentially I needed to configure these to have any sort of access. I specified a redirect URL which is what gets called after a user allows this app to run on their workspace. I also had to configure Features--> OAuth & Permissions, under Scopes --> User Token Scopes I added a new scope chat:write. At the time of this writing dark provides us with a neat URL with the format https://${username}.builtwithdark.com/your/own/paths, for example, the endpoint I provided as redirect URL was https://dasith.builtwithdark.com/redirect-oauth. Now that this was setup I wanted to test if this redirect actually got called so to do that I went to Settings --> Manage Distribution and copied the sharable URL there.

Slack app manage distribution

Slack app manage distribution

Once I pasted it on my browser it took me to a screen that asked me to authorize the app. Once I clicked Allow It gave me an error as expected. However, this did leave a trace on my Dark development instance. When I opened my Dark instance, under 404 section I saw the /redirect-oauth. Clicking on the + button there I was able to get a new endpoint created on my Dark instance.

Oauth redirect dark

Next was setting up what needed to happen once this endpoint was hit. So as the first step I needed to do a POST request to https://slack.com/api/oauth.v2.access with the query parameter code which is available in the incoming request. Slack documentation gave me most of the information on this endpoint. Setting up the logic for it on dark was relatively easy especially with tracing to help you out.

let resp = HttpClient::postv4
             "https://slack.com/api/oauth.v2.access"
             {
               code : request.queryParams.code
             }
             Dict::empty
             Dict::merge
               HttpClient::basicAuth client_id client_secret
               HttpClient::formContentType

let savedToken = DB::setv1
                   {
                     accessToken : resp.body.access_token
                     userAccessToken : resp.body.authed_user.access_token
                     user_id : resp.body.authed_user.id
                     app_id : resp.body.app_id
                     team_id : resp.body.team.id
                   }
                   resp.body.authed_user.id
                   SlackTokens
"success !"

Oauth redirect dark

Here I saved some of the data on a datastore which I will need later when dealing with other requests or making requests to slack. Datastore was relatively easy to create as well after reading up on documentation a bit.

Setting up the slack command for the bot...

Now that I was done with the redirect-oauth , I moved onto setting up the command to activate the slack bot. In my case, it activates when you type /excuse on any slack channel and press enter. As its request URL I gave https://dasith.builtwithdark.com/excuses as the endpoint. Once this /excuse command is typed by a user this will make a POST request to the endpoint https://dasith.builtwithdark.com/excuses.

Oauth redirect dark

Similar to my previous approach for this I typed in /excuse in a channel and then Implemented the logic later with the help of tracing.

let excuseText = getExcuseText
{
  blocks : [{
              type : "section"
              text : {
                       type : "mrkdwn"
                       text : excuseText                     
                     }
            },
            {
              type : "actions"
              elements : [{
                              type : "button"
                              text : {
                                       type : "plain_text"
                                       text : ":heavy_check_mark:"
                                       emoji : true
                                     }
                              value : "accept|" ++ excuseText
                            },
                            {
                              type : "button"
                              text : {
                                       type : "plain_text"
                                       text : ":heavy_multiplication_x:"
                                       emoji : true
                                     }
                              value : "decline|" ++ excuseText
                          }]
              }]
}

Oauth redirect dark

Here I am sending an interactive text block as the response once this endpoint gets hit. The function getExcuseText returns a random excuse. This I added by providing a custom function available on the Functions section on my Dark instance.

let officeLeaveExcuses = DB::queryOneWithExactFields
                           {
                             type : "officeLeaves"
                           }
                           ExcusesList
let randomExcuseIndex = Int::randomv1 0 List::length Dict::keys officeLeaveExcuses.excuses
                        |>toString
"Hi I will be on leave today since " ++ Dict::getv2 officeLeaveExcuses.excuses randomExcuseIndex

Oauth redirect dark

Here I made it so it would fetch data from a database which has the following schema.

Oauth redirect dark

I seeded the database by using a REPL which can be triggered manually any time I want but in my case just needed to trigger once for the initial seeding of the excuses data store.

DB::setv1
  {
    excuses : Dict::empty
              |>Dict::set "0" "i am sick"
              |>Dict::set "1" "my car broke down"
              |>Dict::set "2" "have a family emergency"
              |>Dict::set "3" "got food poisoning"
    type : "officeLeaves"
  }
  DB::generateKey
  ExcusesList

Oauth redirect dark

After all of this is setup, once I typed /excuse in a channel it would give me an output like the following...

Oauth redirect dark

Posting the excuse as myself...

To make it realistic I did have to make sure that this excuse was posted as me and not by a bot so to achieve this what I did was configuring the endpoint for the interaction i.e when ✓ or ✘ is pressed. So I started off by turning on interactions and giving it the endpoint https://dasith.builtwithdark.com/interactive-response.

Oauth redirect dark

Once this is configured and I trigger it by actually pressing one of the two buttons the bot suggests. It would give me a new endpoint trace like before. So I added this endpoint and provided it with this logic to handle the interaction.

let jsonBody = request.body.payload
               |>JSON::parsev1
let actionMessage = jsonBody
                    |>Dict::getv2 "actions"
                    |>List::getAtv1 0
                    |>Dict::getv2 "value"
                    |>String::split "|"
let actionType = actionMessage
                 |>List::getAtv1 0
let excuseMessage = actionMessage
                    |>List::getAtv1 1
if actionType == "accept"
then
  sendSlackExcuse jsonBody.channel.id jsonBody.user.id excuseMessage
else
  "Message not posted please try with /excuse to get a new excuse"

Oauth redirect dark

What this essentially does is to extract the data we want from the request i.e the action type ("accepted" or not) and the excuse to be posted. I separated out the sending slack excuse as a user to another function called sendSlackExcuse.Which has the following logic

let userData = DB::queryOneWithExactFields
                 {
                   user_id : slackUserId
                 }
                 SlackTokens
let officeLeaveExcuses = DB::queryOneWithExactFields
                           {
                             type : "officeLeaves"
                           }
                           ExcusesList
let randomExcuseIndex = Int::randomv1 0 List::length Dict::keys officeLeaveExcuses.excuses
                        |>toString
HttpClient::postv4
  "https://slack.com/api/chat.postMessage"
  {
    channel : slackChannelId
    text : excuseTextMsg
    as_user : true
  }
  {}
  Dict::merge HttpClient::bearerToken userData.userAccessToken HttpClient::jsonContentType

Note: This function takes in 3 params slackChannelId, slackUserId & excuseTxtMsg. More details about this perticular endpoint can be found here.

Oauth redirect dark

Once all of this was configured the app was ready to be reinstalled/published which I did by going to Settings-->Install App and clicking Reinstall App which reinstalled the app on workspace with all the changes. Then I went to Settings-->Manage Distribution and published this app across all workspaces.

You can use the slackbot I implemented by clicking the button below:

Share App

Shoot your questions or suggestions down on the comments section below and also let me know your thoughts !