diff --git a/README.md b/README.md index 9c0f921..4580290 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,16 @@ This is a Google App Script that helps sync your personal calendar to your work 14. Under "Select event source": choose "From calendar" 15. In the blank of "Calendar owner email", paste your personal email id, and press "Save" 16. You are now good to go! + + + +## Ongoing work + +I am thinking to add more features to this script. + +1) The ability to restrict to certain times (i.e. 6AM to 6PM), but an event is considered INSIDE that time zone if ANY part of an event overlaps with that code. +2) More options + 1) copy over the subject name with a prefix [COPY] - ORIGINAL SUBJECT NAME (i.e.) + 2) copy over the location + 3) copy over the description / notes. +3) Automate Testing diff --git a/sync_my_calendar.js b/sync_my_calendar.js index ac1ada1..d5a94fb 100644 --- a/sync_my_calendar.js +++ b/sync_my_calendar.js @@ -1,31 +1,44 @@ +const src_calendar_id = "Your_Personal_Calendar_ID"; // for example your personal calendar id +const res_calendar_id = "Your_Work_Calendar_ID"; // for example your work calendar id +const busy_block_Title = "Personal Busy Block"; +const is_verbose = false; +const is_full_sync = false; + +// Script starts here function myFunction() { - const src_calendar_id = "Your_Personal_Calendar_ID"; - const res_calendar_id = "Your_Work_Calendar_ID"; - const busyBlockTitle = "Personal Busy Block"; - syncEvents(src_calendar_id, res_calendar_id, busyBlockTitle, false); + syncEvents(src_calendar_id, res_calendar_id, busy_block_Title, is_full_sync); +} + +// wrapper function for console.log to control print out debug message or not +function log(...args) { + if (is_verbose) { + console.log(...args); + } } -function syncEvents(src_calendarID, res_calendarID, busyBlockTitle, isFullSync) { +// wrapper function to sync events from src calendar to res calendar +function syncEvents(src_calendarID, res_calendarID, busy_block_Title, is_full_sync) { let properties = PropertiesService.getUserProperties(); let options = {}; - let syncToken; + let syncToken = null; try { // try to get syncToken stored in PropertiesService syncToken = properties.getProperty('syncToken'); - console.log('get synctoken: %s', syncToken); + log('get synctoken: %s', syncToken); } catch (err) { - console.log('Fail to get syncToken property'); - isFullSync = true; + log('Fail to get syncToken'); + is_full_sync = true; } - if (!isFullSync && syncToken) { // if isFullSync === false and syncToken exists + if (!is_full_sync && syncToken) { // if is_full_sync === false and syncToken exists options.syncToken = syncToken; // add syncToken to options } - // Retrieve events one page at a time let events; - let pageToken; + let pageToken = null; + + // Retrieve events one page at a time do { - try { + try { // try to retrieve events options.pageToken = pageToken; events = Calendar.Events.list(src_calendarID, options); } catch (e) { @@ -33,7 +46,7 @@ function syncEvents(src_calendarID, res_calendarID, busyBlockTitle, isFullSync) // if so, perform a full sync instead. if (e.message === 'Sync token is no longer valid, a full sync is required.') { properties.deleteProperty('syncToken'); - syncEvents(src_calendarID, res_calendarID, busyBlockTitle, true); + syncEvents(src_calendarID, res_calendarID, busy_block_Title, true); return; } throw new Error(e.message); @@ -41,80 +54,68 @@ function syncEvents(src_calendarID, res_calendarID, busyBlockTitle, isFullSync) // if there is no changes in the calendar if (events.items && events.items.length === 0) { - console.log('No event changes found.'); + log('No event changes found.'); return; } - console.log('The number of changes: %d', events.items.length); + log('The number of changes: %d', events.items.length); for (const event of events.items) { // for each changes in the source calendar // search for recurring events id in res calendar - const res_recur_event_id = search_recur_event_id(res_calendarID, event.id, event.recurringEventId); // search in res calendar - - if (event.status === 'cancelled') { - // if event is cancelled: 1) cancel single event or recurring all event; 2) cancel a recurring event exception - console.log('Event ID %s was cancelled', event.id); - console.log('event recurrence and recurEventId: %s, %s', event.recurrence, event.recurringEventId); - // check if cancelled event is recurring event - // if yes, print id, recurringEventId, originalStartTime - // if it is a recurring event + const res_recur_event_id = search_recur_event_id(res_calendarID, event.id, event.recurringEventId); + + if (event.status === 'cancelled') { // if event is cancelled + + log('Event ID %s was cancelled', event.id); + log('event recurrence and recurEventId: %s, %s', event.recurrence, event.recurringEventId); + // if event is cancelled: 1) cancel a recurring event exception; 2) cancel single event or recurring all event; + // 1) check if cancelled event is recurring event if ((!event.recurrence) ^ (!event.recurringEventId)) { - //===================== - console.log('Trying to cancel a recurring event exception in res calendar.=====') - //===================== - console.log('In cancel_recur_event function') cancel_recur_event(res_calendarID, res_recur_event_id, event); continue; } - - // if it is a cancelled single event or cancelled all recurring event - console.log('Trying to cancel a single event or cancel recurring all in res calendar ====') - //===================== - const existed_Events = search_res_event(res_calendarID, event.id); + // 2) cancel single event or recurring all event + const existed_Events = search_res_event(res_calendarID, event.id); // find this event in res calendar if (existed_Events === null) { - console.log('Error: the target event to be deleted does not exist in res calendar'); + log('Error: the target event to be deleted does not exist in res calendar'); } else if (existed_Events.items.length > 1) { - console.log('Error: res calendar has multiple busy blocks link to same event_id'); + log('Error: res calendar has multiple busy blocks link to same event_id'); } else { // existed_Events.items.length === 1 - const event_to_delete = existed_Events.items[0]; - Calendar.Events.remove(res_calendarID, event_to_delete.id); - console.log('Successfully remove a single event/recurring all in res calendar'); + const event_to_delete = existed_Events.items[0]; + Calendar.Events.remove(res_calendarID, event_to_delete.id); // remove this event + log('Successfully remove a single event/recurring all event in res calendar'); } continue; } else { // if event is created/changed - console.log('Trying to create/change a event=====') - //===================== - // if it is a recurring event + // Case 1: if it is a recurring event if ((!event.recurrence) ^ (!event.recurringEventId)) { - if (!res_recur_event_id) { // no existed event in res calendar - console.log('Trying to create a new recurring event ====') - create_one_recur_event(res_calendarID, busyBlockTitle, event); + if (!res_recur_event_id) { // if there's no existed event in res calendar + log('Trying to create a new recurring event ====') + create_one_recur_event(res_calendarID, busy_block_Title, event); } else { // else: change/update a recurring event - console.log('Trying to change a recurring event======') - change_recur_event(res_calendarID, res_recur_event_id, busyBlockTitle, event); + log('Trying to change a recurring event======') + change_recur_event(res_calendarID, res_recur_event_id, busy_block_Title, event); } continue; } + // Case 2: if it is a single event + log('single event: %s, %s', event.recurrence, event.recurringEventId); + log('Trying to create/change a single event=====') - //===================== - // deal with single event - console.log('single event: %s, %s', event.recurrence, event.recurringEventId); - console.log('Trying to create/change a single event=====') - - // if it is an all-day event + // Case 2.1: if it is an all-day event if (event.start.date) { // check if event existed in res calendar const existed_Events = search_res_event(res_calendarID, event.id); if (existed_Events === null) { // if event does not exist in res calendar // create all-day block in res calendar - create_allday_event(res_calendarID, busyBlockTitle, event); - console.log('Create new all-day event %s', event.start.date); + create_allday_event(res_calendarID, busy_block_Title, event); + log('Create new all-day event %s', event.start.date); } else if (existed_Events.items.length > 1) { - console.log('Error: res calendar has multiple all-day busy blocks link to same event_id'); + log('Error: res calendar has multiple all-day busy blocks link to same event_id'); } else { // existed_Events.items.length === 1 // update the existed event const event_to_change = existed_Events.items[0]; // get the event in res calendar @@ -131,20 +132,19 @@ function syncEvents(src_calendarID, res_calendarID, busyBlockTitle, isFullSync) }, res_calendarID, event_to_change.id ); - console.log(`Change an all-day event, ID: %s`, event_to_change.id); + log(`Change an all-day event, ID: %s`, event_to_change.id); } continue; } - // For timed events - // check if event existed in res calendar - const existed_Events = search_res_event(res_calendarID, event.id); + // Case 2.2: if it is a timed event + const existed_Events = search_res_event(res_calendarID, event.id); // check if event existed in res calendar if (existed_Events === null) { // create new block in res calendar // create timed block in res calendar - create_timed_event(res_calendarID, busyBlockTitle, event); - console.log('Create new timed event %s', event.start.dateTime); + create_timed_event(res_calendarID, busy_block_Title, event); + log('Create new timed event %s', event.start.dateTime); } else if (existed_Events.items.length > 1) { - console.log('Error: Existed multiple timed busy blocks link to same event_id in src calendar'); + log('Error: Existed multiple timed busy blocks link to same event_id in src calendar'); } else { // existed_Events.items.length === 1 // change the start and end of the existed event const event_to_change = existed_Events.items[0]; @@ -161,13 +161,12 @@ function syncEvents(src_calendarID, res_calendarID, busyBlockTitle, isFullSync) }, res_calendarID, event_to_change.id ); - console.log(`Change a timed event, ID: %s`, event_to_change.id); - + log('Change a timed event, ID: %s', event_to_change.id); } } } - pageToken = events.nextPageToken; + pageToken = events.nextPageToken; // update pageToken } while (pageToken); // continue if there is next page @@ -180,13 +179,12 @@ function syncEvents(src_calendarID, res_calendarID, busyBlockTitle, isFullSync) * Function to find an event in res calendar and return the * event list found * -* Precondition: -* res_calendar_ID: result calendar ID -* src_event_ID: the source event ID +* Arguments: +* - res_calendar_ID: result calendar ID +* - src_event_ID: the source event ID * -* Postcondition: -* return event lists -* if no events found: null +* Returns: +* - return event lists (if no events found: return null) * */ function search_res_event(res_calendar_ID, src_event_ID) { @@ -197,23 +195,35 @@ function search_res_event(res_calendar_ID, src_event_ID) { ); if (!res_events || res_events.items.length === 0) { - console.log('No existed busy blocks found in res calendar.'); + log('No existed busy blocks found in res calendar.'); return null; } - console.log('Number of event found with extended property: %d', res_events.items.length); + log('Number of event found with extended property: %d', res_events.items.length); for (const event of res_events.items) { - console.log('res event shared extended property: %s', event.extendedProperties); + log('res event shared extended property: %s', event.extendedProperties); } + return res_events; } - -function create_timed_event(res_calendar_ID, busyBlockTitle, src_event) { +/** +* Function to create a timed event +* +* Arguments: +* - res_calendar_ID: result calendar ID +* - busy_block_Title +* - src_event: source event +* +* Returns: +* - None +* +*/ +function create_timed_event(res_calendar_ID, busy_block_Title, src_event) { // event details for creating event. let event = { - summary: busyBlockTitle, + summary: busy_block_Title, start: { dateTime: src_event.start.dateTime, timeZone: src_event.start.timeZone @@ -231,17 +241,29 @@ function create_timed_event(res_calendar_ID, busyBlockTitle, src_event) { try { event = Calendar.Events.insert(event, res_calendar_ID); - console.log('Create res event, ID: %s', event.id); + log('Create res event, ID: %s', event.id); } catch (err) { - console.log('Failed with error: %s', err.message); + log('Failed with error: %s', err.message); } } -function create_allday_event(res_calendar_ID, busyBlockTitle, src_event) { +/** +* Function to create an all-day event +* +* Arguments: +* - res_calendar_ID: result calendar ID +* - busy_block_Title +* - src_event: source event +* +* Returns: +* - None +* +*/ +function create_allday_event(res_calendar_ID, busy_block_Title, src_event) { // event details for creating event. let new_event = { - summary: busyBlockTitle, + summary: busy_block_Title, start: { date: src_event.start.date }, @@ -257,18 +279,30 @@ function create_allday_event(res_calendar_ID, busyBlockTitle, src_event) { try { new_event = Calendar.Events.insert(new_event, res_calendar_ID); - console.log('Create res event, ID: %s', new_event.id); + log('Create res event, ID: %s', new_event.id); } catch (err) { - console.log('Failed with error: %s', err.message); + log('Failed with error: %s', err.message); } } -function create_one_recur_event(res_calendar_id, busyBlockTitle, src_event) { +/** +* Function to create a recurring event +* +* Arguments: +* - res_calendar_ID: result calendar ID +* - busy_block_Title +* - src_event: source event +* +* Returns: +* - None +* +*/ +function create_one_recur_event(res_calendar_id, busy_block_Title, src_event) { // if it is an all-day recurring event if (src_event.start.date) { Calendar.Events.insert({ - summary: busyBlockTitle, + summary: busy_block_Title, start: { date: src_event.start.date, timeZone: src_event.start.timeZone @@ -285,11 +319,11 @@ function create_one_recur_event(res_calendar_id, busyBlockTitle, src_event) { }, }, res_calendar_id ); - console.log('Create an all-day recurring event'); + log('Create an all-day recurring event'); } else { // if it is a timed recurring event Calendar.Events.insert({ - summary: busyBlockTitle, + summary: busy_block_Title, start: { dateTime: src_event.start.dateTime, timeZone: src_event.start.timeZone @@ -306,17 +340,28 @@ function create_one_recur_event(res_calendar_id, busyBlockTitle, src_event) { }, }, res_calendar_id ); - console.log('Create a timed recurring event'); + log('Create a timed recurring event'); } } -function change_recur_event(res_calendar_id, res_recur_event_id, busyBlockTitle, src_event) { - // try to access orignalStart time; if error ==> change this and following +/** +* Function to change a recurring event +* +* Arguments: +* - res_calendar_ID: result calendar ID +* - res_recur_event_id: recurring events id in res calendar +* - busy_block_Title +* - src_event: source event +* +* Returns: +* - None +*/ +function change_recur_event(res_calendar_id, res_recur_event_id, busy_block_Title, src_event) { let instance; + // try to access orignalStart time; if error ==> change this and following try { - const original = src_event.originalStartTime; - console.log('src_event id :%s', src_event.id); + log('src_event id :%s', src_event.id); if (src_event.start.date) { // if it is an all-day recurring event instance = Calendar.Events.instances(res_calendar_id, res_recur_event_id, { @@ -331,22 +376,22 @@ function change_recur_event(res_calendar_id, res_recur_event_id, busyBlockTitle, ); } if (instance.items.length === 0) { - console.log('Error: could not find corresponding event in res calendar'); + log('Error: could not find corresponding event in res calendar'); return; // break out of the function } else if (instance.items.length > 1) { - console.log('Error: more than one corresponding events in res calendar'); + log('Error: more than one corresponding events in res calendar'); return; // break out of the function } else { // instance.items.length === 0 instance = instance.items[0] } const rec_rule = src_event.recurrence ? src_event.recurrence : ""; - console.log('recurrence rule: %s', rec_rule); + log('recurrence rule: %s', rec_rule); if (src_event.start.date) { // if it is an all-day recurring event Calendar.Events.update( { - summary: busyBlockTitle, + summary: busy_block_Title, start: { date: src_event.start.date, timeZone: src_event.start.timeZone @@ -358,11 +403,11 @@ function change_recur_event(res_calendar_id, res_recur_event_id, busyBlockTitle, recurrence: rec_rule, }, res_calendar_id, instance.id ); - console.log('Successfully patch an all-day recurring event'); + log('Successfully patch an all-day recurring event'); } else { // it is a timed recurring event Calendar.Events.update( { - summary: busyBlockTitle, + summary: busy_block_Title, start: { dateTime: src_event.start.dateTime, timeZone: src_event.start.timeZone @@ -374,11 +419,11 @@ function change_recur_event(res_calendar_id, res_recur_event_id, busyBlockTitle, recurrence: rec_rule, }, res_calendar_id, instance.id ); - console.log('Successfully patch a timed recurring event'); + log('Successfully patch a timed recurring event'); } } catch (err) { // changes have been made to recurring event by 'this and following' - console.log('original time not defined error======'); + log('original time not defined error======'); instance = Calendar.Events.instances(res_calendar_id, res_recur_event_id, { @@ -391,7 +436,7 @@ function change_recur_event(res_calendar_id, res_recur_event_id, busyBlockTitle, if (src_event.start.date) { // if it is an all-day event Calendar.Events.update( { - summary: busyBlockTitle, + summary: busy_block_Title, start: { date: src_event.start.date, timeZone: src_event.start.timeZone @@ -409,11 +454,11 @@ function change_recur_event(res_calendar_id, res_recur_event_id, busyBlockTitle, }, res_calendar_id, res_recur_event_id ); - console.log('update an all-day recurring event'); + log('update an all-day recurring event'); } else { Calendar.Events.update( { - summary: busyBlockTitle, + summary: busy_block_Title, start: { dateTime: src_event.start.dateTime, timeZone: src_event.start.timeZone @@ -431,28 +476,36 @@ function change_recur_event(res_calendar_id, res_recur_event_id, busyBlockTitle, }, res_calendar_id, res_recur_event_id ); - console.log('update a timed recurring event'); + log('update a timed recurring event'); } } } + /** -* Function: search recurring event id in the res calendar -* Precondition: res calendar id, source event id, source event recurring event id -* Postcondition: return the recurring event id in the res calendar +* Function to search recurring event id in the res calendar +* +* Arguments: +* - res_calendar_ID: result calendar ID +* - src_event_id: event id in source calendar +* - src_recur_event_id: recurring event id in source calendar +* +* Returns: +* - res_recur_event_id: recurring event id in the res calendar */ -function search_recur_event_id(res_calendarID, src_event_id, src_event_recurring_id) { +function search_recur_event_id(res_calendarID, src_event_id, src_recur_event_id) { + let res_recur_event_id = null; // determine the real source recurring Event id let recurringEventId; - if (src_event_id && src_event_recurring_id) { - recurringEventId = src_event_recurring_id; + if (src_event_id && src_recur_event_id) { + recurringEventId = src_recur_event_id; } else { recurringEventId = src_event_id; } // search source recurring Event id in res calendar at shared extended properites - console.log('Search for shared properties in res calendar========'); + log('Search for shared properties in res calendar========'); const events = Calendar.Events.list(res_calendarID, { sharedExtendedProperty: 'src_event_id=' + recurringEventId, @@ -461,54 +514,68 @@ function search_recur_event_id(res_calendarID, src_event_id, src_event_recurring ); if (!events || events.items.length === 0) { - console.log('No recurring events found in res calendar'); - return null; + log('No recurring events found in res calendar'); + return res_recur_event_id; + } else { + res_recur_event_id = events.items[0].id; } for (const event of events.items) { - console.log('event shared extended property in res calendar: %s', event.extendedProperties); - console.log('recurring event id in res calendar: %s', event.recurringEventId); - console.log('recurrence rule in res calendar: %s', event.recurrence); - console.log('event id in res calendar: %s', event.id); + log('event shared extended property in res calendar: %s', event.extendedProperties); + log('recurring event id in res calendar: %s', event.recurringEventId); + log('recurrence rule in res calendar: %s', event.recurrence); + log('event id in res calendar: %s', event.id); } - return events.items[0].id; + return res_recur_event_id; } - +/** +* Function to cancel a recurring event in the res calendar +* +* Arguments: +* - res_calendar_ID: result calendar ID +* - res_recur_event_id: recurring events id in res calendar +* - src_event: source event +* +* Returns: +* - None +*/ function cancel_recur_event(res_calendar_id, res_recur_event_id, src_event) { let instance; - console.log('src event original start %s', src_event.originalStartTime.dateTime) - console.log('src_event id :%s', src_event.id); - if (src_event.originalStartTime.date) { // if it is an all-day recurring event + log('src event original start %s', src_event.originalStartTime.dateTime) + log('src_event id :%s', src_event.id); + + // if it is an all-day recurring event + if (src_event.originalStartTime.date) { console.log('it is an all-day event') instance = Calendar.Events.instances(res_calendar_id, res_recur_event_id, { originalStart: src_event.originalStartTime.date } ); - } else { - console.log('this is a timed event') + } else { // if it is a timed event + log('it is a timed event') instance = Calendar.Events.instances(res_calendar_id, res_recur_event_id, { originalStart: src_event.originalStartTime.dateTime } ); - console.log('instance length: %d', instance.items.length) + log('instance length: %d', instance.items.length) } if (instance.items.length === 0) { - console.log('Error: could not find corresponding event in res calendar'); - return; // break out of the function + log('Error: could not find corresponding event in res calendar'); + return; } else if (instance.items.length > 1) { - console.log('Error: more than one corresponding events in res calendar'); - return; // break out of the function + log('Error: more than one corresponding events in res calendar'); + return; } else { // instance.items.length === 0 instance = instance.items[0] } - //cancel one event =================== + // if it is an all-day event if (instance.start.date) { Calendar.Events.update( { @@ -531,7 +598,7 @@ function cancel_recur_event(res_calendar_id, res_recur_event_id, src_event) { }, res_calendar_id, instance.id ); - } else { + } else { // if it is a timed event Calendar.Events.update( { summary: src_event.summary, @@ -554,6 +621,6 @@ function cancel_recur_event(res_calendar_id, res_recur_event_id, src_event) { res_calendar_id, instance.id ); } - console.log('Successfully cancelled one recurring event exception, id: %s', instance.id); + log('Successfully cancelled one recurring event exception, id: %s', instance.id); }