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:
users.lookupByEmail
: this allows us to associate our participants’ email addresses with their Slack ID, a unique channel-like identifier which serves as the target for a DM. This means that you’ll need to store a user’s email address as part of their user data and ensure that this is the same email address they use for their Slack account.chat.postMessage
: this actually handles the sending of a message.
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:
-
A state change occurring on the server’s end signals that it is now
participant-b
’s turn to submit a response. -
The server responds to this state change by querying Slack for
participant-b
’s User ID. We do this with the following request:Method GET
Headers Authorization: Bearer <token>
URL https://slack.com/api/user.lookupByEmail
Query Parameters email=<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. -
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 subsequentPOST
request tochat.postMessage
, which takes three arguments:Method POST
Headers Authorization: Bearer <token>
URL https://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.