Skip to content

Instantly share code, notes, and snippets.

@cmbuckley
Last active April 22, 2025 13:00
Show Gist options
  • Save cmbuckley/587f913627c843f4becc8c5db9d4a82e to your computer and use it in GitHub Desktop.
Save cmbuckley/587f913627c843f4becc8c5db9d4a82e to your computer and use it in GitHub Desktop.

Revisions

  1. cmbuckley revised this gist Apr 22, 2025. 1 changed file with 98 additions and 28 deletions.
    126 changes: 98 additions & 28 deletions copyCalendarEvents.gs
    Original 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);
    }

    function copyEvents(sourceId) {
    if (!config[sourceId]) {
    throw new Error('Missing config for ' + sourceId);
    // 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');
    }
    // avoid multiple scripts running at the same time
    const lock = LockService.getScriptLock();
    lock.tryLock(1000);
    }

    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;
    const targetId = Session.getActiveUser().getEmail();

    // 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
    // @todo if there's no prefix, it'll delete everything in your calendar!
    if (summaryPrefix) { cleanup(syncDays, summaryPrefix); }
    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 || '(No title)');
    const summary = (event.summary || 'busy');
    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
    });
    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(daysAhead, titlePrefix) {
    let calendar = CalendarApp.getCalendarById(Session.getActiveUser().getEmail());
    function cleanup(targetId, daysAhead, titlePrefix) {
    let calendar = CalendarApp.getCalendarById(targetId);

    let startDate = new Date();
    let endDate = new Date();
  2. cmbuckley revised this gist Jul 29, 2022. 1 changed file with 0 additions and 3 deletions.
    3 changes: 0 additions & 3 deletions copyCalendarEvents.gs
    Original file line number Diff line number Diff line change
    @@ -72,9 +72,6 @@ function copyEvents(sourceId) {
    Utilities.sleep(500);
    });

    Utilities.sleep(500);
    });

    lock.releaseLock();
    console.log('All events copied');
    }
  3. cmbuckley revised this gist Jul 29, 2022. 1 changed file with 8 additions and 4 deletions.
    12 changes: 8 additions & 4 deletions copyCalendarEvents.gs
    Original 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) {
    console.log('Copying event: ' + event.summary + ' (' + (event.start.dateTime || event.start.date) + ')');
    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 + event.summary,
    summary: summaryPrefix + summary,
    description: event.description,
    conferenceData: event.conferenceData,
    colorId: config[sourceId].color || 0,
    visibility: config[sourceId].visibility || event.visibility,
    attendees: [{
    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');
    }
  4. cmbuckley revised this gist Jul 29, 2022. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion copyCalendarEvents.gs
    Original 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
    cleanup(syncDays, summaryPrefix);
    // @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, {
  5. cmbuckley revised this gist Jul 29, 2022. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions copyCalendarEvents.gs
    Original 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();
  6. cmbuckley revised this gist Jul 29, 2022. 1 changed file with 6 additions and 15 deletions.
    21 changes: 6 additions & 15 deletions copyCalendarEvents.gs
    Original file line number Diff line number Diff line change
    @@ -6,19 +6,6 @@ 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. You may now close this window.<p>
    `);
    }

    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
    }, Session.getActiveUser().getEmail(), {
    visibility: config[sourceId].visibility || event.visibility,
    attendees: [{
    email: targetId,
    responseStatus: event.attendees.find(a => a.self).responseStatus
    }]
    }, targetId, {
    conferenceDataVersion: 1
    });

  7. cmbuckley revised this gist Jul 21, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion copyCalendarEvents.gs
    Original 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 + ')');
    console.log('Copying event: ' + event.summary + ' (' + (event.start.dateTime || event.start.date) + ')');

    Calendar.Events.insert({
    start: event.start,
  8. cmbuckley revised this gist Jul 20, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion copyCalendarEvents.gs
    Original 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.<p>
    <p>Your script is now configured. You may now close this window.<p>
    `);
    }

  9. cmbuckley revised this gist Jul 20, 2022. 1 changed file with 13 additions and 0 deletions.
    13 changes: 13 additions & 0 deletions copyCalendarEvents.gs
    Original 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);
    }
  10. cmbuckley revised this gist Jul 20, 2022. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions copyCalendarEvents.gs
    Original 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);
  11. cmbuckley revised this gist Jul 20, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion copyCalendarEvents.gs
    Original 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 + ')');

    const n = Calendar.Events.insert({
    Calendar.Events.insert({
    start: event.start,
    end: event.end,
    summary: summaryPrefix + event.summary,
  12. cmbuckley revised this gist Jul 20, 2022. No changes.
  13. cmbuckley revised this gist Jul 20, 2022. 1 changed file with 0 additions and 4 deletions.
    4 changes: 0 additions & 4 deletions copyCalendarEvents.gs
    Original file line number Diff line number Diff line change
    @@ -6,10 +6,6 @@ const config = {
    },
    };

    function doGet() {
    return HtmlService.createHtmlOutput('<h1>Hello, world!</h1>');
    }

    function onCalendarChanged(trigger) {
    copyEvents(trigger.calendarId);
    }
  14. cmbuckley revised this gist Jul 20, 2022. 1 changed file with 0 additions and 42 deletions.
    42 changes: 0 additions & 42 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,42 +0,0 @@
    # 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. |
  15. cmbuckley revised this gist Jul 20, 2022. No changes.
  16. cmbuckley revised this gist Jul 20, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original 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.
    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.
  17. cmbuckley created this gist Jul 20, 2022.
    42 changes: 42 additions & 0 deletions README.md
    Original 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. |
    91 changes: 91 additions & 0 deletions copyCalendarEvents.gs
    Original 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);
    }
    });
    }