Last active
July 29, 2026 14:29
-
-
Save ewerybody/73e8f11f8073e44e79ae6b333edb6c1a to your computer and use it in GitHub Desktop.
selectiveCalendarSync - Google App Script
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 characters
| function selectiveCalendarSync() { | |
| const startTime = Date.now(); | |
| // Number of days to include in the sync: | |
| const NUM_DAYS = 14; | |
| // Lower case exclude trigger strings to keep matching events from being synced: | |
| const EXCLUDE = ["focus time", "home", "bob ooo", "alice pto"]; | |
| // Universal name for synced events? Make it empty to use original event name: | |
| const NEW_NAME = ""; | |
| // Also sync whole day events without start and end time?: | |
| const FULL_DAY_EVENTS = false; | |
| // Log more or less? | |
| const VERBOSE = true; | |
| const now = new Date(); | |
| const end = new Date(); | |
| end.setDate(now.getDate() + NUM_DAYS); | |
| // Fill with `Calendar ID` from "Integrate calendar" Settings section of your TARGET calendar: | |
| const targetCal = getCalendar( | |
| "1234567890...abcdefghijklmno@group.calendar.google.com", | |
| "target", | |
| ); | |
| if (!targetCal) { | |
| return; | |
| } | |
| // Fill with `Calendar ID` from "Integrate calendar" Settings section of your SOURCE calendar: | |
| // Or keep empty to use your DEFAULT calendar! | |
| const sourceCal = getCalendar("", "source"); | |
| if (!sourceCal) { | |
| return; | |
| } | |
| const targetEvents = targetCal.getEvents(now, end); | |
| const targetMap = {}; | |
| targetEvents.forEach((event) => { | |
| targetMap[makeEventKey(event)] = event; | |
| }); | |
| const sourceEvents = sourceCal.getEvents(now, end); | |
| const keysToSync = new Set(); | |
| if (VERBOSE) Logger.log(`Running selectiveCalendarSync over ${sourceEvents.length} source events from:\n ${now} to\n ${end} ...`); | |
| const myMailAddress = Session.getActiveUser().getEmail(); | |
| sourceEvents.forEach((event) => { | |
| if (skipEvent(event, EXCLUDE, FULL_DAY_EVENTS, VERBOSE, myMailAddress)) { | |
| return; | |
| } | |
| const key = makeEventKey(event); | |
| keysToSync.add(key); | |
| const title = event.getTitle(); | |
| const newTitle = NEW_NAME || title; | |
| const date = getEventDateStr(event); | |
| if (!targetMap[key]) { | |
| if (event.isAllDayEvent()) { | |
| targetCal.createAllDayEvent(newTitle, event.getAllDayStartDate()); | |
| } else { | |
| targetCal.createEvent( | |
| newTitle, | |
| event.getStartTime(), | |
| event.getEndTime(), | |
| ); | |
| } | |
| if (VERBOSE) Logger.log(`${date} Synced event: "${title}"`); | |
| } else { | |
| if (VERBOSE) Logger.log(`${date} Already synced: "${title}"`); | |
| } | |
| }); | |
| targetEvents.forEach((event) => { | |
| const key = makeEventKey(event); | |
| if (!keysToSync.has(key)) { | |
| event.deleteEvent(); | |
| if (VERBOSE) { | |
| Logger.log(`${date} Removed stale event: ${event.getTitle()}`); | |
| } | |
| } | |
| }); | |
| const durationSec = ((Date.now() - startTime) / 1000).toFixed(2); | |
| Logger.log(`Sync finished in ${durationSec} seconds.`); | |
| } | |
| // Create simple 'key' to identify events from start and end time. | |
| // Excluding title is not safe to overbooking with identical times but | |
| // safe for our renaming and good enough for now. | |
| function makeEventKey(event) { | |
| return `${event.getStartTime().getTime()}_${event.getEndTime().getTime()}`; | |
| } | |
| // all skipping rules in one place with optional logging | |
| function skipEvent(event, excludeKeywords, fullDays, verbose, myMailAddress) { | |
| const title = event.getTitle(); | |
| const date = getEventDateStr(event); | |
| if (!fullDays && event.isAllDayEvent()) { | |
| if (verbose) Logger.log(`${date} Skipping: "${title}" is full day!`); | |
| return true; | |
| } | |
| // Skip based on event status. There is OWNER, YES, NO, INVITED, MAYBE | |
| const status = event.getMyStatus(); | |
| if (status === CalendarApp.GuestStatus.OWNER) { | |
| const selfGuest = event.getGuestByEmail(myMailAddress); | |
| if (selfGuest && selfGuest.getGuestStatus() === CalendarApp.GuestStatus.NO) { | |
| if (verbose) Logger.log(`${date} Skipping: "${title}" status OWNER, but declined!`); | |
| return true; | |
| } | |
| } else if (status !== CalendarApp.GuestStatus.YES) { | |
| if (verbose) Logger.log(`${date} Skipping: "${title}" status: ${status}`); | |
| return true; | |
| } | |
| const titleLower = title.toLowerCase(); | |
| const matchedKeyword = excludeKeywords.find((kw) => titleLower.includes(kw)); | |
| if (matchedKeyword) { | |
| if (verbose) Logger.log(`${date} Skipping: "${title}" matching keyword ${matchedKeyword}`); | |
| return true; | |
| } | |
| return false; | |
| } | |
| function getCalendar(calendarId, name) { | |
| const calObject = calendarId | |
| ? CalendarApp.getCalendarById(calendarId) | |
| : CalendarApp.getDefaultCalendar(); | |
| if (!calObject) { | |
| Logger.log(`ERROR: Could not find ${name} calendar by ID "${calendarId}"`); | |
| } | |
| return calObject; | |
| } | |
| function getEventDateStr(event) { | |
| const date = event.isAllDayEvent() ? event.getAllDayStartDate() : event.getStartTime(); | |
| return Utilities.formatDate(date, Session.getScriptTimeZone(), "MMM d"); | |
| } |
ewerybody
commented
Jul 29, 2026
Author
- fixed OWN events that are declined.
- added ${date} to logging.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment