import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs } from "@slack/bolt"; import { type ChatCommandDefinition, type CommandArgs, } from "../../auto-reply/commands-registry.js"; import type { ReplyPayload } from "../../auto-reply/types.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; import { resolveNativeCommandSessionTargets } from "../../channels/native-command-session-targets.js"; import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../../config/commands.js"; import { danger, logVerbose } from "../../globals.js"; import { chunkItems } from "../../utils/chunk-items.js"; import type { ResolvedSlackAccount } from "../accounts.js"; import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "./allow-list.js"; import { resolveSlackEffectiveAllowFrom } from "./auth.js"; import { resolveSlackChannelConfig, type SlackChannelConfigResolved } from "./channel-config.js"; import { buildSlackSlashCommandMatcher, resolveSlackSlashCommandConfig } from "./commands.js"; import type { SlackMonitorContext } from "./context.js"; import { normalizeSlackChannelType } from "./context.js"; import { authorizeSlackDirectMessage } from "./dm-auth.js"; import { createSlackExternalArgMenuStore, SLACK_EXTERNAL_ARG_MENU_PREFIX, type SlackExternalArgMenuChoice, } from "./external-arg-menu-store.js"; import { escapeSlackMrkdwn } from "./mrkdwn.js"; import { isSlackChannelAllowedByPolicy } from "./policy.js"; import { resolveSlackRoomContextHints } from "./room-context.js"; type SlackBlock = { type: string; [key: string]: unknown }; const SLACK_COMMAND_ARG_ACTION_ID = "openclaw_cmdarg"; const SLACK_COMMAND_ARG_VALUE_PREFIX = "cmdarg"; const SLACK_COMMAND_ARG_BUTTON_ROW_SIZE = 5; const SLACK_COMMAND_ARG_OVERFLOW_MIN = 3; const SLACK_COMMAND_ARG_OVERFLOW_MAX = 5; const SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX = 100; const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75; const SLACK_HEADER_TEXT_MAX = 150; let slashCommandsRuntimePromise: Promise | null = null; let slashDispatchRuntimePromise: Promise | null = null; let slashSkillCommandsRuntimePromise: Promise< typeof import("./slash-skill-commands.runtime.js") > | null = null; function loadSlashCommandsRuntime() { slashCommandsRuntimePromise ??= import("./slash-commands.runtime.js"); return slashCommandsRuntimePromise; } function loadSlashDispatchRuntime() { slashDispatchRuntimePromise ??= import("./slash-dispatch.runtime.js"); return slashDispatchRuntimePromise; } function loadSlashSkillCommandsRuntime() { slashSkillCommandsRuntimePromise ??= import("./slash-skill-commands.runtime.js"); return slashSkillCommandsRuntimePromise; } type EncodedMenuChoice = SlackExternalArgMenuChoice; const slackExternalArgMenuStore = createSlackExternalArgMenuStore(); function truncatePlainText(value: string, max: number): string { const trimmed = value.trim(); if (trimmed.length <= max) { return trimmed; } if (max <= 1) { return trimmed.slice(0, max); } return `${trimmed.slice(0, max - 1)}…`; } function buildSlackArgMenuConfirm(params: { command: string; arg: string }) { const command = escapeSlackMrkdwn(params.command); const arg = escapeSlackMrkdwn(params.arg); return { title: { type: "plain_text", text: "Confirm selection" }, text: { type: "mrkdwn", text: `Run */${command}* with *${arg}* set to this value?`, }, confirm: { type: "plain_text", text: "Run command" }, deny: { type: "plain_text", text: "Cancel" }, }; } function storeSlackExternalArgMenu(params: { choices: EncodedMenuChoice[]; userId: string; }): string { return slackExternalArgMenuStore.create({ choices: params.choices, userId: params.userId, }); } function readSlackExternalArgMenuToken(raw: unknown): string | undefined { return slackExternalArgMenuStore.readToken(raw); } function encodeSlackCommandArgValue(parts: { command: string; arg: string; value: string; userId: string; }) { return [ SLACK_COMMAND_ARG_VALUE_PREFIX, encodeURIComponent(parts.command), encodeURIComponent(parts.arg), encodeURIComponent(parts.value), encodeURIComponent(parts.userId), ].join("|"); } function parseSlackCommandArgValue(raw?: string | null): { command: string; arg: string; value: string; userId: string; } | null { if (!raw) { return null; } const parts = raw.split("|"); if (parts.length !== 5 || parts[0] !== SLACK_COMMAND_ARG_VALUE_PREFIX) { return null; } const [, command, arg, value, userId] = parts; if (!command || !arg || !value || !userId) { return null; } const decode = (text: string) => { try { return decodeURIComponent(text); } catch { return null; } }; const decodedCommand = decode(command); const decodedArg = decode(arg); const decodedValue = decode(value); const decodedUserId = decode(userId); if (!decodedCommand || !decodedArg || !decodedValue || !decodedUserId) { return null; } return { command: decodedCommand, arg: decodedArg, value: decodedValue, userId: decodedUserId, }; } function buildSlackArgMenuOptions(choices: EncodedMenuChoice[]) { return choices.map((choice) => ({ text: { type: "plain_text", text: choice.label.slice(0, 75) }, value: choice.value, })); } function buildSlackCommandArgMenuBlocks(params: { title: string; command: string; arg: string; choices: Array<{ value: string; label: string }>; userId: string; supportsExternalSelect: boolean; createExternalMenuToken: (choices: EncodedMenuChoice[]) => string; }) { const encodedChoices = params.choices.map((choice) => ({ label: choice.label, value: encodeSlackCommandArgValue({ command: params.command, arg: params.arg, value: choice.value, userId: params.userId, }), })); const canUseStaticSelect = encodedChoices.every( (choice) => choice.value.length <= SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX, ); const canUseOverflow = canUseStaticSelect && encodedChoices.length >= SLACK_COMMAND_ARG_OVERFLOW_MIN && encodedChoices.length <= SLACK_COMMAND_ARG_OVERFLOW_MAX; const canUseExternalSelect = params.supportsExternalSelect && canUseStaticSelect && encodedChoices.length > SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX; const rows = canUseOverflow ? [ { type: "actions", elements: [ { type: "overflow", action_id: SLACK_COMMAND_ARG_ACTION_ID, confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }), options: buildSlackArgMenuOptions(encodedChoices), }, ], }, ] : canUseExternalSelect ? [ { type: "actions", block_id: `${SLACK_EXTERNAL_ARG_MENU_PREFIX}${params.createExternalMenuToken( encodedChoices, )}`, elements: [ { type: "external_select", action_id: SLACK_COMMAND_ARG_ACTION_ID, confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }), min_query_length: 0, placeholder: { type: "plain_text", text: `Search ${params.arg}`, }, }, ], }, ] : encodedChoices.length <= SLACK_COMMAND_ARG_BUTTON_ROW_SIZE || !canUseStaticSelect ? chunkItems(encodedChoices, SLACK_COMMAND_ARG_BUTTON_ROW_SIZE).map((choices) => ({ type: "actions", elements: choices.map((choice) => ({ type: "button", action_id: SLACK_COMMAND_ARG_ACTION_ID, text: { type: "plain_text", text: choice.label }, value: choice.value, confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }), })), })) : chunkItems(encodedChoices, SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX).map( (choices, index) => ({ type: "actions", elements: [ { type: "static_select", action_id: SLACK_COMMAND_ARG_ACTION_ID, confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }), placeholder: { type: "plain_text", text: index === 0 ? `Choose ${params.arg}` : `Choose ${params.arg} (${index + 1})`, }, options: buildSlackArgMenuOptions(choices), }, ], }), ); const headerText = truncatePlainText( `/${params.command}: choose ${params.arg}`, SLACK_HEADER_TEXT_MAX, ); const sectionText = truncatePlainText(params.title, 3000); const contextText = truncatePlainText( `Select one option to continue /${params.command} (${params.arg})`, 3000, ); return [ { type: "header", text: { type: "plain_text", text: headerText }, }, { type: "section", text: { type: "mrkdwn", text: sectionText }, }, { type: "context", elements: [{ type: "mrkdwn", text: contextText }], }, ...rows, ]; } export async function registerSlackMonitorSlashCommands(params: { ctx: SlackMonitorContext; account: ResolvedSlackAccount; }): Promise { const { ctx, account } = params; const cfg = ctx.cfg; const runtime = ctx.runtime; const supportsInteractiveArgMenus = typeof (ctx.app as { action?: unknown }).action === "function"; let supportsExternalArgMenus = typeof (ctx.app as { options?: unknown }).options === "function"; const slashCommand = resolveSlackSlashCommandConfig( ctx.slashCommand ?? account.config.slashCommand, ); const handleSlashCommand = async (p: { command: SlackCommandMiddlewareArgs["command"]; ack: SlackCommandMiddlewareArgs["ack"]; respond: SlackCommandMiddlewareArgs["respond"]; body?: unknown; prompt: string; commandArgs?: CommandArgs; commandDefinition?: ChatCommandDefinition; }) => { const { command, ack, respond, body, prompt, commandArgs, commandDefinition } = p; try { if (ctx.shouldDropMismatchedSlackEvent?.(body)) { await ack(); runtime.log?.( `slack: drop slash command from user=${command.user_id ?? "unknown"} channel=${command.channel_id ?? "unknown"} (mismatched app/team)`, ); return; } if (!prompt.trim()) { await ack({ text: "Message required.", response_type: "ephemeral", }); return; } await ack(); if (ctx.botUserId && command.user_id === ctx.botUserId) { return; } const channelInfo = await ctx.resolveChannelName(command.channel_id); const rawChannelType = channelInfo?.type ?? (command.channel_name === "directmessage" ? "im" : undefined); const channelType = normalizeSlackChannelType(rawChannelType, command.channel_id); const isDirectMessage = channelType === "im"; const isGroupDm = channelType === "mpim"; const isRoom = channelType === "channel" || channelType === "group"; const isRoomish = isRoom || isGroupDm; if ( !ctx.isChannelAllowed({ channelId: command.channel_id, channelName: channelInfo?.name, channelType, }) ) { await respond({ text: "This channel is not allowed.", response_type: "ephemeral", }); return; } const { allowFromLower: effectiveAllowFromLower } = await resolveSlackEffectiveAllowFrom( ctx, { includePairingStore: isDirectMessage, }, ); // Privileged command surface: compute CommandAuthorized, don't assume true. // Keep this aligned with the Slack message path (message-handler/prepare.ts). let commandAuthorized = false; let channelConfig: SlackChannelConfigResolved | null = null; if (isDirectMessage) { const allowed = await authorizeSlackDirectMessage({ ctx, accountId: ctx.accountId, senderId: command.user_id, allowFromLower: effectiveAllowFromLower, resolveSenderName: ctx.resolveUserName, sendPairingReply: async (text) => { await respond({ text, response_type: "ephemeral", }); }, onDisabled: async () => { await respond({ text: "Slack DMs are disabled.", response_type: "ephemeral", }); }, onUnauthorized: async ({ allowMatchMeta }) => { logVerbose( `slack: blocked slash sender ${command.user_id} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`, ); await respond({ text: "You are not authorized to use this command.", response_type: "ephemeral", }); }, log: logVerbose, }); if (!allowed) { return; } } if (isRoom) { channelConfig = resolveSlackChannelConfig({ channelId: command.channel_id, channelName: channelInfo?.name, channels: ctx.channelsConfig, channelKeys: ctx.channelsConfigKeys, defaultRequireMention: ctx.defaultRequireMention, allowNameMatching: ctx.allowNameMatching, }); if (ctx.useAccessGroups) { const channelAllowlistConfigured = (ctx.channelsConfigKeys?.length ?? 0) > 0; const channelAllowed = channelConfig?.allowed !== false; if ( !isSlackChannelAllowedByPolicy({ groupPolicy: ctx.groupPolicy, channelAllowlistConfigured, channelAllowed, }) ) { await respond({ text: "This channel is not allowed.", response_type: "ephemeral", }); return; } // When groupPolicy is "open", only block channels that are EXPLICITLY denied // (i.e., have a matching config entry with allow:false). Channels not in the // config (matchSource undefined) should be allowed under open policy. const hasExplicitConfig = Boolean(channelConfig?.matchSource); if (!channelAllowed && (ctx.groupPolicy !== "open" || hasExplicitConfig)) { await respond({ text: "This channel is not allowed.", response_type: "ephemeral", }); return; } } } const sender = await ctx.resolveUserName(command.user_id); const senderName = sender?.name ?? command.user_name ?? command.user_id; const channelUsersAllowlistConfigured = isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0; const channelUserAllowed = channelUsersAllowlistConfigured ? resolveSlackUserAllowed({ allowList: channelConfig?.users, userId: command.user_id, userName: senderName, allowNameMatching: ctx.allowNameMatching, }) : false; if (channelUsersAllowlistConfigured && !channelUserAllowed) { await respond({ text: "You are not authorized to use this command here.", response_type: "ephemeral", }); return; } const ownerAllowed = resolveSlackAllowListMatch({ allowList: effectiveAllowFromLower, id: command.user_id, name: senderName, allowNameMatching: ctx.allowNameMatching, }).allowed; // DMs: allow chatting in dmPolicy=open, but keep privileged command gating intact by setting // CommandAuthorized based on allowlists/access-groups (downstream decides which commands need it). commandAuthorized = resolveCommandAuthorizedFromAuthorizers({ useAccessGroups: ctx.useAccessGroups, authorizers: [{ configured: effectiveAllowFromLower.length > 0, allowed: ownerAllowed }], modeWhenAccessGroupsOff: "configured", }); if (isRoomish) { commandAuthorized = resolveCommandAuthorizedFromAuthorizers({ useAccessGroups: ctx.useAccessGroups, authorizers: [ { configured: effectiveAllowFromLower.length > 0, allowed: ownerAllowed }, { configured: channelUsersAllowlistConfigured, allowed: channelUserAllowed }, ], modeWhenAccessGroupsOff: "configured", }); if (ctx.useAccessGroups && !commandAuthorized) { await respond({ text: "You are not authorized to use this command.", response_type: "ephemeral", }); return; } } if (commandDefinition && supportsInteractiveArgMenus) { const { resolveCommandArgMenu } = await loadSlashCommandsRuntime(); const menu = resolveCommandArgMenu({ command: commandDefinition, args: commandArgs, cfg, }); if (menu) { const commandLabel = commandDefinition.nativeName ?? commandDefinition.key; const title = menu.title ?? `Choose ${menu.arg.description || menu.arg.name} for /${commandLabel}.`; const blocks = buildSlackCommandArgMenuBlocks({ title, command: commandLabel, arg: menu.arg.name, choices: menu.choices, userId: command.user_id, supportsExternalSelect: supportsExternalArgMenus, createExternalMenuToken: (choices) => storeSlackExternalArgMenu({ choices, userId: command.user_id }), }); await respond({ text: title, blocks, response_type: "ephemeral", }); return; } } const channelName = channelInfo?.name; const roomLabel = channelName ? `#${channelName}` : `#${command.channel_id}`; const { createReplyPrefixOptions, deliverSlackSlashReplies, dispatchReplyWithDispatcher, finalizeInboundContext, recordInboundSessionMetaSafe, resolveAgentRoute, resolveChunkMode, resolveConversationLabel, resolveMarkdownTableMode, } = await loadSlashDispatchRuntime(); const route = resolveAgentRoute({ cfg, channel: "slack", accountId: account.accountId, teamId: ctx.teamId || undefined, peer: { kind: isDirectMessage ? "direct" : isRoom ? "channel" : "group", id: isDirectMessage ? command.user_id : command.channel_id, }, }); const { untrustedChannelMetadata, groupSystemPrompt } = resolveSlackRoomContextHints({ isRoomish, channelInfo, channelConfig, }); const { sessionKey, commandTargetSessionKey } = resolveNativeCommandSessionTargets({ agentId: route.agentId, sessionPrefix: slashCommand.sessionPrefix, userId: command.user_id, targetSessionKey: route.sessionKey, lowercaseSessionKey: true, }); const ctxPayload = finalizeInboundContext({ Body: prompt, BodyForAgent: prompt, RawBody: prompt, CommandBody: prompt, CommandArgs: commandArgs, From: isDirectMessage ? `slack:${command.user_id}` : isRoom ? `slack:channel:${command.channel_id}` : `slack:group:${command.channel_id}`, To: `slash:${command.user_id}`, ChatType: isDirectMessage ? "direct" : "channel", ConversationLabel: resolveConversationLabel({ ChatType: isDirectMessage ? "direct" : "channel", SenderName: senderName, GroupSubject: isRoomish ? roomLabel : undefined, From: isDirectMessage ? `slack:${command.user_id}` : isRoom ? `slack:channel:${command.channel_id}` : `slack:group:${command.channel_id}`, }) ?? (isDirectMessage ? senderName : roomLabel), GroupSubject: isRoomish ? roomLabel : undefined, GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined, UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined, SenderName: senderName, SenderId: command.user_id, Provider: "slack" as const, Surface: "slack" as const, WasMentioned: true, MessageSid: command.trigger_id, Timestamp: Date.now(), SessionKey: sessionKey, CommandTargetSessionKey: commandTargetSessionKey, AccountId: route.accountId, CommandSource: "native" as const, CommandAuthorized: commandAuthorized, OriginatingChannel: "slack" as const, OriginatingTo: `user:${command.user_id}`, }); await recordInboundSessionMetaSafe({ cfg, agentId: route.agentId, sessionKey: ctxPayload.SessionKey ?? route.sessionKey, ctx: ctxPayload, onError: (err) => runtime.error?.(danger(`slack slash: failed updating session meta: ${String(err)}`)), }); const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ cfg, agentId: route.agentId, channel: "slack", accountId: route.accountId, }); const deliverSlashPayloads = async (replies: ReplyPayload[]) => { await deliverSlackSlashReplies({ replies, respond, ephemeral: slashCommand.ephemeral, textLimit: ctx.textLimit, chunkMode: resolveChunkMode(cfg, "slack", route.accountId), tableMode: resolveMarkdownTableMode({ cfg, channel: "slack", accountId: route.accountId, }), }); }; const { counts } = await dispatchReplyWithDispatcher({ ctx: ctxPayload, cfg, dispatcherOptions: { ...prefixOptions, deliver: async (payload) => deliverSlashPayloads([payload]), onError: (err, info) => { runtime.error?.(danger(`slack slash ${info.kind} reply failed: ${String(err)}`)); }, }, replyOptions: { skillFilter: channelConfig?.skills, onModelSelected, }, }); if (counts.final + counts.tool + counts.block === 0) { await deliverSlashPayloads([]); } } catch (err) { runtime.error?.(danger(`slack slash handler failed: ${String(err)}`)); await respond({ text: "Sorry, something went wrong handling that command.", response_type: "ephemeral", }); } }; const nativeEnabled = resolveNativeCommandsEnabled({ providerId: "slack", providerSetting: account.config.commands?.native, globalSetting: cfg.commands?.native, }); const nativeSkillsEnabled = resolveNativeSkillsEnabled({ providerId: "slack", providerSetting: account.config.commands?.nativeSkills, globalSetting: cfg.commands?.nativeSkills, }); let nativeCommands: Array<{ name: string }> = []; let slashCommandsRuntime: typeof import("./slash-commands.runtime.js") | null = null; if (nativeEnabled) { slashCommandsRuntime = await loadSlashCommandsRuntime(); const skillCommands = nativeSkillsEnabled ? (await loadSlashSkillCommandsRuntime()).listSkillCommandsForAgents({ cfg }) : []; nativeCommands = slashCommandsRuntime.listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "slack", }); } if (nativeCommands.length > 0) { if (!slashCommandsRuntime) { throw new Error("Missing commands runtime for native Slack commands."); } for (const command of nativeCommands) { ctx.app.command( `/${command.name}`, async ({ command: cmd, ack, respond, body }: SlackCommandMiddlewareArgs) => { const commandDefinition = slashCommandsRuntime.findCommandByNativeName( command.name, "slack", ); const rawText = cmd.text?.trim() ?? ""; const commandArgs = commandDefinition ? slashCommandsRuntime.parseCommandArgs(commandDefinition, rawText) : rawText ? ({ raw: rawText } satisfies CommandArgs) : undefined; const prompt = commandDefinition ? slashCommandsRuntime.buildCommandTextFromArgs(commandDefinition, commandArgs) : rawText ? `/${command.name} ${rawText}` : `/${command.name}`; await handleSlashCommand({ command: cmd, ack, respond, body, prompt, commandArgs, commandDefinition: commandDefinition ?? undefined, }); }, ); } } else if (slashCommand.enabled) { ctx.app.command( buildSlackSlashCommandMatcher(slashCommand.name), async ({ command, ack, respond, body }: SlackCommandMiddlewareArgs) => { await handleSlashCommand({ command, ack, respond, body, prompt: command.text?.trim() ?? "", }); }, ); } else { logVerbose("slack: slash commands disabled"); } if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) { return; } const registerArgOptions = () => { const appWithOptions = ctx.app as unknown as { options?: ( actionId: string, handler: (args: { ack: (payload: { options: unknown[] }) => Promise; body: unknown; }) => Promise, ) => void; }; if (typeof appWithOptions.options !== "function") { return; } appWithOptions.options(SLACK_COMMAND_ARG_ACTION_ID, async ({ ack, body }) => { if (ctx.shouldDropMismatchedSlackEvent?.(body)) { await ack({ options: [] }); runtime.log?.("slack: drop slash arg options payload (mismatched app/team)"); return; } const typedBody = body as { value?: string; user?: { id?: string }; actions?: Array<{ block_id?: string }>; block_id?: string; }; const blockId = typedBody.actions?.[0]?.block_id ?? typedBody.block_id; const token = readSlackExternalArgMenuToken(blockId); if (!token) { await ack({ options: [] }); return; } const entry = slackExternalArgMenuStore.get(token); if (!entry) { await ack({ options: [] }); return; } const requesterUserId = typedBody.user?.id?.trim(); if (!requesterUserId || requesterUserId !== entry.userId) { await ack({ options: [] }); return; } const query = typedBody.value?.trim().toLowerCase() ?? ""; const options = entry.choices .filter((choice) => !query || choice.label.toLowerCase().includes(query)) .slice(0, SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX) .map((choice) => ({ text: { type: "plain_text", text: choice.label.slice(0, 75) }, value: choice.value, })); await ack({ options }); }); }; // Treat external arg-menu registration as best-effort: if Bolt's app.options() // throws (e.g. from receiver init issues), disable external selects and fall back // to static_select/button menus instead of crashing the entire provider startup. try { registerArgOptions(); } catch (err) { supportsExternalArgMenus = false; logVerbose( `slack: external arg-menu registration failed, falling back to static menus: ${String(err)}`, ); } const registerArgAction = (actionId: string) => { ( ctx.app as unknown as { action: NonNullable<(typeof ctx.app & { action?: unknown })["action"]>; } ).action(actionId, async (args: SlackActionMiddlewareArgs) => { const { ack, body, respond } = args; const action = args.action as { value?: string; selected_option?: { value?: string } }; await ack(); if (ctx.shouldDropMismatchedSlackEvent?.(body)) { runtime.log?.("slack: drop slash arg action payload (mismatched app/team)"); return; } const respondFn = respond ?? (async (payload: { text: string; blocks?: SlackBlock[]; response_type?: string }) => { if (!body.channel?.id || !body.user?.id) { return; } await ctx.app.client.chat.postEphemeral({ token: ctx.botToken, channel: body.channel.id, user: body.user.id, text: payload.text, blocks: payload.blocks, }); }); const actionValue = action?.value ?? action?.selected_option?.value; const parsed = parseSlackCommandArgValue(actionValue); if (!parsed) { await respondFn({ text: "Sorry, that button is no longer valid.", response_type: "ephemeral", }); return; } if (body.user?.id && parsed.userId !== body.user.id) { await respondFn({ text: "That menu is for another user.", response_type: "ephemeral", }); return; } const { buildCommandTextFromArgs, findCommandByNativeName } = await loadSlashCommandsRuntime(); const commandDefinition = findCommandByNativeName(parsed.command, "slack"); const commandArgs: CommandArgs = { values: { [parsed.arg]: parsed.value }, }; const prompt = commandDefinition ? buildCommandTextFromArgs(commandDefinition, commandArgs) : `/${parsed.command} ${parsed.value}`; const user = body.user; const userName = user && "name" in user && user.name ? user.name : user && "username" in user && user.username ? user.username : (user?.id ?? ""); const triggerId = "trigger_id" in body ? body.trigger_id : undefined; const commandPayload = { user_id: user?.id ?? "", user_name: userName, channel_id: body.channel?.id ?? "", channel_name: body.channel?.name ?? body.channel?.id ?? "", trigger_id: triggerId, } as SlackCommandMiddlewareArgs["command"]; await handleSlashCommand({ command: commandPayload, ack: async () => {}, respond: respondFn, body, prompt, commandArgs, commandDefinition: commandDefinition ?? undefined, }); }); }; registerArgAction(SLACK_COMMAND_ARG_ACTION_ID); }