Created
October 18, 2024 17:37
-
-
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
This file contains 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
// .--------------------------------------------------------------------------. | |
// | 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