[Python] How to write and host a chatbot for slack
by Riley MacDonald, August 27, 2018

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

  1. Using a web browser navigate to the Slack API website and create a new app https://api.slack.com/apps?new_app=1.
  2. Choose your workspace.
  3. Under “Bot Users” create a new bot.
  4. Under “Install App” click “Install App to Workspace”.
  5. 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.")
Open the comment form

Leave a comment:

Comments will be reviewed before they are posted.

User Comments:

Be the first to leave a comment on this post!