Created
August 24, 2025 17:51
-
-
Save PetkevichPavel/eac9087bd84899ca8f1f92b7b3b3a8c7 to your computer and use it in GitHub Desktop.
Example Calendars
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
| /************************************ | |
| * Calendar Sync Template (sanitized) | |
| * - Copies events between two calendars as "Busy" blocks. | |
| * - Skips outside working hours. | |
| * - Removes orphaned "Busy" blocks if source event disappears. | |
| * Customize the CONFIG section only. | |
| ************************************/ | |
| const CONFIG = { | |
| // === Who are we syncing? Use calendar IDs (email address or actual Calendar ID string) === | |
| CALENDAR_A_ID: "user-a@example.com", // e.g., primary work calendar | |
| CALENDAR_B_ID: "user-b@example.com", // e.g., secondary work/personal calendar | |
| // === Time window to sync === | |
| DAYS_AHEAD: 20, | |
| // === Working hours (24h) to allow execution, inclusive start, exclusive end === | |
| WORK_HOURS: { start: 8, end: 22 }, | |
| // === Exclusions when copying FROM B -> A === | |
| // Titles that if they appear anywhere in an event title, the event is skipped | |
| EXCLUDE_TITLES_FROM_B: [ | |
| // "Daily Standup", "Team Lunch" | |
| ], | |
| // Titles that if they start with these prefixes, the event is skipped | |
| EXCLUDE_PREFIXES_FROM_B: [ | |
| // "I:", "[Private]" | |
| ], | |
| // === Colors for created "Busy" events, per target calendar ID === | |
| COLOR_BY_TARGET: { | |
| // e.g. "user-a@example.com": "1", // Blue | |
| // e.g. "user-b@example.com": "6" // Orange | |
| }, | |
| // === Title used for mirrored blocks === | |
| MIRROR_TITLE: "Busy", | |
| // === Enable logs? === | |
| VERBOSE: true | |
| }; | |
| /** | |
| * Entry point for a time-based trigger. | |
| * Skips work outside configured hours. | |
| */ | |
| function syncAndCleanCalendars() { | |
| const now = new Date(); | |
| const hour = now.getHours(); | |
| const { start, end } = CONFIG.WORK_HOURS; | |
| if (hour < start || hour >= end) { | |
| log(`β³ Outside working hours (${hour}:00), skipping.`); | |
| return; | |
| } | |
| log(`π Executing at ${hour}:00`); | |
| syncCalendars(); | |
| } | |
| function syncCalendars() { | |
| // Remove stale mirrored events first (keeps calendars tidy) | |
| removeOrphanedBusyEvents(); | |
| const calA = CalendarApp.getCalendarById(CONFIG.CALENDAR_A_ID); | |
| const calB = CalendarApp.getCalendarById(CONFIG.CALENDAR_B_ID); | |
| const now = new Date(); | |
| const future = new Date(now); | |
| future.setDate(future.getDate() + CONFIG.DAYS_AHEAD); | |
| const eventsA = calA.getEvents(now, future); | |
| const eventsB = calB.getEvents(now, future); | |
| // Copy A -> B (no exclusions by default) | |
| copyEvents({ | |
| sourceEvents: eventsA, | |
| targetCalendar: calB, | |
| targetCalendarId: CONFIG.CALENDAR_B_ID | |
| }); | |
| // Copy B -> A (with exclusions) | |
| copyEvents({ | |
| sourceEvents: eventsB, | |
| targetCalendar: calA, | |
| targetCalendarId: CONFIG.CALENDAR_A_ID, | |
| excludeTitles: CONFIG.EXCLUDE_TITLES_FROM_B, | |
| excludePrefixes: CONFIG.EXCLUDE_PREFIXES_FROM_B | |
| }); | |
| } | |
| /** | |
| * Copy source events to target calendar as MIRROR_TITLE blocks. | |
| * Skips events by title/prefix and avoids duplicates by exact time match. | |
| */ | |
| function copyEvents({ | |
| sourceEvents, | |
| targetCalendar, | |
| targetCalendarId, | |
| excludeTitles = [], | |
| excludePrefixes = [] | |
| }) { | |
| const now = new Date(); | |
| const future = new Date(now); | |
| future.setDate(future.getDate() + CONFIG.DAYS_AHEAD); | |
| const targetEvents = targetCalendar.getEvents(now, future).map(e => ({ | |
| start: e.getStartTime().getTime(), | |
| end: e.getEndTime().getTime(), | |
| title: e.getTitle() | |
| })); | |
| sourceEvents.forEach(event => { | |
| const start = event.getStartTime().getTime(); | |
| const end = event.getEndTime().getTime(); | |
| const title = event.getTitle() || ""; | |
| // Exclude by "contains" | |
| const excludedByTitle = excludeTitles.some(keyword => | |
| title.toLowerCase().includes(String(keyword).toLowerCase()) | |
| ); | |
| // Exclude by prefix | |
| const excludedByPrefix = excludePrefixes.some(prefix => | |
| title.startsWith(prefix) | |
| ); | |
| if (excludedByTitle || excludedByPrefix) { | |
| log(`β Skip excluded: "${title}" (${event.getStartTime()})`); | |
| return; | |
| } | |
| // Avoid duplicates by start/end exact match | |
| const exists = targetEvents.some(te => te.start === start && te.end === end); | |
| if (!exists) { | |
| const newEvent = targetCalendar.createEvent( | |
| CONFIG.MIRROR_TITLE, | |
| new Date(start), | |
| new Date(end) | |
| ); | |
| const color = getColorForCalendar(targetCalendarId); | |
| if (color) newEvent.setColor(color); | |
| log(`β Mirrored to ${targetCalendarId}: ${new Date(start)} β ${new Date(end)}`); | |
| } | |
| }); | |
| } | |
| function getColorForCalendar(targetCalendarId) { | |
| return CONFIG.COLOR_BY_TARGET[targetCalendarId] || ""; // empty = default color | |
| } | |
| /** | |
| * Delete mirrored "Busy" events that no longer have a source counterpart. | |
| * (Keeps only events with exact start/end matches) | |
| */ | |
| function removeOrphanedBusyEvents() { | |
| const calA = CalendarApp.getCalendarById(CONFIG.CALENDAR_A_ID); | |
| const calB = CalendarApp.getCalendarById(CONFIG.CALENDAR_B_ID); | |
| const now = new Date(); | |
| const future = new Date(now); | |
| future.setDate(future.getDate() + CONFIG.DAYS_AHEAD); | |
| const eventsA = calA.getEvents(now, future); | |
| const eventsB = calB.getEvents(now, future); | |
| // helper: build a map of start-end keys from a list of events | |
| const toMap = (events) => { | |
| const m = new Map(); | |
| events.forEach(e => { | |
| m.set(`${e.getStartTime().getTime()}-${e.getEndTime().getTime()}`, true); | |
| }); | |
| return m; | |
| }; | |
| // Remove mirrored blocks in B that don't exist in A | |
| removeOrphans({ | |
| targetCalendar: calB, | |
| sourceMap: toMap(eventsA), | |
| label: `in ${CONFIG.CALENDAR_B_ID}` | |
| }); | |
| // Remove mirrored blocks in A that don't exist in B | |
| removeOrphans({ | |
| targetCalendar: calA, | |
| sourceMap: toMap(eventsB), | |
| label: `in ${CONFIG.CALENDAR_A_ID}` | |
| }); | |
| } | |
| function removeOrphans({ targetCalendar, sourceMap, label }) { | |
| const now = new Date(); | |
| const future = new Date(now); | |
| future.setDate(future.getDate() + CONFIG.DAYS_AHEAD); | |
| const targetEvents = targetCalendar.getEvents(now, future); | |
| targetEvents.forEach(e => { | |
| if (e.getTitle() === CONFIG.MIRROR_TITLE) { | |
| const key = `${e.getStartTime().getTime()}-${e.getEndTime().getTime()}`; | |
| if (!sourceMap.has(key)) { | |
| log(`π Removing orphaned "${CONFIG.MIRROR_TITLE}" ${label}: ${e.getStartTime()}`); | |
| e.deleteEvent(); | |
| } | |
| } | |
| }); | |
| } | |
| function log(msg) { | |
| if (CONFIG.VERBOSE) Logger.log(msg); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment