Skip to content

Instantly share code, notes, and snippets.

@markelliot
Last active March 2, 2022 19: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 markelliot/fffed05369b0029ed939b36eabe5f735 to your computer and use it in GitHub Desktop.
Save markelliot/fffed05369b0029ed939b36eabe5f735 to your computer and use it in GitHub Desktop.
/* 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