From f0efe28deb1fb0107f5e77c5962599ef0d756d07 Mon Sep 17 00:00:00 2001 From: cadeci Date: Tue, 5 Nov 2024 20:27:03 -0500 Subject: [PATCH 1/3] feat: Add notify-busy-slack-thread.ts --- .../slack/queries/notify-busy-slack-thread.ts | 27 +++++++++++++++++++ .../slack/use-cases/add-slack-message.ts | 3 +++ 2 files changed, 30 insertions(+) create mode 100644 packages/core/src/modules/slack/queries/notify-busy-slack-thread.ts diff --git a/packages/core/src/modules/slack/queries/notify-busy-slack-thread.ts b/packages/core/src/modules/slack/queries/notify-busy-slack-thread.ts new file mode 100644 index 000000000..6c316de46 --- /dev/null +++ b/packages/core/src/modules/slack/queries/notify-busy-slack-thread.ts @@ -0,0 +1,27 @@ +import { db } from '@oyster/db'; + +import { job } from '@/infrastructure/bull/use-cases/job'; + +export async function notifyBusySlackThread(threadId: string) { + const row = await db + .selectFrom('slackMessages') + .select((eb) => [eb.fn.countAll().as('count'), 'channelId']) // channelId is needed for the message link + .where('threadId', '=', threadId) + .executeTakeFirstOrThrow(); + + const count = Number(row.count); + + if (count == 100) { + const channelId = row.channelId; + const uri = `https://colorstack-family.slack.com/archives/${channelId}/p${threadId}`; + const message = ` + 🚨 Uh-oh! Thread <${uri}|#${threadId}> has gone over 💯 replies! + Better see what's going on. 👀 + `; + + job('notification.slack.send', { + message: message, + workspace: 'internal', + }); + } +} diff --git a/packages/core/src/modules/slack/use-cases/add-slack-message.ts b/packages/core/src/modules/slack/use-cases/add-slack-message.ts index 2b2f57a89..f70a9073d 100644 --- a/packages/core/src/modules/slack/use-cases/add-slack-message.ts +++ b/packages/core/src/modules/slack/use-cases/add-slack-message.ts @@ -7,6 +7,7 @@ import { isFeatureFlagEnabled } from '@/modules/feature-flag/queries/is-feature- import { ErrorWithContext } from '@/shared/errors'; import { retryWithBackoff } from '@/shared/utils/core.utils'; import { getSlackMessage } from '../services/slack-message.service'; +import { notifyBusySlackThread } from '@/modules/slack/queries/notify-busy-slack-thread'; export async function addSlackMessage( data: GetBullJobData<'slack.message.add'> @@ -45,6 +46,8 @@ export async function addSlackMessage( threadRepliedTo: data.threadId, type: 'reply_to_thread', }); + + notifyBusySlackThread(data.threadId); } } From bd47d8e70ea932c1746f45831d3077264f551a75 Mon Sep 17 00:00:00 2001 From: Tomas Salgado <91388965+tomas-salgado@users.noreply.github.com> Date: Sat, 30 Nov 2024 21:18:24 -0600 Subject: [PATCH 2/3] change order of imports to fix lint error --- packages/core/src/modules/slack/use-cases/add-slack-message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/modules/slack/use-cases/add-slack-message.ts b/packages/core/src/modules/slack/use-cases/add-slack-message.ts index f70a9073d..4f43b7ebd 100644 --- a/packages/core/src/modules/slack/use-cases/add-slack-message.ts +++ b/packages/core/src/modules/slack/use-cases/add-slack-message.ts @@ -4,10 +4,10 @@ import { type GetBullJobData } from '@/infrastructure/bull/bull.types'; import { job } from '@/infrastructure/bull/use-cases/job'; import { redis } from '@/infrastructure/redis'; import { isFeatureFlagEnabled } from '@/modules/feature-flag/queries/is-feature-flag-enabled'; +import { notifyBusySlackThread } from '@/modules/slack/queries/notify-busy-slack-thread'; import { ErrorWithContext } from '@/shared/errors'; import { retryWithBackoff } from '@/shared/utils/core.utils'; import { getSlackMessage } from '../services/slack-message.service'; -import { notifyBusySlackThread } from '@/modules/slack/queries/notify-busy-slack-thread'; export async function addSlackMessage( data: GetBullJobData<'slack.message.add'> From 8451b34a951c2584def0abbc2d0b502fb7de2a45 Mon Sep 17 00:00:00 2001 From: Rami Abdou Date: Fri, 6 Dec 2024 17:36:59 -0800 Subject: [PATCH 3/3] get reply count from slack webhook --- apps/api/src/routers/slack.router.ts | 1 + apps/api/src/routers/slack.types.ts | 5 +++ .../src/infrastructure/bull/bull.types.ts | 1 + .../slack/queries/notify-busy-slack-thread.ts | 27 ------------ .../slack/use-cases/add-slack-message.ts | 42 +++++++++++++++---- 5 files changed, 40 insertions(+), 36 deletions(-) delete mode 100644 packages/core/src/modules/slack/queries/notify-busy-slack-thread.ts diff --git a/apps/api/src/routers/slack.router.ts b/apps/api/src/routers/slack.router.ts index 98e36b034..8eac0bfb3 100644 --- a/apps/api/src/routers/slack.router.ts +++ b/apps/api/src/routers/slack.router.ts @@ -110,6 +110,7 @@ slackEventRouter.post('/slack/events', async (req: RawBodyRequest, res) => { hasFile: !!event.files && !!event.files.length, id: event.ts!, isBot: !!event.app_id || !!event.bot_id, + replyCount: event.message?.reply_count, text: event.text!, threadId: event.ts && event.thread_ts && event.ts !== event.thread_ts diff --git a/apps/api/src/routers/slack.types.ts b/apps/api/src/routers/slack.types.ts index e0a16816e..05d026848 100644 --- a/apps/api/src/routers/slack.types.ts +++ b/apps/api/src/routers/slack.types.ts @@ -58,12 +58,17 @@ type SlackMessageDeletedEvent = { /** * @see https://api.slack.com/events/message + * @see https://api.slack.com/events/message/message_replied */ type SlackMessageSentEvent = { app_id?: string; bot_id?: string; channel: string; files?: unknown[]; + message?: { + // Only present if the message is a reply. + reply_count?: number; + }; subtype: undefined; text: string; thread_ts: string | undefined; diff --git a/packages/core/src/infrastructure/bull/bull.types.ts b/packages/core/src/infrastructure/bull/bull.types.ts index 3239e99f0..920977508 100644 --- a/packages/core/src/infrastructure/bull/bull.types.ts +++ b/packages/core/src/infrastructure/bull/bull.types.ts @@ -490,6 +490,7 @@ export const SlackBullJob = z.discriminatedUnion('name', [ }).extend({ hasFile: z.boolean().optional(), isBot: z.boolean().optional(), + replyCount: z.number().int().optional(), }), }), z.object({ diff --git a/packages/core/src/modules/slack/queries/notify-busy-slack-thread.ts b/packages/core/src/modules/slack/queries/notify-busy-slack-thread.ts deleted file mode 100644 index 6c316de46..000000000 --- a/packages/core/src/modules/slack/queries/notify-busy-slack-thread.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { db } from '@oyster/db'; - -import { job } from '@/infrastructure/bull/use-cases/job'; - -export async function notifyBusySlackThread(threadId: string) { - const row = await db - .selectFrom('slackMessages') - .select((eb) => [eb.fn.countAll().as('count'), 'channelId']) // channelId is needed for the message link - .where('threadId', '=', threadId) - .executeTakeFirstOrThrow(); - - const count = Number(row.count); - - if (count == 100) { - const channelId = row.channelId; - const uri = `https://colorstack-family.slack.com/archives/${channelId}/p${threadId}`; - const message = ` - 🚨 Uh-oh! Thread <${uri}|#${threadId}> has gone over 💯 replies! - Better see what's going on. 👀 - `; - - job('notification.slack.send', { - message: message, - workspace: 'internal', - }); - } -} diff --git a/packages/core/src/modules/slack/use-cases/add-slack-message.ts b/packages/core/src/modules/slack/use-cases/add-slack-message.ts index 4f43b7ebd..1d99ad5c8 100644 --- a/packages/core/src/modules/slack/use-cases/add-slack-message.ts +++ b/packages/core/src/modules/slack/use-cases/add-slack-message.ts @@ -4,14 +4,14 @@ import { type GetBullJobData } from '@/infrastructure/bull/bull.types'; import { job } from '@/infrastructure/bull/use-cases/job'; import { redis } from '@/infrastructure/redis'; import { isFeatureFlagEnabled } from '@/modules/feature-flag/queries/is-feature-flag-enabled'; -import { notifyBusySlackThread } from '@/modules/slack/queries/notify-busy-slack-thread'; +import { slack } from '@/modules/slack/instances'; import { ErrorWithContext } from '@/shared/errors'; import { retryWithBackoff } from '@/shared/utils/core.utils'; import { getSlackMessage } from '../services/slack-message.service'; -export async function addSlackMessage( - data: GetBullJobData<'slack.message.add'> -) { +type AddSlackMessageInput = GetBullJobData<'slack.message.add'>; + +export async function addSlackMessage(data: AddSlackMessageInput) { await ensureThreadExistsIfNecessary(data); const student = await db @@ -46,8 +46,6 @@ export async function addSlackMessage( threadRepliedTo: data.threadId, type: 'reply_to_thread', }); - - notifyBusySlackThread(data.threadId); } } @@ -56,6 +54,14 @@ export async function addSlackMessage( threadId: data.threadId || data.id, }); + if (data.replyCount === 100) { + // We don't need to await this since it's not a critical path. + notifyBusySlackThread({ + channelId: data.channelId, + id: data.id, + }); + } + // We'll do some additional checks for top-level threads... if (!data.threadId) { const [ @@ -114,9 +120,7 @@ export async function addSlackMessage( } } -async function ensureThreadExistsIfNecessary( - data: GetBullJobData<'slack.message.add'> -) { +async function ensureThreadExistsIfNecessary(data: AddSlackMessageInput) { // Don't need to bother if there is no thread. if (!data.threadId) { return; @@ -177,3 +181,23 @@ async function ensureThreadExistsIfNecessary( } ); } + +/** + * Sends a notification to the internal team when a thread gets over 100 + * replies. The motivation is that some threads can get a little too spicy + * and we want to moderate them quickly in case of abuse. + */ +async function notifyBusySlackThread({ + channelId, + id, +}: Pick) { + const { permalink } = await slack.chat.getPermalink({ + channel: channelId, + message_ts: id, + }); + + job('notification.slack.send', { + message: `🚨 Heads up! This <${permalink}|thread> has over 💯 replies!`, + workspace: 'internal', + }); +}