In my previous blog post, I covered the basics of Slack application setup and configuration. I also defined the specification for a real-life project I am developing:
"I need to be able to provide the details of a job opening inside Slack, and I expect it to be posted directly to the public channel which is considered the best place for such announcements.
Once this job opening is posted, it should automatically be pinned to that channel.
Each time a new user is added to the Slack Team, the Bounty Hunter App should pick that up and send me a nice message asking if that user was a referral.
If the new user was a referral, the job opening should be automatically removed from the public channel, and the person who referred the new hire should receive their $100 (or at least a notification about it)
Also, I would like all active job openings to be automatically reposted to the channel each week."
Let's continue where we left off getting into more advanced subjects of Slack App development.
So far, my Slack application provides a simple interface of a Slash Command. I can run the /add-job-opening
command and provide a simple description of a new opening. The application then posts that description in a configured public channel.
At this point, I can add functionality that will automatically pin a new job opening to a Slack Channel. After that, I will look at the referral system and discuss potential solutions for it. Since we will require more details when creating a new job opening, the Slash Command interface will not be suitable anymore. We will look into alternatives: the Interactive Components and the Dialog. Let's start!
Each message can be pinned to the channel it is posted in. Pinning a message to the channel adds it to the Pins list, which is accessible with channel shortcuts. All pinned messages have special a styling applied in the channel, which makes them more visible:
I want to use pins to create an easy-access list of all job openings but also to highlight all job openings between any other channel discussions and messages. A message should be pinned just after it is posted to the channel.
Let's take a look at the Slack Web API documentation: https://api.slack.com/methods/pins.add
At first glance, this method looks like any other but reading its requirements, I can see it requires one of the optional arguments to work. Depending on which argument I use, it will have a different effect. I can pin a file, file_comment, or a message. I, of course, want to pin a message, and in order to do that, I will need to provide a timestamp
argument. Where will I get this timestamp?
Turns out, each message in the chat has its own timestamp
as its unique identifier. If we want to reference any message, we will always provide a combination of a channel
and timestamp
. In my case, the best place to obtain the timestamp
of a new job opening message I posted is in the response to the chat.postMessage
request. Let's head over to the Web API tester to see what we get in return from chat.postMessage
: https://api.slack.com/methods/chat.postMessage/test
{
"ok": true,
"channel": "C9PMNN316",
"ts": "1522753845.000102",
"message": {
"text": "Test",
"username": "Slack API Tester",
"bot_id": "B9QEFCXC3",
"type": "message",
"subtype": "bot_message",
"ts": "1522753845.000102"
}
}
As we can see in this example, creating new message in the channel returned in response its ts
, which is short for timestamp
. This is the ID we will refer to in the future when using, for example, the pins.add
method.
Looking again at the pins.add
documentation page, I also noticed that the pins.write
application scope is required to use this method. I can add it to my app's configuration page and re-install it in my workspace again.
The code required to implement the automatic pins functionality is minimal: https://github.com/jacekelgda/slack-app-dev-post/commit/4f3088df9df3f278c25bc8a1148139705ae5515e
We will circle back to the pins
method in the later stages of this app's development.
Getting started on the referral functionality, many ideas come to my mind: from more traditional emails to HR or some external tool integration to more Slack-based Slash Commands, Events, or Threads.
Let's briefly discuss all of the above and pick the best.
To use external services, which are outside of our development scopes, we could use integration providers such as IFTTT or Zapier. They integrate very well with Slack and traditional solutions like email or webhooks.
Staying in Slack, I could create another Slash Command which would require posting the email of the referred person. I could experiment with Slack events and look for a specific type of a message in the chat from any user and act on it. I could also watch threads under each job opening, which at this point appeals to me the most. So, let's get into it!
Let's describe how this will work:
After a job opening is posted, anyone can start a thread under that opening. Anyone can post in that thread as well. If we limit referring to simply sharing an email address as a thread comment, it might create a nice workflow. When a referral is shared, the HR group should get notifications about it. Also, the user who made the referral should be somehow saved for later, when the referred person actually starts working for the company.
Slack has events. You can see the full list of events here: https://api.slack.com/events
There are two ways you can consume Slack Events: the RTM API and the Events API. The main difference between them is that the RTM API requires a bigger setup on your side. I will require creating a client which will have to spawn a listener to Slack events. You need to take care of authentication, reconnections, and limits. The Events API is subscription based, and Slack provides much of the setup. I am not saying one method is better than other. I use both methods, depending what my needs are. For example, when I deploy a serverless application, I can't use the RTM API due to websockets limitations of serverless. RTM listeners are often part of bigger libraries for slack (for example. https://www.botkit.ai).
I will use the Events API in the case of our Headhunter Bot. It will later allow us to deploy to serverless.
"Since replies are just messages, if you subscribe or listen to message events in both of our event-driven APIs, you will receive them. We also include some bonus message subtype events, letting your app know how the thread is knitting together." -- from https://api.slack.com/docs/message-threading
Let's configure event subscription to start receiving notifications about thread replies. I go to my apps configuration page and enable events in the "Event Subscriptions" section.
In the configuration form, I need to provide the "Request URL" so I already know I will need to create a new endpoint in my app's API.
Previously, I created an endpoint to handle all Slash Command requests under /api/commands
, so I will add the /api/events
endpoint.
NOTE: There is one additional requirement from Slack when handling their requests. We need to respond with the value of the challenge parameter sent along with each request.
Code: https://github.com/jacekelgda/slack-app-dev-post/commit/d76f64d0143c2eaebe84f4805ae902d03bbb156a
The next part of the configuration will require me to select which events I want to subscribe to. I select message.channels
, which requires the channels:history
scope. I save the changes, and the scope is added automatically. All I need to do now is to reinstall the app.
Now, let's take one step back and take a look at our setup. We have subscribed to the event which is triggered everytime anyone posts any message in any channel. Just a quick reminder: a Slack Channel is a public channel and a Slack Group is a private channel.
I will need to filter out only those events that are important to me, i.e. job opening thread replies. Let's compare three types of messages to understand the differences between regular messages posted by a user in a channel, apps'/bots' messages in a channel, and messages posted in a thread:
A User posts in a channel:
{ token: 'vakfIsjkxQFubfC4qVSWMHBO',
team_id: 'T9PQN8EQH',
api_app_id: 'A9R711J06',
event:
{ type: 'message',
user: 'U9Q8M1JM9',
text: 'hello',
ts: '1522759721.000557',
channel: 'C9PQN8MEV',
event_ts: '1522759721.000557' },
type: 'event_callback',
event_id: 'EvA09H5HCJ',
event_time: 1522759721,
authed_users: [ 'U9Q8M1JM9' ] }
BountyHunter posts in a channel and then pins the message:
{ token: 'vakfIsjkxQFubfC4qVSWMHBO',
team_id: 'T9PQN8EQH',
api_app_id: 'A9R711J06',
event:
{ text: 'Test events',
username: 'Bounty Hunter',
bot_id: 'B9QG7P3V0',
type: 'message',
subtype: 'bot_message',
ts: '1522760082.000415',
channel: 'C9PQN8MEV',
event_ts: '1522760082.000415' },
type: 'event_callback',
event_id: 'Ev9ZJF8K7A',
event_time: 1522760082,
authed_users: [ 'U9Q8M1JM9' ] }
{ token: 'vakfIsjkxQFubfC4qVSWMHBO',
team_id: 'T9PQN8EQH',
api_app_id: 'A9R711J06',
event:
{ type: 'message',
subtype: 'pinned_item',
user: 'U9Q8M1JM9',
item_type: 'C',
attachments: [ [Object] ],
text: '<@U9Q8M1JM9> pinned a message to this channel.',
ts: '1522760083.000025',
channel: 'C9PQN8MEV',
event_ts: '1522760083.000025' },
type: 'event_callback',
event_id: 'EvA02R498U',
event_time: 1522760083,
authed_users: [ 'U9Q8M1JM9' ] }
A User replies in the thread:
{ token: 'vakfIsjkxQFubfC4qVSWMHBO',
team_id: 'T9PQN8EQH',
api_app_id: 'A9R711J06',
event:
{ type: 'message',
user: 'U9Q8M1JM9',
text: 'My reply',
thread_ts: '1522760082.000415',
ts: '1522760198.000378',
channel: 'C9PQN8MEV',
event_ts: '1522760198.000378' },
type: 'event_callback',
event_id: 'EvA09MGLJE',
event_time: 1522760198,
authed_users: [ 'U9Q8M1JM9' ] }
A few things are visible right away after reading the request data:
All non-user events have the subtype
: bot_message
or pinned_item
, which gives enough info to understand what is happening. A regular message differed from a thread reply by the event
property. The reply had the thread_ts
property present.
That's all I need to prepare the filter middleware: I will look only for thread reply messages to job openings. I know a job opening is a message posted by the "Bounty Hunter" bot user. Since there is no direct method to get the message details, I can use two methods: channels.history
or pins.list
. Using channels.history
, I can specify a point in the channel history, using a timestamp, from which I want to fetch a number of messages. I can specify one and then get the message I am looking for. I could also fetch a full list of pins from a channel and filter for the message I am looking for by ts. Each of the approaches requires us creating a request to the Slack Web API, which might become suboptimal, given that we will send that request on each thread reply. We could limit it to one channel for now and later worry about performance and limits.
Code: https://github.com/jacekelgda/slack-app-dev-post/commit/56d1a9838666e8a4a9bc8ed0f8e4a361746478ed
Now that we are filtering out all irrelevant comments we need to look for the referral email in one of the comments.
Code: https://github.com/jacekelgda/slack-app-dev-post/commit/d747a4c89101198ba1694fb41185457767ba80e0
In case someone actually referred someone by email, we should notify people from HR in a separate group (private channel).
I created a new group and used channels.list
to get the ID of that group: https://api.slack.com/methods/channels.list/test
I needed to use the special method of the Web API to generate a link to the original job opening message. Due to lack of better options, I use chat.getPermalink
, which provided with the message timestamp will return the valid URL with a direct link to the message. It does not require any additional permission scopes.
Code: https://github.com/jacekelgda/slack-app-dev-post/commit/2966df9327da9661dfc1d1a0c53d2f0ea2db57d8
In this blog post, I explained the Events API and how to consume it. I showed how we can go about complicated workflows inside a Slack platform.
In my next blog post, I will refactor job opening creation to use Interactive Components and Dialog.