A Slack bot with Python’s 3.5 asyncio
await/async are a big change in Python (3.5+) but there aren’t much documentation around. This article aims at addressing the situation by walking you through the creation of a minimalistic bot for Slack. The first step for you is to create a bot user for your team. This bot will share most of rights you have, so be cautious with your token.
To be convenient, we will put the TOKEN and a DEBUG variable into a separate config.py file. Easy to reuse and easy to exclude from your repository before pushing it on Github. A better practice would be to have them in the environment, go check out the Twelve-Factors App (kudos to earthboundkid).
Testing the token
As an introduction to async, we will verify that the token is correct with the Slack Web API. In the code below, we are relying on the aiohttp library to perform asynchronous calls. You may already be familiar with Kenneth Reitz’s requests. Sadly, the requests library is blocking. It means that you won’t be able to easily make multiple HTTP requests at the same time. Hence, we rely on aiohttp like we usually would with requests.
A couple of new concepts to grasp in order to understand the code above:
- Any async def is called a coroutine. It’s a function that can be stopped and resumed from any await it contains.
- async with defines a context that can resume its execution during the enter and the exit phases.
- And the return await response.json() will pause this piece of code and resume it when the JSON version of the response body is ready.
Coroutines cannot be called directly and must be tied to an event loop, hence the boilerplate code at the bottom.
Connecting the bot
The bot coroutine does two things :
- it authenticates itself to the RTM API using the normal API (on HTTP);
- it connects to the WebSocket and waits for new messages before printing them on the console as they come.
Pushing messages further
Our bot simply prints the incoming messages. What if we wanted to work with them in an asynchronous fashion? Easy, we just have to spawn a new coroutine with the message. That can be done using asyncio.ensure_future(). This call will not interrupt the current flow and maybe be processed during the future interruption (read await).
The good side of this construct is that a lot of messages won’t impact much the listening part. It doesn’t spawn threads or other processes. The drawback of this is that the messages may not be processed in their incoming order and only one process works at anytime. But concurrency is not parallelism, right?
Communicating using queues, although not required in our example, will fix the order of the incoming messages. Be aware that you should be able to process messages faster than they can arrive or you may run into troubles.
In this case, the program has two never-ending coroutines communicating through a Queue. And thanks to Python, both coroutines only see their end of the channel (get and put).
Sending messages back to Slack
There are two ways that your bot may interact with Slack in this situation.
- you may simply send messages over the WebSocket connection whenever you feel like it using ws.send_str().
- for bigger and more complex messages, like files.upload; you’ll have to use the Web API via api_call.
Nota bene : to simplify as much as possible the matter : errors, edge cases or how to gracefully quit the program were not addressed here and are left as an exercise for the reader.