DEV Community

Joseph D. Marhee
Joseph D. Marhee

Posted on • Originally published at Medium on

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

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

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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__ )
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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' }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;

}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" {}
Enter fullscreen mode Exit fullscreen mode

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"

}
Enter fullscreen mode Exit fullscreen mode

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

}

}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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}"**

}
Enter fullscreen mode Exit fullscreen mode

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 = ""
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

in order to initialize the Heroku Terraform provider:

terraform init
Enter fullscreen mode Exit fullscreen mode

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

terraform plan
terraform apply -auto-approve
Enter fullscreen mode Exit fullscreen mode

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 !

Discussion (0)