How to build a SMS-to-Slack Bridge with Python and Twilio

April 21, 2020
Written by
Dotun Jolaoso
Contributor
Opinions expressed by Twilio contributors are their own

How to Build a SMS-to-Slack Bridge with Python and Twilio

In this tutorial, we’ll be looking at how to build a SMS-to-Slack bridge using Python and Twilio. The bridge will work in such a way that every time your Twilio phone number receives an SMS message, we’ll forward the content of the SMS message to a channel in Slack. Furthermore, any threaded replies in Slack to the message that was posted will automatically be sent as an SMS message to the originating number.

Technical requirements

To follow along, you’ll need the following:

  • A free Twilio Account. If you use this link to register, you will receive $10 credit when you upgrade to a paid account.
  • A free Slack account, and a Slack workspace you have administrator access to.
  • Python 3
  • Ngrok. This will make the development version of our application accessible over the Internet.

Creating a Python  environment

Let’s create a directory where our project will reside. From the terminal, run the following command:

$ mkdir twilio_slack

Next, cd into the project directory and run the following command to create a virtual environment.

$ python -m venv venv

To activate the virtual environment, run the following command:

$ source venv/bin/activate

If you are using a Windows computer, then the activation command is different:

$ venv\Scripts\activate

Next, we’ll need to install all the dependencies our project will need:

  • Flask: a Python web framework.
  • Twilio-Python: A helper library that makes it easy to interact with the Twilio API.
  • Python Slack Client: A helper library for interacting with the Slack API.
  • Python-dotenv: A library for importing environment variables from a .env file.

To install all the dependencies at once, run the following command:

$ pip install flask twilio slackclient python-dotenv

Creating a Slack webhook endpoint

As part of creating the Slack bot, we need to define an endpoint where Slack can forward messages that are posted in a channel. Before we can successfully configure that endpoint, Slack needs to verify and ensure that the endpoint is valid, so we’ll begin by implementing the verification portion of our Slack endpoint.

To pass Slack’s verification, the endpoint needs to return a response with the value of a challenge key contained in the payload Slack sends to it in the verification request. You can read more about this process here.

Create a main.py file at the root of the project’s directory and add the following code to the file:

from flask import Flask, request, Response

app = Flask(__name__)


@app.route('/incoming/slack', methods=['POST'])
def send_incoming_slack():
    attributes = request.get_json()
    if 'challenge' in attributes:
        return Response(attributes['challenge'], mimetype="text/plain")
    return Response()

if __name__ == '__main__':
    app.run()

Here, we’ve created an endpoint with the /incoming/slack URL and specified the HTTP method to be POST. The challenge key is obtained from the request object from the Flask framework and returned back as a response in plaintext, according to the verification requirements from Slack.

Run the following command in your terminal to start the Flask application:

$ python main.py

Setting up Ngrok

Since our application is currently local, there’s no way Slack will be able to make a POST request to the endpoint we just created. We can use Ngrok to set up a temporary public URL so that our app is accessible over the web.

Run the following command on a second terminal window to start ngrok:

$ ngrok http 5000

In this command, 5000 refers to the port your Flask application is currently listening on.

You should now be presented with a screen similar to the one below:

ngrok screenshot

Take note of the https:// “Forwarding” URL as we’ll be making use of it shortly.

Creating the Slack bot

To be able to post and receive messages on behalf of our application to a channel on Slack, we need to create a Slack bot. Head over to Slack apps and select “Create New App”. Next, you'll be prompted with a screen similar to the one below.

create a slack app

Assign the name “twilio-slack” to the bot, select the Slack workspace you’ll use the app on, and then click the “Create App” button.

Once the app has been created, we need to assign the right Scopes to it. Scopes define the API methods an app is allowed to call. Select “Add features and functionality” and then select the “Permissions” box:

slack app configuration

Next, scroll down to the Scopes section and assign the following scopes to the app:

  • “channels:history”
  • “channels:join”
  • “channels:read”
  • “chat:write”
  • “chat:write.customize”
  • “chat:write.public”

slack application scopes

Our Slack bot needs a way to be notified whenever a message is posted to the channel where we’ll be sending the SMS notification to. To do so, the bot needs to subscribe to an event Slack provides. Head back to the “Basic information” page, open the “Add features and functionality” dropdown and select “Event Subscriptions”. Next, toggle “Event Subscriptions” on.

slack enable events

For the “Request URL” field, add the forwarding URL from ngrok with the /incoming/slack path for the endpoint we created above added at the end. Your Request URL should look like https://aa3be367.ngrok.io/incoming/slack, but note that the first part of the ngrok domain will be different in your case.

slack event endpoint

After you’ve entered the URL, Slack will send a POST request to the endpoint to verify it, so make sure both the Flask application and ngrok are running. Once the verification is achieved, scroll down to the “Subscribe to events on behalf of users” section and add the following event :

  • “message.channels”

slack event subscriptions

This allows our application to be notified whenever a message is posted to a channel in Slack. Next click “Save Changes”.

After you’ve saved these settings, we need to install the bot to a Slack workspace so that an API token can be generated for us. Head back to the “Basic information” page and open the “Install your app to your workspace” dropdown. Click the “Install App to Workspace” and grant the necessary permissions. Once this is done, select “OAuth & Permissions” under the “Features” tab and take note of the “Bot User OAuth Access Token” that was generated for the bot. We’ll be needing it shortly.

slack token

Setting up Twilio

After you sign up for an account on Twilio, head over to your Twilio Console and click on Phone Numbers. If you already have one or more phone numbers assigned to your account, select the number you would like to use for this project. If this is a brand new account, buy a new phone number to use on this project.

Note that if you are using a trial Twilio account you will need to verify the phone number you’ll be sending SMS messages to. You can do that here.

In the phone number configuration scroll down to the “Messaging” section and under the “A message comes in” field, use the forwarding URL from ngrok with /incoming/twilio appended. At this point, it’s important to note this endpoint doesn’t yet exist in our application. We shall be creating it shortly.

twilio endpoint configuration

Ensure the request method is set to HTTP POST and then click the “Save” button at the bottom of the page to save the settings.

On your Twilio Console, take note of your Account SID and Auth Token. We are going to need these values to authenticate with the Twilio service.

twilio account sid and auth token

Coding our bot

In the Python project, create a .env file at the root of the project’s directory and edit the file with all the credentials and settings we’ve noted thus far:

SLACK_BOT_TOKEN=xxxx
TWILIO_ACCOUNT_SID=xxxx
TWILIO_AUTH_TOKEN=xxxx
TWILIO_NUMBER=xxxx

For the phone number use the canonical E.164 format. Next, in the main.py, add the following import statements at the top of the file:

import os
import slack
import re
from dotenv import load_dotenv
from flask import Flask, request, Response
from twilio.rest import Client
from twilio.twiml.messaging_response import MessagingResponse

load_dotenv()
app = Flask(__name__)

Here, we’ve imported all the major dependencies our project will be needing. Next, add the following code just below the app variable:

slack_token = os.getenv("SLACK_BOT_TOKEN")
slack_client = slack.WebClient(slack_token)
twilio_client = Client()

The slack_client instance will be used to interact with the Slack API, while the twilio_client will be used to interact with the Twilio API.

Forwarding SMS messages to Slack

Next, we’ll add the endpoint we configured in the Twilio SMS configuration. This function will send incoming SMS messages to Slack.

@app.route('/incoming/twilio', methods=['POST'])
def send_incoming_message():
    from_number = request.form['From']
    sms_message = request.form['Body']
    message = f"Text message from {from_number}: {sms_message}"
    slack_message = slack_client.chat_postMessage(
        channel='#general', text=message, icon_emoji=':robot_face:')
    response = MessagingResponse()
    return Response(response.to_xml(), mimetype="text/html")

The first thing we did was to obtain the message and phone number that sent the SMS. They both come in the payload of the POST request with a key of Body and From respectively.

A Slack message is then constructed and posted to the “#general” channel of our workspace using the slack_client instance. You can change the Slack channel according to your needs.

Twilio expects the response from this endpoint should be in TWIML or Twilio Markup Language. But in this project we really have nothing to do on the Twilio side. Thankfully, the Twilio Python helper library comes bundled with some classes that make generating an empty response easy.

Sending Slack threaded replies via SMS

We’ve handled one side of our bot’s logic. The next thing to do is to handle sending out SMS messages whenever a threaded reply is written on a Slack message that was posted by the Twilio endpoint.

Edit the send_incoming_slack() function we created earlier with the following code:

@app.route('/incoming/slack', methods=['POST'])
def send_incoming_slack():
    attributes = request.get_json()
    if 'challenge' in attributes:
        return Response(attributes['challenge'], mimetype="text/plain")
    incoming_slack_message_id, slack_message, channel = parse_message(attributes)
    if incoming_slack_message_id and slack_message:
        to_number = get_to_number(incoming_slack_message_id, channel)
        if to_number:
            messages = twilio_client.messages.create(
                to=to_number, from_=os.getenv("TWILIO_NUMBER"), body=slack_message)
        return Response()
    return Response()

Next, add the following auxiliary functions also in the main.py file:

def parse_message(attributes):
    if 'event' in attributes and 'thread_ts' in attributes['event']:
        return attributes['event']['thread_ts'], attributes['event']['text'], attributes['event']['channel']
    return None, None, None


def get_to_number(incoming_slack_message_id, channel):
    data = slack_client.conversations_history(channel=channel, latest=incoming_slack_message_id, limit=1, inclusive=1)
    if 'subtype' in data['messages'][0] and data['messages'][0]['subtype'] == 'bot_message':
        text = data['messages'][0]['text']
        phone_number = extract_phone_number(text)
        return phone_number
    return None


def extract_phone_number(text): 
    data = re.findall(r'\w+', text)
    if len(data) >= 4: 
      return data[3]
    return None

Once Slack makes a POST request to the /incoming/slack endpoint and we determine it is not a verification message, the parse_message() function checks to see if there’s a thread_ts key contained in the payload. This is important, because the presence of that key indicates that the message is a threaded reply. If the key exists, the function returns the value of the thread_ts key, the text of the threaded reply message and the id of the channel where the message was posted. If the message was not a threaded reply, we return None for the three values and with that the request does not have anything else to do and ends with an empty response.

When the message is a threaded reply, the get_to_number() function is invoked. This function sends a request to the Slack API to retrieve the text of the parent message the threaded reply belongs to. A check is carried out to ensure that a subtype key exists in the payload that was returned and has a value of bot_message. This allows us to know that the parent message was a message from our bot.

Next, the content of the parent message is passed as an argument to the extract_phone_number() function. Using Python’s regular expression module, this function takes the message, and extracts each word contained in the message to a list while ignoring punctuation marks. Based on our bot’s messaging structure, the item at index 3 is returned which will be the phone number from where the original message came from. The get_to_number() function returns this number back to the send_incoming_slack() function.

To complete the action of the endpoint, an SMS message with the content of the threaded reply is sent to the phone number of the original message using the twilio_client instance.

Testing

To test the application make sure that you are running the Flask application and ngrok. Keep in mind that ngrok creates a different URL every time it runs, so after a restart you will need to update the Twilio endpoint in the Twilio Console and the Slack endpoint in the Slack app configuration to match the new URL assigned by ngrok. This is obviously only a concern during development, as a production deployment will use a public URL without the need to use ngrok.

To test the service, send an SMS message to your Twilio phone number from your phone. Head over to the Slack workspace on which you configured the bot and you should see the message appear in the #general channel.

message received in slack

Next, add a threaded reply to the message in Slack, and the message should be forwarded back to your phone in an SMS!

reply received in phone

Conclusion

In this tutorial we’ve seen how we can build a Slack bot that receives and sends SMS messages using Python and Twilio. The GitHub repository with the complete code for this project can be found here.

Dotun Jolaoso

Website: https://dotunj.dev/
Github: https://github.com/Dotunj
Twitter: https://twitter.com/Dotunj_