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.
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.
- Go to the App Directory, https://my.slack.com/apps.
- Search for and select
Slash Commands
. - Click
Add configuration
. - Specify a name for the command.
- 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.
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:
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.