Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement /meeting-reminder command #67

Merged
merged 22 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8915a89
Replace instances of console.log with SlackLogger logging
QuantumManiac Feb 4, 2024
b0be0f2
Add code block parameter for logging
QuantumManiac Feb 4, 2024
3d038c6
Add deploy commit hash to startup log message
QuantumManiac Feb 5, 2024
becca37
Remove error testing from help command
QuantumManiac Feb 5, 2024
682acb1
Reformat log messages
QuantumManiac Feb 5, 2024
b1106a6
Add comments to SlackLogger class
QuantumManiac Feb 5, 2024
a137645
Change OOGLE_ACCOUNT_OAUTH_REDIRECT to be var instead of secret
QuantumManiac Feb 5, 2024
edec0ae
Stop piping logs to rtail
QuantumManiac Feb 5, 2024
92cee18
Refactor throwing and handling errors
QuantumManiac Feb 5, 2024
8d30521
Format log message
QuantumManiac Feb 5, 2024
3a0f17c
Allow logger to handle passing uncasted errors
QuantumManiac Feb 5, 2024
9f650c4
Change code block logging format for errors
QuantumManiac Feb 5, 2024
f7c06fc
Refactor throwing of bubbled-up errors
QuantumManiac Feb 5, 2024
e8a6c8d
Make SlackLogger.formatCodeBlockContent static
QuantumManiac Feb 5, 2024
22962f5
Update error messages
QuantumManiac Feb 6, 2024
5fcd7f6
Preserve newlines when parsing calendar descriptions
QuantumManiac Feb 6, 2024
fef8000
Implement meeting reminder command
QuantumManiac Feb 7, 2024
d9dd6d2
Refactor logging for Slack API calls
QuantumManiac Feb 7, 2024
2c0143e
Update tests and jsdocs
QuantumManiac Feb 7, 2024
6ae91f3
Sync with main
QuantumManiac Feb 11, 2024
ab53843
Change function signature for meetingReminderCommandHandler
QuantumManiac Feb 13, 2024
7bd485e
Sync with main
QuantumManiac Feb 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/classes/SlackLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export enum LogLevel {
*/
export class SlackLogger {
private static instance: SlackLogger;
slackClient: WebClient;
private slackClient: WebClient;

/**
* Singleton constructor for SlackLogger
Expand Down Expand Up @@ -125,6 +125,18 @@ export class SlackLogger {
* @returns The formatted code block content
*/
private static formatCodeBlockContent(codeBlockContent: unknown): string {
if (codeBlockContent instanceof Error) {
// Recursively get the causes of the error
let errorContent = `${codeBlockContent}`;
let cause = (codeBlockContent as Error).cause;
while (cause) {
errorContent += `\t[cause]: ${cause}`;

cause = (cause as Error)?.cause;
}
return `\`\`\`${errorContent}\`\`\``;
}

return `\`\`\`${codeBlockContent}\`\`\``;
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const app = new App({
});

// Register listeners
registerListeners(app);
registerListeners(app, auth);

// Schedule tasks
scheduleTasks(app.client, auth);
Expand Down
24 changes: 18 additions & 6 deletions src/listeners/commands/helpCommand.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { Middleware, SlackCommandMiddlewareArgs } from "@slack/bolt";
import { AllMiddlewareArgs, SlackCommandMiddlewareArgs } from "@slack/bolt";
import { logCommandUsed } from "../../utils/logging";
import { postEphemeralMessage } from "../../utils/slack";
import { SlackLogger } from "../../classes/SlackLogger";

const message = `For more information, check out <https://github.com/waterloo-rocketry/minerva-rewrite/blob/main/README.md|minerva's README>.`;

export const helpCommandHandler: Middleware<SlackCommandMiddlewareArgs> = async ({ command, ack, client }) => {
await ack();
await logCommandUsed(command);
/**
* Handler for the /help command
* @param payload The payload of the command
*/
export async function helpCommandHandler(payload: SlackCommandMiddlewareArgs & AllMiddlewareArgs): Promise<void> {
const { command, ack, client } = payload;

await postEphemeralMessage(client, command.channel_id, command.user_id, message);
};
ack();

logCommandUsed(command);

try {
await postEphemeralMessage(client, command.channel_id, command.user_id, message);
} catch (error) {
SlackLogger.getInstance().error("Failed to send help message", error);
}
}
6 changes: 5 additions & 1 deletion src/listeners/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
// commands/index.ts
import { App } from "@slack/bolt";
import { OAuth2Client } from "google-auth-library";

import { helpCommandHandler } from "./helpCommand";
import { meetingReminderCommandHandler } from "./meetingReminderCommand";

const register = (app: App): void => {
const register = (app: App, googleAuth: OAuth2Client): void => {
app.command("/help", helpCommandHandler);
app.command("/meeting_reminder", (payload) => meetingReminderCommandHandler(payload, googleAuth));
// Other command registrations would go here
};

Expand Down
54 changes: 54 additions & 0 deletions src/listeners/commands/meetingReminderCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { AllMiddlewareArgs, SlackCommandMiddlewareArgs } from "@slack/bolt";
import { OAuth2Client } from "google-auth-library";

import { filterEventsForChannels, getEvents, parseEvents } from "../../utils/googleCalendar";
import { getAllSlackChannels } from "../../utils/channels";
import { logCommandUsed } from "../../utils/logging";
import { postEphemeralMessage } from "../../utils/slack";
import { EventReminderType, remindUpcomingEvent } from "../../utils/eventReminders";
import { SlackLogger } from "../../classes/SlackLogger";

/**
* Handler for the /meeting_reminder command
* @param obj The arguments for the middleware
* @param obj.ack The Bolt app's `ack()` function
* @param obj.command The command payload
* @param obj.client The Bolt app client
* @param googleAuth The OAuth2Client for Google Calendar
*/
export async function meetingReminderCommandHandler(
{ command, ack, client }: SlackCommandMiddlewareArgs & AllMiddlewareArgs,
googleAuth: OAuth2Client,
): Promise<void> {
ack();
logCommandUsed(command);

try {
const slackChannels = await getAllSlackChannels(client);
const fetchedEvents = await getEvents(googleAuth);
const events = parseEvents(fetchedEvents, slackChannels);
const eventsInChannel = filterEventsForChannels(events, [command.channel_name]);

if (eventsInChannel.length === 0) {
await postEphemeralMessage(client, command.channel_id, command.user_id, "No upcoming events in this channel.");
} else {
const soonestEvent = eventsInChannel.sort((a, b) => a.start.getTime() - b.start.getTime())[0];

const commandText = command.text.trim().toLowerCase();

await remindUpcomingEvent(
soonestEvent,
client,
commandText == "ping" ? EventReminderType.MANUAL_PING : EventReminderType.MANUAL,
);
await postEphemeralMessage(
client,
command.channel_id,
command.user_id,
"Manual reminder sent for next event in channel.",
);
}
} catch (error) {
SlackLogger.getInstance().error("Failed to send manual meeting reminder", error);
}
}
6 changes: 4 additions & 2 deletions src/listeners/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { App } from "@slack/bolt";
import { OAuth2Client } from "google-auth-library";

import actions from "./actions";
import commands from "./commands";
import events from "./events";
import messages from "./messages";
import shortcuts from "./shortcuts";
import views from "./views";

const registerListeners = (app: App): void => {
const registerListeners = (app: App, googleClient: OAuth2Client): void => {
actions.register(app);
commands.register(app);
commands.register(app, googleClient);
events.register(app);
messages.register(app);
shortcuts.register(app);
Expand Down
28 changes: 19 additions & 9 deletions src/utils/eventReminders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const TIME_CHECK_INTERVAL = 1000 * 60 * 5; // 5 minutes in milliseconds
* The types of event reminders that can be sent
*/
export enum EventReminderType {
MANUAL = 1,
MANUAL_PING = 2,
FIVE_MINUTES = 1000 * 60 * 5, // 5 minutes in milliseconds
SIX_HOURS = 1000 * 60 * 60 * 6, // 6 hours in milliseconds
}
Expand Down Expand Up @@ -59,12 +61,14 @@ export function getEventReminderType(event: CalendarEvent): EventReminderType |
* Posts a reminder for the given event to the channel it is associated with
* @param event The event to post a reminder for
* @param client Slack Web API client
* @param reminderType The type of reminder to post
* @param defaultSlackChannels The default Slack channels to post reminders to. If not provided, the default channels will be fetched from the Slack API
* @param allSlackUsersInWorkspace All Slack users in the workspace. If not provided, the users will be fetched from the Slack API
*/
export async function remindUpcomingEvent(
event: CalendarEvent,
client: WebClient,
reminderType: EventReminderType | null,
defaultSlackChannels?: SlackChannel[],
allSlackUsersInWorkspace?: SlackUser[],
): Promise<void> {
Expand All @@ -73,9 +77,7 @@ export async function remindUpcomingEvent(
return;
}

const reminderType = getEventReminderType(event);

if (!reminderType) {
if (reminderType == null) {
return;
}

Expand Down Expand Up @@ -104,10 +106,15 @@ export async function remindUpcomingEvent(
allSlackUsersInWorkspace,
);

const reminderTypeString = reminderType === EventReminderType.FIVE_MINUTES ? "5 minute" : "6 hour";
const reminderTypeStrings = {
[EventReminderType.MANUAL]: "manually triggered",
[EventReminderType.MANUAL_PING]: "manually triggered (with ping)",
[EventReminderType.FIVE_MINUTES]: "5 minute",
[EventReminderType.SIX_HOURS]: "6 hour",
};

SlackLogger.getInstance().info(
`Sent ${reminderTypeString} reminder for event \`${
`Sent ${reminderTypeStrings[reminderType]} reminder for event \`${
event.title
}\` at \`${event.start.toISOString()}\` to channel \`${
event.minervaEventMetadata.channel.name
Expand Down Expand Up @@ -206,7 +213,8 @@ export async function remindUpcomingEvents(client: WebClient, events: CalendarEv
const allSlackUsersInWorkspace = await getAllSlackUsers(client);

events.forEach((event) => {
remindUpcomingEvent(event, client, defaultSlackChannels, allSlackUsersInWorkspace);
const reminderType = getEventReminderType(event);
remindUpcomingEvent(event, client, reminderType, defaultSlackChannels, allSlackUsersInWorkspace);
});
}

Expand All @@ -217,9 +225,11 @@ export async function remindUpcomingEvents(client: WebClient, events: CalendarEv
* @returns The generated reminder text
*/
export function generateEventReminderChannelText(event: CalendarEvent, reminderType: EventReminderType): string {
let message = `${reminderType == EventReminderType.FIVE_MINUTES ? "<!channel>\n" : ""}Reminder: *${
event.title
}* is occurring`;
let message = `${
reminderType == EventReminderType.FIVE_MINUTES || reminderType == EventReminderType.MANUAL_PING
? "<!channel>\n"
: ""
}Reminder: *${event.title}* is occurring`;

if (reminderType === EventReminderType.FIVE_MINUTES) {
const timeUntilEvent = event.start.getTime() - new Date().getTime();
Expand Down
31 changes: 10 additions & 21 deletions src/utils/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,7 @@ export async function postEphemeralMessage(

return res;
} catch (error) {
SlackLogger.getInstance().error(
`Failed to post ephemeral message to user \`${user}\` in channel \`${channel}\` with error:`,
error,
);
throw error;
throw Error(`Failed to post ephemeral message to user \`${user}\` in channel \`${channel}\``, { cause: error });
}
}

Expand All @@ -101,8 +97,7 @@ export async function getAllEmoji(client: WebClient): Promise<string[]> {
}
return Object.keys(result.emoji);
} catch (error) {
SlackLogger.getInstance().error(`Failed to get emojis for workspace:`, error);
throw error;
throw Error("Failed to get emojis for workspace", { cause: error });
}
}

Expand Down Expand Up @@ -141,8 +136,7 @@ export async function getAllSlackUsers(
break;
}
} catch (error) {
SlackLogger.getInstance().error(`Failed to get users from workspace:`, error);
throw error;
throw Error("Failed to get users from workspace", { cause: error });
}
}
return usersList;
Expand Down Expand Up @@ -177,8 +171,7 @@ export async function getChannelMembers(client: WebClient, channelId: string): P
}
return members.length > 0 ? members : [];
} catch (error) {
SlackLogger.getInstance().error(`Failed to get members for channel with id \`${channelId}\`:`, error);
throw error;
throw Error(`Failed to get members for channel with id \`${channelId}\``, { cause: error });
}
}

Expand Down Expand Up @@ -207,11 +200,9 @@ export function addReactionToMessage(
timestamp: timestampStr,
});
} catch (error) {
SlackLogger.getInstance().error(
`Failed to add reaction \`${emoji}\` to message \`${timestampStr}\` in \`${channel}\`:`,
error,
);
throw error;
throw Error(`Failed to add reaction \`${emoji}\` to message \`${timestampStr}\` in \`${channel}\``, {
cause: error,
});
}
}

Expand All @@ -234,11 +225,9 @@ export async function getMessagePermalink(
message_ts: timestamp,
});
} catch (error) {
SlackLogger.getInstance().error(
`Error fetching message permalink for message with timestamp \`${timestamp}\` in \`${channel}\`:`,
error,
);
throw error;
throw Error(`Failed to get permalink for message with timestamp \`${timestamp}\` in \`${channel}\``, {
cause: error,
});
}

if (res?.ok) {
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/utils/meetingReminders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,41 @@ Ways to attend:
);
});

it("should generate a manually triggered reminder with the meeting link and location provided", () => {
jest.useFakeTimers().setSystemTime(new Date("2023-01-01T00:00:00.000Z").getTime());
event.location = "Test location";
event.minervaEventMetadata = {
channel: slackChannels[0],
meetingLink: "https://example.com",
};
const result = generateEventReminderChannelText(event, EventReminderType.MANUAL);

expect(result).toBe(
`Reminder: *Test event* is occurring at *January 1st, 2023 at 1:00 AM*
<https://www.google.com/calendar/event?eid=MGJyczFiMjJuZHJjZzRnZmx0Z2c1OGRocmkgdXdhdGVybG9vLnJvY2tldHJ5LmRxxxxx|Event Details>
Ways to attend:
\t:office: In person @ Test location
\t:globe_with_meridians: Online @ https://example.com`,
);
});

it("should generate a manually triggered reminder with a ping with the meeting link and location provided", () => {
jest.useFakeTimers().setSystemTime(new Date("2023-01-01T00:00:00.000Z").getTime());
event.location = "Test location";
event.minervaEventMetadata = {
channel: slackChannels[0],
meetingLink: "https://example.com",
};
const result = generateEventReminderChannelText(event, EventReminderType.MANUAL_PING);

expect(result).toBe(`<!channel>
Reminder: *Test event* is occurring at *January 1st, 2023 at 1:00 AM*
<https://www.google.com/calendar/event?eid=MGJyczFiMjJuZHJjZzRnZmx0Z2c1OGRocmkgdXdhdGVybG9vLnJvY2tldHJ5LmRxxxxx|Event Details>
Ways to attend:
\t:office: In person @ Test location
\t:globe_with_meridians: Online @ https://example.com`);
});

it("should generate a reminder for 6 hours with no meeting link or location provided", () => {
jest.useFakeTimers().setSystemTime(new Date("2023-01-01T00:00:00.000Z").getTime());
event.minervaEventMetadata = {
Expand Down
Loading