Building an interactive slack bot that makes excuses with 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.
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.
Look no further and just press this button :
see demo of the bot here
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.
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.
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.
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.
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 !"
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.
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
.
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
}]
}]
}
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
Here I made it so it would fetch data from a database which has the following schema.
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
After all of this is setup, once I typed /excuse
in a channel it would give me an output like the following...
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
.
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"
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.
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:
Shoot your questions or suggestions down on the comments section below and also let me know your thoughts !