As a regular user of Slack I was curious how applications and bots work. At the same time I’m looking to strengthen my full stack and python development skills. This post shows how I wrote a Slack chatbot using Python, chatterbot, the Slack API and Ubuntu Server. The chatbot created in this tutorial listens for mentions, pipes the input through ChatterBot
AI and sends the output back to Slack.
Prerequisites
This guide was written using the following tools:
- Linux – Ubuntu (Lubuntu) 17.10 (or linux based development machine)
- Linux – Ubuntu Server 16.04 LTS (or linux based server)
- Python 2.7
- virtualenv
- PyCharm (or your favorite Python editor)
- Slack workspace which you have administrative access to
Developer Environment Setup
We’ll start by running the bot from the local development machine in order to develop and debug the chatbot before deploying it to the server. Start by initializing a git repository in a working directory of your choice. I chose ~/Documents/python/slack_bot
.
~ $ cd ~/Documents/python/slack_bot ~/Documents/python/slack_bot $ git init Initialized empty Git repository in ~/Documents/python/slack_bot |
Since I want the chatbot to be deployed to my server I chose to use virtualenv in order to create an isolated python environment. This helps to get around issues regarding dependency conflicts, versions and indirect permission issues. Install virtualenv
:
~/Documents/python/slack_bot $ sudo apt install virtualenv |
Switch into a new working directory and initialize virtualenv
:
~/Documents/python/slack_bot $ mkdir rileybot ~/Documents/python/slack_bot $ cd rileybot ~/Documents/python/slack_bot/rileybot $ virtualenv rileybot |
This will initialize the virtualenv
environment and all the files required.
Enter the virtualenv
environment by sourcing it:
~/Documents/python/slack_bot/rileybot $ source bin/activate (rileybot) ~/Documents/python/slack_bot/rileybot $ |
You should see a (rileybot)
in your prompt indicating you’ve sourced correctly. You can exit virtualenv
by using the deactivate
command.
Since we’re going to be working with the slack API we’ll need slackclient
. To get a basic chatbot we’ll use the chatterbot
library https://github.com/gunthercox/ChatterBot.
~/Documents/python/slack_bot/rileybot $ pip install slackclient ~/Documents/python/slack_bot/rileybot $ pip install chatterbot |
Now you should have everything you need to begin developing the chatbot.
Create a new Slack Application
- Using a web browser navigate to the Slack API website and create a new app https://api.slack.com/apps?new_app=1.
- Choose your workspace.
- Under “Bot Users” create a new bot.
- Under “Install App” click “Install App to Workspace”.
- Once installed copy the “Bot User OAuth Access Token” for later use.
Writing the Slack API / Chatterbot Python Code
Now that the environment is prepared it’s time to write the Python code. Let’s begin by importing the required modules which should all be installed from the steps above.
1 2 3 4 5 6 | import os import time import re from chatterbot import ChatBot from slackclient import SlackClient |
Listening for Slack Messaging Events
First the SlackClient must be initialized using the “Bot User OAuth Access Token” provided after creating the Slack Bot above. Create a variable to hold the chatbot ID (which we’ll fetch later).
1 2 | slack_client = SlackClient([YOUR_BOT_USER_OAUTH_ACCESS_TOKEN]) rileybot_id = None |
Now onto the main method. The first step is to establish a Real Time Messaging API session. This is accomplished using slack_client.rtm_connect(with_team_state=False)
. The argument with_team_state
Connects via rtm.start
to pull workspace state information. False
connects via rtm.connect
, which is lighter weight and better for very large teams. Fortunately this method returns a bool
so you can easily determine if the connection was successful.
Once successfully connected you can fetch the bot id by making a test authentication call using slack_client.api_call("auth_test")["user_id"]
.
Now add a while True
loop to listen for bot mentions which sleeps for 1
seconds (RTM_READ_DELAY
which can be adjusted as desired). We can listen for messages from the Real Time Messaging API by invoking slack_client.rtm_read()
which returns messaging events. We’ll implement the parse_bot_command()
in the next section. If a bot command was found we’ll handle that command in the handle_command
method which we’ll implement in the next section. The entire main method should look something like this:
1 2 3 4 5 6 7 8 9 10 | if __name__ == "__main__": if slack_client.rtm_connect(with_team_state=False): rileybot_id = slack_client.api_call("auth.test")["user_id"] while True: command, channel = parse_bot_commands(slack_client.rtm_read()) if command: handle_command(command, channel) time.sleep(RTM_READ_DELAY) else: print("Connection failed. Exception traceback printed above.") |
Parsing Message Events for Mentions
The purpose of this tutorial is to listen for mentions to the bot and send a response back. For each event in slack we want to determine if the event is a mention to the bot. We’ll handle this in the methods parse_bot_commands()
and parse_direct_mention
.
parse_bot_commands()
loops over the JSON
of events returned. It determines if the event type is a message and that there’s no subtype
present. The text
value is provided to parse_direct_mention()
to determine if the text is an at mention. If it is the message
and user_id
are returned otherwise None, None
. If the user_id
is our bots id then we return the message
text and channel
which the event occurred. The methods should look something like:
1 2 3 4 5 6 7 8 9 10 11 12 13 | def parse_bot_commands(slack_events): for event in slack_events: if event["type"] == "message" and not "subtype" in event: user_id, message = parse_direct_mention(event["text"]) if user_id == rileybot_id: return message, event["channel"] return None, None def parse_direct_mention(message_text): MENTION_REGEX = "^<@(|[WU].+?)>(.*)" matches = re.search(MENTION_REGEX, message_text) return (matches.group(1), matches.group(2).strip()) if matches else (None, None) |
Handling Bot Mentions using ChatterBot
Start by initializing ChatterBot just under the imports. See the ChatterBot Documentation here for further customization.
1 2 3 4 5 6 | chatbot = ChatBot( 'RileyBot', trainer='chatterbot.trainers.ChatterBotCorpusTrainer' ) chatbot.train("chatterbot.corpus.english") |
At this point we know the bot has been mentioned. It’s time to handle the mention in the handle_command()
method. This method will feed the input the message into the ChatterBot library and return the output back to slack. This is accomplished by simply executing chatbot.get_response(message)
and posting the message back to Slack:
1 2 3 4 5 6 7 8 | def handle_command(command, channel): response = chatbot.get_response(command) slack_client.api_call( "chat.postMessage", channel=channel, text=response or default_response ) |
The handle_command
method can be extended with additional functionality at a later time! The full code can be found below.
Give it a try!
Time to test out the bot and resolve and errors.
~/Documents/python/slack_bot/rileybot $ python rileybot.py |
The ChatterBot library should output verbose information about what’s building before the bot is ready. Once the dependencies have built you can head over to slack and give it a try.
Deploy the Bot to the Server
Now that the Bot functionality has been verified it can be installed on a server. If you used a VCS
such as Git
you can simply checkout the code on the server and install it as a systemd
process. See my post on [Linux Server] Configuring applications to run at startup for more detailed instructions on this. It’s important to to use virtualenv python interpreter as opposed to the systems to avoid any permission errors. You may need to update file permissions based on your server. Use systemctl status
and journalctl --unit=[service_name]
to debug any failures.
1 2 3 4 5 6 7 8 9 10 11 12 | [Unit] Description=RileyBot Slack Bot After=multi-user.target syslog.target network.target [Service] Restart=on-failure RestartSec=20 5 Type=idle ExecStart=/[path_to_chatbot]/bin/python /[path_to_chatbot]/rileybot/rileybot.py [Install] WantedBy=multi-user.target |
Full Python Code
Here’s the full code for reference:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | import os import time import re from chatterbot import ChatBot from slackclient import SlackClient chatbot = ChatBot( 'RileyBot', trainer='chatterbot.trainers.ChatterBotCorpusTrainer' ) chatbot.train("chatterbot.corpus.english") slack_client = SlackClient([YOUR_BOT_USER_OAUTH_ACCESS_TOKEN]) rileybot_id = None RTM_READ_DELAY = 1 MENTION_REGEX = "^<@(|[WU].+?)>(.*)" def parse_bot_commands(slack_events): for event in slack_events: if event["type"] == "message" and not "subtype" in event: user_id, message = parse_direct_mention(event["text"]) if user_id == rileybot_id: return message, event["channel"] return None, None def parse_direct_mention(message_text): matches = re.search(MENTION_REGEX, message_text) return (matches.group(1), matches.group(2).strip()) if matches else (None, None) def handle_command(command, channel): response = chatbot.get_response(command) slack_client.api_call( "chat.postMessage", channel=channel, text=response ) if __name__ == "__main__": if slack_client.rtm_connect(with_team_state=False): rileybot_id = slack_client.api_call("auth.test")["user_id"] while True: command, channel = parse_bot_commands(slack_client.rtm_read()) if command: handle_command(command, channel) time.sleep(RTM_READ_DELAY) else: print("Connection failed. Exception traceback printed above.") |