From 8c9a6f169355ae0ae788ff95c20fe2d5d72bbe02 Mon Sep 17 00:00:00 2001 From: CherrelleTucker <106271365+CherrelleTucker@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:39:09 -0500 Subject: [PATCH] Update piInfoToSlack.js updated weekly trigger, message formatting, linking --- piInfoToSlack.js | 533 +++++++++++++++++++++-------------------------- 1 file changed, 242 insertions(+), 291 deletions(-) diff --git a/piInfoToSlack.js b/piInfoToSlack.js index 565319c..2c2be0d 100644 --- a/piInfoToSlack.js +++ b/piInfoToSlack.js @@ -1,326 +1,277 @@ const SLACK_WEBHOOK_URL = "SLACK_WEBHOOK"; // Replace with your Slack webhook URL const CALENDAR_ID = "CALENDAR_ID"; -class SlackCalendarBot { - constructor(calendarId, slackWebhookUrl) { - this.calendar = CalendarApp.getCalendarById(calendarId); - this.slackWebhookUrl = slackWebhookUrl; - } - - sendDailyTimeSpecificEvents() { - const today = new Date(); - today.setHours(0, 0, 0, 0); // Start of the day - const endOfDay = new Date(today); - endOfDay.setHours(23, 59, 59, 999); // End of the day - - const events = this.calendar.getEvents(today, endOfDay); - const timeSpecificEvents = events.filter(event => - !event.isAllDayEvent() && - (event.getStartTime().getTime() >= today.getTime() && event.getEndTime().getTime() <= endOfDay.getTime()) - ); - - const message = this.buildEventMessage(timeSpecificEvents, "Today's PI events:", this.formatTimeSpecificEvent); - - if (message) { - this.sendToSlack(message); - } - } - - sendWeeklyAllDayEvents() { - const today = new Date(); - const monday = this.getMondayOfCurrentWeek(today); - const sunday = this.getSundayOfCurrentWeek(monday); - - const events = this.calendar.getEvents(monday, sunday); - const allDayEvents = events.filter(event => event.isAllDayEvent() || this.isMultiDayEvent(event, monday, sunday)); - - const message = this.buildEventMessage(allDayEvents, `This week's PI events:`, this.formatWeeklyEvent); - - if (message) { - this.sendToSlack(message); - } +/** + * Sends time-specific events for the current day to Slack. + * Filters out all-day events and formats the remaining events with their descriptions and locations. + */ +function sendDailyTimeSpecificEvents() { + const today = new Date(); + today.setHours(0, 0, 0, 0); // Start of the day + const endOfDay = new Date(today); + endOfDay.setHours(23, 59, 59, 999); // End of the day + + const events = CalendarApp.getCalendarById(CALENDAR_ID).getEvents(today, endOfDay); + const timeSpecificEvents = events.filter(event => + !event.isAllDayEvent() && + (event.getStartTime().getTime() >= today.getTime() && event.getEndTime().getTime() <= endOfDay.getTime()) + ); + + const message = buildEventMessage(timeSpecificEvents, "Today's PI events:", formatTimeSpecificEvent); + + if (message) { + sendToSlack(message); } +} - getDocsForRecentAndUpcomingEvents() { - const now = new Date(); - const events = this.calendar.getEvents(new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()), new Date(now.getFullYear() + 1, now.getMonth(), now.getDate())); - - const timeSpecificEvents = events.filter(event => { - const startTime = event.getStartTime(); - const endTime = event.getEndTime(); - const duration = (endTime - startTime) / (1000 * 60 * 60); // Duration in hours - return !event.isAllDayEvent() && duration <= 4; - }); - - const pastEvents = timeSpecificEvents.filter(event => event.getEndTime() < now).sort((a, b) => b.getEndTime() - a.getEndTime()); - const futureEvents = timeSpecificEvents.filter(event => event.getStartTime() > now).sort((a, b) => a.getStartTime() - b.getStartTime()); - - const pastEvent = pastEvents.length > 0 ? pastEvents[0] : null; - const futureEvent = futureEvents.length > 0 ? futureEvents[0] : null; - - const pastEventDescription = pastEvent ? this.cleanHtml(pastEvent.getDescription()) || "This file has not yet been created" : "This file has not yet been created"; - const futureEventDescription = futureEvent ? this.cleanHtml(futureEvent.getDescription()) || "This file has not yet been created" : "This file has not yet been created"; - - let message = "Most Recent Past Event:\n"; - message += pastEvent ? `• ${pastEventDescription}` : "No past event found."; - - message += "\n\nNext Event:\n"; - message += futureEvent ? `• ${futureEventDescription}` : "No future event found."; - - return message; +/** + * Sends all-day and multi-day events for the current week to Slack. + * Identifies all-day and multi-day events, formats them, and includes their descriptions and locations. + */ +function sendWeeklyAllDayEvents() { + const today = new Date(); + const monday = new Date(today); + monday.setDate(today.getDate() - today.getDay() + 1); // Get the Monday of the current week + monday.setHours(0, 0, 0, 0); + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 6); // Get the Sunday of the current week + sunday.setHours(23, 59, 59, 999); + + const events = CalendarApp.getCalendarById(CALENDAR_ID).getEvents(monday, sunday); + const allDayEvents = events.filter(event => (event.isAllDayEvent() || isMultiDayEvent(event, monday, sunday)) && event.getStartTime().getDay() !== 6); + + const currentWeekEvents = CalendarApp.getCalendarById(CALENDAR_ID).getEventsForDay(today); + const currentWeekEvent = currentWeekEvents.find(event => event.getStartTime() < today && event.getEndTime() > today); + + let allEvents = allDayEvents; + if (currentWeekEvent && !allDayEvents.includes(currentWeekEvent)) { + allEvents.push(currentWeekEvent); } - getCurrentMultiDayEvents() { - const today = new Date(); - today.setHours(0, 0, 0, 0); // Start of the day - const endOfDay = new Date(today); - endOfDay.setHours(23, 59, 59, 999); // End of the day - - const events = this.calendar.getEvents(today, endOfDay); - const multiDayEvents = events.filter(event => this.isMultiDayEvent(event, today, endOfDay)); - - let message = "Current PI & sprint:\n"; - if (multiDayEvents.length > 0) { - message += multiDayEvents.map(event => `• ${event.getTitle()}`).join("\n"); - } else { - message += "No multi-day events found for today."; - } + allEvents.reverse(); // Reverse the order of events - return message; - } + const message = buildEventMessage(allEvents, `This week's PI events:`, formatWeeklyEvent); - isMultiDayEvent(event, start, end) { - const startTime = event.getStartTime(); - const endTime = event.getEndTime(); - return (startTime < end && endTime > start) || (endTime.getDate() !== startTime.getDate()); + if (message) { + sendToSlack(message); } +} - buildEventMessage(events, header, formatEvent) { - if (events.length === 0) { - return null; - } - - const message = events.map(formatEvent).join("\n\n"); - - return `${header}\n${message}`; - } +/** + * Checks if an event spans multiple days. + * + * @param {CalendarEvent} event - The event to check. + * @param {Date} weekStart - The start date of the week. + * @param {Date} weekEnd - The end date of the week. + * @returns {boolean} True if the event is multi-day, otherwise false. + */ +function isMultiDayEvent(event, weekStart, weekEnd) { + const startTime = event.getStartTime(); + const endTime = event.getEndTime(); + return (startTime < weekEnd && endTime > weekStart); +} - formatTimeSpecificEvent(event) { - const start = event.getStartTime(); - const end = event.getEndTime(); - return ` • ${event.getTitle()} (${start.toTimeString().split(' ')[0]} - ${end.toTimeString().split(' ')[0]})`; +/** + * Builds a formatted message string for a list of events. + * + * @param {CalendarEvent[]} events - The list of events to format. + * @param {string} header - The header to include in the message. + * @param {function} formatEvent - The function to format individual events. + * @returns {string} The formatted message string. + */ +function buildEventMessage(events, header, formatEvent) { + if (events.length === 0) { + return null; } - formatWeeklyEvent(event) { - const start = event.getStartTime(); - const end = event.getEndTime(); - let formattedEvent; + const message = events.map(formatEvent).join("\n\n"); - if (event.isAllDayEvent() || start.toDateString() !== end.toDateString()) { - formattedEvent = `• ${event.getTitle()} (${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${end.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })})`; - } else { - formattedEvent = event.getTitle().startsWith("PI") - ? `• ${event.getTitle()} (${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} ${start.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })})` - : ` ${event.getTitle()} (${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} ${start.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })})`; - } - - return formattedEvent; - } + return `${header}\n${message}`; +} - cleanHtml(html) { - if (!html) return ''; +/** + * Formats a time-specific event for inclusion in the message. + * + * @param {CalendarEvent} event - The event to format. + * @returns {string} The formatted event string. + */ +function formatTimeSpecificEvent(event) { + const start = event.getStartTime(); + const end = event.getEndTime(); + let formattedEvent = ` • ${event.getTitle()} (${start.toTimeString().split(' ')[0]} - ${end.toTimeString().split(' ')[0]})`; + + return formattedEvent; +} - // Remove HTML tags and extra whitespace - const plainText = html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); - return plainText; +/** + * Formats an all-day or multi-day event for inclusion in the message. + * + * @param {CalendarEvent} event - The event to format. + * @returns {string} The formatted event string. + */ +function formatWeeklyEvent(event) { + const start = event.getStartTime(); + const end = event.getEndTime(); + const description = cleanHtml(event.getDescription()); + + let formattedEvent; + + if (description) { + formattedEvent = `• ${description}`; + } else if (event.isAllDayEvent() || start.toDateString() !== end.toDateString()) { + formattedEvent = `• ${event.getTitle()} (${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${end.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })})`; + } else { + formattedEvent = `• ${event.getTitle()} (${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} ${start.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })})`; } - sendToSlack(message) { - const payload = { - text: message - }; - - const options = { - method: "post", - contentType: "application/json", - payload: JSON.stringify(payload) - }; - - UrlFetchApp.fetch(this.slackWebhookUrl, options); - } + return formattedEvent; +} - getMondayOfCurrentWeek(today) { - const monday = new Date(today); - monday.setDate(today.getDate() - today.getDay() + 1); // Get the Monday of the current week - monday.setHours(0, 0, 0, 0); - return monday; - } +/** + * Cleans HTML content by removing tags and extra whitespace. + * + * @param {string} html - The HTML content to clean. + * @returns {string} The cleaned text content. + */ +function cleanHtml(html) { + if (!html) return ''; + + // Remove HTML tags and extra whitespace + const plainText = html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); + return plainText; +} - getSundayOfCurrentWeek(monday) { - const sunday = new Date(monday); - sunday.setDate(monday.getDate() + 6); // Get the Sunday of the current week - sunday.setHours(23, 59, 59, 999); - return sunday; - } +/** + * Sends a message to Slack using the specified webhook URL. + * + * @param {string} message - The message to send. + */ +function sendToSlack(message) { + const payload = { + text: message + }; + + const options = { + method: "post", + contentType: "application/json", + payload: JSON.stringify(payload) + }; + + UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options); +} - getCurrentPI() { - const today = new Date(); - today.setHours(0, 0, 0, 0); // Start of the day - const endOfDay = new Date(today); - endOfDay.setHours(23, 59, 59, 999); // End of the day - - const events = this.calendar.getEvents(today, endOfDay); - const currentPIEvent = events.find(event => { - const title = event.getTitle(); - return this.isMultiDayEvent(event, today, endOfDay) && /PI \d+\.\d+ Sprint \d+/.test(title); - }); - - if (currentPIEvent) { - const match = currentPIEvent.getTitle().match(/PI (\d+\.\d+)/); - return match ? match[1] : null; +/** + * Webhook handler to respond to Slack commands. + * Parses the command text and searches for events with titles matching the query. + * + * @param {object} e - The event parameter containing the request data. + * @returns {object} The response to send back to Slack. + */ +function doPost(e) { + try { + const slackData = parseFormData(e.postData.contents); + Logger.log(`slackData: ${JSON.stringify(slackData)}`); // Log the slackData for debugging + const searchTerm = slackData.text.trim(); + Logger.log(`Command text: ${searchTerm}`); // Log the command text for debugging + + if (searchTerm) { + const events = searchEventsByTitle(searchTerm); + Logger.log(`Filtered events: ${JSON.stringify(events.map(event => event.getTitle()))}`); // Log the event titles for debugging + const message = buildEventMessage(events, `Events matching "${searchTerm}":`, formatWeeklyEvent) || `No event found for ${searchTerm}`; + + return ContentService.createTextOutput(JSON.stringify({ text: message })) + .setMimeType(ContentService.MimeType.JSON); } - return null; + return ContentService.createTextOutput(JSON.stringify({ text: "Invalid command. Please use the format: /picalendar [search term]" })) + .setMimeType(ContentService.MimeType.JSON); + } catch (error) { + // Log the error for debugging + Logger.log(error.toString()); + return ContentService.createTextOutput(JSON.stringify({ text: "An error occurred. Please try again." })) + .setMimeType(ContentService.MimeType.JSON); } +} - doPost(e) { - try { - const slackData = this.parseFormData(e.postData.contents); - Logger.log(`slackData: ${JSON.stringify(slackData)}`); // Log the slackData for debugging - const commandText = slackData.command.trim(); - const searchTerm = slackData.text.trim(); - Logger.log(`Command text: ${commandText}`); // Log the command text for debugging - Logger.log(`Search term: ${searchTerm}`); // Log the search term for debugging - - if (commandText === '/picalendar') { - let termToSearch = searchTerm; - if (!termToSearch) { - termToSearch = this.getCurrentPI(); - if (!termToSearch) { - return ContentService.createTextOutput(JSON.stringify({ text: "No current PI found." })) - .setMimeType(ContentService.MimeType.JSON); - } - } - - const events = this.searchEventsByTitle(termToSearch); - Logger.log(`Filtered events: ${JSON.stringify(events.map(event => event.getTitle()))}`); // Log the event titles for debugging - const message = this.buildEventMessage(events, `Events matching "${termToSearch}":`, this.formatWeeklyEvent) || `No event found for ${termToSearch}`; - - return ContentService.createTextOutput(JSON.stringify({ text: message })) - .setMimeType(ContentService.MimeType.JSON); - } else if (commandText === '/pidocs') { - const message = this.getDocsForRecentAndUpcomingEvents(); - return ContentService.createTextOutput(JSON.stringify({ text: message })) - .setMimeType(ContentService.MimeType.JSON); - } else if (commandText === '/picurrent') { - const message = this.getCurrentMultiDayEvents(); - return ContentService.createTextOutput(JSON.stringify({ text: message })) - .setMimeType(ContentService.MimeType.JSON); - } else { - return ContentService.createTextOutput(JSON.stringify({ text: "Invalid command. Please use the format: /picalendar [search term], /pidocs, or /picurrent" })) - .setMimeType(ContentService.MimeType.JSON); - } - } catch (error) { - // Log the error for debugging - Logger.log(error.toString()); - return ContentService.createTextOutput(JSON.stringify({ text: "An error occurred. Please try again." })) - .setMimeType(ContentService.MimeType.JSON); +/** + * Test function to mimic the doPost function and output logs to the Execution log. + */ +function testDoPost() { + const testPayload = { + token: "testToken", + team_id: "T0001", + team_domain: "example", + channel_id: "C2147483705", + channel_name: "test", + user_id: "U2147483697", + user_name: "Steve", + command: "/picalendar", + text: "24.1", + response_url: "https://hooks.slack.com/commands/1234/5678", + trigger_id: "13345224609.738474920.8088930838d88f008e0" + }; + + const e = { + postData: { + contents: Object.entries(testPayload).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&') } - } - - parseFormData(data) { - const result = {}; - const pairs = data.split('&'); - for (let i = 0; i < pairs.length; i++) { - const pair = pairs[i].split('='); - result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || ''); + }; + + try { + const slackData = parseFormData(e.postData.contents); + Logger.log(`slackData: ${JSON.stringify(slackData)}`); // Log the slackData for debugging + const searchTerm = slackData.text.trim(); + Logger.log(`Command text: ${searchTerm}`); // Log the command text for debugging + + if (searchTerm) { + const events = searchEventsByTitle(searchTerm); + Logger.log(`Filtered events: ${JSON.stringify(events.map(event => event.getTitle()))}`); // Log the event titles for debugging + const message = buildEventMessage(events, `Events matching "${searchTerm}":`, formatWeeklyEvent) || `No event found for ${searchTerm}`; + Logger.log(`Message: ${message}`); // Log the message for debugging + } else { + Logger.log("Invalid command. Please use the format: /picalendar [search term]"); } - return result; - } - - searchEventsByTitle(searchTerm) { - const startDate = new Date(); - startDate.setFullYear(startDate.getFullYear() - 1); // Search for events from one year ago - const endDate = new Date(startDate); - endDate.setFullYear(startDate.getFullYear() + 2); // Search for events up to one year in the future - - const events = this.calendar.getEvents(startDate, endDate); - Logger.log(`Total events: ${events.length}`); // Log the total number of events for debugging - Logger.log(`Event titles: ${events.map(event => event.getTitle()).join(', ')}`); // Log all event titles for debugging - const regex = new RegExp(searchTerm, 'i'); // Create a case-insensitive regex for the search term - return events.filter(event => regex.test(event.getTitle())); + } catch (error) { + // Log the error for debugging + Logger.log(error.toString()); } +} - testDoPost() { - const testPayload = { - token: "testToken", - team_id: "T0001", - team_domain: "example", - channel_id: "C2147483705", - channel_name: "test", - user_id: "U2147483697", - user_name: "Steve", - command: "/picalendar", - text: "", - response_url: "https://hooks.slack.com/commands/1234/5678", - trigger_id: "13345224609.738474920.8088930838d88f008e0" - }; - - const e = { - postData: { - contents: Object.entries(testPayload).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&') - } - }; - - try { - const slackData = this.parseFormData(e.postData.contents); - Logger.log(`slackData: ${JSON.stringify(slackData)}`); // Log the slackData for debugging - const commandText = slackData.command.trim(); - const searchTerm = slackData.text.trim(); - Logger.log(`Command text: ${commandText}`); // Log the command text for debugging - Logger.log(`Search term: ${searchTerm}`); // Log the search term for debugging - - if (commandText === '/picalendar') { - let termToSearch = searchTerm; - if (!termToSearch) { - termToSearch = this.getCurrentPI(); - if (!termToSearch) { - Logger.log("No current PI found."); - return; - } - } - - const events = this.searchEventsByTitle(termToSearch); - Logger.log(`Filtered events: ${JSON.stringify(events.map(event => event.getTitle()))}`); // Log the event titles for debugging - const message = this.buildEventMessage(events, `Events matching "${termToSearch}":`, this.formatWeeklyEvent) || `No event found for ${termToSearch}`; - Logger.log(`Message: ${message}`); // Log the message for debugging - } else if (commandText === '/pidocs') { - const message = this.getDocsForRecentAndUpcomingEvents(); - Logger.log(`Message: ${message}`); // Log the message for debugging - } else if (commandText === '/picurrent') { - const message = this.getCurrentMultiDayEvents(); - Logger.log(`Message: ${message}`); // Log the message for debugging - } else { - Logger.log("Invalid command. Please use the format: /picalendar [search term], /pidocs, or /picurrent"); - } - } catch (error) { - // Log the error for debugging - Logger.log(error.toString()); - } +/** + * Parses URL-encoded form data into an object. + * + * @param {string} data - The URL-encoded form data. + * @returns {object} The parsed data. + */ +function parseFormData(data) { + const result = {}; + const pairs = data.split('&'); + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i].split('='); + result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || ''); } + return result; } -// Example usage: -// Set up triggers in the Apps Script UI: -// 1. Weekly trigger for bot.sendWeeklyAllDayEvents() on Monday -// 2. Daily trigger for bot.sendDailyTimeSpecificEvents() every day - -const bot = new SlackCalendarBot(CALENDAR_ID, SLACK_WEBHOOK_URL); - -function doPost(e) { - return bot.doPost(e); +/** + * Searches for events with titles matching the given search term. + * + * @param {string} searchTerm - The term to search for in event titles. + * @returns {CalendarEvent[]} The list of matching events. + */ +function searchEventsByTitle(searchTerm) { + const startDate = new Date(); + startDate.setFullYear(startDate.getFullYear() - 1); // Search for events from one year ago + const endDate = new Date(startDate); + endDate.setFullYear(startDate.getFullYear() + 2); // Search for events up to one year in the future + + const events = CalendarApp.getCalendarById(CALENDAR_ID).getEvents(startDate, endDate); + Logger.log(`Total events: ${events.length}`); // Log the total number of events for debugging + Logger.log(`Event titles: ${events.map(event => event.getTitle()).join(', ')}`); // Log all event titles for debugging + const regex = new RegExp(searchTerm, 'i'); // Create a case-insensitive regex for the search term + return events.filter(event => regex.test(event.getTitle())); } -function testDoPost() { - bot.testDoPost(); -} +// Set up triggers in the Apps Script UI: +// 1. Weekly trigger for sendWeeklyAllDayEvents() on Monday +// 2. Daily trigger for sendDailyTimeSpecificEvents() every day