Last active
February 27, 2024 15:51
-
-
Save hamon-e/d5a653a5ae7865e45ca90cb81252ec1b to your computer and use it in GitHub Desktop.
sync/merge google calendars
This file contains 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
// Calendars to merge from. | |
// "[X]" is what is placed in front of your calendar event in the shared calendar. | |
// Use "" if you want none. | |
const CALENDARS_TO_MERGE = [ | |
{ | |
"name": "", | |
"id": "", | |
"color": INTEGER | |
}, | |
{ | |
"name": "", | |
"id": "", | |
"color": INTEGER | |
} | |
] | |
// The ID of the shared calendar | |
const CALENDAR_TO_MERGE_INTO = "" | |
// Number of days in the past and future to sync. | |
const SYNC_DAYS_IN_PAST = INTEGER; | |
const SYNC_DAYS_IN_FUTURE = INTEGER; | |
// Unique character to use in the title of the event to identify it as a clone. | |
// This is used to delete the old events. | |
// https://unicode-table.com/en/200B/ | |
const SEARCH_CHARACTER = '\u200B'; | |
// ---------------------------------------------------------------------------- | |
// DO NOT TOUCH FROM HERE ON | |
// ---------------------------------------------------------------------------- | |
const ENDPOINT_BASE = 'https://www.googleapis.com/calendar/v3/calendars'; | |
function SyncCalendarsIntoOne() { | |
// Midnight today | |
const startTime = new Date(); | |
startTime.setHours(0, 0, 0, 0); | |
startTime.setDate(startTime.getDate() - SYNC_DAYS_IN_PAST); | |
const endTime = new Date(); | |
endTime.setHours(0, 0, 0, 0); | |
endTime.setDate(endTime.getDate() + SYNC_DAYS_IN_FUTURE + 1); | |
syncEvents(startTime, endTime); | |
} | |
function submitRequest(requestBody, action){ | |
if (requestBody && requestBody.length) { | |
const result = new BatchRequest({ | |
batchPath: 'batch/calendar/v3', | |
requests: requestBody, | |
}); | |
for (res of result) { | |
if (res.error) { | |
console.log(res) | |
} | |
} | |
console.log(`${result.length} events ${action}d.`); | |
} else { | |
console.log(`No events to ${action}.`); | |
} | |
} | |
function syncEvents(startTime, endTime) { | |
let requestBody_delete = []; | |
// get target calendar events | |
let target_events_tmp = Calendar.Events.list(CALENDAR_TO_MERGE_INTO, { | |
timeMin: startTime.toISOString(), | |
timeMax: endTime.toISOString(), | |
singleEvents: true, | |
orderBy: 'startTime', | |
}) | |
const target_events = target_events_tmp.items.filter((event) => event.summary.includes(SEARCH_CHARACTER)); | |
while (target_events_tmp.nextPageToken) { | |
target_events_tmp = Calendar.Events.list(CALENDAR_TO_MERGE_INTO, { | |
timeMin: startTime.toISOString(), | |
timeMax: endTime.toISOString(), | |
singleEvents: true, | |
orderBy: 'startTime', | |
pageToken: target_events_tmp.nextPageToken, | |
}); | |
target_events.push(...target_events_tmp.items.filter((event) => event.summary.includes(SEARCH_CHARACTER))) | |
} | |
for (let calendar of CALENDARS_TO_MERGE) { | |
// get source calendar events | |
const calendarId = calendar.id; | |
if (!CalendarApp.getCalendarById(calendarId)) { | |
console.log("Calendar not found: '%s'.", calendarId); | |
continue; | |
} | |
let source_events_tmp = Calendar.Events.list(calendarId, { | |
timeMin: startTime.toISOString(), | |
timeMax: endTime.toISOString(), | |
singleEvents: true, | |
orderBy: 'startTime', | |
}); | |
// If nothing is found, move to next calendar | |
if (!(source_events_tmp.items && source_events_tmp.items.length > 0)) { | |
continue; | |
} | |
const source_events = source_events_tmp.items | |
while (source_events_tmp.nextPageToken) { | |
source_events_tmp = Calendar.Events.list(calendarId, { | |
timeMin: startTime.toISOString(), | |
timeMax: endTime.toISOString(), | |
singleEvents: true, | |
orderBy: 'startTime', | |
pageToken: source_events_tmp.nextPageToken, | |
}); | |
source_events.push(...source_events_tmp.items) | |
} | |
// delete target event if its not in the source calendar anymore | |
for (target_event of target_events) { | |
if (target_event.colorId == calendar.color) { | |
const source_event = source_events.filter((candidate_event) => (toId(candidate_event.id) == target_event.id))[0]; | |
if (!source_event){ | |
requestBody_delete.push({ | |
method: 'DELETE', | |
endpoint: `${ENDPOINT_BASE}/${CALENDAR_TO_MERGE_INTO}/events/${target_event.id}`, | |
}); | |
} | |
} | |
} | |
submitRequest(requestBody_delete, "delete") | |
// check if target event exists and update it if it does. if not, then create it. | |
requestBody_update = [] | |
requestBody_create = [] | |
let new_description = "" | |
source_events.forEach((source_event) => { | |
// Only consider events that are not "free". | |
if (true || source_event.transparency !== 'transparent') { | |
const target_event = target_events.filter((candidate_event) => (candidate_event.id == toId(source_event.id)))[0]; | |
// if target event is found, update it. else create it. | |
if (target_event) { | |
if (target_event.summary != `${SEARCH_CHARACTER}${source_event.summary} ${calendar.name}` || | |
target_event.location != source_event.location || | |
target_event.description != source_event.description || | |
target_event.start.dateTime != source_event.start.dateTime || | |
target_event.end.dateTime != source_event.end.dateTime){ | |
requestBody_update.push({ | |
method: 'PUT', | |
endpoint: `${ENDPOINT_BASE}/${CALENDAR_TO_MERGE_INTO}/events/${target_event.id}`, | |
requestBody: { | |
summary: `${SEARCH_CHARACTER}${source_event.summary} ${calendar.name}`, | |
location: source_event.location, | |
description: source_event.description, | |
start: source_event.start, | |
end: source_event.end, | |
colorId: calendar.color, | |
}, | |
}); | |
} | |
} else { | |
requestBody_create.push({ | |
method: 'POST', | |
endpoint: `${ENDPOINT_BASE}/${CALENDAR_TO_MERGE_INTO}/events`, | |
requestBody: { | |
summary: `${SEARCH_CHARACTER}${source_event.summary} ${calendar.name}`, | |
location: source_event.location, | |
description: source_event.description, | |
start: source_event.start, | |
end: source_event.end, | |
id: toId(source_event.id), | |
colorId: calendar.color | |
}, | |
}); | |
} | |
} | |
}); | |
submitRequest(requestBody_update, "update") | |
submitRequest(requestBody_create, "create") | |
} | |
} | |
function toId(str) { | |
return str.toLowerCase().replace('_', '').replace('z', '') | |
} | |
function deleteEvents() { | |
// Midnight today | |
const startTime = new Date(); | |
startTime.setHours(0, 0, 0, 0); | |
startTime.setDate(startTime.getDate() - SYNC_DAYS_IN_PAST); | |
const endTime = new Date(); | |
endTime.setHours(0, 0, 0, 0); | |
endTime.setDate(endTime.getDate() + SYNC_DAYS_IN_FUTURE + 1); | |
const sharedCalendar = CalendarApp.getCalendarById(CALENDAR_TO_MERGE_INTO) | |
// Find events with the search character in the title. | |
// The `.filter` method is used since the getEvents method seems to return all events at the moment. It's a safety check. | |
const events = sharedCalendar | |
.getEvents(startTime, endTime, { search: SEARCH_CHARACTER }) | |
.filter((event) => event.getTitle().includes(SEARCH_CHARACTER)) | |
const requestBody = events.map((e, i) => ({ | |
method: "DELETE", | |
endpoint: `${ENDPOINT_BASE}/${CALENDAR_TO_MERGE_INTO}/events/${e.getId().replace("@google.com", "")}`, | |
})) | |
if (requestBody && requestBody.length) { | |
const result = new BatchRequest({ | |
useFetchAll: true, | |
batchPath: "batch/calendar/v3", | |
requests: requestBody, | |
}) | |
if (result.length !== requestBody.length) { | |
console.log(result) | |
} | |
console.log(`${result.length} deleted events between ${startTime} and ${endTime}.`) | |
} else { | |
console.log("No events to delete.") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment