Make Slack app respond to reacji.
This post continues the series on creating a Slack app in Python, picking up after adding an App Home view. A lot of the subtle, emergent communication patterns within Slack happen by reacting to messages with emoji, and I thought it would be fun to take advantage of that playfulness within the app we’re building.
post starts at commit 4120, and ends at commit 08eb
Slack app in Python series
- Creating a Slack App in Python on GCP
- Adding App Home to Slack app in Python
- Make Slack app respond to reacji
- Using Cloud Firestore to power a Slack app
- Distributing your Slack application
Reacji and custom emoji
Slack’s emoji reactions or reacji are a common way for folks to interact with messages in a channel.
For the application we’re building, I was thinking it might be neat to add :ididit:
and
:udidit:
emoji, which folks could use to add items they did to their list of accomplishments
as well as use to add accomplishments to other folks’ lists.
I’ve noticed some folks discount or dislike tracking their own accomplishments,
so this could be a playful way to get their team and community to help.
Add those emoji via the Customize Slack option in the top-left menu within your Slack workspace.
From there click on Add custom emoji and create an image somehow. I used Omnigraffle because I already had it open, but yeah, probably you’ll use something else.
Add your image, name it :udidit:
and click Save.
Then go ahead and do the same for the :ididit:
emoji as well.
Now we have our custom emoji, and we just need to figure out how to get notified when they’re used.
Subscribing to reaction_added
events
Whenever an emoji is added, an associated reaction_added
event can be fired to the Events API, if you’ve subscribed to it.
To subscribe, head over to Event Subscriptions in your App dashboard, open up Subscribe to workspace events and select reaction_added.
Remember to click Save Changes below, and reinstall your application with those additional permissions.
Handling reaction_added
events
Now that we’re receiving these events, we need to extend event_callback_event
to handle reaction_added
events rather than erroring on them.
def event_callback_event(request, parsed): event_type = parsed['event']['type'] if event_type == 'app_home_opened': return app_home_opened_event(request, parsed) elif event_type == 'reaction_added': return reaction_added_event(request, parsed) else: raise Exception("unable to handle event_callback event type: %s" % \ (event_type,))
For our first implementation of reaction_added_event
, let’s just
log the incoming message so we can get a look at its parameters.
def reaction_added_event(request, parsed): print(parsed) return "Ok"
I added :relaxed:
to a message and the handler emitted this
message from Slack.
{ 'token': 'ttttttttttttt', 'team_id': 'tiiiiiiiiiiii', 'api_app_id': 'aaaaaaaa', 'event': { 'type': 'reaction_added', 'user': 'uuuuuuuuu', 'item': { 'type': 'message', 'channel': 'ccccccccc', 'ts': '1573231294.000900' }, 'reaction': 'relaxed', 'item_user': 'iuuuuuuuu', 'event_ts': '1573250406.000200' }, 'type': 'event_callback', 'event_id': 'eeeeeeeee', 'event_time': 1573250406, 'authed_users': ['uuuuuuuuuu'] }
The first thing we want to do is to filter down to reactions
we’re interested in.
We only want to
act on the two reactions we added, :ididit:
and :udidit:
,
and we only want to handle reactions to message
items,
ignoring files and what not.
def reaction_added_event(request, parsed): event = parsed['event'] if event['reaction'] in ('ididit', 'udidit'): if event['item']['type'] == 'message': print(parsed) print("yes, handling this message") return "Ok"
Now that we’ve filtered down to appropriate messages,
we’re still not really sure about the contents of the message:
what did you or they actually do? To answer that, we’ll
need to make a call to conversations.history
as described in the Retrieving messages docs.
Adding channels:history
scope
Before we can call the channels.history
endpoint, we first need to request
the channels:history
OAuth scope.
Go into your app dashboard, click on OAuth & Permissions, scroll down
until you see Scopes and then add channels:history
.
After that, reinstall your app to request the additional OAuth scope.
We’ve been using the bot token so far, not the OAuth token, so we’ll
also need to the OAuth token to our env.yaml
file. Your OAuth token
is in your App admin under OAuth & Permissions in the field labeled OAuth Access Token.
SLACK_SIGN_SECRET: your-secret
SLACK_BOT_TOKEN: your-bot-token
SLACK_OAUTH_TOKEN: your-oauth-token
With that set, we can take advantage of our new scope.
Calling channels.history
We previously implemented the slack_api
utility function to
simplify calling the Slack API, but it turns out that we can’t reuse
it easily for two reasons. First, conversations.history
wants
application/x-www-form-urlencoded
requests whereas the other endpoint
accepted application/json
. Second, this endpoint wants a GET
instead of
a POST
, although that’s fixed easily enough by using the requests.requests
function
which accepts the HTTP method as a string.
To work around those constraints quickly, we’ll write a get_message
function which will call
into the API directly, instead of building on slack_api
,
even though that’s a bit on the sad side.
def get_message(channel, msg_ts): url = "https://slack.com/api/conversations.history" bot_token = os.environ['SLACK_OAUTH_TOKEN'].encode('utf-8') params = { 'token': bot_token, 'channel': channel, 'latest': msg_ts, 'limit': 1, 'inclusive': True } resp = requests.get(url, params=params) return resp.json()
Then we’ll update reaction_added_event
to use this new function.
def reaction_added_event(request, parsed): print(parsed) event = parsed['event'] if event['reaction'] in ('ididit', 'udidit'): item = event['item'] if item['type'] == 'message': print("yes, handling this message") channel = item['channel'] msg_ts = item['ts'] msg = get_message(channel, msg_ts) print(msg) return "Ok"
Deploy the event_post
function, add :ididit:
to a message,
and we can get a look at the response format for conversations.history
.
{ 'ok': True, 'latest': '1573231294.000900', 'messages': [ { 'client_msg_id': 'cmiiiiiiiiiiiiiiiii', 'type': 'message', 'text': "I've finished upgrading the hosts!", 'user': 'uuuuuuu', 'ts': '1573231294.000900', 'team': 'ttttttttt', 'blocks': ["a lot of stuff omitted"], 'reactions': [ { 'name': 'ididit', 'users': ['uuuuuuu'], 'count': 1 } ] } ]
What we really care about are messages/0/user
and messages/0/text
, which
we’ll be able to use to add this message either to the speaking or emojing
user depending on whether it’s an :ididthis:
or udidthis
respectively.
Pulling all together
Somewhat conspicuously, we still don’t have a database to store
all of this, and we’ll solve that in the next post, not this one. For now
we’ll create an interface for storing this data, the reflect
function.
def reflect(team_id, user_id, text): print("Reflected(%s, %s): %s" % (team_id, user_id, text))
Then we’ll update reaction_added_event
to call that function.
def reaction_added_event(request, parsed): event = parsed['event'] if event['reaction'] in ('ididit', 'udidit'): item = event['item'] event_user_id = event['user'] if item['type'] == 'message': channel = item['channel'] msg_ts = item['ts'] msg_resp = get_message(channel, msg_ts) msg = msg_resp['messages'][0] msg_team_id, msg_user_id, text = \ msg['team'], msg['user'], msg['text'] if event['reaction'] == 'ididit': reflect(msg_team_id, msg_user_id, text) elif event['reaction'] == 'udidit': reflect(msg_team_id, event_user_id, text) return "Ok"
Deploy again, and we’ve integrated reacji!
Reacji as interface
When I first started using Slack, I assumed reacji were a gimmick,
but then I remember the lightbuld going off when I first saw folks
organically start voting on a message using the :plus:
emoji.
No one had asked them to vote, it just started happening, and it’s
that organic freedom, constrained with the rigid constraints (they are just small images
with a count next to them) that lead to so the novel usage patterns.
No Slack App ever needs to integrate with reacji, but I’ve seen a bunch of creative integrations that acknowledge the patterns that folks already have and then enhance those patterns with automated action.
Another great aspect of reacji as user interface is they are more discoverable than Slash Commands, which are usually hidden from other users. Organic growth and adoption are underpinning of a successful app, and reacji are a powerful mechanism to that end.
Integration friction
I’ll say that I was a bit surprised at how long it took me to get reacji working, because I’d come into this post assuming I was already done with most of the necessary work and would just be introducing a new event.
Instead I needed to add a new style of API integration since conversations.history
didn’t support the JSON format, and a new API token since previously I’d been using the bot
token rather than the user token.
Individually, each of the Slack APIs are extremely well designed, it’s only
collectively that they start to surface some degree of friction.
This is a common challenge for broad, sophisticated APIs. I’m currently reading Building Evolutionary Architectures, which is better, more structured coverage of the ideas I wrote about in Reclaim unreasonable software. API deterioration can be prevented, but requires very deliberate usage of “asserted properties” in my post’s nomenclature or “fitness functions” in Ford/Parsons/Kua’s.
Next
We’ve now reached commit 08eb, and are down to two more goals: integrating a database, and publishing this into the applications directory. I’ll work on the database next, as it’s hard to publish an app that is exclusively stub data, and then we can complete the publishing step.
series continues in Using Cloud Firestore to power a Slack app