Using The Google API Ruby Client with Google Calendar API

Google offers robust support for their APIs, including the Google Calendar API, and the Google API Ruby Client gem offers a comprehensive wrapper for this API, among others.

Integrating these tools into a Rails 5 app still proved to be something of a challenge, involving some guess work and some heavy leaning on the Google Calendar API documentation. So, I thought I'd share my approach for any similarly struggling folks out there.

Overview

This post describes a Rails 5 app that uses Devise OmniAuth with Google to authenticate users and post events to their google calendar. A couple of challenges I faced in getting this feature up and running:

  • Retrieving an authenticated user's refresh token, which is necessary in order to re-authenticate a user before posting to their calendar (Google's access tokens expire relatively quickly and a user usually needs to be re-authenticated before posting to the calendar).
  • Retrieving the calendar ID of the calendar to which a user wants to post events, when that user has more than one calendar associated with their account.
  • Requesting information about existing events from a user's calendar, so as not to double-book events.

Now that we're familiar with the challenges at hand, let's get started!

Part I: Authenticating Users with Devise and Google OmniAuth

Step 1: Add the Gems

The first pain point I encountered was getting Devise to work with Rails 5. I learned that, if you're using Rails 5, you need to use the following version of the Devise gem.

# Gemfile
gem 'devise', github: 'twalpole/devise', branch: 'rails5'  

To use Devise with Google OmniAuth, add the following to your Gemfile and bundle install:

# Gemfile
gem 'devise', github: 'twalpole/devise', branch: 'rails5'  
gem 'omniauth-google-oauth2'  
Step 2: Configure Devise

Gotcha Alert: Normally, when setting up Devise with our Rails app, we can run rails generate devise:install after we bundle install.

However, I couldn't get this to work––Rails threw various errors and eventually I hacked around it by manually creating a config/initializers/devise.rb file and copying over the content of that file from another project.

Once I did that, I was able to run rails generate devise:install, followed by rails generate devise user. Make sure you already have a users table and model. Devise will generate a migration to add Devise attributes to the users table and append Devise attributes to your existing User model.

Step 3: Configure Google OmniAuth

Generate another migration to add access_token and refresh_token to your users table. These attributes are necessary for us to communicate with the Google Calendar API and we will retrieve them from the payload sent back to our app, from Google, when Rails sends an authentication request to Google.

Next, we need to get our Client ID and Secret from Google.

Register your App with Google

Head over to the Google Developer Console to register your app and get your Google client ID and secret.

Once your new app is created in the Developer Console, visit the API Manager tab and click "Credentials". In the Credentials screen, click "Create Credentials" and select "OAuth client id".

You'll be prompted to configure your consent screen and define a callback URL. Since we're using Devise, we'll set our callback URL to localhost:3000/users/auth/google_oauth2

This should be all you need to get your client ID and client secret.

Set the following environment variables: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET equal to your ID and secret. Personally, I prefer Figaro for setting and hiding environment variables, but you may be using Dotenv.

Now we can finish configuring Devise.

Configure Your Devise Initializer

In config/initializers/devise.rb, add the following:

config.omniauth :google_oauth2, ENV['GOOGLE_CLIENT_ID'],  
  ENV['GOOGLE_CLIENT_SECRET'],
  { access_type: "offline", 
    prompt: "consent",
    select_account: true,
    scope: 'userinfo.email,calendar' }

There are a couple of important things to note here:

  • In order for Google to send us back not only an access token, but a refresh token, we need to specify a few options:
    • prompt: "consent"
    • access_type: "offline"
  • Secondly, we need to include calendar in the scope, so that we can interact with the Google Calendar API later on.

We're almost done with Part I. Next up, we need to write our custom callback controller method and our custom user authentication method.

Step 4: Custom Callback Controller and User Authorization Method

When we ran rails generate devise:install, the following was appended to our routes.rb file:

Rails.application.routes.draw do  
  devise_for :users
end  

We'll customizing the Devise Callbacks Controller with our own find_for_google_oauth2 method.

First, specify which controller Devise should use as the Callbacks Controller in your Routes file:

Rails.application.routes.draw do  
  devise_for :users, :controllers => { :omniauth_callbacks => "callbacks" }
end  

Next, define the your Callbacks Controller to inherit from the Devise::OmniauthCallbacksController.

class CallbacksController < Devise::OmniauthCallbacksController  
  def google_oauth2
    @user = User.find_for_google_oauth2(request.env["omniauth.auth"])
    if @user
      sign_in @user
      redirect_to root_path
    else
      redirect_to new_user_session_path, notice: 'Access Denied.'
    end
  end
end  

Lastly, we'll define the find_for_google_oauth2 method on our User model.

class User < ApplicationRecord  
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :omniauthable

  def self.find_for_google_oauth2(auth)
    data = auth.info
    if validate_email(auth)
      user = User.where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
        user.provider = auth.provider
        user.uid = auth.uid
        user.email = auth.info.email
        user.password = Devise.friendly_token[0,20]
      end
      user.token = auth.credentials.token
      user.refresh_token = auth.credentials.refresh_token
      user.save
      return user
    else
      return nil
    end
  end

Something to note about the find_for_google_oauth2 method: make sure that you are re-assigning and persisting the token and refresh token sent from Google each time you authorize the user. We need to retrieve these new tokens every time we authenticate a user.

We're (actually) almost done with Part I!

Lastly, add the "Sign In with Google" Link to your view:

# app/views/application.html.erb

<%= link_to "Sign In with Google", user_omniauth_authorize_path(:google_oauth2)%>  

Alright, we're ready to move on to Part II.

Part II: Using The Google API Ruby Client with The Google Calendar API

First, a little background. In this app, we have a Schedule resource (table, model, controller). Schedules have many activities and an activity has a start time and an end time. A schedule also has a specific calendar to which it needs to POST events, given that a user can have many Google calendars associated with their account. The schedule has the Google calendar id of the calendar to which it needs to post.

Let's pick it up with building our own GoogleCalendarWrapper class to communicate with the Google Calendar API.

First things first, add the Google API Ruby Client gem to your Gemfile and bundle install:

gem 'google-api-client', '0.8.2', require: 'google/api_client'  

Gotcha Alert: I couldn't get this working in Rails 5 with the most recent version of the API Client gem. After digging around GitHub issues for a little while, I followed a tip to switch to version 0.8.2, and this got it working for me.

Okay, back to our wrapper. We'll be using our GoogleCalendarWrapper in the Schedules Controller. Something like this:

# app/controllers/schedules/controller.rb
...
def make_google_calendar_reservations  
  @schedule = @cohort.schedules.find_by(slug:  
    params[:slug])
  @calendar = GoogleCalWrapper.new(current_user)
  @calendar.book_rooms(@schedule)
end  

Our GoogleCalendarWrapper class needs to initialize the Google API Ruby client and initialize the Google Calendar API service. Only then can it post events to the user's Google calendar.

Initializing the Client

Let's start by initializing the client and calendar service:

class GoogleCalendarWrapper

  def initialize(current_user)
    configure_client(current_user)
  end

  def configure_client(current_user)
    @client = Google::APIClient.new
    @client.authorization.access_token = current_user.token
    @client.authorization.refresh_token = current_user.refresh_token
    @client.authorization.client_id = ENV['GOOGLE_CLIENT_ID']
    @client.authorization.client_secret = ENV['GOOGLE_CLIENT_SECRET']
    @client.authorization.refresh!
    @service = @client.discovered_api('calendar', 'v3')
  end
end  

Here, we use the access token and refresh token that we persisted to our user when the user was first authorized with Google OmniAuth, together with the client ID and secret Google assigned us when we registered our app.

Gotcha Alert: Take note of one line in particular:

@client.authorization.refresh!

We need to refresh our user's access token and refresh token in order to effectively re-authorize our user before interacting with the Google Calendar API. This is because our original access token and refresh token may have expired by this time.

Okay, we're just about ready to build out our book_events method. But first, we need to get the ID of the user's calendar.

Getting the User's Calendar ID

Another obstacle I faced in getting this feature working was retrieving the calendar ID of the appropriate calendar. A user can have many Google calendars, but we need to POST events to a specific one of the user's calendars––the calendar associated to the specific schedule we are dealing with.

So, how do we access a list of the user's calendars in order to identify the right one?

With the following code:

# GoogleCalendarWrapper
response = @client.execute(api_method: @service.calendar_list.list)  

This API call will return the following payload:

calendars = JSON.parse(response.body)

# => {"kind"=>"calendar#calendarList",
 "etag"=>"\"1458339303628000\"",
 "nextSyncToken"=>"COCXXXXXXXhdGlyb25zY2hvb2wuY29t",
 "items"=>
  [{"kind"=>"calendar#calendarListEntry",
    "etag"=>"\"145XX6847112000\"",
    "id"=>    "s@gmail.com36@resource.calendar.google.com",
    "summary"=>"Conference Room - 1",
    "timeZone"=>"America/New_York",
    "colorId"=>"16",
    "backgroundColor"=>"#4986e7",
    "foregroundColor"=>"#000000",
    "selected"=>true,
    "accessRole"=>"reader",
    "defaultReminders"=>[]},
   {"kind"=>"calendar#calendarListEntry",
    "etag"=>"\"1458068365807000\"",
    "id"=>
     "my_company.com_313938@resource.calendar.google.com",
    "summary"=>"Conference Room - 2",
    "timeZone"=>"America/Los_Angeles",
    "colorId"=>"13",
    "backgroundColor"=>"#92e1c0",
    "foregroundColor"=>"#000000",
    "selected"=>true,
    "accessRole"=>"reader",
    "defaultReminders"=>[]},
   {"kind"=>"calendar#calendarListEntry",
    "etag"=>"\"1458071051048000\"",
    "id"=>"sophie@gmail.com",
    "summary"=>"sophie@gmail.com",
    "timeZone"=>"America/New_York",
    "colorId"=>"14",
    "backgroundColor"=>"#9fe1e7",
    "foregroundColor"=>"#000000",
    "selected"=>true,
    "accessRole"=>"owner",
    "defaultReminders"=>[{"method"=>"popup", "minutes"=>10}],
  "notificationSettings"=>
     {"notifications"=>
       [{"type"=>"eventCreation", "method"=>"email"},
        {"type"=>"eventChange", "method"=>"email"},
        {"type"=>"eventCancellation", "method"=>"email"},
        {"type"=>"eventResponse", "method"=>"email"}]},
    "primary"=>true}
}

Our individual calendar hashes can be found in calendars["items"]. Now, we can parse this collection of calendars for the id of the calendar associated with our schedule:

#GoogleCalendarWrapper
...
def calendar_id(schedule)  
  response = @client.execute(api_method:     
    @service.calendar_list.list)
  calendars = JSON.parse(response.body)
  calendar = calendars["items"].select {|cal| 
    cal["id"].downcase == schedule.calendar_id}
  calendar["id"]
end  

Now that we have the calendar ID we need, we're almost ready to book calendar events. First, we need to check for calendar availability. To do this, we need to use the freebusy query methods made available to us by the Google Calendar API.

Querying Calendar Availability
@client.execute(api_method: @service.freebusy.query, 
  body: JSON.dump({timeMin: start_time,
  timeMax: end_time,
  timeZone: "EST",
  items: [calendar_id]}),
  headers: {'Content-Type' => 'application/json'})

The freebusy query methods must be accompanied by a request body that specifies the following:

  • The start time and end time of the period for which we want to query events.
  • items, which should be set equal to an array of calendar ids (i.e. the ids of the calendar whose availability we want to query).
  • headers, which should set the content type to application/json.

Gotcha Alert: It took me a while to get the start_time and end_time just right. First of all, the DateTime needs to be formatted exactly right. Like this:

2016-03-20T11:04:00+0000  

Secondly, I found I had to cast a wide net to return accurate information regarding a calendars existing events. Only if I set the start and end times to frame a 24-hour period would I see a list of existing events returned to me.

Okay, so, if we JSON.parse the response of the above query, we get a collection of hashes, each of which represents a pre-existing calendar event. We can therefore compare the start and end times of the existing events with the start and end times of the event we want to book, in order to avoid double booking.

Now we're ready to book those events!

Inserting New Events to the Calendar

Okay, we're in the home stretch. Let's assume we've checked a new event's start and end time against the start and end times of existing events and determined that the calendar is free. Let's book that new event!

The code we use to POST new events to a user's calendar looks like this:

@client.execute(:api_method => @service.events.insert,
  :parameters => {'calendarId' => calendar_id,
    'sendNotifications' => true},
  :body => JSON.dump(event),
  :headers => {'Content-Type' => 'application/json'})

The event object that we are POST-ing to the user's calendar should be structured like this:

event = {  
  summary: "Board of Directors Meeting", 
  location: "Conference Room - 1",
  start: {dateTime: 2016-03-20T11:04:00+0000},  
  end: {dateTime: 2016-03-20T12:04:00+0000},  
  description: "important meeting with the board",  
}

And that's it! Before you go, check out the brief summary of Google API Ruby Client + Google Calendar API methods below.

Google Calendar API Method Summary

Configure the Client

@client = Google::APIClient.new
@client.authorization.access_token = current_user.token
@client.authorization.refresh_token = current_user.refresh_token
@client.authorization.client_id = ENV['GOOGLE_CLIENT_ID']
@client.authorization.client_secret = ENV['GOOGLE_CLIENT_SECRET']
@client.authorization.refresh!

Configure Google Calendar Service

@service = @client.discovered_api('calendar', 'v3')

Get User's Calendars

@client.execute(api_method:     
  @service.calendar_list.list)
  calendars = JSON.parse(response.body)

Get Calendar Events

@client.execute(api_method: @service.freebusy.query, 
  body: JSON.dump({timeMin: start_time,
  timeMax: end_time,
  timeZone: "EST",
  items: [calendar_id]}),
  headers: {'Content-Type' => 'application/json'})

Insert Calendar Event

event = {  
  summary: "Board of Directors Meeting", 
  location: "Conference Room - 1",
  start: {dateTime: 2016-03-20T11:04:00+0000},  
  end: {dateTime: 2016-03-20T12:04:00+0000},  
  description: "important meeting with the board",  
}

@client.execute(:api_method => @service.events.insert,
  :parameters => {'calendarId' => calendar_id,
    'sendNotifications' => true},
  :body => JSON.dump(event),
  :headers => {'Content-Type' => 'application/json'})

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus