From da501a62dd2f1723b4a2375f93b4609be8b03d94 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 11 Feb 2026 16:51:00 -0800 Subject: [PATCH] fix(slack): add missing QStash endpoint for DM messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The event router was dispatching DM messages to /api/integrations/slack/jobs/process-assistant-message via QStash, but no route handler existed — every DM job hit a 404. Also adds resolveUserMentions to the DM handler to match the mention handler, so <@U12345> refs get resolved to display names. --- .../process-assistant-message.ts | 13 +++- .../jobs/process-assistant-message/route.ts | 61 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts diff --git a/apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts b/apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts index 5f2d9993531..00beeb4a14d 100644 --- a/apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts +++ b/apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts @@ -2,7 +2,11 @@ import type { GenericMessageEvent } from "@slack/types"; import { db } from "@superset/db/client"; import { integrationConnections } from "@superset/db/schema"; import { and, eq } from "drizzle-orm"; -import { formatErrorForSlack, runSlackAgent } from "../utils/run-agent"; +import { + formatErrorForSlack, + resolveUserMentions, + runSlackAgent, +} from "../utils/run-agent"; import { formatSideEffectsMessage } from "../utils/slack-blocks"; import { createSlackClient } from "../utils/slack-client"; @@ -60,8 +64,13 @@ export async function processAssistantMessage({ } try { + const resolve = await resolveUserMentions({ + texts: [event.text ?? ""], + slack, + }); + const result = await runSlackAgent({ - prompt: event.text ?? "", + prompt: resolve(event.text ?? ""), channelId: event.channel, threadTs, organizationId: connection.organizationId, diff --git a/apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts b/apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts new file mode 100644 index 00000000000..b925e1b65c9 --- /dev/null +++ b/apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts @@ -0,0 +1,61 @@ +import { Receiver } from "@upstash/qstash"; +import { z } from "zod"; + +import { env } from "@/env"; +import { processAssistantMessage } from "../../events/process-assistant-message"; + +const receiver = new Receiver({ + currentSigningKey: env.QSTASH_CURRENT_SIGNING_KEY, + nextSigningKey: env.QSTASH_NEXT_SIGNING_KEY, +}); + +const payloadSchema = z.object({ + event: z.object({ + type: z.literal("message"), + user: z.string(), + text: z.string().optional(), + ts: z.string(), + channel: z.string(), + channel_type: z.literal("im"), + event_ts: z.string(), + thread_ts: z.string().optional(), + }), + teamId: z.string(), + eventId: z.string(), +}); + +export async function POST(request: Request) { + const body = await request.text(); + const signature = request.headers.get("upstash-signature"); + + if (!signature) { + return Response.json({ error: "Missing signature" }, { status: 401 }); + } + + const isValid = await receiver.verify({ + body, + signature, + url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/jobs/process-assistant-message`, + }); + + if (!isValid) { + return Response.json({ error: "Invalid signature" }, { status: 401 }); + } + + const parsed = payloadSchema.safeParse(JSON.parse(body)); + if (!parsed.success) { + console.error( + "[slack/process-assistant-message] Invalid payload:", + parsed.error, + ); + return Response.json({ error: "Invalid payload" }, { status: 400 }); + } + + await processAssistantMessage({ + event: { ...parsed.data.event, subtype: undefined }, + teamId: parsed.data.teamId, + eventId: parsed.data.eventId, + }); + + return Response.json({ success: true }); +}