A fortune slack slash command using plumber

2 min read

slack plumber web api fortunes ngrok

Most of our internal communication at ThinkR goes through Slack, and I’m also popping up to other slack teams (e.g. the very active ropensci team) from time to time.

The slackr package is great to send stuff (text, plots, …) to slack from R, but let’s do the opposite, i.e. query R from slack with a slash command.

This is my excuse to learn about plumber and finally haver something to say when people ask me about the plumber hex sticker on my laptop. plumber is a neat R package that allows you to simply create web apis that are mapped to R functions.

Here is the canonical example:

# plumber.R

#* @get /mean
normalMean <- function(samples=10){
  data <- rnorm(samples)

#* @post /sum
addTwo <- function(a, b){
  as.numeric(a) + as.numeric(b)

We can turn the plumber.R file into a web server with the api endpoints /mean and /sum with the plumb function.

r <- plumb("plumber.R")  # Where 'plumber.R' is the location of the file shown above

The documentation covers several hosting options for a plumber server, but for the purpose of this post, I’ll just host it on my local host and follow the lead from this article and use ngrok to tunnel the command in. I’ve installed ngrok on my mac with homebrew and run it directly on a terminal

$ brew cask install ngrok
$ ngrok http 3000

This gives you a temporary url that tunnels requests to the 3000 port on your machine:

Next we need to create a custom integration at http://my.slack.com/services/new/slash-commands

The important part is the URL field that is filled with the url we just got from ngrok. Once this is done, we have a slash command called /fortune that uses a panda emoji because why not and send commands to our plumber server via ngrok.

The last thing we need is to make the api with plumber. The api is fairly simple and just retrieves a fortune from the fortunes package, but we sort of need to go around the flexible interface of fortunes::fortune

#* @get /
#* @serializer unboxedJSON
fortune <- function( text = NULL){
  if( !is.null(text) ){
    as_num <- suppressWarnings( as.numeric(text) )
    if( !is.na(as_num) ) text <- as_num
    response_type = "in_channel",
    text = paste( capture.output( fortunes::fortune(which=text) ), collapse = "\n")  

So that we can support a variety of use cases

  • get a random fortune
  • get a fortune by id
  • search fortune by author or content