Skip to content

Instantly share code, notes, and snippets.

@evansims
Last active February 16, 2023 00:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save evansims/eec2562dcae9ed2fdda2f28c6ccad988 to your computer and use it in GitHub Desktop.
Save evansims/eec2562dcae9ed2fdda2f28c6ccad988 to your computer and use it in GitHub Desktop.
Google Apps Script: Sync Google Group members Out of Office events to a shared calendar, including recurring OOF events, using the outOfOffice eventType.
/**
This is based on the example provided at:
https://developers.google.com/apps-script/samples/automations/vacation-calendar
The following changes were made:
- Uses the outOfOffice eventType to identify events to sync. It does not use a keyword search.
- Events use the full names of members, pulled from the Google Workspace API (AdminDirectory.)
- Recurring OOF events are supported.
- Creates "shadow events" instead of importing the actual events, to circumvent "forbidden" errors in some cases, and issues with recurring events.
# Setup:
## Create a team vacation calendar
- Open Google Calendar.
- Create a new calendar, name it something like "Team Vacations."
- In the calendar's settings, under Integrate calendar, copy the Calendar ID.
## Create the Apps Script project
- Create a new Apps Script project at https://script.google.com.
- Under files, create a new script, and paste the body of this gist.
- Change TEAM_CALENDAR_ID to the Calendar ID you copied above.
- Change GROUP_EMAIL to the email address of a Google Group containing your team members.
- Under services, add Google Calendar and AdminDirectory.
## Run the script
- From your script file, in the function dropdown, select setup.
- Click Run.
- Authorize the script if prompted.
Once complete, return to Google Calendar and confirm your Team Vacations calendar has populated with events.
**/
let TEAM_CALENDAR_ID = '...';
let GROUP_EMAIL = '...';
function setup() {
let triggers = ScriptApp.getProjectTriggers();
if (triggers.length === 0) {
ScriptApp.newTrigger('sync').timeBased().everyHours(1).create();
}
sync();
}
function sync() {
let today = new Date();
let maxDate = new Date();
let lastRun = PropertiesService.getScriptProperties().getProperty('lastRun');
let users = GroupsApp.getGroupByEmail(GROUP_EMAIL).getUsers();
let countImported = 0;
let countUsers = 0;
today.setMonth(today.getMonth() - 1);
maxDate.setMonth(maxDate.getMonth() + 3);
lastRun = lastRun ? new Date(lastRun) : null;
users.forEach(function(user) {
countUsers++;
let email = user.getEmail();
let contact = AdminDirectory.Users.get(email, {fields:'name', viewType:'domain_public'});
let events = [];
console.log("Updating " + contact.name.fullName + "...");
events = findEvents(email, today, maxDate, lastRun);
events.forEach(function (event) {
importEvent(contact.name.fullName, event);
countImported++;
});
});
PropertiesService.getScriptProperties().setProperty('lastRun', today);
console.log('Imported ' + countImported + ' events from ' + countUsers + ' users.');
}
function findEvents(email, start, end, optSince) {
let pageToken = null;
let events = [];
let params = {
timeMin: formatDateAsRFC3339(start),
timeMax: formatDateAsRFC3339(end),
showDeleted: true,
};
if (optSince) {
params.updatedMin = formatDateAsRFC3339(optSince);
}
do {
let response;
params.pageToken = pageToken;
try {
response = Calendar.Events.list(email, params);
} catch (e) {
console.error('Error retrieving events for %s, %s: %s; skipping', email, e.toString());
continue;
}
response.items.forEach(function(occurrence) {
let occurrenceId = occurrence.id;
let recurring = false;
if (occurrence.recurringEventId || occurrence.iCalUID !== occurrence.id + '@google.com') {
recurring = true;
}
event = Calendar.Events.get(email, occurrenceId);
if(event.eventType == "outOfOffice" && (!event.organizer || event.organizer.email == "unknownorganizer@calendar.google.com")) {
if (!event.attendees) {
return false;
}
let matching = event.attendees.filter(function(attendee) {
return attendee.self;
});
if (matching.length > 0 && matching[0].responseStatus == 'accepted') {
let unique = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, JSON.stringify({id: occurrenceId, owner: email})).reduce(function (str,chr) {
chr = (chr < 0 ? chr + 256 : chr).toString(16);
return str + (chr.length==1?'0':'') + chr;
},'');
let shadow = {
id: unique,
iCalUID: unique + '@google.com',
description: '',
summary: '',
start: event.start,
end: event.end,
status: event.status,
eventType: 'outOfOffice',
};
if (recurring) {
shadow.description = 'Recurring. ';
}
if (event.summary.length) {
shadow.description += event.summary;
if (shadow.description.slice(-1) !== '.') {
shadow.description += '.';
}
}
events.push(shadow);
}
}
});
pageToken = response.nextPageToken;
} while (pageToken);
return events;
}
function importEvent(name, event) {
event.summary = name + ' OOF';
event.organizer = {
id: TEAM_CALENDAR_ID,
};
sorted = Object.keys(event)
.sort()
.reduce((acc, key) => ({
...acc, [key]: event[key]
}), {})
console.log('Importing: %s', JSON.stringify(sorted));
try {
Calendar.Events.import(event, TEAM_CALENDAR_ID);
} catch (e) {
console.error('Error attempting to import event: %s. Skipping.', e.toString());
}
}
function formatDateAsRFC3339(date) {
return Utilities.formatDate(date, 'UTC', 'yyyy-MM-dd\'T\'HH:mm:ssZ');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment