Sending myself text messages with my calendar events for the day
I can be a bit scatterbrained sometimes, and I wanted an easy way to jog my memory about events in the day, so I made a quick and dirty thing that sends me a text message (SMS) with the events for the day.
The code
To jump straight into the code: https://github.com/nkhil/gcal
Architecture
As this code only really needs to be executed once a day, I decided to use a cloud function that will wake up once a day to do the work, and then go back to sleep.
As I was using Google Cloud Platform (GCP), I found out the easiest way to do this was to use Google’s Cron Scheduler to schedule a cron job that will invoke a cloud function at 8am (for my local timezone, currently London).
Now, GCP’s cron scheduler can trigger events in a couple of ways:
- HTTP request
- Publishing an event on a Pub-Sub topic
I wanted to get something up and running asap while only using the components that were necessary.
Essentially, our cron scheduler will make an HTTP request to our cloud function. Note that our cloud function endpoint isn’t public, so I configured the cron scheduler to use an auth token with the appropriate scope in the POST request headers.
Using @google-cloud/functions-framework
Sidenote: Using the @google-cloud/functions-framework did make it super easy to run my cloud function locally to test it!
Talking to Google Calendar
Our objective here is to get a long lasting refresh_token for my Google account that the cloud function can use to retrieve my calendar events. I didn’t want to build this into the cloud function itself as I wanted to get something working within a day.
Here’s what the script for that looks like. Since this is the Oauth 2 flow, its divided into 2 steps.
Note that I needed to set up a new project within GCP for this mini-project, and the clientId and clientSecret come from GCP.
const { google } = require('googleapis');const oauth2Client = new google.auth.OAuth2(
clientId, // My GCP project client id
clientSecret, // My GCP project client secret
redirectUrl, // In this case, this redirect URL can be anything since we're going to do this manually (i.e. not programatically)
)// generate a url that asks permissions for Google Calendar scopes
const scopes = [
'https://www.googleapis.com/auth/calendar.events',
'https://www.googleapis.com/auth/calendar',
];const url = oauth2Client.generateAuthUrl({
// 'online' (default) or 'offline' (gets refresh_token)
access_type: 'offline',
scope: scopes
})console.log('Click on this URL to get going', url)
Once you click on the URL the Oauth client generates, you’ll be asked to give your app the right permissions, after which you will get redirected to the redirect URL that you supplied (note that you will need to add the redirect URL to your GCP project, or it won’t work). The address bar in your browser will contain some params that look like this:
https://yourredirecturl.com?CODE=4/0AX4XfWgOoAyp_vbsSrowzgXlP4R-XAhpiXGCbnn9HB2nqAi4HwkxBzCsn5JmZBrFGyLa9w?SCOPES=https://www.googleapis.com/auth/calendar.events%20https://www.googleapis.com/auth/calendar.events
The CODE
is what we need.
Note: The code will be URL encoded and you will need to decode this before use. I used this VSCode plugin to do it.
Here’s how we use the CODE
we just got from Google. Please use the same instance of oauth2Client we generated earlier.
const CODE = '4/0AX4XfWgOoAyp_vbsSrowzgXlP4R-XAhpiXGCbnn9HB2nqAi4HwkxBzCsn5JmZBrFGyLa9w'const { tokens } = await oauth2Client.getToken(CODE)
Here’s what tokens
looks like:
{
access_token: '<short_lived_access_token>`,
refresh_token: '<long_lived_refresh_token>',
scope: '<scopes_you_supplied>',
token_type: 'Bearer',
expiry_date: '<expiry_date>',
}
For our use case, we only need the refresh_token
. You can use the refresh token to get the events like so:
const { google } = require('googleapis')
const dateFns = require('date-fns')const { oauth2Client } = require('./o-auth-2-client') // we initialised it earlierconst tokens = {
refresh_token: '<refresh_token>',
scope: '<scopes>',
}oauth2Client.setCredentials(TOKENS)const calendar = google.calendar({
version: 'v3',
auth: oauth2Client,
})const res = await calendar.events.list({
calendarId: 'primary',
timeMax: dateFns.endOfDay(new Date()),
timeMin: dateFns.startOfDay(new Date()),
})
^ This should get you a response with the events between the timeMin
and timeMax
params you provided. This is the paydirt we’re after. The next steps are relatively easy:
- Format the data into what we need (i.e. change the ISO timestamp into a human readable
HH:mm am/pm
time, as well as adding line breaks for readability in the text message - Use the Twilio API to send the message
I’m not going to go into detail about using the Twilio API since their docs do a better job, but its worth mentioning that I had to use a US number, as other numbers can’t send text messages at the time of writing (April 2022).
Timezone gotcha
In the Payload Google returns, the times look like so:
start: {
dateTime: '2022-03-30T12:30:00+01:00', timeZone: 'Europe/London'
},
end: {
dateTime: '2022-03-30T13:00:00+01:00', timeZone: 'Europe/London'
}
Note that the date string here is missing a timezone (i.e. the Z
that denotes the timezone), so when the code parsed the times in the cloud function environment (which is UTC), the times were wrong, for eg: a 12.30pm appointment got parsed as 1.30pm. This is because the cloud function runs in UTC time.
I ended up using the date-fns-tz package to parse the time correctly when my code runs in the UTC environment.