Building your own personal assistant with Twilio and Google Calendar

February 23, 2015
Written by

Twilio PA

Being a developer evangelist means I get to work from anywhere. Often times I find myself on the road going to wicked events and having great conversations with developers from all around the world.

The flipside is that most of my colleagues are also remotes and are usually doing the same things around the communities they serve. It can be difficult to have meetings with them, or anyone else that is not physically located in the same city, country or continent as I am.

We usually resolve this situation by holding meetings over the phone, but one of the problems that affects me is the fact that my calendar will pop up 15 minutes before and I will snooze it until 5 minutes before the meeting.

I will also sometimes hit dismiss accidentally only to be reminded via instant messenger of the fact I was meant to be on a meeting that started about eleven minutes ago.

One morning I was thinking about this situation and the fact that there has to be a better way of solving this problem other then adding multiple reminders on my calendar or phone.

My first solution was simple: “I need to get myself a Personal Assistant!

Now let’s take a step back – Twilio is all about making my life easier, but I don’t think I have enough appointments to justify hiring my own PA.

Being a resourceful developer I thought about the next best thing: build myself my own software PA. What if I could build a script that keeps tabs on my Google Calendar and whenever it found I’m meant to have a call with someone it would connect me with that person automatically by dialing out to both of us?

That sounds like something I can knock out in an afternoon” – I said!

To build our very own PA, we will need the following:

If you don’t want to go through the entire build, feel free to checkout the code repository here.

Finding your Google Calendar Credentials

Google Calendar credentials can be tricky to get if you’ve not handled them before. In the steps bellow I will walk you through the process getting those credentials.

Start by heading to Google Developer Console and click Create Project.

twilio-pa_0.png

The subsequent screen will ask you to give this project a name. We will call it Twilio PA and leave the Project ID with its default value.

After the project is created click on APIs & Auth on the left-hand side menu then click APIs. You should see a big list with all the APIs available to that project. The one we’re interested in is Google Calendar so let’s turn that on.

twilio-pa_1.png

On the left-hand side menu click on Credentials under APIs & Auth and click Create new Client ID under OAuth. Because we are creating a Web Application, we will choose this option.

Click on Configure consent screen and you will be redirected to a page that requires your email address and a Product Name. Fill that in with Twilio PA as the product name and click save.

twilio-pa_8.png

You will then be presented with a modal screen that lets you fill in a couple of URLs. For the purposes of this article our project will run on localhost on port 2002. Use the following values before clicking Create Client ID:

twilio-pa_2.png

This page will set up our application and give us the credentials. If you are seeing a screen similar to the one below, you have gone through the process successfully. Take note of the CLIENT ID and CLIENT SECRET.

twilio-pa_3.png

Choosing the calendar you want to monitor

I have many different calendars on my account. Usually I have personal appointments separated from my work ones to make sure I’m giving work appointments higher priority.

Because I want to make sure I never miss any of my work calls, I will now make a note of the ID for my “work” calendar. The easiest way to find out this ID is by going to your Google Calendar and clicking on Settings. Settings is represented by a cog icon on the right-hand side. You should now be on a screen called Calendar Settings. Clicking on the Calendars icon on the top menu will take you to a list with all the calendars you own. Click on your chosen calendar and on the subsequent screen you will see a category called Calendar Address.

twilio-pa_4.png

Make a note of the Calendar ID. This is the identifier of the calendar we want to use.

Setting up a phone number

Go to Twilio numbers page and click Buy a Number if you don’t already have one available.

twilio-pa_0_1.png

Pick a number with voice and SMS capabilities and click Buy.

twilio-pa_0_2.png

Click Buy This Number and you will see a confirmation screen.

twilio-pa_0_3.png

How is this going to work?

We now have successfully collected all the pieces we need in order to build this application. Every time the application runs, it will collect all the meetings scheduled for that day and create scheduled tasks on our MongoDB instance, as the diagram below shows.

twilio-pa (2) (1).png

As you can see on the diagram, for every event two scheduled tasks are created. Those tasks consist of one SMS message to be sent to us 5 minutes prior to the meeting and another task is triggered exactly at the start of the meeting and consists of a call to our telephone number which is then chained with another call to the number of the person we are meeting with.

Setting it all up

Let’s start by putting together a small Node.js application that uses Express for routing. You can do this anywhere you like, but in my case I’ve created a directory called twilio-pa under ~/Projects/JavaScript/.

Create a file under that directory called package.json. This file will contain the definitions of this project and have all the following dependencies:

  • Express as our web application framework and route management.
  • Googleapis for authentication and interaction with the Calendar API.
  • Agenda for our scheduled tasks.
  • DateJS to help us dealing with dates and times.
  • Twilio for interaction with Twilio’s API.
  • MongoDB for database interaction

{
  "name": "twilio-pa",
  "version": "0.0.1",
  "description": "A Google calendar personal assistant powered by Twilio",
  "author": "Marcos Placona",
  "dependencies": {
    "express": "~4.11.1",
    "googleapis": "~1.1.1",
    "agenda": "~0.6.26",
    "datejs": "~1.0.0-rc3",
    "twilio": "~1.10.0",
    "mongodb": "~1.4.30"
  }
}

 

If you want to read more about what each of the attributes mean, you can find their definitions here.

We need ensure these dependencies are installed in our project folder. We do so by going into terminal and running:

$ npm install

Notice your project folder has a new directory called node_modules and under it a bunch of other directories for each of the dependencies specified on package.json.

The next thing we’ll do is create two new files called config.js and app.js on the root of our project which in our example is ~/Projects/JavaScript/twilio-pa. We now have three files in that directory, config.js and app.js which are currently empty, and package.json.

In the config.js file we will add configurations for our application. These are the Google credentials we collected earlier, our Twilio telephone number and credentials. You can grab your Twilio credentials from the dashboard.


var config = {};

// HTTP Port to run our web application
config.port = process.env.PORT || 2002;

// My own telephone number for notifications and calls
config.ownNumber = process.env.TELEPHONE_NUMBER;

// Your Twilio account SID and auth token, both found at:
// https://www.twilio.com/user/account
// A good practice is to store these string values as system environment variables, and load them from there as we are doing below. 
// Alternately, you could hard code these values here as strings.
config.twilioConfig = {
    accountSid: process.env.TWILIO_ACCOUNT_SID,
    authToken: process.env.TWILIO_AUTH_TOKEN,
    // A Twilio number you control - choose one from:
    // https://www.twilio.com/user/account/phone-numbers/incoming
    number: process.env.TWILIO_NUMBER
  }
// Google OAuth Configuration
config.googleConfig = {
  clientID: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  calendarId: process.env.GOOGLE_CALENDAR_ID,
  // same as configured at the Developer Console
  redirectURL: 'http://localhost:2002/auth'
};
// MongoDB Settings
config.mongoConfig = {
    ip: '127.0.0.1',
    port: 27017,
    name: 'twilio-pa'
  }
// Export configuration object
module.exports = config;

We have configured two variables here we have not talked about yet. One of them is called ownNumber, which is your telephone number. This is the number our application will text and dial every time before the call starts.

The other configuration is mongoConfig with information about the MongoDB instance.

Edit app.js to make sure our setup is correct and all dependencies are properly installed.

var config = require('./config');

// Dependency setup
var express = require('express'),
  google = require('googleapis'),
  date = require('datejs'),
  twilio = require('twilio');

// Initialization
var app = express();

app.get('/', function(req, res) {
  res.send('Hello World!');
});

var server = app.listen(config.port, function() {
  var host = server.address().address;
  var port = server.address().port;

  console.log('Listening at http://%s:%s', host, port);
});

Save that, and make sure your MongoDB instance is started.

On a terminal window type:

$ node app.js

This will start up your application.

On the browser, you can navigate to http://127.0.0.1:2002. If you see “hello world” on the screen you’ve done it all right. We’re just about to get serious.

twilio-pa_5.png

As previously mentioned, we will be doing some database interaction in this app to store some of the authentication information to the database. Connecting to MongoDB is straightforward, but opening and closing connections every time is not cool, so we will create a new file called connection.js right on the root of our application.

$ touch ~/Projects/JavaScript/twilio-pa/connection.js

This file will deal with our connection and also make sure we only ever open one of them no matter how many times we access the database.

var config = require('./config');
var MongoClient = require('mongodb').MongoClient;

var dbSingleton = null;

var getConnection = function getConnection(callback) {
  if (dbSingleton) {
    callback(null, dbSingleton);
  } else {
    var connURL = 'mongodb://' + config.mongoConfig.ip + ':' + config.mongoConfig.port + '/' + config.mongoConfig.name;
    MongoClient.connect(connURL, function(err, db) {

      if (err)
        console.log("Error creating new connection " + err);
      else {
        dbSingleton = db;
        console.log("created new connection");

      }
      callback(err, dbSingleton);
      return;
    });
  }
}

module.exports = getConnection;

We now have a connection manager, so let’s do a bit of refactoring in app.js around the initialization. We need to authenticate via OAuth2 in order to get access to our Google Calendar and also add a dependency to our new connection manager. We do so by changing our code as per highlighted section:


var config = require('./config');
var getConnection = require('./connection');

// Dependency setup
var express = require('express'),
  google = require('googleapis'),
  date = require('datejs'),
  twilio = require('twilio');

// Initialization
var app = express(),
  calendar = google.calendar('v3'),
  oAuthClient = new google.auth.OAuth2(config.googleConfig.clientID, config.googleConfig.clientSecret, config.googleConfig.redirectURL);

All the information under the config scope is already available to us from the moment we included config.js to our file.

Let’s also create a Calendar Event class under the code above. This class will be helpful for when we need to pass event information around.

// Event object
var CalendarEvent = function(id, description, location, startTime) {
  this.id = id;
  this.eventName = description;
  this.number = location;
  this.eventTime = Date.parse(startTime);
  this.smsTime = Date.parse(startTime).addMinutes(-5);
};

Our next task is to modify our existing route so instead of returning “hello world” we get it to do something more meaningful.

app.get('/', function(req, res) {
  getConnection(function(err, db) {
    var collection = db.collection("tokens");
    collection.findOne({}, function(err, tokens) {
      // Check for results
      if (tokens) {
        // If going through here always refresh
        tokenUtils.refreshToken(tokens.refresh_token);
        res.send('authenticated');
      } else {
        tokenUtils.requestToken(res);
      }
    });
  });
});

A few things are going on here. We start off by by checking whether there are already any tokens in the database. They won’t exist the first time we run this script after starting our Node server, so we will always fall into the not authenticated category. This will call a function called requestToken, which we haven’t implemented yet.

The next time we run this same script, the logic at the top of the function will tell us we already have tokens on the database, therefore we only need to refresh them by calling refreshToken, which we also haven’t implemented yet.

We need to refresh the token is because Google only gives us tokens that are valid for 60 minutes for security reasons. If you try to use them after that, your authentication will fail since the token is already invalid. Refreshing it tells Google we need access to that account for a bit longer, and prompts Google to issue us with a new access token valid for another 60 minutes.

Let’s implement some of these utility functions on a new file called ~/Projects/JavaScript/twilio-pa/token-utils.js.  Add the following to it.

var getConnection = require('./connection');

/* 
  Receives a token object and stores it for the first time. 
  This includes the refresh token
*/
var storeToken = function(token) {
    getConnection(function(err, db) {
      // Store our credentials in the database
      var collection = db.collection("tokens");
      var settings = {};
      settings._id = 'token';
      settings.access_token = token.access_token;
      settings.expires_at = new Date(token.expiry_date);
      settings.refresh_token = token.refresh_token;

      collection.save(settings, {
        w: 0
      });
    });
  }
/* 
  Updates an existing token taking care of only updating necessary 
  information. We want to preserve our refresh_token
 */
var updateToken = function(token, db) {
  getConnection(function(err, db) {
    var collection = db.collection("tokens");
    collection.update({
      _id: 'token'
    }, {
      $set: {
        access_token: token.access_token,
        expires_at: new Date(token.expiry_date)
      }
    }, {
      w: 0
    });
  });
}

The above are a couple of utility functions that we use to store and update our tokens. There will always be one single entry in our database after the first authentication and it should always contain a refresh token.

The updateToken function has one caveat: it only updates the data we want it to update instead of updating the entire document. This guarantees our refresh token is always present and never gets overwritten.

Google only returns a refresh token to us the first time we authenticate, so we have to make sure we store it at that time. If you fail to store it for some reason, you can always head to Google Account Permissions and revoke the access to Twilio PA. Next time you authenticate again, you will be given a new refresh token.

twilio-pa_9.png

Moving on we will now create two authentication functions that will deal with Google’s authentication servers. These functions can both be used in a situation where we still haven’t authenticated and will need a full set of tokens, or when we have already authenticated and just need retrieve the tokens from the database.

/* 
  When authenticating for the first time this will generate 
  a token including the refresh token using the code returned by
  Google's authentication page
*/
var authenticateWithCode = function(code, callback, oAuthClient) {
  oAuthClient.getToken(code, function(err, tokens) {
    if (err) {
      console.log('Error authenticating');
      console.log(err);
      return callback(err);
    } else {
      console.log('Successfully authenticated!');
      // Save that token
      storeToken(tokens);

      setCredentials(tokens.access_token, tokens.refresh_token, oAuthClient);
      return callback(null, tokens);
    }
  });
}

/* 
  When authenticating at any other time this will try to 
  authenticate the user with the tokens stored on the DB.
  Failing that (i.e. the token has expired), it will
  refresh that token and store a new one.
*/
var authenticateWithDB = function(oAuthClient) {
  getConnection(function(err, db) {
    var collection = db.collection("tokens");
    collection.findOne({}, function(err, tokens) {
      // if current time < what's saved
      if (Date.compare(Date.today().setTimeToNow(), Date.parse(tokens.expires_at)) == -1) {
        console.log('using existing tokens');
        setCredentials(tokens.access_token, tokens.refresh_token, oAuthClient);
      } else {
        // Token is expired, so needs a refresh
        console.log('getting new tokens');
        setCredentials(tokens.access_token, tokens.refresh_token, oAuthClient);
        refreshToken(tokens.refresh_token, oAuthClient);
      }
    });
  });
}

The function authenticateWithCode is only used when we’re authenticating for the first time. It will take a code we received back from Google authentication servers and generate a set of tokens, which we store on the database. This function will also tell any calling functions when it’s completed via a callback. This callback is especially useful for when we call this function the first time in app.js and need to make sure the authentication has occurred before we redirect the user.

When we authenticate from the database using authenticateWithDB, we have to check whether the access token is still valid so we can use it. In case it has already expired, we use the function refreshToken as you can see below:

// Refreshes the tokens and gives a new access token
var refreshToken = function(refresh_token, oAuthClient) {
  oAuthClient.refreshAccessToken(function(err, tokens) {
    updateToken(tokens);

    setCredentials(tokens.access_token, refresh_token, oAuthClient);
  });
  console.log('access token refreshed');
}

var setCredentials = function(access_token, refresh_token, oAuthClient) {
  oAuthClient.setCredentials({
    access_token: access_token,
    refresh_token: refresh_token
  });
}

var requestToken = function(res, oAuthClient) {
  // Generate an OAuth URL and redirect there
  var url = oAuthClient.generateAuthUrl({
    access_type: 'offline',
    scope: 'https://www.googleapis.com/auth/calendar.readonly'
  });
  res.redirect(url);
}

The refreshToken and requestToken functions will take care of requesting a new token from Google, or refreshing it should it already be expired. These functions are the only two that actually communicate with google’s authentication servers in order to retrieve or refresh a token.

Finally, we need to make sure some of these functions are available outside of the scope of this file, we do so by exporting them as follows:

module.exports = function(oAuthClient){
  var module = {};

  module.refreshToken = function(refresh_token){
    refreshToken(refresh_token, oAuthClient);
  };

  module.requestToken = function(res){
    requestToken(res, oAuthClient);
  };

  module.authenticateWithCode = function(code, callback){
    authenticateWithCode(code, function(err, data){
      if(err){
        return callback(err)
      }
      callback(null, data);
    }, oAuthClient);
  };

  module.authenticateWithDB = function(){
    authenticateWithDB(oAuthClient);
  };

  return module;
};

Open up app.js and add the dependency to our new file right after we initialise oAuthClient. We need to do it at this stage since we will pass a reference to oAuthClient into our new file so it can reuse it.


// Initialization
var app = express(),
  calendar = google.calendar('v3'),
  oAuthClient = new google.auth.OAuth2(config.googleConfig.clientID, config.googleConfig.clientSecret, config.googleConfig.redirectURL),
  tokenUtils = require('./token-utils')(oAuthClient);

Google’s authentication server needs to know where to redirect us to once it has validated that we are authenticated. We already told the authentication servers where to redirect to by setting Authorize Redirect URIs in the developer console to http://localhost:2002/auth so all we need to do now is create the auth route in app.js underneath the “/” route.

// Return point for oAuth flow
app.get('/auth', function(req, res) {

  var code = req.query.code;

  if (code) {
    tokenUtils.authenticateWithCode(code, function(err, data) {
      if (err) {
        console.log(err);
      } else {
        res.redirect('/');
      }
    });
  }
});

We receive a code back from the request and then get an authentication token generated by passing it to the authenticateWithCode function. We are calling the authenticateWithCode function with a callback as this allows us to know when the authentication cycle has completed.

If the authentication is successful the user is redirected to our entry route “/”.

In case we find the authentication failed, we show an error on the console describing why the authentication failed. This failure could be because you typed your password wrong, or failed to give the correct permissions to view that calendar. Google has extensive documentation about their OAuth flow in case you want to learn more.

There is one last thing we need to do here, which is change our server initialisation so it tries to authenticate the user upon start-up. This initialisation flow will guarantee we always have a fresh access tokens immediately after we finish loading the database.


var server = app.listen(config.port, function() {
  var host = server.address().address;
  var port = server.address().port;

  // make sure user is authenticated but check for existing tokens first
  getConnection(function(err, db) {
    var collection = db.collection("tokens");
    collection.findOne({}, function(err, tokens) {
      if (tokens) {
        tokenUtils.authenticateWithDB();
      }
    });
  });

  console.log('Listening at http://%s:%s', host, port);
});

Start your Node server again and try to hit the application on http://127.0.0.1:2002 you should see the following screen:

twilio-pa_6.png

If you see a screen that asks you to select which account you would like to use, chances are you are already logged in. Clicking on the user that owns the calendar you chose would redirect you to a screen that says “authenticated”.

Congratulations! We have gone through the most excruciating part of this post and everything after here will be a lot more fun.

Schedules

Our scheduled tasks will be created using the Agenda module. Agenda is a great module for task management as it persists your tasks to a database, so even if you restart your application your tasks will still be runnable when the application starts again.

On your terminal, create a new file called job-schedule.js in the root directory of the application. In my case it is ~/Projects/JavaScript/twilio-pa.

$ mkdir ~/Projects/JavaScript/twilio-pa/job-schedule.js

Open this file up and add the following code to it.

var Agenda = require("Agenda");
var config = require('./config');
agenda = new Agenda({
  db: {
	address: config.mongoConfig.ip + ':' + config.mongoConfig.port + '/' + config.mongoConfig.name
  }
});
exports.agenda = agenda;

All the information to initialize the module is being read from our configuration file. If later on we decided to change one of the settings like which port MongoDB runs, all we would have to do is change the configuration file.

Create a new directory in the root of our application called jobs. This directory contains the definitions for the two scheduled tasks we want to create – Send SMS and Start Call.

On your terminal you can create the directory as such:

$ mkdir ~/Projects/JavaScript/twilio-pa/jobs

In this directory create a file called send-sms.js. This file is one of our scheduled tasks definitions and contains all the logic to send outbound SMS.


var config = require('../config');
var twilio = require('twilio');
// Create a new REST API client to make authenticated requests against the
// twilio back end
var client = new twilio.RestClient(config.twilioConfig.accountSid, config.twilioConfig.authToken);

exports.send = function(agenda, event, task, number) {
  agenda.define(task, function(job, done) {
    client.sendSms({
      to: number,
      from: config.twilioConfig.number,
      body: 'Your call ('+ event.eventName +') will start in 5 minutes. Make sure you\'re in a quiet place'
    }, function(error, message) {
      if (!error) {
        console.log('Success! The SID for this SMS message is:');
        console.log(message.sid);
        console.log('Message sent on:');
        console.log(message.dateCreated);
        console.log(message.to);
      } else {
        console.log(error);
        console.log('Oops! There was an error.');
      }
    });
    done();
  });
  agenda.create(task).schedule(event.smsTime).unique({'id': event.id}).save();
}

We are again using our configuration file but also now importing the Twilio library which will make it much easier for us to interact with the Twilio API.

All the code surrounded by the highlighted bit of code is what we want to have executed on our scheduled task.

The send method takes four arguments, the Agenda object initialized by job-schedule.js, a CalendarEvent object, a task name and a telephone number.

In the last highlighted bit, we make sure our tasks are scheduled at the time we want them to run as defined in the CalendarEvent object and are also making sure our tasks are created uniquely. This step is very important as it will guarantee the tasks aren’t created over and over again every time the script runs. It will also take care of any updates to that event such as a different telephone number or time changes.

Create another job called start-call.js. The job is similar to the one we just created above but as the name says it is responsible for starting telephone calls. This is the job that is going to run at the time of the meeting and connect us with the person we are meant to be with on a call.

For this step Twilio will need to be able to connect to our local application. We could handle the connection in two ways, by either deploying to it a public web server, or using ngrok to expose our local environment via tunneling. We will go with option two here to make things easier. My colleague Kevin Whinery wrote a great blog post on getting up and running with ngrok.

Once you have Ngrok installed get it connected to your app by opening up another terminal screen and running:

$ ngrok 2002

Once it is running, it will acquire a unique URL for our application, and this is what we will use as the URL attribute for the makeCall method.

twilio-pa_7.png

Make a note of this Forwarding URL as you will need to use it on the code below.


var config = require('../config');
var twilio = require('twilio');
// Create a new REST API client to make authenticated requests against the
// twilio back end
var client = new twilio.RestClient(config.twilioConfig.accountSid, config.twilioConfig.authToken);

exports.call = function(agenda, event, task, number) {
  agenda.define(task, function(job, done) {
    // Place a phone call, and respond with TwiML instructions from the given URL
    client.makeCall({

      to: number, // Any number Twilio can call
      from: config.twilioConfig.number, // A number you bought from Twilio and can use for outbound communication
      url: 'http://300dcd5b.ngrok.com/call/?number=' + event.number + '&eventName=' + encodeURIComponent(event.eventName) // A URL that produces an XML document (TwiML) which contains instructions for the call

    }, function(err, responseData) {
      if (err) {
        console.log(err);
      } else {
        // executed when the call has been initiated.
        console.log(responseData.from);
      }
    });
    done();
  });
  agenda.create(task).schedule(event.eventTime).unique({
    'id': event.id
  }).save();
}

Open up app.js on the project root again and include the schedule task definitions we’ve just created. These should go between our initialization and Event Object:


// Initialization
var app = express(),
  calendar = google.calendar('v3'),
  oAuthClient = new google.auth.OAuth2(config.googleConfig.clientID, config.googleConfig.clientSecret, config.googleConfig.redirectURL),
  tokenUtils = require('./token-utils')(oAuthClient);

// Schedule setup
var jobSchedule = require('./job-schedule.js'),
  smsJob = require('./jobs/send-sms.js'),
  callJob = require('./jobs/start-call.js');
// Event object
var CalendarEvent = function(id, description, location, startTime) {
  this.id = id;
  this.eventName = description;
  this.number = location;
  this.eventTime = Date.parse(startTime);
  this.smsTime = Date.parse(startTime).addMinutes(-5);
};

On our server initialization, we will now add another recurring scheduled task that will fetch any calendar events for us every 10 minutes. That way, you don’t need to manually run the script every time


var server = app.listen(config.port, function() {
  var host = server.address().address;
  var port = server.address().port;

  // make sure user is authenticated but check for existing tokens first
  getConnection(function(err, db) {
    var collection = db.collection("tokens");
    collection.findOne({}, function(err, tokens) {
      if (tokens) {
        tokenUtils.authenticateWithDB(db);
      }
    });
  });

  jobSchedule.agenda.define('fetch events', function(job, done) {
    fetchAndSchedule();
    done();
  });

  jobSchedule.agenda.every('10 minutes', 'fetch events');

  // Initialize the task scheduler
  jobSchedule.agenda.start();

  console.log('Listening at http://%s:%s', host, port);
});

The highlighted parts are the ones we have just added for the recurring task.

You will notice we are making a call to a function called fetchAndSchedule. This function is responsible for fetching all our calendar events every time it’s called by the scheduled task. Under the CalendarEvent function place the following code:


function fetchAndSchedule() {
  // Set obj variables
  var id, eventName, number, start;

  // Call google to fetch events for today on our calendar
  calendar.events.list({
    calendarId: config.googleConfig.calendarId,
    maxResults: 20,
    timeMax: Date.parse('tomorrow').addSeconds(-1).toISOString(), // any entries until the end of today
    updatedMin: new Date().clearTime().toISOString(), // that have been created today
    auth: oAuthClient
  }, function(err, events) {
    if (err) {
      console.log('Error fetching events');
      console.log(err);
    } else {
      // Send our JSON response back to the browser
      console.log('Successfully fetched events');

      for (var i = 0; i < events.items.length; i++) {
        // populate CalendarEvent object with the event info
        event = new CalendarEvent(events.items[i].id, events.items[i].summary, events.items[i].location, events.items[i].start.dateTime);

        // Filter results 
        // ones with telephone numbers in them 
        // that are happening in the future (current time < event time)
        if (event.number.match(/\+[0-9 ]+/) && (Date.compare(Date.today().setTimeToNow(), Date.parse(event.eventTime)) == -1)) {

          // SMS Job
          smsJob.send(jobSchedule.agenda, event, 'sms#1', config.ownNumber);

          // Call Job
          callJob.call(jobSchedule.agenda, event, "call#1", config.ownNumber);
        }
      }

    }
  });
}

Once today’s events return we loop through them to get their information.

We then filter these events with a regular expression to only get the ones that have a telephone number in the location field, and also check for events happening after the current time. Each match then populates a new CalendarEvent object with the information from the event.

Lastly, we set up the scheduled tasks by passing event information through to each of our task definitions. We do that once for SMS and once for a call.

Making the call

It is a good time for us to create our /call route. Still on app.js add the following just below the fetchAndSchedule function.

app.post('/call', function(req, res) {
  var number = req.query.number;
  var eventName = req.query.eventName;
  var resp = new twilio.TwimlResponse();
  resp.say('Your meeting ' + eventName + ' is starting.', {
    voice: 'alice',
    language: 'en-gb'
  }).dial(number);

  res.writeHead(200, {
    'Content-Type': 'text/xml'
  });
  res.end(resp.toString());
});

What the above code does is play a message and dial out to the person you’re supposed to have a call with.

Running our app

At this point our application is ready, and we should be able to run it and authenticate.

Make sure you have a calendar entry for today and that the entry has a telephone number on the location field.

Start your Node server again.

$ node app.js

Fire up your browser to http://127.0.0.1:2002 and you should the the word “Authenticated” show up on the screen. If you have calendar entries for today, the script will pick them up and add them to the database.

If you would like to confirm whether any new entries have been added as scheduled tasks, you can query MongoDB via terminal on a new terminal screen:

$ mongo twilio-pa
$ db.agendaJobs.find()

All the entries you have with names and timestamps will now be listed. Sure enough you will notice an SMS message coming in 5 minutes prior to the meeting starting and then a call from your Twilio number at the time of the meeting.

You can do the same to query your token information by running:

$ db.tokens.find()

Your Twilio PA is alive!

And there you go, within only a few steps we have built our own Twilio PA, which will handle our outgoing calls for us and guarantee we will never again be 11 minutes late to that call.

Ideally, I’d like my meeting reminders to be more forceful and also trigger a siren at the time it sends me an SMS. That would for sure get me out of the room and ready for that call. How about adding an interface that lets you see all future scheduled tasks? Well, maybe that’s an idea for Twilio PA v2.0.

I would love to hear about the wonderful ways you can make your Twilio PA do more work for you and let you focus on other work that can’t be easily automated.

I look forward to seeing your hacks. Reach me out on Twitter @marcos_placona, by email on marcos@twilio.com or MarcosPlacona on G+ to tell me more about it.