Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save yuhui/a3103d31c505a41aa9db58ae7192b894 to your computer and use it in GitHub Desktop.
Save yuhui/a3103d31c505a41aa9db58ae7192b894 to your computer and use it in GitHub Desktop.
Add this script to a Google Apps Script project.
See the JSdoc at `autoDeclineMeetings()` for trigger setup instructions.
const CALENDAR_ID = 'primary';
const DAYS_OF_WEEK = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
];
const INTERNAL_DOMAINS = [
'domain1.com',
'domain2.com',
];
const MANAGER_EMAIL_ADDRESSES = [
'[email protected]',
'[email protected]',
];
const MEETING_DECLINE_REASON = 'Declined because there is no agenda.';
/**
* Main function.
* When there is an internal meeting without a description, then auto-decline it.
*
* Use this with a Trigger. Configure the trigger as follows:
* - Event source: "From calendar"
* - Event details: "Calendar updated", your own email address
*/
function autoDeclineMeetings() {
const meetings = getMeetingsWithoutDescriptions_();
meetings.forEach((meeting) => {
const { id, summary, creator, start, end } = meeting;
const log = [
`${summary} (${id})`,
`When: ${start} - ${end}`,
`Creator: ${JSON.stringify(creator)}`,
'--',
];
Logger.log(`${log.join('\n')}`);
meeting.attendees.forEach((attendee) => {
if (attendee.self) {
attendee.responseStatus = 'declined';
attendee.comment = `${MEETING_DECLINE_REASON}`;
}
});
Calendar.Events.update(meeting, CALENDAR_ID, meeting.id, {}, { 'If-Match': meeting.etag });
const { email } = creator;
const replySubject = `Declined: ${summary} @ ${stringifyEventDateTime_(meeting)} (${email})`;
const replyBody = `${MEETING_DECLINE_REASON}`;
mail_(email, replySubject, replyBody, email);
// also, send an email to self as a notification of the auto-decline
const mailTo = `${Session.getActiveUser().getEmail()}`;
const mailSubject = `Auto-declined Invitation: ${summary} @ ${stringifyEventDateTime_(meeting)}`;
const mailBody = `Sent from ${scriptUrl_()} .`;
mail_(mailTo, mailSubject, mailBody);
});
}
/**
* Keep meetings that have the following conditions:
* - no description
* - not created by myself
* - not created by my manager
* - pending my response
*
* @return {Array} Meetings without descriptions that are pending RSVP.
*/
const getMeetingsWithoutDescriptions_ = () => {
const meetings = getMeetings_();
const meetingsWithoutDescriptions_ = meetings.filter((meeting) => {
const { attendees, creator, description } = meeting;
const { email, self } = creator;
const isInternalCreator = INTERNAL_DOMAINS.some((domain) => email.endsWith(domain));
const isMissingDescription = !description;
const isNotCreatedByManager = !MANAGER_EMAIL_ADDRESSES.includes(email);
const isNotCreatedBySelf = !self;
const myResponseNeedsAction = attendees.some((attendee) => {
const { self, responseStatus } = attendee;
return self && responseStatus === 'needsAction';
});
return isInternalCreator && isMissingDescription && isNotCreatedByManager && isNotCreatedBySelf && myResponseNeedsAction;
});
return meetingsWithoutDescriptions_;
};
/**
* Get all meetings from the primary calendar.
*
* @param {Boolean} fullSync (optional) If `true`, get all events in the last 30 days and re-sync update token.
*
* @return {Array} Meetings with attendees.
*/
const getMeetings_ = (fullSync = false) => {
const options = {};
const properties = PropertiesService.getUserProperties();
const syncToken = properties.getProperty('syncToken');
if (syncToken && !fullSync) {
options.syncToken = syncToken;
} else {
// Sync events up to thirty days in the past.
options.timeMin = getRelativeDate_(-30, 0).toISOString();
}
// Retrieve events one page at a time.
let allMeetings = [];
let pageToken;
do {
let events;
try {
options.pageToken = pageToken;
events = Calendar.Events.list(CALENDAR_ID, options);
const items = events.items;
// keep only those items that need my auto-decline
const meetings = items.filter((item) => {
/**
* Keep items with the following conditions:
* - "default" eventTypes, because these are actual meetings
* - have more than 1 attendee (the creator and someone else)
*/
const { attendees, eventType } = item;
const hasAttendees = !!attendees && attendees.length > 1;
const isDefaultEventType = eventType === 'default';
return hasAttendees && isDefaultEventType;
});
allMeetings = allMeetings.concat(meetings);
if (events.nextPageToken) {
pageToken = events.nextPageToken;
} else if (events.nextSyncToken) {
properties.setProperty('syncToken', events.nextSyncToken);
}
} catch (e) {
// Check to see if the sync token was invalidated by the server;
// if so, perform a full sync instead.
if (e.message === 'Sync token is no longer valid, a full sync is required.') {
properties.deleteProperty('syncToken');
allMeetings = getMeetings_(true);
return allMeetings;
}
}
} while (pageToken);
return allMeetings;
};
/**
* Helper function to get a new Date object relative to the current date.
*
* @param {Number} daysOffset The number of days in the future for the new date.
* @param {Number} hour The hour of the day for the new date, in the time zone of the script.
*
* @return {Date} The new date.
*/
const getRelativeDate_ = (daysOffset, hour) => {
const date = new Date();
date.setDate(date.getDate() + daysOffset);
date.setHours(hour);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date;
};
/**
* Helper function to get the event's start and end date-times in a string that is similar to that used in Google Calendar responses.
*/
const stringifyEventDateTime_ = (event) => {
const { start, end } = event;
const startDateTime = new Date(start.dateTime);
const endDateTime = new Date(end.dateTime);
const dateTimeString = `${stringifyDate_(startDateTime)} ${stringifyTime_(startDateTime)} - ${stringifyTime_(endDateTime)} (SGT)`;
return dateTimeString;
};
/**
* Helper function to get the date in a string that is similar to that used in Google Calendar responses.
*/
const stringifyDate_ = (dateTime) => {
const dateDay = DAYS_OF_WEEK[dateTime.getDay()];
const dateYear = dateTime.getFullYear();
const dateMonth = dateTime.getMonth() + 1;
const dateDate = dateTime.getDate();
const dateString = `${dateDay} ${dateYear}-${padZero_(dateMonth)}-${padZero_(dateDate)}`;
return dateString;
};
/**
* Helper function to get the time in a string that is similar to that used in Google Calendar responses.
*/
const stringifyTime_ = (dateTime) => {
const timeHours = dateTime.getHours();
const timeMinutes = dateTime.getMinutes();
const timeOfDay = timeHours < 12 ? 'am' : 'pm';
const timeString = `${padZero_(timeHours)}:${padZero_(timeMinutes)}${timeOfDay}`.replace(':00', '');
return timeString;
};
/**
* Add a leading '0' to a number if the number is less than 10.
*/
const padZero_ = (int) => `${int < 10 ? '0' : ''}${int}`;
/**
* Send an email using Gmail.
*
* @param {String} to Address to send the email to.
* @param {String} subject Subject of the email.
* @param {String} body Body of the email as plaintext.
* @param {String} replyTo (optional) Address for the user to reply to. If it's a blank string, then set `noReply` to `true`.
*/
const mail_ = (to, subject, body, replyTo = '') => {
const options = {};
if (!replyTo) {
options.noReply = true;
} else {
options.replyTo = replyTo;
}
GmailApp.sendEmail(to, subject, body, options);
};
/**
* Helper function to get the URL of this script.
*/
const scriptUrl_ = () => {
const scriptId = ScriptApp.getScriptId();
const scriptUrl = `https://script.google.com/home/projects/${scriptId}/edit`;
return scriptUrl;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment