Building an App to Make Browser-based Calls to Congress with Flask and Twilio.js on Heroku
Joseph D. Marhee
Posted on December 25, 2020
Building an App to Make Browser-based Calls to Congress with Flask and Twilio.js on Heroku
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:
- You will need a Twilio number capable of making outbound calls.
- You will need your Twilio API Key and SID.
- A TwiML Application (which you will need the SID for this); when you set up the TwiML app, you will have the “Voice URL” set to something like “http://${your_domain}/voice” (the URI used in the app we’ll walk through is /voice so unless you modify that, this should match)
- A Google Civic API key (for retrieving the representative’s contact information)
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, jsonify
import os
import requests
from twilio.rest import Client
from twilio.jwt.client import ClientCapabilityToken
from twilio.twiml.voice_response import VoiceResponse, Dial
import urllib
import base64
import random, string
TWILIO_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.
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):
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 / :
the following template is returned:
So, once you submit your Zip code, it is POST ‘d to the /reps URI:
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:
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
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 request
xmlHttp.send( null );
_return_ xmlHttp.responseText;
}
which looks like this, back in app.py :
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:
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:
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: , 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 repo
variable "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 !
Posted on December 25, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
December 25, 2020