Skip to main content

Concepts (Google)

Recurring Events

Introduction

Recurring events in Google Calendar allow users to create events that repeat on a regular basis (daily, weekly, monthly, etc.). This feature is useful for scheduling regular meetings, appointments, or reminders.

Purpose

The purpose of this documentation is to explain how to create, manage, and retrieve recurring events using the Google Calendar API and sync them with Scrims Calendar API.

Key Concepts

  1. Recurrence Rules:
    • Defined using the RRULE (Recurrence Rule) property in the iCalendar format.
    • Commonly used rules include FREQ (frequency), INTERVAL, BYDAY (day of the week), and UNTIL (end date).
  2. Master Event:
    • The main event that defines the recurrence pattern. All occurrences of the event are generated based on this master event.
  3. Exception Events:
    • Individual occurrences of the recurring event that have been modified (e.g., rescheduled or cancelled). These are treated as exceptions to the recurrence rule. Exceptions are identified by recurrence_id property on Scrims.
    • On Google calendar exceptions are identified by the composition of the events "id" field, for example an exception of a recurring event will contain {parentId}_R{timeString} as an id.
    • For nested exceptions on Google calendar recurringEventId field will be present. However, note that exceptions that are recurring (created with this-and-upcoming events update) can not have recurringEventId.

Implementation Steps

The upsertGoogleEvents function manages the creation, updating, and deletion of events on Google Calendar. Here's how recurring events are handled within the function:

Identifying Recurring Events

The function checks if an event is recurring by examining the recurrence array in the localEvent object. It looks for recurrence rules (RRULE) to identify recurring events:

let localEventRecurrence = localEvent.recurrence.find(r => r.includes("RRULE"));

Handling Recurring Exceptions

If the event is an exception to a recurring event (has a recurrence_id but no localEventRecurrence), the function prepares to handle it differently:

if (localEvent.recurrence_id !== "" && !localEventRecurrence) { ... }

Creating New Exceptions

For exceptions, the function generates a new instance ID based on the recurrence_id:

if (!existingEvent) {
  let underlyingGoogleEvent = await Events.findOne({ atomcal_event_id: underlyingEventId }).lean();
  if (underlyingGoogleEvent) {
    let timestamp = localEvent.recurrence_id.split("_R")[1];
    let newInstanceId = underlyingGoogleEvent.google_event_id.split(/(?:_|_r|_R|r|R)(?=\d{8}T\d{6}Z?$)/)[0] + "_" + timestamp;
    finalId = newInstanceId;
  }
}

Handling Existing Events

For existing events, the function tries to find the underlying event ID and set the recurringEventId appropriately:

if (existingEvent) {
  let underlyingEventId = localEvent.recurrence_id.split("_")[0];
  let underlyingGoogleEvent = await Events.findOne({ atomcal_event_id: underlyingEventId }).lean();
  if (underlyingGoogleEvent) {
    recurringEventIdObj = { recurringEventId: underlyingGoogleEvent.google_event_id };
  }
}

Determining the HTTP Method

The function decides whether to use POST or PATCH based on whether it is creating a new event or updating an existing one:

let method = finalId === "" ? got.post : got.patch;
crudEventUri = crudEventUri + finalId;

Creating the Request Body

The function constructs the request body with all necessary details, including the recurrence rules and any necessary time zone adjustments:

body = { ...body, ...location, extendedProperties: { shared: { image_url: localEvent.image_url } }, recurrence, ...recurringEventIdObj, summary: localEvent.title, description: localEvent.description, end: { dateTime: isAllDay || isMultiDay ? undefined : new Date(endDateTime).toISOString(), timeZone: isAllDay || isMultiDay || localEvent.end_timezone === '' ? 'UTC' : localEvent.end_timezone, date: isAllDay || isMultiDay ? new Date(endDateTimeWithOffset).toISOString().split('T')[0] : undefined }, start: { dateTime: isAllDay || isMultiDay ? undefined : new Date(startDateTime).toISOString(), timeZone: isAllDay || isMultiDay || localEvent.start_timezone === '' ? 'UTC' : localEvent.start_timezone, date: isAllDay || isMultiDay ? new Date(startDateTimeWithOffset).toISOString().split('T')[0] : undefined }, };

Handling Archived Events

If the event is archived, the function deletes the event or marks it as canceled:

if (localEvent.archived) {
  if (existingEvent) {
    method = got.delete;
    bodyObj = {};
    if (body.recurringEventId !== existingEvent.recurringEventId && body.recurringEventId && body.recurringEventId !== "") {
      let timestamp = localEvent.recurrence_id.split("_R")[1];
      crudEventUri = `https://www.googleapis.com/calendar/v3/calendars/${calendarId}/events/` + body.recurringEventId + "_" + timestamp;
    }
  } else {
    bodyObj = { body: JSON.stringify({ ...body, status: "cancelled" }) };
  }
}

Executing the Request

The function executes the HTTP request to Google Calendar API using the appropriate method (POST, PATCH, or DELETE):

let gotGoogleEvent = await method(crudEventUri, { headers: { Authorization, "Content-Type": "application/json" }, ...bodyObj });

Updating the Local Database

Finally, the function updates the local database with the event details returned from Google API:

let googleEvent = await Events.findOneAndUpdate({ google_event_id: googleAPIEvent.id }, { atomcal_event_id: localEvent._id, google_calendar_id: calendar_id, raw_json: googleAPIEvent }, { new: true, upsert: true });

Summary

The upsertGoogleEvents function carefully handles recurring events by checking for recurrence rules, managing exceptions, setting appropriate IDs, constructing the correct request body, and finally executing the necessary API requests to Google Calendar. This ensures that recurring events are created, updated, or deleted accurately, reflecting the local application's event details on Google Calendar.