R to Slack from Scratch

5 minute read

Published:

I was recently automating a number of steps to gather, clean, and process data in a data pipeline at work. These steps are run daily from a Docker container and do things like scrape web pages for new data and check the status of data in our pipeline. Eventually, these automated steps will lead to some needed human intervention, like manually reconciling data values. I used to run these steps myself and see their output, so I would know what needed to be done; now, I’d need some other way to keep up-to-date on the pipeline without checking myself every so often.

We use Slack at work and I knew about the slackr package from twitter. This seemed like a perfect fit to build into my R programs to update myself and others about what needed to be done on the data pipeline. I loaded up the package but found that the suggested setup for slackr was defunct on the Slack side - the token registration was deprecated.

I reached out to the package author on twitter and he pointed me to slackteams for setup. I was a little bit wary of adding this app to my organization’s Slack workspace and ultimately decided not to, so I decided to see if I could sort this out another way.

Creating a bot

I’ve parsed my way through plenty of developer APIs in R before and figured this wouldn’t be too different. Step one was to head to api.slack.com/apps while signed in to Slack and create a new app. It took a bit of poking around to figure out the permissions but I got it eventually.

First, I added a Bot Token and then give it some permission scopes. I settled on just three permissions (seen below) to join a channel, send messages, and upload files (for sending charts as images or data as CSVs, etc.). You also need to install the app to your workplace, which may require admin-level privileges.

Bot TokenSetting permissions
setting a bot tokensetting scopes

Once you have installed your app to your workplace, you’ll get your Bot User OAuth Access Token, which you will need to send messages to Slack from your bot in R, and add your bot to the channels in which you wish to use it (use this method as your bot).

Sending messages

With all that settled, I got to the fun part of sorting through the Slack API documentation to figure out how to translate it into R. The Slack documentation for the various methods is straightforward. I’d need to use the chat.postMessage and decided to create a wrapper function using the httr and rjson packages.

The Slack API uses the Block Kit framework to format the message inside the POST request.

This first function, sendSimpleSlack(), will send a text block in markdown format, so you can add special formatting in the message argument. You need to pass the token and channel as arguments as well.

sendSimpleSlack <- function(chnl, token, message) {
  httr::POST(
    "https://slack.com/api/chat.postMessage",
    add_headers(
      `Content-Type` = "application/json;charset=UTF-8",
      Authorization = paste0('Bearer ', token)
    ),
    encode = 'json',
    body = list(
      channel = chnl,
      blocks = rjson::toJSON(list(list(
        'type' = 'section',
        'text' = list(
          'type' = 'mrkdwn',
          'text' = message
        )
      )))
    )
  )
}

Use the function like so:

response <- sendSimpleSlack(
  chnl = "#sample-channel",
  token = slack_token,
  message = "Sending a message with some *markdown* formatting :+1::+1:"
)

You’ll get a response object as an output, which you can use to debug in case the message doesn’t work.

jsonlite::prettify(httr::content(x, as = 'text'))

Next-level messages

I also put together another wrapper function for when I wanted to send anything more complicated than a single text string. The blks argument in this function needs to be a JSON formatted object following the Block Kit framework; the previous function just set one up and lets you adjust the text in it. It is basically some nested lists converted with toJSON().

sendBlockSlack <- function(chnl, token, message, blks) {
  res <- httr::POST(
    "https://slack.com/api/chat.postMessage",
    add_headers(
      `Content-Type` = "application/json;charset=UTF-8",
      Authorization = paste0('Bearer ', token)
    ),
    encode = 'json',
    body = list(
      channel = chnl,
      blocks = blks
    )
  )
  res
}

I also put together a function to help create a markdown text table that fits in the Slack interface. One limitation here is that it only goes about a couple hundred characters wide before it starts to wrap in Slack, so you need to be careful with number of columns and number of characters in those columns. And Slack has a max character limit for messages of 3,000, so the function abbreviates anything longer than that to just the first five rows.

createSlackTable <- function(table) {
  out <- paste0(
    "```\n",
    paste0(knitr::kable(table), collapse = "\n"),
    "\n```"
  )
  
  if(nchar(out)>3000){
    out <- paste0(
      "```\n",
      paste0(knitr::kable(head(table)), collapse = "\n"),
      "\n...[too long to print, ", nrow(table)-5," rows omitted]...\n```"
    )
  }
  out
}

See an example here combining the two latest functions. This sends two block “sections”, but you could tack on as many as you like in the same way.

sendBlockSlack(
      "#sample-channel",
      slack_token,
      blks = rjson::toJSON(
        list(
          list(
            'type' = 'section',
            'text' = list(
              'type' = 'mrkdwn',
              'text' = paste0('*Update 1:* \n ',
                              length(unique(iris$Species)), " unique species identified")
            )
          ),
          list(
            'type' = 'section',
            'text' = list(
              'type' = 'mrkdwn',
              'text' = createSlackTable(iris)
            )
          )
        )
      )
    )

And just like that, I was able to bypass the slackr package and create my own Slack bot! I added my Slack tokens as environment variables in my deployed Docker environment and used the functions here to send nicely formatted updates at a bunch of different steps in the pipeline.