How to Add Phone Calling Bots to Slack with Python

May 24, 2016
Written by
Matt Makai
Twilion

Make phone calls from Slack using a Python bot

Slack is awesome for text and emoji-based conversations with colleagues. However, sometimes it’s far easier to quickly answer a question over the phone. While Slack is just starting to add voice calling between users, there is no way to patch someone in by their good old telephone number. Let’s add phone calls to Slack by creating a bot with Python, Twilio and the Slack Real Time Messaging API.

Tools We Need

Our bot, which we’ll name callbot, requires a few libraries and APIs. To build our bot we need:

Here’s a handy step-by-step guide to setting up Python, pip and virtualenv.

Our Twilio requirements are:

The Slack dependencies are:

Ensure that Python version 2 or 3 is installed. We’ll configure everything else throughout the remainder of this tutorial.

You can follow along by writing the code in this post or skip ahead to the finished project by cloning the companion GitHub repository.

Configuring Our Environment

Now that we know what tools we need, go to the terminal (or Command Prompt on Windows) and change into a directory where you want to store this project. Within that directory, create a new virtualenv to isolate our application dependencies from other Python projects you’re working on.

 

virtualenv callbot

 

Activate the virtualenv:

 

source callbot/bin/activate

 

Depending on how your virtualenv and shell are set up, your prompt should look like something like this screenshot.

virtualenv.png

We’ll use the official slackclient API helper library to access their API to send and receive messages from a Slack channel. Install the slackclient and Twilio helper libraries along with phonenumbers into your virtualenv with the pip command:

 

pip install slackclient twilio phonenumbers

 

We next need to obtain an access token for the Slack Bot and our Twilio API credentials.

Slack Real Time Messaging API

Slack provides programmatic access to their chat application through a web API. Open up the landing page for the Slack Web API and sign up to create a Slack team or sign into your existing account. You can create a new team for free if you don’t have admin privileges on an existing team.

slack-api-sign-in.png

After you have signed in go to the Bot Users page.

custom-bot-users.jpg

Give your bot the name “callbot” and click the “Add bot integration” button.

callbot.png

The page will reload and you’ll see a new generated access token. You can also change the logo to a custom design, like I did with this bot by giving it the Twilio logo.

slack-token.png

Scroll down and click the “Save Integration” button. Your bot is now ready to access the Slack API.

A common practice for Python developers is to export secret tokens like our Slack token as environment variables. Export the token with the name SLACK_BOT_TOKEN:

 

export SLACK_BOT_TOKEN='your slack token pasted here'

 

Awesome. We’re authorized to use the Slack API as a bot. Now we just need a Twilio account and credentials to start handling phone calls.

Twilio Phone Numbers

We need access to the Twilio API to make phone calls from our application. Sign up for a free Twilio account or log into your existing account if you already have one. Our Slack bot will only dial outbound phone calls. Therefore, nothing needs to change on the number configuration screen.

twilio-phone-number.png

With our phone number in hand, go to the Console Dashboard screen and look for your Twilio Account SID and Auth Token:

console-account-sid.png

As we did earlier with the SLACK_BOT_TOKEN, we will use the newly-exported environment variables in our Python script.

On the command line, export Twilio credentials as an environment variables:

 

(callbot)$ export TWILIO_ACCOUNT_SID='your twilio account sid'
(callbot)$ export TWILIO_AUTH_TOKEN='your twilio auth token'
(callbot)$ export TWILIO_NUMBER='your twilio phone number, for example  12025551234'

 

There is one more bit of information we need: our bot’s ID in Slack. Next we’ll write a short script to snag that from the Slack API.

Obtaining Our Bot’s ID

Time to write some Python code! We’re going to get warmed up by writing a quick helper Python script to get callbot’s ID because it varies based on the Slack team. We need the callbot ID because it will allow our application code to determine if messages parsed from the Slack Real Time Messaging API are directed at our bot.

This script will also help test that our SLACK_BOT_TOKEN environment variable is set properly. Create a new file named get_bot_id.py with the following code.

 

import os
from slackclient import SlackClient


BOT_NAME = 'callbot'

slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN'))


if __name__ == "__main__":
    api_call = slack_client.api_call("users.list")
    if api_call.get('ok'):
        # retrieve all users so we can find our bot
        users = api_call.get('members')
        for user in users:
            if 'name' in user and user.get('name') == BOT_NAME:
                print("Bot ID for '" + user['name'] + "' is " + user.get('id'))
    else:
        print("could not find bot user with the name " + BOT_NAME)

 

The above code imports SlackClient and instantiates it with our SLACK_BOT_TOKEN. When the script is executed by the python command we hit the API for a list of Slack users and get the ID for the one that matches the name callbot.

We only need to run this script once to obtain our bot’s ID.

 

python get_bot_id.py

 

When we run the script, we’ll get a single line of output with our Bot’s ID.

get-callbot-id.png

Copy the ID and export it as an environment variable named BOT_ID.

 

(callbot)$ export BOT_ID='bot id returned by script'

 

Again, the script only needs to be run once to make sure we have the appropriate bot ID for our Slack team. Now we’re ready to code up our Python application that’ll run our callbot.

Coding Our CallBot

We have all the appropriate environment variables set for our Python code to appropriately use the Twilio and Slack APIs. Create a new file named callbot.py and add the following imports.

 

import os
import phonenumbers
import time
import uuid
from slackclient import SlackClient
from twilio.rest import TwilioRestClient

 

The os and SlackClient imports should look familiar because we used them earlier in the get_bot_id.py script.

With our dependencies imported we can use them to grab those environment variable values and instantiate the Slack and Twilio clients.

 

# environment variables
BOT_ID = os.environ.get("BOT_ID")
TWILIO_NUMBER = os.environ.get("TWILIO_NUMBER")

# constants
AT_BOT = "<@" + BOT_ID + ">:"
CALL_COMMAND = "call"
TWIMLET = "https://twimlets.com/echo?Twiml=%3CResponse%3E%0A%20%20%3CDial%3E%3CConference%3E{{name}}%3C%2FConference%3E%3C%2FDial%3E%0A%3C%2FResponse%3E&"

# instantiate Slack & Twilio clients
slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN'))
twilio_client = TwilioRestClient()

 

Our code instantiates the SlackClient with our SLACK_BOT_TOKEN from an environment variable. TwilioRestClient automatically pulls the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN from environment variables with those exact names during its declaration. Continue the Python script with the following lines of code that will handle starting the bot.

 

if __name__ == "__main__":
    READ_WEBSOCKET_DELAY = 1 # 1 second delay between reading from firehose
    if slack_client.rtm_connect():
        print("CallBot connected and running!")
        while True:
            command, channel = parse_slack_output(slack_client.rtm_read())
            if command and channel:
                handle_command(command, channel)
            time.sleep(READ_WEBSOCKET_DELAY)
    else:
        print("Connection failed. Invalid Slack token or bot ID?")

 

SlackClient connects to the Slack Real Time Messaging API WebSocket connection then continuously loops and parses messages from the Messaging firehose. If any of those messages are directed at our bot, a function named handle_command will determine what to do with the command.

Above the Python code we just wrote, add two new functions to parse Slack output and handle commands.

 

def handle_command(command, channel):
    """
        Receives commands directed at the bot and determines if they
        are valid commands. If so, then acts on the commands. If not,
        returns back what it needs for clarification.
    """
    response = "Not sure what you mean. Use the *" + \ 
               CALL_COMMAND + "* command with numbers, delimited by spaces."
    if command.startswith(CALL_COMMAND):
        response = "calling stub..."
    slack_client.api_call("chat.postMessage", channel=channel,
                          text=response, as_user=True)


def parse_slack_output(slack_rtm_output):
    """
        The Slack Real Time Messaging API is a firehose of data, so
        this parsing function returns None unless a message is
        directed at the Bot, based on its ID.
    """
    output_list = slack_rtm_output
    if output_list and len(output_list) > 0:
        for output in output_list:
            if output and 'text' in output and AT_BOT in output['text']:
                # return text after the @ mention, whitespace removed
                return output['text'].split(AT_BOT)[1].strip(), 
                       output['channel']
    return None, None

 

The parse_slack_output function takes messages from Slack and determines if they are directed at our Slack CallBot. If a message starts with a direct message to our bot ID, then we know our bot needs to handle a command. handle_command function is currently a stub function that either passes back a generic help message or has a stub within a condition if the command starts with “call”.

With most of our code in place, let’s test our CallBot by using the python callbot.py command.

callbot-running.png

Go into the Slack channel with CallBot and enter “@callbot: call 14045551234 14155550909” (or replace these two numbers with your own test phone numbers). CallBot will answer back but not really dial numbers.

Our CallBot can respond to commands but it doesn’t place calls yet. We can fix that by adding two new functions named call_command and validate_phone_numbers. handle_command can then invoke call_command instead of just serving as a stub. Change your code to match the entire callbot.py application below. Code changes from our previous version are highlighted.

 


import os
import phonenumbers
import time
import uuid
from slackclient import SlackClient
from twilio.rest import TwilioRestClient


# environment variables
BOT_ID = os.environ.get("BOT_ID")
TWILIO_NUMBER = os.environ.get("TWILIO_NUMBER")

# constants
AT_BOT = "<@" + BOT_ID + ">:"
CALL_COMMAND = "call"
TWIMLET = "https://twimlets.com/echo?Twiml=%3CResponse%3E%0A%20%20%3CDial%3E%3CConference%3E{{name}}%3C%2FConference%3E%3C%2FDial%3E%0A%3C%2FResponse%3E&"

# instantiate Slack & Twilio clients
slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN'))
twilio_client = TwilioRestClient()


def handle_command(command, channel):
    """
        Receives commands directed at the bot and determines if they
        are valid commands. If so, then acts on the commands. If not,
        returns back what it needs for clarification.
    """
    response = "Not sure what you mean. Use the *" + \
               CALL_COMMAND + "* command with numbers, delimited by spaces."
    if command.startswith(CALL_COMMAND):
        response = call_command(command[len(CALL_COMMAND):].strip())
    slack_client.api_call("chat.postMessage", channel=channel,
                          text=response, as_user=True)


def call_command(phone_numbers_list_as_string):
    """
        Validates a string of phone numbers, delimited by spaces, then
        dials everyone into a single call if they are all valid.
    """
    # generate random ID for this conference call
    conference_name = str(uuid.uuid4())
    # split phone numbers by spaces
    phone_numbers = phone_numbers_list_as_string.split(" ")
    # make sure at least 2 phone numbers are specified
    if len(phone_numbers) > 1:
        # check that phone numbers are in a valid format
        are_numbers_valid, response = validate_phone_numbers(phone_numbers)
        if are_numbers_valid:
            # all phone numbers are valid, so dial them together
            for phone_number in phone_numbers:
                twilio_client.calls.create(to=phone_number,
                                           from_=TWILIO_NUMBER,
                                           url=TWIMLET.replace('{{name}}',
                                           conference_name))
            response = "calling: " + phone_numbers_list_as_string
    else:
        response = "the *call* command requires at least 2 phone numbers"
    return response


def validate_phone_numbers(phone_numbers):
    """
        Uses the python-phonenumbers library to make sure each phone number
        is in a valid format.
    """
    invalid_response = " is not a valid phone number format. Please " + \
                       "correct the number and retry. No calls have yet " + \   
                       "been dialed."
    for phone_number in phone_numbers:
        try:
            validate_phone_number = phonenumbers.parse(phone_number)
            if not phonenumbers.is_valid_number(validate_phone_number):
                return False, phone_number   invalid_response
        except:
            return False, phone_number   invalid_response
    return True, None


def parse_slack_output(slack_rtm_output):
    """
        The Slack Real Time Messaging API is a firehose of data, so
        this parsing function returns None unless a message is
        directed at the Bot, based on its ID.
    """
    output_list = slack_rtm_output
    if output_list and len(output_list) > 0:
        for output in output_list:
            if output and 'text' in output and AT_BOT in output['text']:
                # return text after the @ mention, whitespace removed
                return output['text'].split(AT_BOT)[1].strip(), 
                       output['channel']
    return None, None


if __name__ == "__main__":
    READ_WEBSOCKET_DELAY = 1 # 1 second delay between reading from firehose
    if slack_client.rtm_connect():
        print("CallBot connected and running!")
        while True:
            command, channel = parse_slack_output(slack_client.rtm_read())
            if command and channel:
                handle_command(command, channel)
            time.sleep(READ_WEBSOCKET_DELAY)
    else:
        print("Connection failed. Invalid Slack token or bot ID?")

 

The above two new functions, call_command and validate_phone_numbers, do the bulk of the work for CallBot. validate_phone_numbers uses the phonenumbers Python library to ensure each phone number is parsable and conforms to at least one phone number type from around the world. call_command ensures that at least two phone numbers are specified and calls validate_phone_numbers for some additional checks. If every phone number is valid then call_command invokes the Twilio Voice API to place each outbound phone call.

Time to run our bot now that all of our code in place. On the command line, execute python callbot.py.

callbot-running.png

In Slack, start giving CallBot commands. You can start testing it with invalid phone numbers. If we specify an invalid phone number format for one or more of the numbers, we’ll get back a helpful error message.

not-valid-format.png

Now try with two legitimate phone numbers.

calling-success.png

Wait a second for the incoming call…
success.png

Now we’re on a conference call with one or more people we’ve dialed through Slack. Time to hash out those questions over the phone so we can get back to coding.

Wrapping Up

Woohoo, our new callbot is all done! Actually, there so much more that can be done with the Slack and Twilio APIs. Here are several more ideas to try out now that you’ve got the basics down:

  1. Implement a persistent backend like PostgreSQL and use it to store a phone number for each username
  2. Add SMS capability to the bot
  3. Use Twilio Lookup to determine if a number is truly valid instead of just parseable
  4. Boost the parser with better parsing and natural language processing so it’s more natural for Slack users to interact with

Questions? Drop a comment below or contact me on these channels:

Twitter: @mattmakai
GitHub: mattmakai
Email: makai@twilio.com
Twitch (Python & Swift live coding):  Team Twilio