We use Slack at work and for a while I’d been tinkering with the idea of creating a Slack Slash Command to display what’s for lunch, e.g. /lunch. The one thing stopping me was the canteen menu only being available on a printed paper posted every Monday and as a lazy developer, I couldn’t be bothered with having to input it manually every week. However, this all changed when a co-worker informed me about the canteen having an app.

Apps Everywhere

When there is an app, there is an API

I installed the app and configured my mobile to use Fiddler as a proxy to intercept the API requests. In the app, I selected my canteen and viewed the menu, which resulted in this interesting request:

GET http://www.tibeapp.no/hosted/albatross/xlsx/test.php?fileurl=http://netpresenter.albatross-as.no/xlkantiner/Kanalsletta4.xlsx

I won’t delve into the API as a PHP script named test.php pretty much speaks for itself, but the returned JSON is easy to work with and consists of an array with two items for each weekday. The property names are in Norwegian, dag is the day (Mandag=Monday, Tirsdag=Tuesday etc.) and rett is a description of the day’s dish.

{
   "results":[
      {
         "dag":"Mandag",    
         "rett":"Omelett"
      },
      {
         "dag":"Mandag",
         "rett":""
      },
      {
         "dag":"Tirsdag",
         "rett":"Soyamarinert laks med "
      },
      ... more days ...
   ]
}

So, omelette on Monday and soy-marinated salmon on Tuesday.

Azure Functions - a perfect match

Azure Functions is Microsoft’s take on serverless computing, which is a confusing term that means that the server is abstracted away and you don’t need to worry about it, not that there is no server. In other words, it is a solution for running small pieces of code (functions) in the cloud (Azure), and as such, a perfect match for the integration between Slack and the API. Diagram created with ASCIIFlow <3

+---------+          +------------------+          +-------+
|         |          |                  |          |       |
|  Slack  | <------> |  Azure Function  | <------> |  API  |
|         |          |                  |          |       |
+---------+          +------------------+          +-------+

With the server out of the way, it is easy to get started with Azure Functions. I recommend starting with the Azure Functions portal to develop and test directly in the browser. When ready to take the next step, you can configure continuous deployment from a Git repository and download what you developed in the portal using Kudu, which is found in the Function app settings. A function app is a container for functions and continuous deployment is enabled per function app. Once enabled, the portal goes into a read-only mode for the given function app. This is a nice feature to prevent changes being made in the portal when under source control.

The default hosting plan for Azure Functions is the Consumption Plan. Here you only pay for the compute resources used and scaling is handled automatically. It includes a monthly free grant of 1 million requests and 400,000 GB-s of resource consumption per month, so there is no reason for not trying out Azure Functions. Alternatively, you can use an App Service Plan, if you want a more predictable pricing and using more than what the Consumption Plan offers for free. The hosting plan choice is made during the creation of a function app and cannot be changed after creation without re-creating the function app.

Configuring the function

Azure Functions are event-driven and support many triggers, but only one per function. The type of trigger and its bindings are specified in a configuration file named function.json:

{
  "bindings": [
    {
      "authLevel": "function",
      "name": "req",
      "type": "httpTrigger",
      "direction": "in"
    },
    {
      "name": "res",
      "type": "http",
      "direction": "out"
    }
  ],
  "disabled": false
}

Our Azure Function is triggered by a HTTP request from Slack, so we specify type as httpTrigger. authLevel specifies what keys, if any, that are required in the request. function indicates that a function-specific API key is required. name, used both in the in and out binding, specifies the name of the input and output identifier respectively. disabled specifies whether the function is active or not.

When creating a function in the Azure Functions portal you’ll be walked through selecting a language and a template that fits your scenario as well as having a GUI for the configuration.

Writing the code

In this blog post I’ve used F#, but Azure Functions supports a variety of other languages, including JavaScript and C#, as well as scripting options such as Python, PHP, Bash, Batch, and PowerShell.

Type providers are magical and we’ll use the JSON Type Provider to access the JSON returned from the API in a statically typed manner. The JsonProvider takes a sample as input, which it uses to infer types and expose properties.

type provider = JsonProvider<""" {"results": [{ "dag": "Day", "rett": "Dish" }] }""">

The default entry point of an Azure Function is a function called Run, but this is configurable. In our input binding, we specified that the name of the HTTP request was req, which gives us a function signature looking like this:

let Run(req: HttpRequestMessage) =

We also want the Slack Slash Command to support an optional day parameter, e.g. /lunch [day], in case you want to know what’s for lunch tomorrow or the rest of the week. Slack passes everything after the Slash Command in a text parameter, so we’ll check for this and also transform the day to the right case as it will be used for filtering later.

let pair =
    req.GetQueryNameValuePairs()
    |> Seq.tryFind (fun q -> q.Key = "text" && q.Value <> "")

let day =
    let cultureInfo = CultureInfo("nb-NO") 
    match pair with
    | Some x -> cultureInfo.TextInfo.ToTitleCase (x.Value.ToLower())
    | None -> cultureInfo.TextInfo.ToTitleCase (cultureInfo.DateTimeFormat.GetDayName(DateTime.Now.DayOfWeek))

Finally, we determine the value to return to Slack. First, we use the type provider to retrieve the JSON from the API. Next, we filter so that we only get items for the day in question, concatenate the Rett (dish description) values and prefix with the day value. E.g. Mandag: Omelett.

let dayDish =
    provider.Load("http://www.tibeapp.no/hosted/albatross/xlsx/test.php?fileurl=http://netpresenter.albatross-as.no/xlkantiner/Kanalsletta4.xlsx").Results
        |> Array.filter (fun item -> item.Dag = day) 
        |> Array.map (fun item -> item.Rett)  
        |> String.concat " "
        |> sprintf "%s: %s" day 

return req.CreateResponse(HttpStatusCode.OK, dayDish)

I deliberately focused on the happy path in this blog post, so no handling of misspelled days.

Slack

The last piece of the puzzle is to integrate Slack with the Azure Function using a Slash Command.

Slash Commands allow you to listen for custom triggers in chat messages across all Slack channels. When a Slash Command is triggered, relevant data will be sent to an external URL in real-time.

  1. Go to the App Directory, https://my.slack.com/apps.
  2. Search for and select Slash Commands.
  3. Click Add configuration.
  4. Specify a name for the command.
  5. Click Add Slash Command Integration.

The placeholder Slash Command in Slack is /lunch, but we’ll be using the Norwegian term /lunsj as the result also happens to be in Norwegian. URL is the URL of the Azure Function and can be retrieved from the Develop section in the Azure Functions portal. Token is not used as the Azure Function already includes a token, an API key named code in the URL and built-in functionality for verifying this. The rest of the settings should be self-explanatory.

Slack Integration Settings

With the Slash Command configured, we can now execute it in any channel. /lunsj to get today’s lunch or /lunsj onsdag for getting Wednesday’s lunch:

Lunch Slack Slash Command

Bacon burger with fries on Wednesday. Score!

Summary

Azure Functions is a lightweight and fast way to develop, which keeps costs to a minimum and by using F# we were also able to keep the code to a minimum. For a more in-depth look at serverless, I highly recommend reading Serverless Architectures.

The source code is available on GitHub.