Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save 9999years/6cf6cf6200e44244ec030f99e6ff7dfa to your computer and use it in GitHub Desktop.
Save 9999years/6cf6cf6200e44244ec030f99e6ff7dfa to your computer and use it in GitHub Desktop.
Zapier: Send a daily Slack alert for Google Calendar events which need an RSVP
// .--------------------------------------------------------------------------.
// | Send a daily Slack alert for Google Calendar events which need an RSVP |
// `--------------------------------------------------------------------------'
//
// # What and why
//
// I kept getting surprised by interviews and other meetings the morning they
// happened. Here's instructions for a Zapier "Zap" that checks if you haven't
// RSVP'd to any of the next 50 events (by default) on your primary calendar,
// and sends you a Slack DM alerting you of the events in question. It runs
// every weekday at 9:00AM, but you can tweak that if you need.
//
// Here's what the messages look like:
//
// Hello! You have invitations on Google Calendar that require a response:
// • Engineering All Hands Thursday, November 7, 2024 at 10:00 AM — 10:45 AM
//
// (The event names are bolded links to the events on Google Calendar, so it's
// easy to tell them apart from the timestamps in practice.)
//
// The heart of this Zap is a big chunk of Javascript that inspects the Google
// Calendar API response and finds the events which need an RSVP. This is
// because of several show-stopper issues with Zapier's data processing
// capabilities:
//
// - The built-in "Filter by Zapier" action has no concept of filtering an
// array, even though the response data can be an array.
//
// - Online guides suggest you're supposed to deal with arrays of values as
// comma-delimited strings (eugh), but if you try this, Zapier will strip
// leading and trailing commas before starting the actions, transforming
// `,true,` into `true` and losing the array structure. This makes it
// impossible to correlate the `attendees[].self` field with the
// `attendees[].responseStatus` field.
//
// Most of the JavaScript is just to format the event times nicely; events can
// span an entire single day, multiple entire days, part of a single day, or
// part of multiple days, so there's a bunch of different cases for that, and
// dealing with the JavaScript datetime APIs adds a bunch of hassle.
//
// This would use the "Slack Raw API Request" action, but I couldn't get it to
// work properly -- I think Zapier runs that Slack action with different scopes
// than the "Slack Send Direct Message" action, because I've written Slack
// integrations before and I just couldn't get messages to show up. This means
// that the formatting is slightly uglier than it would otherwise be, but it's
// Fine.
//
// Unfortunately, the JavaScript that makes this Zap work is also its achilles
// heel: when I went to create a template for this Zap to share it with my
// teammates, I got a toast saying "Failed to create Zap template: uses a
// restricted app". Inspecting the raw API response showed me a node ID that
// matched the "Code by Zapier" action. There is a feature to restrict access
// to apps in Zapier, but it's only available on the Enterprise plan, which
// we're not subscribed to, so I'm not sure what the deal is here.
//
//
// # The steps to construct this Zap for yourself
//
// Step 1: Schedule by Zapier
// Trigger event: Every Day
// Time of Day: 9:00 AM
// Trigger on weekends? no
//
// Step 2: Google Calendar
// Description: Get next 50 calendar events
// Action event: API Request (Beta)
// Account: (Connect your account)
// HTTP Method: GET
// URL: https://www.googleapis.com/calendar/v3/calendars/primary/events
// Query String Parameters:
// q: (Anything you want to search for in event titles/descriptions/etc.,
// like "interview".)
// timeMin: <Schedule by Zapier/ID>
// (The ID is apparently an ISO 8601 formatted datetime, which is
// what we want.)
// singleEvents: true
// orderBy: startTime
// timeZone: UTC
// maxResults: 50
// (This is flexible.)
//
// Step 3: Code by Zapier
// Action event: Run Javascript
// Input Data:
// rawJSON: <Google Calendar/Response Body>
// Code: (Listed below.)
//
// Step 4: Paths by Zapier
// Add a single path (see next)
//
// Step 5: Paths by Zapier
// Path conditions:
// Only continue if:
// Value: <Code by Zapier/Send>
// Condition: (Boolean) Is true
//
// Step 6: Slack
// Action event: Find User by Email
// Account: (Connect your account)
// Email: <Google Calendar/Response Data Summary>
// (This is the email associated with the primary calendar in your
// Google Account. Not sure why it's called "Summary", but whatever.)
// Return Raw Results: False
// (But I doubt it matters.)
// Should this step be considered a success if no search results are found?
// No
//
// Step 7: Slack
// Action event: Send Direct Message
// Account: (Connect your account)
// Send Multi Message: No
// (This is used for creating a group DM.)
// To Username: <Slack/ID>
// (You need to click the ... menu to the side of the field and
// select "Custom" to enter the user ID from the previous step.)
// Message Text: <Code by Zapier/Message>
var { rawJSON } = inputData;
const response = JSON.parse(rawJSON);
const dateFormatter = new Intl.DateTimeFormat('en-US', {
timeZone: response.timeZone,
dateStyle: "full",
})
const dateTimeFormatter = new Intl.DateTimeFormat('en-US', {
timeZone: response.timeZone,
dateStyle: "full",
timeStyle: "short",
})
const timeFormatter = new Intl.DateTimeFormat('en-US', {
timeZone: response.timeZone,
timeStyle: "short",
})
const formatEventTimes = event => {
var start = undefined;
var isAllDay = false;
if (event.start.date !== undefined) {
start = event.start.date;
isAllDay = true;
} else if (event.start.dateTime !== undefined) {
start = event.start.dateTime;
}
var end = undefined;
if (event.end.date !== undefined) {
end = event.end.date;
} else if (event.end.dateTime !== undefined) {
end = event.end.dateTime;
}
if (start === undefined || end === undefined) {
return "";
} else {
if (isAllDay) {
// Horrible: "When the time zone offset is absent, date-only forms are
// interpreted as a UTC time and date-time forms are interpreted as local
// time."
// Google Calendar API responses don't list a time zone offset (or time)
// for dates, but do for date-times. Therefore we append `T00:00:00`
// (with no time zone) here, so that it's interpreted as a local time.
start = dateFormatter.format(new Date(`${start}T00:00:00`));
end = dateFormatter.format(new Date(`${end}T00:00:00`));
if (start === end) {
// One day.
return start;
} else {
return `${start} – ${end}`;
}
} else {
start = new Date(start);
end = new Date(end);
if (
start.getDate() !== end.getDate()
|| start.getMonth() !== end.getMonth()
|| start.getFullYear() !== end.getFullYear()
) {
// Multiple days.
start = dateTimeFormatter.format(start);
end = dateTimeFormatter.format(end);
return `${start} — ${end}`;
} else {
// One day.
start = dateTimeFormatter.format(start);
end = timeFormatter.format(end);
return `${start} — ${end}`;
}
}
}
};
const formatEvent = event => `• *<${event.htmlLink}|${event.summary}>* ${formatEventTimes(event)}`;
var needsAction = new Array()
for (const event of response.items) {
if (event.attendeesOmitted || event.attendees === undefined) {
continue;
}
for (const attendee of event.attendees) {
if (attendee.self) {
if (attendee.responseStatus == "needsAction") {
needsAction.push(formatEvent(event));
}
break;
}
}
}
if (needsAction.length == 0) {
output = {
send: false,
message: "",
}
} else {
output = {
send: true,
message: "Hello! You have invitations on Google Calendar that require a response:\n"
+ needsAction.join("\n"),
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment