Building a Slackbot to DM Users

25 February 2023

Ingredients: two API calls and a server; Time: 1hr; Serves anonymized DMs to facilitate asynchronous data collection.

My lab encountered a problem with one of the projects we’re working on: how do we reduce the time in takes to collect asynchronous responses from participants in a turn-based experiment? Because the task is both asynchronous (to reduce the commitment required to recruit participants and alleviate scheduling conflicts) and turn-based (inherent to the data we’re collecting), we often find ourselves in the situation where participant A is waiting on participant B to take their turn so that they can contribute.

Since all of our participants are already in a private slack channel, it would be great if we could DM a participant whenever it is their turn, and if participant A is waiting for participant B they could just ask them to go over Slack. Unfortunately, there is a complication: our experiments require anonymity, so participant A and B can’t know who the other is.

To solve this, we created a Slackbot integrated with the server we’re using to run the experiments. When it becomes a participant’s turn, our server uses the Slackbot to DM that participant telling them they can now submit their response. If participant A is waiting on participant B to take their turn, they can ‘poke’ participant B by requesting the server to send an additional DM, all without knowing who participant B is.

Creating a Slackbot

Slackbots are surprisingly easy to create; you’ll only need default-user permissions in your Slack workspace. Sign in to your account on api.slack.com and then follow the instructions to ‘Create a New App.’ We chose to create one ‘from scratch’ instead of using a pre-built manifest.

Once created, you’ll need to do three things. First, under the ‘Add features and functionality’ section of ‘Basic Information’, you’ll need to give ‘Bot’ permissions to your Slack app. Second, you’ll need to grant the app the required permissions (im:write, users:read, and users:read.email) to be able to use the two required APIs (as detailed in the next section). Finally, you’ll need to install your app to your Slack workspace.

Two APIs

To be able to DM participants when it’s their turn, we’ll need to use two Slack APIs:

In order to use these APIs, we need to give our Slackbot the necessary permissions. users.lookupByEmail requires the users:read.email scope, which in turn requires users:read. chat.postMessage requires the im:write scope. All three of these can be added directly to the Slackbot’s App Manifest as part of the "oauth_config" block:

"oauth_config": {
  "scopes": {
    "bot": [
      "im:write",
      "users:read",
      "users:read.email"
    ]
  }
}

Part of the reason I thought it would be useful to do this writeup is to mention that these are the only API calls required; in the process of building this system, I initially thought that the Incoming Webhooks feature would be necessary, since it also facilitates posting content from an external source via a POST request, but this turns out to be unnecessary.

Server-side Integration

To make use of these APIs, we need to build some server-side functionality to make calls at the relevant times. The specifics of how this should be implemented will vary from project to project; in our case, the server is a custom-built Scala web-app. I assume that the user model contains, at minimum, something like the following:

user {
  email: string
}

At a high level, the process looks like this:

  1. A state change occurring on the server’s end signals that it is now participant-b’s turn to submit a response.

  2. The server responds to this state change by querying Slack for participant-b’s User ID. We do this with the following request:

    MethodGET
    HeadersAuthorization: Bearer <token>
    URLhttps://slack.com/api/user.lookupByEmail
    Query Parametersemail=<participant-b.email>

    Here, <token> refers to the Slackbot’s Bot User OAuth Token which can be found on the ‘OAuth & Permissions’ page. Also note that <participant-b.email> must be URL-encoded.

  3. If successful, this will respond with a JSON object describing the user; the relevant piece of information is the response["user"]["id"] property, which is the user’s Slack identifier. We take this value and use it in a subsequent POST request to chat.postMessage, which takes three arguments:

    MethodPOST
    HeadersAuthorization: Bearer <token>
    URLhttps://slack.com/api/chat.postMessage
    Query Parameters
    • channel=<response["user"]["id"]>
    • text=<message>

    As with the previous request, <response["user"]["id"]> and <message> should be URL-encoded.

And that’s it! We don’t need to specify anything about the channel or workspace which contains the users since these are determined by the Slackbot’s authorization token.