Participate in Local Issues with a SMS Chatbot

Lizzie Siegle - Jul 23 '20 - - Dev Community

If you have opinions about how your city is run, it’s more important than ever to make your voice heard. This blog post will go over how to build a SMS chatbot using Twilio Autopilot, Twilio SendGrid, Twilio Functions, TwiML Bins, and JavaScript to engage in local civic issues.

One example might be San Francisco, where the city is planning to permanently cut 40 of 68 bus lines and free bus rides for youth.

In this blog post, concerned citizens can tell members of the San Francisco County Transportation Authority to not cut bus lines. Read on for a step-by-step process to build the bot, and you can text the +14153068517 number to see the example!
sms example 1

Set Up your SMS Chatbot

To follow along with this post, you need three things:

Go to your Autopilot console and under Select a Bot click Start from Scratch.
build a bot autopilot button

Give your bot a title like "MuniCall" and click Create bot.
unique name button

Next to the Greeting task, click Program and replace the JSON there with a Say and Listen Action announcing what services the bot can provide.

{
        "actions": [
                {
                        "say": "Hi! I'm a chatbot that will automatically mass email or call ALL 11 San Francisco County Transportation Authority members for you. \n\n Send something like `email`, `call`, or `resources` to get started."
                },
                {
                        "listen": true
                }
        ]
}
Enter fullscreen mode Exit fullscreen mode

Click save.

Using Twilio Functions and TwiML Bins for Calls to Decision Makers

Click All Tasks and add a task called Call.

On the same line as that Call task, click Train to add samples that will trigger this task. Samples are different words or phrases the user might say to call their representatives, running this task. For the Call task you might add samples like "phone", "voicemail", "call representatives", "call reps", "dial", "leave a voicemail", and other similar phrases.

After adding your samples, click Switch to program task near the top-right.
samples in UI

Replace the JSON in the JSON bin with the following which uses a Collect Action to ask a series of questions and group them and a user's answers together.

{
        "actions": [
                {
                        "collect": {
                                "name": "call_collect",
                                "questions": [
                                        {
                                                "question": "Let us call all 11 members of the San Francisco County Transportation Authority or leave a voicemail! What is your first name?",
                                                "name": "name"
                                        },
                                        {
                                                "question": "What is your SF zip code? If you need one, use 94109.",
                                                "name": "zip_code",
                                                "type": "Twilio.NUMBER"
                                        }
                                ],
                                "on_complete": {
                                        "redirect": {
                                                "method": "POST",
                                                "uri": "REPLACE-WITH-YOUR-FUNCTION-IN-THE-NEXT-PART.twil.io/municall"
                                        }
                                }
                        }
                }
        ]
}
Enter fullscreen mode Exit fullscreen mode

After asking the user all the questions in the questions array, this task redirects to a Twilio Function to make the phone calls using JavaScript. Create a new Function here by clicking the red plus button. Make a blank function and give it a name like "Muni Call" and add on to the path as shown below. Don't forget to replace the Function path in your Call task above!
call Function name, path

Replace the code in the Function with the following JavaScript:

exports.handler = function(context, event, callback) {
    //Memory from the answered question
    const memory = JSON.parse(event.Memory);
    //get answers from Memory
    let name = memory.twilio.collected_data.call_collect.answers.name.answer; 
    let zipCode = memory.twilio.collected_data.call_collect.answers.zip_code.answer;

    const bodyCall = `Hello {{Supe}}. My name is ${name} and my zip code is ${zipCode}. Muni is a critical service to SF. Keep SF healthy, affordable, liveable, and accessible for all, including working families. The populations who ride Muni the most are the people our city is ALREADY failing to serve.`;

    const numbers = {
        'Aaron': '+14155547450',
        'Rafael': '+14155546968', 
        'Dean': '+14155547630',
        'Sandra': '+14155547410', 
        'Matt':'+14155547970', 
        'Gordon': '+14155547460', 
        'Hilary':'+14155545144',
        'Ahsha':'+14155546975',
        'Catherine': '+14155547752', 
        'Shamann': '+14155547670', 
        'Norman':'+14155546516'
    };
    const client = context.getTwilioClient();
    var ctr = 0;
    Object.keys(numbers).forEach(function(key) {
        console.log(key, numbers[key]);
        client.calls
        .create({
            machineDetection: 'Enable',
            url: `https://handler.twilio.com/twiml/EH19fc2a326d3871000810eb5495c2d861/?Supe=${key}&Name=${name}&Zip=${zipCode}`,
            to: numbers[key], 
            from: 'YOUR-TWILIO-NUMBER'
        }).then(function(call) {
            console.log(call.sid);
            ctr ++;
            console.log(ctr);
            if (ctr == Object.keys(numbers).length) {
                const resp = {
                    actions: [
                        {
                            say: "Thank you for calling all 11 reps of SF County Transit Authority. \n\n The call to them said: " + bodyCall + "\n\n You can also leave them a voicemail, sign this petition https://bit.ly/2ZNEfbv, spread the word to your friends, send more emails, and more. Want to get more involved? Fill out this form: https://forms.gle/FHqt7W62D9W2t164A"
                        }         
                    ]
                };
                callback(null, resp);
            }
        })
        .catch(err => {
            callback(err);
        });
    });
};
Enter fullscreen mode Exit fullscreen mode

To make and customize your own civic engagement bot, you would replace the bodyCall with your own message to representatives about the local issue you want to change and then replace numbers to the phone numbers of the representatives you wish to call. Don't forget to include your name and precinct in the text!

First we access the memory to get the user's answers and save them as variables name and zipCode. bodyCall is a short transcript (of what the phone call will say to each representative) to be texted back to the user and numbers is an object containing the names of who to call and their corresponding phone numbers. We loop through that object and make an outbound call to each number with answering machine detection (AMD) enabled, passing it a URL to a TwiML bin containing the following TwiML:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say voice="Polly.Joanna">Hello {{Supe}}. My name is {{Name}} and my zip code is {{Zip}}. Muni is a critical service to SF.  Keep SF healthy, affordable, liveable, and accessible for all, including working families.</Say>
</Response>
Enter fullscreen mode Exit fullscreen mode

You can read more on generating dynamic TwiML and templating with Twilio Bins here.

Once we reach the end of the object, go back in the Twilio Function to see that a text message is returned to the user confirming that each representative was called.
sms example

Contacting Decision Makers with Twilio SendGrid and Twilio Functions

To contact decision makers on a different medium, we're going to go back to your bot in your Autopilot console and add a new task called Email. On the same line as Email, click Train to add some samples that will trigger this task such as "email", "email reps", "email representatives", "email SFMTA", "send email", and similar phrases.

Then click Switch to program task near the top-right and add the following JSON that uses a similar Collect flow to the Call task:

{
        "actions": [
                {
                        "collect": {
                                "name": "email_collect",
                                "questions": [
                                        {
                                                "question": "Let us email all 11 members of the San Francisco County Transportation Authority! What is your name?",
                                                "name": "name"
                                        },
                                        {
                                                "question": "What SF neighborhood do you live (or work)? If you need one, maybe try FiDi or Japantown.",
                                                "name": "live_work"
                                        },
                                        {
                                                "question": "What is your SF zip code? If you need one, use 94109.",
                                                "name": "zip_code"
                                        },
                                        {
                                                "question": "What are your demands? If you send `default` we will send one for you (ie. move funds from the police dept. to Muni.)",
                                                "name": "demands"
                                        },
                                        {
                                                "question": "Why is Muni important to you? How does it affect your day-to-day life? If you send `default` we will answer this for you (ie. `Muni is important to me and other San Franciscans because I take it to get to work, volunteer, see friends.`)",
                                                "name": "important_to_you"
                                        }
                                ],
                                "on_complete": {
                                        "redirect": {
                                                "method": "POST",
                                                "uri": "https://YOUR-TWILIO-FUNCTION.twil.io/muni_email"
                                        }
                                }
                        }
                }
        ]
}
Enter fullscreen mode Exit fullscreen mode

The last two questions in the Collect flow above are more open-ended: the user can type out a longer response or they can send "default" for the chatbot to instead use a default response.

Grab your SendGrid API key. In the Twilio Functions Configuration section, save it as the environment variable SENDGRID_API_KEY like so:
env variable

Now it can be referenced with context.SENDGRID_API_KEY in any of your Twilio Functions.

Make another new Twilio Function that the email task will redirect to. Give it a name and path like this below, and don't forget to replace the Function path in your email task.
email function path and name

In your Function, replace the code with the following JavaScript:

const sgMail = require('@sendgrid/mail');
exports.handler = function(context, event, callback) {
    sgMail.setApiKey(context.SENDGRID_API_KEY);
    //Memory from the answered question
    let memory = JSON.parse(event.Memory);
    //get answers from Memory
    let name = memory.twilio.collected_data.email_collect.answers.name.answer; 
    let liveWork = memory.twilio.collected_data.email_collect.answers.live_work.answer;
    let zipCode = memory.twilio.collected_data.email_collect.answers.zip_code.answer;
    let demands = memory.twilio.collected_data.email_collect.answers.demands.answer.toLowerCase();
    let important_to_you = memory.twilio.collected_data.email_collect.answers.important_to_you.answer.toLowerCase();

    if (demands === 'default') {
        demands = "We demand that Muni for Youth and the 40 Muni bus lines that are meant to be cut as a result of lack of funding are reinstated. It is routes like the 8, 9, 14, 29, etc.. that serve lower-income communities that are the most PACKED and clearly need MORE lines, not fewer.";
    }

    if (important_to_you == 'default') {
        important_to_you = "Every SFUSD student benefits from Muni. A progressive state does not let its most critical services fail. Muni is about accessibility and connecting people--connecting us to family, friends, and what makes SF special.";
    }
    const messages = [
        {
            to: 'Aaron.Peskin@sfgov.org', 
            from: `${name} <we_love_and_need_muni@sf.com>`,
            subject: '🍩Muni is a critical service to SF. 🍩',
            html: `Hi Aaron! Keep SF healthy, affordable, liveable, and accessible for all, including working families. <p>40% of emissions in SF come from transportation.</p><p>My name is ${name}, I live and work in ${liveWork}, and my zip code is ${zipCode}.${demands} ${important_to_you}</p> <p>Thank you for your time.</p>`,
        //...copy and paste this for each representative. Complete code https://github.com/elizabethsiegle/muni_call_email_representatives_chatbot/blob/master/email.mjs
        },
    ];
    sgMail.send(messages)
    .then(response => {
        const resp = {
            actions: [
            {
                say: "Thank you for emailing all 11 members of the SF County Transit Authority. You can also leave them a voicemail, sign this petition https://bit.ly/2ZNEfbv, spread the word to your friends, send more emails, and more. Want to get more involved? Fill out this form: https://forms.gle/FHqt7W62D9W2t164A. \n\n If you want to contact just one representative, try SF Transport Authority Chair/District 3 supervisor Aaron Peskin at (415) 554-7450 and Aaron.Peskin@sfgov.org or District 9 supervisor Hillary Ronen (also on the Metropolitan Transportation Commission) at (415) 554-5144 and RonenStaff@sfgov.org. You can also send more calls and emails by chatting with this bot."
            },
            {
                listen: true
            }
                ]
        };
        callback(null, resp);
    })
    .catch(err => {
      callback(err);
    });
};
Enter fullscreen mode Exit fullscreen mode

This code imports SendGrid at the top, gets the answers to each question in the email task, and checks if the user sent "default" for the last two questions. If so, we send ready-made blurbs on why Muni is important and create an array called messages to send bulk emails with SendGrid. When each email is sent, the chatbot sends a confirmation message back to the user. Check out this blog post for more information on sending bulk emails with SendGrid in Node.js.

email sms example
You can test your chatbot in the Twilio console's Autopilot simulator, but now let's also hook up the bot to a Twilio phone number so your friends can text it!

Configure your Bot with a Twilio Number

Go to the Autopilot console and select Channels from the left-hand menu. Click Programmable Messaging.
channels
Copy that Messaging URL and in a new tab configure your Twilio number in your phone number console.
prog sms console
If you don't have a Twilio number yet, go to the Phone Numbers section of your Twilio Console and search for a phone number in your country and region, making sure the SMS checkbox is ticked.
buy a number
In the Messaging section of your purchased number, in the A Message Comes In section, set the Webhook to be your Messaging URL and hit Save.
configure phone number in console
Now you can get out your phone, text the number, and share the number with people to contact San Francisco County Transportation Authority representatives, or whoever you would like to reach! The complete code for this blog post and chatbot can be found here on GitHub.

What's Next for Civic-Minded Chatbots?

Twilio makes it easy to programmatically call, text, email, and generally communicate. You can make your chatbot more complex by adding other tasks to provide information such as:

Here's to hoping we can help keep mass transportation funded and better serve those who rely on it. Thanks for reading and let us know online or in the comments what you're building to make the world a better place!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .