Building an App to Make Browser-based Calls to Congress with Flask and Twilio.js on Heroku

Image for post
Image for post
Your leaders should be accessible to the public

In 2015, I wanted to build an app to provide a way for administrator of public networks (school, libraries, etc.) to provide a look-up and dial tool for members of congress and have it deployable on any target (comparatively low-power machines, or on a personal laptop, or wherever phone access or this information is inaccessible for whatever reason), as well as as a platform application, which we built using these concepts.

Twilio seemed like a natural solution for this. I recently re-architectured the application, mostly to bring it into compliance with the latest Twilio JavaScript tool, and to refresh some of the clunkier parts of the original application. I elected to use Flask for this, and ultimately deployed it to Heroku.

To see the live product, you can visit: https://dial.public.engineering

More information about the project can be found on our twitter, at-publiceng.

If you’re ready to check out how we went about building this tool…

Setup

This application has a few external dependencies:

Your application will make use of environmental variables to set this, so when you deploy your application (in our case on Heroku), whatever facility (a PaaS like Heroku, or via a provisioning tool like Terraform, or on a flat Linux system) may exist for this should be used to set the following variables:

export twilio_sid=${twilio_sid}
export twilio_token=${twilio_token}
export twilio_twiml_sid=${twiml_sid}
export numbers_outbound="+12345678900"
export GOOGLE_API_KEY=${google_civic_api_key}

In your project root, you’ll need a requirements.txt :

Flask==1.1.2
gunicorn==20.0.4 # Only if you plan to deploy to Heroku
requests==2.24.0
twilio==6.47.0
jsonify==0.5

In your app.py , import the following, and we’ll make use of the above variables, before proceeding:

from flask import Flask, render_template, request, jsonifyimport osimport requestsfrom twilio.rest import Clientfrom twilio.jwt.client import ClientCapabilityTokenfrom twilio.twiml.voice_response import VoiceResponse, Dialimport urllibimport base64import random, stringTWILIO_SID = os.environ['twilio_sid']TWILIO_TOKEN = os.environ['twilio_token']TWILIO_TWIML_SID = os.environ['twilio_twiml_sid']NUMBERS_OUTBOUND = os.environ['numbers_outbound']GOOGLE_API_KEY = os.environ['GOOGLE_API_KEY']app = Flask(__name__)

Building the application: Functions

The app relies heavily on the passing and receiving of dictionaries as a messaging format, so most functions will send or receive one such dictionary, and these will eventually be used to populate the templates for the web UI itself.

Image for post
Image for post

First, a function to take a zip code, and retrieve representative contact info, and build a response containing formatting numbers, and other data I might use from that datasource. Then, I proceed to get some aesthetic data for the UI, like the name of the locality this area covers (for the House of Representatives, for example):

Image for post
Image for post

From there, we go into the actual work of using this data, and making some calls. A small function to generate, and then set a default_client which will be important for the callback from your TwiML application, which is a requirement to be able to make the outgoing calls:

def randomword(length):   letters = string.ascii_lowercase   return ''.join(random.choice(letters) for i in range(length))default_client = "call-your-representatives-%s" % (randomword(8))

then a function to validate a phone number to ensure it comes from this datasource:

def numberVerify(zipCode, unformatted_number):    reps = get_reps(zipCode)    nums_found = []    for r in reps:        if unformatted_number in r['unformatted_phone']:            nums_found.append(r['name'])            photoUrl = r['photo']   if len(nums_found) != 0:       return { 'status': 'OK', 'zipCode': zipCode, 'name': nums_found[0], 'photo': photoUrl }   else:       return { 'status': 'FAILED' }

The Flask Application and URL Routes

With the helper functions completed, you’ll see how they are consumed in the decorated functions for Flask that run when a route is hit using a designated HTTP method, for example, for / :

Image for post
Image for post

the following template is returned:

Image for post
Image for post

So, once you submit your Zip code, it is POST ‘d to the /reps URI:

Image for post
Image for post

which, you’ll see, consumes the helper functions we wrote above: from the form in the template above, it retrieves your zip code, hands it to location_name to get your locality name, to representatives to build a dict of your representatives and their info, and we use the default_client we specified above which the Twilio.js tool (which I’ll demonstrate in a moment) will connect to in order to make the call from your browser. We use all of that data in the template, to populate a page like:

Image for post
Image for post

You’ll see at the top, your default_client will have a status indicator, and when it is ready, you can click Start Call on whichever representative to initiate a phone call from the browser.

In the template file, in this case call.html , anywhere in the <head> section, you’ll use the Twilio JS script:

<script src="https://media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js"></script>

and then use the following function inside of another script block to call your token endpoint:

function httpGet(Url){var xmlHttp = new XMLHttpRequest();xmlHttp.open( "GET", Url, false ); // false for synchronous requestxmlHttp.send( null );return xmlHttp.responseText;}

which looks like this, back in app.py :

Image for post
Image for post

This uses your Twilio token and SID to create a capability token, and then you can add capabilities using the TwiML SID, and for example, allow incoming callbacks using your default client to allow Twilio to connect a call from your browser back to the application.

So when you start the call, in the template, by clicking the button:

Image for post
Image for post

The onclick action will connect your Twilio.Device to the phone number from that iteration of the representatives dictionary.

This will hand off the new token, the client ID, and the number you wish to call to the above Twilio device, which once received, will use the TwiML application’s callback URL, in this case, /voice to connect the browser to the call. The /voice function is somewhat involved and was probably one of the more complicated pieces to figure out, as some of this diverged pretty distinctly from the documentation as compiled:

Image for post
Image for post

The purpose of TwiML apps is to provide a response to a call to Twilio APIs/phone number, and in this case, we’re providing a VoiceResponse() , so we need from the request it received the phone number to send that voice response to, which we’re splitting out of the request form as number:<whatever> , and in the absence of a number, the default_client. NUMBERS_OUTBOUND is your Twilio programmable voice number you acquired at the beginning, which will appear on the caller ID, and the Dial class will facilitate the rest.

Deploying to Heroku

I have a repository (I will link to all of this again at the end) for deploying to DigitalOcean and to Heroku (where the app lives now), to show a couple of different methods of how I’ve handled deploying this app over time, however, this will focus on the application layout, and a baseline approach to deploying to Heroku with Terraform.

In your project root, you’ll need a Procfile which will inform Heroku how to run the application, in this case:

web: gunicorn app:app

This is one of the packages you might remember from your requirements.txt , and since Heroku prefers the Pipenv format for managing the application as a virtualenv, we can use it to generate the appropriate package manifest:

python3 -m pipenv install -r requirements.txt

and commit the resulting Pipenv file instead along with the Procfile.

With the Heroku requirements committed to your Git repo, you can proceed to create, in another directory, your Terraform project.

You’ll create the following vars.tf file:

variable "release_archive" {} #The Download URL of your git repovariable "heroku_app_name" {}variable "release" {    default = "HEAD"}variable "twilio_sid" {}variable "twilio_token" {}variable "twilio_twiml_sid" {}variable "numbers_outbound" {}variable "google_api_key" {}

then, in main.tf we can start laying out the deployment:

provider "heroku" {    version = "~> 2.0"}resource "heroku_app" "dialer" {    name   = "${var.heroku_app_name}"    region = "us"}

Then we’ll specify what Heroku should be building:

resource "heroku_build" "dialer_build" {app        = "${heroku_app.dialer.name}"buildpacks = ["https://github.com/heroku/heroku-buildpack-python.git"]source = {    url     = var.release_archive    version = var.release}}

I am using the release variable to be something you can update in order to have Terraform redeploy the application, rather than anything to do with what version it deploys from; you’ll want to specify a tag or a branch in your release_archive URL which will be something like:

release_archive      = "https://${git_server}/${org}/call-your-representatives_heroku/archive/${branch_or_tag}.tar.gz"

this process allows you to re-apply the same version, but still have the state update in Terraform as a detectable change. The buildpack line just refers to the Heroku environment to use, in our case, their default Python stack:

buildpacks = ["https://github.com/heroku/heroku-buildpack-python.git"]

Now, our application which has a lot of environment variables, and because they’re credentials, we want them handled properly, we are going to specify the following blocks for our above Heroku application:

resource "heroku_config" "common" {    vars = {        LOG_LEVEL = "info"    }    sensitive_vars = {        twilio_sid       = var.twilio_sid        twilio_token     = var.twilio_token        twilio_twiml_sid = var.twilio_twiml_sid        numbers_outbound = var.numbers_outbound        release          = var.release        GOOGLE_API_KEY   = var.google_api_key    }}resource "heroku_app_config_association" "dialer_config" {    app_id = "${heroku_app.dialer.id}"    vars           = "${heroku_config.common.vars}"    sensitive_vars = "${heroku_config.common.sensitive_vars}"}

You’ll specify all of these values in your Terraform variables, or in your terraform.tfvars file:

release              = "20201108-706aa6be-e5de"release_archive      = "https://git.cool.info/tools/call-your-representatives/archive/master.tar.gz"heroku_app_name      = "dialer"twilio_sid           = ""twilio_token         = ""twilio_twiml_sid     = ""numbers_outbound     = "+"google_api_key       = ""

There are other optional items (a Heroku formation, domain name stuff, and output), but this covers the deployment aspect from the above application layout, so you can proceed to set your Heroku API key:

HEROKU_API_KEY=${your_key}
HEROKU_EMAIL=${your_email}

in order to initialize the Heroku Terraform provider:

terraform init

then you can check your deployment before you fire it off:

terraform plan
terraform apply -auto-approve

and then head to http://${heroku_app_name}.herokuapp.com to see the deployed state.

More Resources

Follow public.engineering on Twitter

Call Your Respentatives app source

Call Your Representatives deployment scripts

Single-use VPN Deployer app source

Single-use VPN Deployer deployment scripts (also includes DigitalOcean and Terraform deployment plans)

If you’d like to support the platform in keeping up with fees for the price of the calls, and that of hosting, or would just like to enable ongoing development for these types of projects, and to keep them free for the public’s use, please consider donating!

Written by

Systems Engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store