Heading image for post: Integrating with Google Calendar as a Service App

Integrating with Google Calendar as a Service App

Profile picture of Mary Lee

Recently, I was working on a project where the goal was to have a shared calendar for the application that all the users would have access to, but no individual user would have ownership of. This seemed like the perfect use case to have a service app owned calendar that gave access to users based on email addresses.

I started by going to the Google developers console, creating a new project, creating service app account credentials, and enabling the Google Calendar API.

I used two libraries for setting up the integration:

Basic App Set Up

I was working with users that had access to many calendars, and those calendars had many events.

class User < ApplicationRecord
  has_many :calendar_users
  has_many :calendars, through: :calendar_users
end

class Calendar < ApplicationRecord
  has_many :calendar_users
  has_many :users, through: :calendar_users
  has_many :events
end

class CalendarUser < ApplicationRecord
  belongs_to :user
  belongs_to :calendar
end

class Event < ApplicationRecord
  belongs_to :calendar
end

Authorization

There are a few ways to authenticate with the googleauth gem, depending on how you want to handle your credentials. For me, it was easiest to use environment variables. I set four environment variables to make my authentication work:

  • GOOGLE_ACCOUNT_TYPE
  • GOOGLE_CLIENT_ID
  • GOOGLE_CLIENT_EMAIL
  • GOOGLE_PRIVATE_KEY

Once those environment variables were set, I could use the get_application_default method from the authentication library to authorize as the service app, passing in the calendar authentication scopes I needed. Because I wanted to be able to manage calendars and events, I needed two scopes, calendar and calendar.events.

Google::Auth.get_application_default([
  "https://www.googleapis.com/auth/calendar",
  "https://www.googleapis.com/auth/calendar.events"
])

Next, I needed to share my authentication with the Google API client library. To do this, I initialized a calendar service and passed it my authentication. Since I knew that I was going to reuse my authentication process to authorize users, create calendars, and create events, I decided to make it a class. The end result was this:

module GoogleCalendar
  class Base
    def calendar_api
      @calendar_api ||= build_and_authorize_api_client
    end

    private

    def build_and_authorize_api_client
      client  = Google::Apis::CalendarV3::CalendarService.new
      client.authorization = auth
      client
    end

    def auth
      # Default service app authentication, using environment variables
      Google::Auth.get_application_default(scopes)
    end

    def scopes
      # Scopes for access calendars and their events
      [
        "https://www.googleapis.com/auth/calendar",
        "https://www.googleapis.com/auth/calendar.events"
      ]
    end
  end
end

Creating a Calendar

My first step was creating the calendars on Google. Since I was only creating calendars, the only method I used on the API client was the insert_calendar method, which accepts as an argument a Google::Apis::CalendarV3::Calendar object.

Using the Google reference docs for inserting calendars, I knew all I needed to supply to the Google::Apis::CalendarV3::Calendar object was a summary.

Google::Apis::CalendarV3::Calendar.new(summary: "My New Calendar Name")

With the object initialized, I could then pass it to the calendar_api I had set up in the GoogleCalendar::Base class. I created a new class that would manage my calendar-related API calls, setting it to inherit from my base class.

module GoogleCalendar
  class Calendar < Base
    attr_reader :name

    def self.add!(name)
      new(name).add!
    end

    def initialize(name)
      @name = name
    end

    # Add a calendar to the service app's calendar list
    def add!
      calendar_api.insert_calendar(calendar)
    end

    private

    def calendar
      Google::Apis::CalendarV3::Calendar.new(summary: name)
    end
  end
end

I could now insert a calendar in one call with GoogleCalendar::Calendar.add!("My New Calendar Name"). This method would return the created calendar from Google, allowing me to store the Google calendar id wherever I needed it.

calendar = Calendar.new(name: "Example Calendar")
google_calendar = GoogleCalendar::Calendar.add!(calendar.name)
calendar.google_id = google_calendar.id

Managing User Access

The next step in the process was to allow users access to the new calendar. I knew I would need to be able to do two things for user access -- allow and revoke. This would require two methods from the Google API client.

To grant access, I would need to create an ACL rule for the calendar, using the insert_acl method. This method takes as an argument the calendar id to give access to, and a Google::Apis::CalendarV3::AclRule object. From the reference docs for creating ACL rules, I knew I would need a role and scope for the rule.

Since the users in my case only needed read access to the calendar (they weren't making edits), my role was "reader". The scope was a little more complicated, because it was a nested object in the API, and the client library handled it by requiring another object to be instantiated, this time a Google::Apis::CalendarV3::AclRule::Scope. The scope required two things in my case, a type and value, with type being "user" and value being the user's email address.

scope = Google::Apis::CalendarV3::AclRule::Scope.new(type: "user", value: "user@example.com")
Google::Apis::CalendarV3::AclRule.new(role: "reader", scope: scope)

Once the rule was set up, I could then pass it and the calendar id to the insert_acl method, granting the user access to the calendar.

To revoke access for a user, I used the delete_acl method, passing the calendar id, and a rule identifier.

I created a new class that would manage my user access, setting it to inherit from my base class. I could then call both methods on the calendar_api method.

module GoogleCalendar
  class Access < Base
    attr_reader :calendar_id, :email, :rule_id

    def self.allow!(calendar_id, email)
      new(calendar_id: calendar_id, email: email).allow!
    end

    def self.revoke!(calendar_id, rule_id)
      new(calendar_id: calendar_id, rule_id: rule_id).revoke!
    end

    def initialize(calendar_id:, email: nil, rule_id: nil)
      @calendar_id = calendar_id
      @email = email
      @rule_id = rule_id
    end

    # Create a rule that gives a user access to the calendar
    def allow!
      calendar_api.insert_acl(calendar_id, rule)
    end

    # Remove the rule that gives the user access to the calendar
    def revoke!
      calendar_api.delete_acl(calendar_id, rule_id)
    end

    private

    def rule
      Google::Apis::CalendarV3::AclRule.new(role: "reader", scope: scope)
    end

    def scope
      Google::Apis::CalendarV3::AclRule::Scope.new(type: "user", value: email)
    end
  end
end

With my API logic in place, I could now allow and revoke calendar access for my users.

user = User.new(email: "user@example.com")
calendar_id = user.calendars.first.google_id

# Grant a user access
rule = GoogleCalendar::Access.allow!(calendar_id, user.email)
user.google_calendar_access_id = rule.id

# Revoke the user's access
GoogleCalendar::Access.revoke!(calendar_id, user.google_calendar_access_id)
user.google_calendar_access_id = nil

Managing Events

Finally, with a calendar to work with, and with users able to see the calendar, I could start managing events on the calendar. I knew that I was going to need to be able to create, edit, and remove events from the calendar, using the insert_event, patch_event and delete_event methods from the client library.

To insert an event, a calendar id and a Google::Apis::CalendarV3::Event object are required. From the reference docs, I knew that the bare minimum requirements to insert an event were a start and end time, but I wanted to be a bit more descriptive, so I chose to also include a summary, location, and description. I passed all of this information to the Event object.

Google::Apis::CalendarV3::Event.new(
  summary: "My New Event",
  location: "123 Main St, Jacksonville Beach, FL 32250",
  description: "New event for testing the Events API",
  start: { date_time: Date.current.iso8601 },
  end: { date_time: 1.hour.from_now.iso8601 }
)

With the event object instantiated, I could pass it along with the calendar id to the insert_event method, adding it to the calendar.

To update the event, I could use the same event object, passing along the event id I was choosing to update, along with the calendar id. Similarly, I could remove the event with the calendar id and event id.

I again created a new class, this time to manage my events.

module GoogleCalendar
  class Event < Base
    attr_reader :event

    def self.add!(event)
      new(event).add!
    end

    def self.update!(event)
      new(event).update!
    end

    def self.remove!(event)
      new(event).remove!
    end

    def initialize(event)
      @event = event
    end

    def calendar_id
      event.calendar.google_id
    end

    # The Google calendar event id if the event has already been added to the calendar
    def event_id
      event.google_calendar_event_id
    end

    # Add the event to the Google calendar
    def add!
      return unless event.time.present?
      calendar_api.insert_event(calendar_id, google_event)
    end

    # Update the event on the Google calendar
    def update!
      return unless event.time.present? && event_id.present?
      calendar_api.patch_event(calendar_id, event_id, google_event)
    end

    # Remove the event from the Google calendar
    def remove!
      return unless event_id.present?
      calendar_api.delete_event(calendar_id, event_id)
    end

    private

    # Set a default duration that can be overridden
    def event_duration
      (ENV["LEAD_EVENT_DURATION"] || 90).minutes
    end

    # This formats the time with the time zone in a way that Google can use
    def start_time
      event.time.iso8601
    end

    # This formats the time with the time zone in a way that Google can use
    def end_time
      (event.time + event_duration).iso8601
    end

    def google_event
      Google::Apis::CalendarV3::Event.new(
        summary: event.calendar_summary,
        location: event.address_text,
        description: event.calendar_description,
        start: { date_time: start_time },
        end: { date_time: end_time }
      )
    end
  end
end

Now I could easily manage my calendar events.

event = Event.last

# Add the event to the calendar
google_event = GoogleCalendar::Event.add!(event)
event.google_calendar_event_id = google_event.id

# Update the event on the calendar
GoogleCalendar::Event.update!(event)

# Remove the event from the calendar
GoogleCalendar::Event.remove!(event)
event.google_calendar_event_id = nil

Wrapping Up

I still need to add testing and error handling to my GoogleCalendar classes, particularly in the case of API failures. I will also probably need to implement editing and removing of calendars at some point, I just didn't have a use case for it right now.

The reference documents from Google for the Calendar API were a great help when it came to required fields and formatting specifications. Using them in tandem with the source code for the API client calendar service and calendar classes was the easiest way to figure out how things were meant to work. I was impressed by how detailed the inline documentation was for the client library. Make sure to check them out!

More posts about rails