Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save PetkevichPavel/eac9087bd84899ca8f1f92b7b3b3a8c7 to your computer and use it in GitHub Desktop.

Select an option

Save PetkevichPavel/eac9087bd84899ca8f1f92b7b3b3a8c7 to your computer and use it in GitHub Desktop.
Example Calendars
/************************************
* 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