Dusted Codes

while (true) { Programming in .NET }

Creating a Slack bot with F# and Suave in less than 5 minutes

Slack has quickly gained a lot of popularity and became one of the leading team communication tools for developers and technology companies. One of the main compelling features was the great amount of integrations with other tools and services which are essential for development teams and project managers. Even though the list of apps seems to be huge, sometimes you need to write your own custom integration and Slack wants it to be as easy and simple as possible. How easy and how fast this can be done I will show you in this blog post.

A simple slash command

In most cases you will probably need to create one or more new slash commands which can be used by slack users to perform some actions.

For this tutorial let's assume I would like to create a new slash command to hash a given string with the SHA-512 algorithm. I would like my slack users to be able to type /sha512 <some string> into a channel and a slack bot to reply with the correct hash code.

The easiest way to achieve this is to create a new web service which will perform the hashing of the string and integrate it with the Slash Commands API.

Building an F# web service which integrates with Slash commands

Let's begin with the web service by creating a new F# console application and installing the Suave web framework NuGet package.

The Slash Commands API will make a HTTP POST request to a configurable endpoint and submit a bunch of data which will provide all relevant information to perform our action. First I want to model a SlackRequest type which will represent the incoming POST data from the Slash Commands API:

type SlackRequest =
    {
        Token       : string
        TeamId      : string
        TeamDomain  : string
        ChannelId   : string
        ChannelName : string
        UserId      : string
        UserName    : string
        Command     : string
        Text        : string
        ResponseUrl : string
    }

For this simple web service the only two relevant pieces of information are the token and the text which get submitted. The token represents a secret string value which can be used to validate the origin of the request and the text value represents the entire string which the user typed after the slash command. For example if I type /sha512 dusted codes then the text property will contain dusted codes in the POST data.

Inside this record type I'm also adding a little helper function to extract the POST data from a Suave.Http.HttpContext object:

static member FromHttpContext (ctx : HttpContext) =
    let get key =
        match ctx.request.formData key with
        | Choice1Of2 x  -> x
        | _             -> ""
    {
        Token       = get "token"
        TeamId      = get "team_id"
        TeamDomain  = get "team_domain"
        ChannelId   = get "channel_id"
        ChannelName = get "channel_name"
        UserId      = get "user_id"
        UserName    = get "user_name"
        Command     = get "command"
        Text        = get "text"
        ResponseUrl = get "response_url"
    }

Next I'll create a function to perform the actual SHA-512 hashing:

let sha512 (text : string) =
    use alg = SHA512.Create()
    text
    |> Encoding.UTF8.GetBytes
    |> alg.ComputeHash
    |> Convert.ToBase64String

Finally I will create a new Suave WebPart to handle an incoming web request and register it with a new route /sha512 which listens for POST requests:

let sha512Handler =
    fun (ctx : HttpContext) ->
        (SlackRequest.FromHttpContext ctx
        |> fun req ->
            req.Text
            |> sha512
            |> OK) ctx

let app = POST >=> path "/sha512" >=> sha512Handler

[<EntryPoint>]
let main argv =
    startWebServer defaultConfig app
    0

With that the entire web service - even though very primitive - is completed. The entire implementation is less than 60 lines of code:

open System
open System.Security.Cryptography
open System.Text
open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful

type SlackRequest =
    {
        Token       : string
        TeamId      : string
        TeamDomain  : string
        ChannelId   : string
        ChannelName : string
        UserId      : string
        UserName    : string
        Command     : string
        Text        : string
        ResponseUrl : string
    }
    static member FromHttpContext (ctx : HttpContext) =
        let get key =
            match ctx.request.formData key with
            | Choice1Of2 x  -> x
            | _             -> ""
        {
            Token       = get "token"
            TeamId      = get "team_id"
            TeamDomain  = get "team_domain"
            ChannelId   = get "channel_id"
            ChannelName = get "channel_name"
            UserId      = get "user_id"
            UserName    = get "user_name"
            Command     = get "command"
            Text        = get "text"
            ResponseUrl = get "response_url"
        }

let sha512 (text : string) =
    use alg = SHA512.Create()
    text
    |> Encoding.UTF8.GetBytes
    |> alg.ComputeHash
    |> Convert.ToBase64String

let sha512Handler =
    fun (ctx : HttpContext) ->
        (SlackRequest.FromHttpContext ctx
        |> fun req ->
            req.Text
            |> sha512
            |> OK) ctx

let app = POST >=> path "/sha512" >=> sha512Handler

[<EntryPoint>]
let main argv =
    startWebServer defaultConfig app
    0

Now I just need to build, ship and deploy the application.

Configuring Slash Commands

Once deployed I am ready to add a new Slash Commands integration.

  1. Go into your team's Slack configuration page for custom integrations.
    e.g.: https://{your-team-name}.slack.com/apps/manage/custom-integrations

  2. Pick Slash Commands and then click on the "Add Configuration" button:

slack-slash-commands-add-configuration

  1. Choose a command and confirm by clicking on "Add Slash Command Integration":

slack-slash-commands-choose-a-command

  1. Finally type in the URL to your public endpoint and make sure the method is set to POST:

slack-slash-commands-integration-settings

  1. Optionally you can set a name, an icon and additional meta data for the bot and then click on the "Save Integration" button.

Congrats, if you've got everything right then you should be able to go into your team's Slack channel and type /sha512 test to get a successful response from your newly created Slack integration now.

If you are interested in a more elaborate example with token validation and Docker integration then check out my glossary micro service.

BuildStats.info |> F#

After working on the project on and off for a few months I finally found enough time to finish the migration to F#, Suave and Docker of BuildStats.info.

The migration was essentially a complete rewrite in F# and a great exercise to learn F# and Suave.io as part of a small side project which runs in a production environment now. Working with F# and Suave was so much fun that I'm already planning to develop a couple more small projects in the very near future, but more on this at another time.

Apart from migrating to F# and Suave I had also dockerised the application and switched my hosting from an Azure Web App to Amazon EC2 Container Service, because it is considerably cheaper than Microsoft's ACS at the time of writing. I was also considering Docker Cloud and Google Container Service, but the fact that I can run a micro instance in Amazon for free for 12 months was the deciding factor which pushed me towards AWS and I am very happy so far.

Small side projects like this are always a great opportunity to try and learn new technologies beyond a simple hello world application and with that in mind I also decided to document the service endpoint with RAML, which is a very intuitive language for describing web service APIs. RAML was not entirely new to me, but it was the first time I used version 1.0 and some of its new features.

Last but not least I also switched my CI system from AppVeyor to TravisCI. I wasn't really planning to do this, but because I wanted to build the application with Mono on Linux I had to make that transition as well. Nevertheless I am still a big fan of AppVeyor and will continue using it as my primary CI sytem for all Windows based builds and Travis is just as great as well.

A lot of (good) things have been going on in my private life in the last 6 months and I didn't get as much time to blog and work on side projects as I wanted to do, but now that things have gotten a bit calmer again I hope to get more time to keep this blog more updated again and talk about all the stuff that I am doing every day.

Wish you all a great weekend and stay tuned :)

Older Posts