How I keep my mom updated on my travel schedule with Python, Twilio, and Google Calendar

June 12, 2018
Written by

Screen Shot 2018-06-12 at 11.34.16 AM

I travel a lot for both work and pleasure. My mom loves to know where I’m jetsetting off to and I was failing to keep her properly updated. I could share my location via Find my Friends, but that doesn’t solve the problem of upcoming travel events. I could create a shared document or calendar, but she isn’t always in front of a computer.

Enter the Where’s Kelley bot.

Using Twilio SMS, I hooked up a phone number to my Google calendar and created a simple text message bot that my mom can ask about my current location and upcoming travel schedule.

IMG_B23D3BDE0F77-1

This post will walk through how to build one for yourself! Check out the final code here or follow along with this tutorial to build your own Text Travel Tracker from scratch.

Google Calendar API and Service Accounts

First things first, you’ll need a calendar to track. The way I’ve set this up is to have a separate calendar that I only add my travel schedule to and I recommend you do the same. To create a new calendar head to this page. Give your new calendar a name like Travel Tracker and create the calendar.

While we’re at it, click on your new calendar and go to Integration Settings. Grab the Calendar ID and copy it; we’ll need this later. Add a couple events in your calendar: one that’s happening today and a few in the future.

To programmatically access your calendar, you’ll need to create a service account and OAuth2 credentials in the Google API Console. If you’ve been traumatized by OAuth2 development before, don’t worry; service accounts are way easier to use.

Follow along with the steps and video below. You’ll be in and out of the console in 60 seconds (much like Nic Cage in your favorite Nic Cage movie).

  1. Go to the Google APIs Console.
  2. Create a new project.
  3. Click Enable API. Search for and enable the Google Calendar API.
  4. Create credentials for a Web Server to access Application Data.
  5. Name the service account and grant it a Project Role of Editor.
  6. Download the JSON file.
  7. Copy the JSON file to your code directory and rename it to client_secret.json

https://www.twilio.com/blog/wp-content/uploads/2018/06/calendar-service-account-setup.mp4

There is one last required step to authorize your app, and it’s easy to miss!

Find the client_email inside client_secret.json. Back in your calendar settings, click “Share with specific people” and add that client_email by clicking ADD PEOPLE.

Excellent, the boring part is done and now we can write some code!

Let’s write some code

To code along with this post, you’ll need to:

In an empty directory for your project, create a new file called requirements.txt and add the following dependencies:

flask==1.0.2
google-api-python-client==1.7.3
oauth2client==4.1.2
twilio==6.14.4

The Google API python client provides a nice interface for talking to the Calendar API while the oauth2client helps us connect our Service Account to our code. We’ll be using Flask and Twilio to manage our text bot.

Set up your virtualenv and install the dependencies by running:

virtualenv env
source env/bin/activate
pip install -r requirements.txt

Create a file called app_config.py and add the following code:

import os

CALENDAR_ID = os.environ["CALENDAR_ID"]
TRAVELER = "<Your name here>"

TWILIO_AUTH_TOKEN = os.environ["TWILIO_AUTH_TOKEN"]
TWILIO_ACCOUNT_SID = os.environ["TWILIO_ACCOUNT_SID"]

This expects that your Calendar ID and Twilio credentials are set as environment variables. We grabbed our Calendar ID earlier and you can find your Twilio credentials in the Console under Project Info. For more information on how to set environment variables, check out this handy post.

Now that you have the dependencies taken care of, create a file called tracker.py in the same directory as app_config.py. This will be the code for the Flask app that will receive the messages.

Here’s the code you’ll need to parse your calendar and respond to SMS messages. Copy and paste it into tracker.py and save the file:

from oauth2client.service_account import ServiceAccountCredentials
from apiclient import discovery

from flask import Flask, Response, request
from datetime import datetime, timedelta

from twilio.rest import Client
from twilio.twiml.messaging_response import MessagingResponse


app = Flask(__name__)
app.config.from_object('app_config')

client = Client(app.config.get("TWILIO_ACCOUNT_SID"), app.config.get("TWILIO_AUTH_TOKEN"))


def _build_service():
    scope = 'https://www.googleapis.com/auth/calendar.readonly'
    credentials = ServiceAccountCredentials.from_json_keyfile_name('client_secret.json', scope)
    service = discovery.build('calendar', 'v3', credentials=credentials)
    return service


def where_you_at(service, calendar_id):
    """Find out if I'm traveling right now, respond with current location or the next trip if not currently traveling."""
    now = datetime.utcnow()
    today = now.isoformat()   'Z'
    tomorrow = (now   timedelta(days=1)).isoformat()   'Z'

    # let google do the datetime math
    currentEvent = service.events().list(
        calendarId=calendar_id,
        timeMin=today,
        timeMax=tomorrow,
        maxResults=1,
        singleEvents=True,
        orderBy='startTime'
    ).execute()

    currentlyTravelingTo = currentEvent.get('items', [])

    if not currentlyTravelingTo:
        futureEvents = service.events().list(
            calendarId=calendar_id,
            timeMin=today,
            maxResults=1,
            singleEvents=True,
            orderBy='startTime'
        ).execute()

        event = futureEvents.get('items', [])[0]
        start = event['start'].get('dateTime', event['start'].get('date'))
        msg = "Doesn't look like {} is currently traveling. The next trip is to {} on {}".format(
            app.config.get("TRAVELER"),
            event['summary'],
            start)
    else:
        event = currentlyTravelingTo[0]
        end = event['end'].get('dateTime', event['end'].get('date'))
        msg = "{} is currently in {} until {}".format(
            app.config.get("TRAVELER"),
            event['summary'],
            end)

    resp = MessagingResponse()
    resp.message(msg)
    return str(resp)


def _event_info(event):
    start = event['start'].get('date')
    end = event['end'].get('date')
    summary = event['summary']

    return "{} from {} to {}".format(summary, start, end)


def travel_schedule(service, calendar_id):
    """Look up the next 5 events in our calendar and format them as a message."""
    now = datetime.utcnow()
    today = now.isoformat()   'Z'
    tomorrow = (now   timedelta(days=1)).isoformat()   'Z'

    # let google do the datetime math
    event_list = service.events().list(
        calendarId=calendar_id,
        timeMin=today,
        maxResults=5,
        singleEvents=True,
        orderBy='startTime'
    ).execute()

    event_response = ["My next five planned events are:n"]

    for event in event_list.get("items", []):
        event_response.append(_event_info(event))

    msg = "n".join(event_response)
    resp = MessagingResponse()
    resp.message(msg)
    return str(resp)


def help_response():
    """The default response if the user texts in an unknown command."""
    resp = MessagingResponse()
    resp.message("Ask me 'Where is {}?' to see my current whereabouts or 'Travel schedule' to see what's coming up.".format(app.config.get("TRAVELER")))
    return str(resp)


@app.route("/sms", methods=["GET", "POST"])
def main():
    """Respond to an incoming SMS message based on the body of the message."""
    incoming_message = request.values.get("Body")

    service = _build_service()
    calendar_id = app.config.get("CALENDAR_ID")

    normalized_message = incoming_message.lower()

    if "where" in normalized_message:
        return where_you_at(service, calendar_id)
    elif "schedule" in normalized_message:
        return travel_schedule(service, calendar_id)
    else:
        return help_response()


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

This code creates a Flask server with a single endpoint for responding to an incoming SMS message. We take a look at the body of the message and respond to the user in one of 3 ways:

  1. Current location
  2. Travel schedule
  3. Help / default response

If the message is asking for either our current location or travel schedule, we use the Google Calendar API to look up current and future events and format those as a friendly message back to the user. Otherwise we respond with a help response that tells the user what they can ask.

Start up the application by running python tracker.py from your terminal.

Connecting Your Calendar to Twilio SMS

We’ll need to make our application accessible on a public port so that Twilio can connect to what’s running on your local computer. We’ll use a tool called ngrok for that.

Fire up ngrok from a new terminal window or tab and point it to your Flask application, which will be running on Port 5000

ngrok http 5000

Copy your ngrok url, http://e978addb.ngrok.io in this example, and head over to the configuration page for the Twilio phone number you purchased. Paste your ngrok url plus /sms in your Twilio phone number configuration as a webhook for when A MESSAGE COMES IN.


Grab your phone and send a text to your Twilio number and et voila:
Screen Shot 2018-06-12 at 11.27.53 AM

Go Forth and Travel

I’ve added some special flavor to my own script, and I encourage you to do the same. You could restrict responses to a list of known numbers or support more incoming questions. I wasn’t sure how much use this bot would get, but it’s been a huge hit. My mom doesn’t feel guilty bothering me about my schedule and my friends have even used it to see when I’m in town.

Now that you are familiar with using Service Accounts, check out how to read data from a Google Sheet with Python. If you have any thoughts, questions, or travel suggestions leave me a note on Twitter @kelleyrobinson.