Last active
April 22, 2025 13:00
-
-
Save cmbuckley/587f913627c843f4becc8c5db9d4a82e to your computer and use it in GitHub Desktop.
This file contains hidden or 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
const config = { | |
'[email protected]': { | |
prefix: 'Source', | |
color: CalendarApp.EventColor.PALE_RED, | |
visibility: 'private' | |
}, | |
'[email protected]': { | |
target: 'Other Calendar Name', | |
attendees: false, | |
prefix: 'Company', | |
filterColor: CalendarApp.EventColor.YELLOW, | |
}, | |
}; | |
function getCalendars() { | |
let pageToken; | |
let calendars = []; | |
do { | |
const list = Calendar.CalendarList.list({pageToken, showHidden: true}); | |
pageToken = list.pageToken; | |
calendars = calendars.concat(list.items); | |
} while (pageToken); | |
calendars.sort(compareUsing(sortableId)); | |
return calendars; | |
} | |
function compareUsing(fn) { | |
return ((a, b) => fn(a).localeCompare(fn(b))); | |
} | |
function sortableId(calendar) { | |
return calendar.id.split('@').reverse().join('@'); | |
} | |
function colorName(id) { | |
return Object.keys(CalendarApp.EventColor).find(c => CalendarApp.EventColor[c] == id); | |
} | |
function onCalendarChanged(trigger) { | |
copyEvents(trigger.calendarId); | |
} | |
function copyAll() { | |
Object.keys(config).forEach(copyEvents); | |
} | |
// avoid multiple scripts running at the same time | |
function getLock(retries = 3) { | |
try { | |
const lock = LockService.getScriptLock(); | |
lock.tryLock(1000); | |
return lock; | |
} catch (err) { | |
if (retries) { | |
return getLock(retries - 1); | |
} | |
throw new Error('Could not obtain script lock'); | |
} | |
} | |
function copyEvents(sourceId) { | |
const lock = getLock(); | |
if (!lock.hasLock()) { | |
console.log('Process already running'); | |
return; | |
} | |
if (!config[sourceId]) { | |
console.log('No config for ' + sourceId); | |
return; | |
} | |
console.log('Copying events from ' + sourceId); | |
const summaryPrefix = (config[sourceId].prefix ? `[${config[sourceId].prefix}] ` : ''); | |
const syncDays = config[sourceId].syncDays || 14; | |
// set target calendar | |
let targetId = Session.getActiveUser().getEmail(); | |
if (config[sourceId].target) { | |
targetId = getCalendars().find(c => c.summary == config[sourceId].target).id; | |
} | |
// start and end dates | |
let startDate = new Date(); | |
let endDate = new Date(); | |
endDate.setDate(startDate.getDate() + syncDays); | |
// delete and recreate in case things move | |
cleanup(targetId, syncDays, summaryPrefix); | |
// get all events | |
const listResponse = Calendar.Events.list(sourceId, { | |
timeMin: startDate.toISOString(), | |
timeMax: endDate.toISOString(), | |
singleEvents: true, | |
}); | |
const targetEventIds = Calendar.Events.list(targetId, { | |
timeMin: startDate.toISOString(), | |
timeMax: endDate.toISOString(), | |
singleEvents: true | |
}).items.map(e => e.id); | |
// loop over and copy | |
listResponse.items.forEach(function (event) { | |
const summary = (event.summary || 'busy'); | |
console.log('Copying event: ' + summary + ' (' + (event.start.dateTime || event.start.date) + ')'); | |
if (targetEventIds.includes(event.id)) { | |
console.log('Event is already shared with the target calendar, ignoring'); | |
return; | |
} | |
if (config[sourceId].filterColor && config[sourceId].filterColor != event.colorId) { | |
console.log('Ignoring event with colour: ' + (colorName(event.colorId) || 'default')); | |
return; | |
} | |
try { | |
Calendar.Events.insert({ | |
start: event.start, | |
end: event.end, | |
summary: summaryPrefix + summary, | |
description: event.description, | |
conferenceData: event.conferenceData, | |
colorId: config[sourceId].color || 0, | |
visibility: config[sourceId].visibility || event.visibility || 'default', | |
attendees: event.attendees ? [{ | |
email: targetId, | |
responseStatus: event.attendees.find(a => a.self).responseStatus | |
}] : [] | |
}, targetId, { | |
conferenceDataVersion: 1 | |
}); | |
} catch (err) { | |
console.log(err.toString()); | |
} | |
Utilities.sleep(500); | |
}); | |
lock.releaseLock(); | |
console.log('All events copied'); | |
} | |
function cleanup(targetId, daysAhead, titlePrefix) { | |
let calendar = CalendarApp.getCalendarById(targetId); | |
let startDate = new Date(); | |
let endDate = new Date(); | |
endDate.setDate(startDate.getDate() + daysAhead); | |
calendar.getEvents(startDate, endDate).forEach(function (event) { | |
if (event.getTitle().startsWith(titlePrefix)) { | |
console.log('Deleting event: ' + event.getTitle() + ' (' + event.getStartTime().toLocaleString('en-GB') + ')'); | |
try { | |
event.deleteEvent(); | |
} catch (e) { | |
console.log('Failed to delete: ' + e); | |
} | |
Utilities.sleep(500); | |
} | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Copy Calendar Events
This Apps Script copies calendar events from any number of source calendars. This can be useful
when use multiple work calendars and need colleagues to see your true availability.
Note
This script currently deletes events from your target calendar and recreates them. Not only is this a naïve approach, it is risky that it might delete events not part of the sync that just happen to have a similar title.
Installation
Code.gs
with the contents ofcopyCalendarEvents.gs
. Update the configuration appropriately (see below).Configuration
The script begins with a config object taking the following options:
<source_calendar>
<source_calendar>.prefix
Prefix
, the title is prefixed with[Prefix]
. Defaults to empty string, which adds no prefix to the title.<source_calendar>.color
EventColor
enum values. Defaults to 0, which does not change the event's colour.<source_calendar>.visibility
"default"
,"public"
or"private"
. Overrides the source calendar event's visibility, for instance to hide sensitive information. Defaults to the event's existing visibility.<source_calendar>.syncDays