Last active
March 2, 2022 19:43
-
-
Save markelliot/fffed05369b0029ed939b36eabe5f735 to your computer and use it in GitHub Desktop.
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 script will synchronize two Google-hosted calendars as a Google Apps Script. | |
* | |
* Personal events will show on your work calendar by default with the label "Blocked (Personal)". | |
* Work events will show on your personal calendar by default with the label "Blocked (Work)". | |
* | |
* The description of the "Blocked" events contains an identifier that allows the script to update | |
* event times if they change. | |
* | |
* Note that this script will not copy or update events where: | |
* - you have not accepted the invite | |
* - the start date is in the past | |
* - the start date is after `today + daysToSync` | |
* - if `includeWeekends` is false and the event occurs on a weekend | |
* | |
* As a result, events that were eligible to be synchronized (e.g. because you had accepted them) that | |
* are no longer eligible to be synchronized (e.g. because you have now declined) will not be updated | |
* or removed. | |
* | |
* Cancellations of a meeting from either calendar will result in deleting a previously synchronized event. | |
* | |
* Additionally, out of respect for your work's information security policies, no work event information is | |
* copied. | |
* | |
* To deploy: | |
* - Share your personal calendar with your work Google account and ensure permissions allow "Make changes to events" | |
* - Navigate to https://drive.google.com for your _work_ account | |
* - Select "New" | |
* - Select "More" | |
* - Select "Google Apps Script" | |
* - Paste the content of the script into the editor | |
* - Update the settings below | |
* (your personalCalendarId will be something like you@gmail.com) | |
* (your workCalendarId will be something like you@company.com) | |
* - Save your changes | |
* - Configure a Trigger (nav panel on the left) to run as frequently as you like | |
*/ | |
// Settings | |
const personalCalendarId = "you@gmail.com"; | |
const workCalendarId = "you@company.com" | |
const daysToSync = 30; | |
const blockedForPersonal = "Blocked (Personal)"; | |
const blockedForWork = "Blocked (Work)"; | |
const includeWeekends = false; | |
// Constants | |
const SYNC_ID_PREFIX = "sync-id:" | |
function events(calendarId, today, endDate) { | |
const events = Calendar.Events.list(calendarId, { | |
// return instances of recurring events rather than the recurring event | |
singleEvents: true, | |
timeMin: today.toISOString(), | |
timeMax: endDate.toISOString(), | |
// try to avoid having to page through results, hopefully less than 2500 events in 30 days! | |
maxResults: 2500 | |
}); | |
// TODO(markelliot): should check events.nextPageToken is undefined to prove we don't need to iterate through pages | |
return events.items | |
// canceled recurring events may still appear in result | |
.filter(e => e.start !== undefined) | |
} | |
function deletedEvents(calendarId, today, endDate) { | |
const events = Calendar.Events.list(calendarId, { | |
showDeleted: true, | |
// return instances of recurring events rather than the recurring event | |
singleEvents: true, | |
timeMin: today.toISOString(), | |
timeMax: endDate.toISOString(), | |
// try to avoid having to page through results, hopefully less than 2500 events in 30 days! | |
maxResults: 2500 | |
}); | |
// TODO(markelliot): should check events.nextPageToken is undefined to prove we don't need to iterate through pages | |
return events.items | |
// canceled recurring events may still appear in result | |
.filter(e => e.status == "cancelled") | |
} | |
function eventDetails(start, end, id, summary) { | |
return { | |
start: start, | |
end: end, | |
summary: summary, | |
description: SYNC_ID_PREFIX + id, | |
reminders: { | |
overrides: [], | |
useDefault: false | |
} | |
} | |
} | |
function copyEvents(fromEvents, toEvents, fromName, toName, toCalendarId, summary) { | |
fromEvents | |
// skip events with no title | |
.filter(e => e.summary !== undefined) | |
// skip events that this script synchronizes | |
.filter(e => e.description === undefined || !e.description.startsWith(SYNC_ID_PREFIX)) | |
// skip multi-attendee events that aren't accepted by the current user | |
.filter(e => { | |
const me = e.attendees !== undefined ? e.attendees.find(a => a.self) : undefined; | |
return me === undefined || me.responseStatus === "accepted"; | |
}) | |
.filter(e => { | |
const dayOfWeek = new Date(e.start.dateTime).getDay(); | |
return includeWeekends || (1 <= dayOfWeek && dayOfWeek <= 5); | |
}) | |
.forEach(e => { | |
const copy = toEvents.find(we => we.description !== undefined && we.description.startsWith(SYNC_ID_PREFIX + e.id)); | |
if (copy !== undefined) { | |
// we've already copied this event from -> to, check if we need to update the time: | |
if (copy.start.dateTime !== e.start.dateTime || copy.end.dateTime !== e.end.dateTime) { | |
console.log(`Updating ${toName} calendar with new ${fromName} event time for event: ${e.summary}`); | |
Calendar.Events.update(eventDetails(e.start, e.end, e.id, summary), toCalendarId, copy.id); | |
} | |
} else { | |
// we haven't seen this one before, so copy it | |
console.log(`Copying ${fromName} calendar event to ${toName} calendar: ${e.summary}`) | |
Calendar.Events.insert(eventDetails(e.start, e.end, e.id, summary), toCalendarId); | |
} | |
}); | |
} | |
function removeEvents(deletedEvents, toName, toCalendarId, toEvents) { | |
deletedEvents | |
.forEach(e => { | |
const toDelete = toEvents.find(we => we.description !== undefined && we.description.startsWith(SYNC_ID_PREFIX + e.id)); | |
if (toDelete !== undefined) { | |
console.log(`Deleting ${e.summary} (${e.start.dateTime}) from ${toName}`) | |
Calendar.Events.remove(toCalendarId, toDelete.id); | |
} | |
}); | |
} | |
function main() { | |
const today = new Date(); | |
const endDate = new Date(today.getTime() + daysToSync * 86400000); | |
const personalEvents = events(personalCalendarId, today, endDate); | |
const workEvents = events(workCalendarId, today, endDate); | |
copyEvents(personalEvents, workEvents, "personal", "work", workCalendarId, blockedForPersonal); | |
copyEvents(workEvents, personalEvents, "work", "personal", personalCalendarId, blockedForWork); | |
const deletedWorkEvents = deletedEvents(workCalendarId, today, endDate); | |
const deletedPersonalEvents = deletedEvents(personalCalendarId, today, endDate); | |
removeEvents(deletedWorkEvents, "personal", personalCalendarId, personalEvents) | |
removeEvents(deletedPersonalEvents, "work", workCalendarId, workEvents) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment