Write a Slackbot with Unison Cloud

Slackbots are simple to write and deploy on the Cloud. By the end of this walk-through you'll have deployed a bot that can respond to messages or other Slack events and done the following:

  • Deployed an Http service
  • Given your service a static ServiceName
  • Loaded a secret value in secure Config
  • Used the Unison Slack library

Our bot is simple—it will respond to any message containing the word coffee with the coffee emoji ☕️, but feel free to add functionality as you like!

Prerequsites

Follow along with our video tutorial

Slack setup

Head to api.slack.com/apps and click "Create a new App". You're creating your bot "from scratch" so select that option and give your app a memorable name.

In the administrative menu, head to "OAuth and Permissions". For the purposes of this tutorial, you want the bot to be able to write emoji reactions and join channels.

Install the app in your chosen workspace, click "Allow" and you'll see a "Bot User OAuth token". Grab the value for this token and keep it a secret! For now, you can save it to an environment variable.

$ export SLACK_API_TOKEN="myTokenValueFromSlack"

Head to the "Basic Information" tab of the administrative menu and scroll down to "App Credentials. Copy the value for "Signing Secret" and save that as an environment variable as well.

$ export SLACK_SIGNING_SECRET="mySigningSecretValue"

Writing the Unison Slackbot

Start Unison with the ucm command in your terminal. Then create a project in the UCM to house your Slackbot with project.create. The UCM will download the standard lib, and once its finished you should install the Slack library by issuing pull @runarorama/slack/releases/latest lib.slack.

.> project.create coffeebot

🎉 I've created the project coffeebot.

  I'll now fetch the latest version of the base Unison
  library...

coffeebot/main> pull @runarorama/slack/releases/latest lib.slack

Storing encrypted values in the Cloud

Let's start by writing a function that stores your Slackbot's configuration values. This function will need to:

  • Create an Environment where your Config values will be set
  • Read your api token and secret from environment variables
  • Save the values securely in the Cloud for future use

Secrets and other configuration values can be safely stored in the Cloud using Environment and Config. They work in concert: you write values to an Environment and later read them with the Config ability. This relationship means that Config values are scoped to a particular Environment, which provides basic access management, as only a service deployed to the same Environment can access its Config values.

config : '{Exception, IO} ()
config =
  Cloud.main do
    env = Environment.create "coffeebot"
    Environment.setValue env "api-token" (getEnv "SLACK_API_TOKEN")
    Environment.setValue env "signing-secret" (getEnv "SLACK_SIGNING_SECRET")

Remember the text values "api-token" and "signing-secret"! You'll need them to read these values again.

In the UCM console, use the run command to save your values to the Cloud.

coffeebot/main> run config

Writing the CoffeeBot code

We'll be writing our coffee bot core logic next. The coffeeBot function should:

  • Scan every Slack message
  • Check if contains the word "coffee"
  • Add an emoji reaction to those messages, otherwise skip the message

Let's start with a signature:

coffeeBot : '{SlackWeb, SlackEvents, Exception, Log} ()
coffeeBot = todo "write coffeeBot"

The Slack library API has two abilities for interacting with Slack. They mirror the two main APIs published by Slack.

There's a SlackWeb ability which maps to the Http Web API, and a SlackEvent ability which allows you to respond to a subset of incoming Slack events from the Events API.

The Log ability in the signature comes from the Cloud client. It's handy for debugging or gathering info.

From the SlackEvents api, we can gather all the incoming messages with the api.message function and skip the ones that we don't care about with SlackEvents.skip:

coffeeBot : '{Exception, Log, SlackWeb, SlackEvents} ()
coffeeBot = do
    message = !api.message
    messageText = Text.toLowercase (Message.text message)
    if Text.contains "coffee" messageText then
      todo "react to message"
    else
      SlackEvents.skip

Then we'll use the react function from the SlackWeb api to add the "coffee" emoji in response.

coffeeBot : '{Exception, Log, SlackWeb, SlackEvents} ()
coffeeBot = do
    message = !api.message
    messageText = Text.toLowercase (Message.text message)
    if Text.contains "coffee" messageText then
       react message "coffee"
    else
      SlackEvents.skip

You'll notice this function just returns Unit, and we haven't handled the two Slack abilities yet. The handler that transforms the Slack abilities into an Http service is called runBot. runBot expects the Text config keys that you used for storing the slack token and signing secret. (It'll do the lookup in Cloud Config for us.) Then it accepts the Slack interaction logic as its final argument.

runBot "api-token" "signing-secret" coffeeBot

We'll show how to call runBot in a Cloud deployment next.

Deploying the CoffeeBot

The last function we'll need is a Cloud deployment function. It starts with a Cloud.main block where we specify the Environment for our service deployment. We'll want to use the same environment name, "coffeebot", since Environment creation is idempotent and we need this service to have access to our Slack secret values.

coffeebot.deploy =
  Cloud.main do
    env = Environment.create "coffeebot"

    todo "deploy http service"

Since runBot turns our coffeeBot Slack interactions into a function from HttpRequest to HttpResponse, it can be passed to the cloud client's deployHttp function. This will return a ServiceHash representing the code being deployed.

coffeebot.deploy =
  Cloud.main do
    env = Environment.create "coffeebot"
    serviceHash = deployHttp env
      (runBot "api-token" "signing-secret" coffeeBot)

    todo "name service"

Finally, let's give the ServiceHash value—which will vary depending on the code being deployed—a static, human readable service name.

coffeebot.deploy =
  Cloud.main do
    env = Environment.create "coffeebot"
    serviceHash = deployHttp env
      (runBot "api-token" "signing-secret" coffeeBot)
    name = ServiceName.create "coffeebot"
    ServiceName.assign name serviceHash

At this point, update your codebase to save your work and then issue the run command in the UCM.

coffeebot/main> update
coffeebot/main> run coffeebot.deploy

The Cloud will deploy your Slackbot, and when you converse with your coworkers about coffee, it should react accordingly. ☕️