Last active
February 16, 2023 00:43
-
-
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
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 == "[email protected]")) { | |
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