Skip to content

Instantly share code, notes, and snippets.

@hamon-e
Last active February 27, 2024 15:51
Show Gist options
  • Save hamon-e/d5a653a5ae7865e45ca90cb81252ec1b to your computer and use it in GitHub Desktop.
Save hamon-e/d5a653a5ae7865e45ca90cb81252ec1b to your computer and use it in GitHub Desktop.
sync/merge google calendars
// 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