Build a first simple Slack Bot with Python

Alexandre Attia
Alex Attia Blog
Published in
5 min readJan 25, 2018

I have always liked the chat bots, even if some of them are overrated and there is often no intelligence behind a chat bot. I like the principle to have an simple assistant in Slack.

With this post, I quickly explain how to build a simple Yoda bot (We are big fans of Star Wars in the team). Its tasks would be really simple. Most of my work is centered around deep learning and object recognition. So, I sometimes have to manually annotate data and draw bounding boxes in pictures. When I mention the Yoda bot, I want it to tell him how many pictures I have annotated. Moreover, I want it to tell me what’s the duration in traffic to a place (using Google Maps API). It’s a rule-based bot (no AI or fancy stuff behind it).

In the following, there will be some Python code lines, it’s in Markdown so it won’t be very pleasant to read !

Requirements

  • Python 3.6, pip (/virtualenv)
  • Slack workspace and account

Install the Slack API via

pip install slackclient

Create the Slack App

Create your slack app on the official Slack API website get an API token for the bot. Set a name and the workspace you want to work into.

Go to the Bot Users part to set the display name and the default username.

Then go the Install the App part to get the token. Save bot user oauth access token.

token = ‘xoxb-XXXYYYZZZ’

Set up and basic functions

The Slack API really makes things easy. Concretely, we just need to connect (using the token), check if the bot is connected, then read each message received (i.e posted in the workspace) and eventually, answer to the query.

We connect with :

slack_client = SlackClient(token)

We check the connection with :

bool = slack_client.rtm_connect(with_team_state=False)

We read the message (returns a list) with:

events = slack_client.rtm_read()
# events = [{'type': XXX, 'text': YYY, 'user': ZZZ}, ...]

We post a message with :

slack_client.api_call(“chat.postMessage”, channel=channel, text=t)

NB : To post into a channel, you must invite the bot to this channel (exception for direct message with the bot)

Coding the Yoda bot : number of annotated data

My annotation file is a csv with the format img_name, x1, y1, x2, y2, class. I want to count the total number of rows and the number of rows equal to a particular class.

The function to post the number of annotated pictures or another message is below :

def post_annotation(token, text=None, channel='bot', response_to=''):
"""
Post a message into a channel the number of annotated pictures or a other text.
:param token: Slack API token
:param text: text to post. If None, the text is the information about the annotations
:param channel: channel where the bot post the message
:param reponse_to: if we want to mention some one
"""

# connection
slack_client = SlackClient(token)
# Number of rows and positive samples
with open("annotation.txt", 'r') as f:
annotation = f.read().split('\n')[:-1]
tot = len(annotation),
pos =len([p for p in annotation if p.split(', ')[5] == '1'])
# Mention someone (need an user ID, not an username)
if response_to != '' :
response_to = '<@%s>' % response_to
# Pre-defined message
if not text:
text = '%s _%s_\nYou have annotated *%d* pictures \with *%d* fractures :+1:' % (response_to, np.random.choice(sw_sentences), tot, pos)
# Post message
slack_client.api_call(
"chat.postMessage",
channel=channel,
text=text)

The sw_sentences is a list of pre-defined Star Wars quote (mostly Yoda quotes) to share some wisdom with my team.

Coding the Yoda bot : duration in traffic

For the traffic, I used the official Google Maps API (free for <2500 requests/day) and Python client. I just want to know the duration in traffic between two addresses. For the Google Maps API, you have to register an app here and get an API token (again!). Then, install the Python package.

pip install -U googlemaps

The function to get the duration in traffic (no Slack post yet) is below :

def get_directions_duration(address_dep, address_arriv, name_dest):
"""
Get the duration in traffic using Google Maps API
:param address_dep: departure address (as on Google Maps)
:param address_arriv: arrival address
:param name_dest: name of the destination
:return: text with the ETA
"""

# Connect to the API
gmaps = googlemaps.Client(key=gm_key)
now = datetime.datetime.now()
# Compute the directions
results = gmaps.directions(address_dep, address_arriv, mode="driving",departure_time=now)
directions_result = results[0]['legs'][0]
# Get resukts
duration = directions_result['duration_in_traffic']
# Text (fstring) with well formatted addresses
t = f'_YodaWaze_ - Time to {name_dest} : %s _(%s to %s)_' % (duration['text'], directions_result['start_address'], directions_result['end_address'])
return t

Coding the Yoda bot : handling messages

Now we need to differentiate the different type of messages to answer differently depending of the use case. I have three type of events :

  • “@YodaBot help” : to post the explanations of the bot
  • “@Yodabot” : to post the number of annotated data
  • “@Yodabot give me the time from my place to Elie’s place” : to post the ETA in traffic from two places

I have to take into account only message of the users and not the bot posts (it would result in an infinite loop…).

To match the users and the addresses, I have to define a dictionary with the addresses and I want to be able to use “my place” instead of my first name in the post (it would be weird to use the 3rd person for myself) so I have to match the user ids with the user names:

adress = {‘alex’ : ‘1 Fake St San Francisco’, ‘elie’: ‘5 Fake St Palo Alto’, ‘eliott’: ’10 Fake St San Jose’}
ids = {'UXXYY': 'alex', 'UYYZZ': 'elie', 'UZZXX':'eliott'}

The function to handle message is :

def parse_bot_commands(slack_events):
“””
Handling the posts and answer to them
:param slack_events: slack_client.rtm_read()
“””

for event in slack_events:
# only message from users
if event[‘type’] == ‘message’ and not “subtype” in event:
# get the user_id and the text of the post
user_id, text_received, channel = event[‘user’], event[‘text’], event[‘channel’]
# the bot is activated only if we mention it

if ‘@%s’ % yodabot_id in text_received:
# Activate help if ‘help’ or ‘sos’ in the post
if any([k in text_received for k in [‘help’, ‘sos’]]):
post_annotation(token, text=help_text, channel=channel)
# Activate Google Maps if ‘distance’ or ‘time’
elif any([k in text_received for k in [‘distance’, ‘time’]]):
# search the users names in the post
r = re.compile(r’\bELIE\b | \bELIOTT\b | \bALEX\b | \bMY PLACE\b’, flags=re.I | re.X)
matched = r.findall(text_received)
# replace ‘my place’ by the user name
if ‘my place’ in matched:
matched[matched.index(‘moi’)] = ids[user_id]
# check if we have the addresses of all users
if all([k in adress for k in matched]):
name_dest = matched[1].title()
# Compute direction
directions_text = get_directions_duration( adress[matched[0]], adress[matched[1]], name_dest)
# Post message
post_annotation(token, text=directions_text, channel=channel)

# Activate annotations information
else:
post_annotation(token, channel=channel)

Coding the Yoda bot : wrapping up

The remaining part consists only in wrapping up the different part into a slackbot.py file. We need the yodabot_id for the mentions.

import os
import time
import numpy as np
import re
from slackclient import SlackClient
import googlemaps
import datetime
if __name__ == “__main__”:
slack_client = SlackClient(token)
if slack_client.rtm_connect(with_team_state=False):
yodabot_id = slack_client.api_call(“auth.test”)[“user_id”]
while True:
parse_bot_commands(slack_client.rtm_read())
time.sleep(1)

NB : We need a time.sleep otherwise, it would run indefinitely and use the full CPU even when there isn’t any post. With the time.sleep(1), we have at least one second of delay.

To launch it on an Ubuntu server, just use :

nohup nice python slackbot.py &  

Hope it will be useful !

--

--