Last active
April 22, 2025 13:00
-
-
Save cmbuckley/587f913627c843f4becc8c5db9d4a82e to your computer and use it in GitHub Desktop.
Revisions
-
cmbuckley revised this gist
Apr 22, 2025 . 1 changed file with 98 additions and 28 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -4,8 +4,40 @@ const config = { 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); } @@ -14,32 +46,50 @@ 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, { @@ -48,26 +98,46 @@ function copyEvents(sourceId) { 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); }); @@ -76,8 +146,8 @@ function copyEvents(sourceId) { console.log('All events copied'); } function cleanup(targetId, daysAhead, titlePrefix) { let calendar = CalendarApp.getCalendarById(targetId); let startDate = new Date(); let endDate = new Date(); -
cmbuckley revised this gist
Jul 29, 2022 . 1 changed file with 0 additions and 3 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -72,9 +72,6 @@ function copyEvents(sourceId) { Utilities.sleep(500); }); lock.releaseLock(); console.log('All events copied'); } -
cmbuckley revised this gist
Jul 29, 2022 . 1 changed file with 8 additions and 4 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -50,27 +50,31 @@ function copyEvents(sourceId) { // loop over and copy listResponse.items.forEach(function (event) { const summary = (event.summary || '(No title)'); console.log('Copying event: ' + summary + ' (' + (event.start.dateTime || event.start.date) + ')'); 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, attendees: event.attendees ? [{ email: targetId, responseStatus: event.attendees.find(a => a.self).responseStatus }] : [] }, targetId, { conferenceDataVersion: 1 }); Utilities.sleep(500); }); Utilities.sleep(500); }); lock.releaseLock(); console.log('All events copied'); } -
cmbuckley revised this gist
Jul 29, 2022 . 1 changed file with 2 additions and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -38,7 +38,8 @@ function copyEvents(sourceId) { endDate.setDate(startDate.getDate() + syncDays); // delete and recreate in case things move // @todo if there's no prefix, it'll delete everything in your calendar! if (summaryPrefix) { cleanup(syncDays, summaryPrefix); } // get all events const listResponse = Calendar.Events.list(sourceId, { -
cmbuckley revised this gist
Jul 29, 2022 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -30,6 +30,7 @@ function copyEvents(sourceId) { console.log('Copying events from ' + sourceId); const summaryPrefix = (config[sourceId].prefix ? `[${config[sourceId].prefix}] ` : ''); const syncDays = config[sourceId].syncDays || 14; const targetId = Session.getActiveUser().getEmail(); // start and end dates let startDate = new Date(); -
cmbuckley revised this gist
Jul 29, 2022 . 1 changed file with 6 additions and 15 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -6,19 +6,6 @@ const config = { }, }; function onCalendarChanged(trigger) { copyEvents(trigger.calendarId); } @@ -70,8 +57,12 @@ function copyEvents(sourceId) { description: event.description, conferenceData: event.conferenceData, colorId: config[sourceId].color || 0, visibility: config[sourceId].visibility || event.visibility, attendees: [{ email: targetId, responseStatus: event.attendees.find(a => a.self).responseStatus }] }, targetId, { conferenceDataVersion: 1 }); -
cmbuckley revised this gist
Jul 21, 2022 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -61,7 +61,7 @@ function copyEvents(sourceId) { // loop over and copy listResponse.items.forEach(function (event) { console.log('Copying event: ' + event.summary + ' (' + (event.start.dateTime || event.start.date) + ')'); Calendar.Events.insert({ start: event.start, -
cmbuckley revised this gist
Jul 20, 2022 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -15,7 +15,7 @@ function doGet() { return HtmlService.createHtmlOutput(` <h1>Copy Calendar Events</h1> <p>Your script is now configured. You may now close this window.<p> `); } -
cmbuckley revised this gist
Jul 20, 2022 . 1 changed file with 13 additions and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -6,6 +6,19 @@ const config = { }, }; function doGet() { ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t)); ScriptApp.newTrigger('onCalendarChanged') .forUserCalendar(Session.getActiveUser().getEmail()) .onEventUpdated() .create(); return HtmlService.createHtmlOutput(` <h1>Copy Calendar Events</h1> <p>Your script is now configured.<p> `); } function onCalendarChanged(trigger) { copyEvents(trigger.calendarId); } -
cmbuckley revised this gist
Jul 20, 2022 . 1 changed file with 4 additions and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -15,6 +15,10 @@ function copyAll() { } function copyEvents(sourceId) { if (!config[sourceId]) { throw new Error('Missing config for ' + sourceId); } // avoid multiple scripts running at the same time const lock = LockService.getScriptLock(); lock.tryLock(1000); -
cmbuckley revised this gist
Jul 20, 2022 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -46,7 +46,7 @@ function copyEvents(sourceId) { listResponse.items.forEach(function (event) { console.log('Copying event: ' + event.summary + ' (' + event.start + ')'); Calendar.Events.insert({ start: event.start, end: event.end, summary: summaryPrefix + event.summary, -
cmbuckley revised this gist
Jul 20, 2022 . No changes.There are no files selected for viewing
-
cmbuckley revised this gist
Jul 20, 2022 . 1 changed file with 0 additions and 4 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -6,10 +6,6 @@ const config = { }, }; function onCalendarChanged(trigger) { copyEvents(trigger.calendarId); } -
cmbuckley revised this gist
Jul 20, 2022 . 1 changed file with 0 additions and 42 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,42 +0,0 @@ -
cmbuckley revised this gist
Jul 20, 2022 . No changes.There are no files selected for viewing
-
cmbuckley revised this gist
Jul 20, 2022 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -6,7 +6,7 @@ when use multiple work calendars and need colleages to see your true availabilit ## Installation 1. In your source calendar, click the **⋮** actions button next to the calendar, and click **Settings and sharing**. 2. Scroll to "Share with specific people", and add the email address of your target calendar. 3. Choose "See all event details" if you want the target calendar to contain the full event details; otherwise select "See only free/busy (hide details)". 4. You will receive an email to your target account sharing this calendar. Click **Add this calendar**. 5. Open [Google Apps Script](https://script.google.com/) and log in as your target account. -
cmbuckley created this gist
Jul 20, 2022 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,42 @@ # 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 colleages to see your true availability. ## Installation 1. In your source calendar, click the **⋮** actions button next to the calendar, and click **Settings and sharing**. - 2. Scroll to "Share with specific people", and add the email address of your target calendar. 3. Choose "See all event details" if you want the target calendar to contain the full event details; otherwise select "See only free/busy (hide details)". 4. You will receive an email to your target account sharing this calendar. Click **Add this calendar**. 5. Open [Google Apps Script](https://script.google.com/) and log in as your target account. 6. Click "New project" to create the project for the script. 7. Click "Untitled project" at the top to give the project a name, such as "Copy calendar events", and click **Rename**. 8. Copy the contents of `copyCalendarEvents.gs` into the file created. Update the configuration appropriately (see below). 9. Click "Save project". 10. Next to "Services" click **+** to add a service. 11. Select "Google Calendar API" and click **Add**. 12. Click **Deploy**, then **New deployment**. 13. Click the cog next to "Select type" and choose "Web app". 14. From the left navigation, click **Triggers**, then click **Add Trigger**. 15. Select the following options for the trigger: * Choose which function to run: **onCalendarChanged** * Choose which deployment should run: **Head** * Select event source: **From calendar** * Calendar owner email: *Email address of the source calendar* * Failure notification settings: **Notify me immediately** 16. Click **Save**. 17. In the popup that opens (you may need to allow popups for this to work), sign in as your target account and click **Allow**. 18. Click **Save** again to save the trigger. ## Configuration The script begins with a config object taking the following options: | Paramter | Value | Description | |--------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `<source_calendar>` | string | Email address of the source calendar. The target calendar must be permitted to view this calendar. | | `<source_calendar>.prefix` | string | A prefix for the event titles. If the prefix is `Prefix`, the title is prefixed with `[Prefix] `. Defaults to empty string, which adds no prefix to the title. | | `<source_calendar>.color` | integer | One of the [`EventColor`](https://developers.google.com/apps-script/reference/calendar/event-color) enum values. Defaults to 0, which does not change the event's colour. | | `<source_calendar>.visibility` | string | One of `"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` | integer | How many days ahead to sync calendar events. Defaults to 14. | 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,91 @@ const config = { '[email protected]': { prefix: 'Source', color: CalendarApp.EventColor.PALE_RED, visibility: 'private' }, }; function doGet() { return HtmlService.createHtmlOutput('<h1>Hello, world!</h1>'); } function onCalendarChanged(trigger) { copyEvents(trigger.calendarId); } function copyAll() { Object.keys(config).forEach(copyEvents); } function copyEvents(sourceId) { // avoid multiple scripts running at the same time const lock = LockService.getScriptLock(); lock.tryLock(1000); if (!lock.hasLock()) { console.log('Process already running'); return; } console.log('Copying events from ' + sourceId); const summaryPrefix = (config[sourceId].prefix ? `[${config[sourceId].prefix}] ` : ''); const syncDays = config[sourceId].syncDays || 14; // 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(syncDays, summaryPrefix); // get all events const listResponse = Calendar.Events.list(sourceId, { timeMin: startDate.toISOString(), timeMax: endDate.toISOString(), singleEvents: true, }); // loop over and copy listResponse.items.forEach(function (event) { console.log('Copying event: ' + event.summary + ' (' + event.start + ')'); const n = Calendar.Events.insert({ start: event.start, end: event.end, summary: summaryPrefix + event.summary, description: event.description, conferenceData: event.conferenceData, colorId: config[sourceId].color || 0, visibility: config[sourceId].visibility || event.visibility }, Session.getActiveUser().getEmail(), { conferenceDataVersion: 1 }); Utilities.sleep(500); }); lock.releaseLock(); console.log('All events copied'); } function cleanup(daysAhead, titlePrefix) { let calendar = CalendarApp.getCalendarById(Session.getActiveUser().getEmail()); 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); } }); }