Ruby
Integrating with Google Calendar as a Service App
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:
- googleauth for authenticating the service app
- google-api-client for communicating with the Google Calendar API
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!