mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 07:12:42 +00:00
Slack: add external select flow for large arg menus
This commit is contained in:
@@ -5,6 +5,7 @@ vi.mock("../../auto-reply/commands-registry.js", () => {
|
|||||||
const usageCommand = { key: "usage", nativeName: "usage" };
|
const usageCommand = { key: "usage", nativeName: "usage" };
|
||||||
const reportCommand = { key: "report", nativeName: "report" };
|
const reportCommand = { key: "report", nativeName: "report" };
|
||||||
const reportCompactCommand = { key: "reportcompact", nativeName: "reportcompact" };
|
const reportCompactCommand = { key: "reportcompact", nativeName: "reportcompact" };
|
||||||
|
const reportExternalCommand = { key: "reportexternal", nativeName: "reportexternal" };
|
||||||
const reportLongCommand = { key: "reportlong", nativeName: "reportlong" };
|
const reportLongCommand = { key: "reportlong", nativeName: "reportlong" };
|
||||||
const unsafeConfirmCommand = { key: "unsafeconfirm", nativeName: "unsafeconfirm" };
|
const unsafeConfirmCommand = { key: "unsafeconfirm", nativeName: "unsafeconfirm" };
|
||||||
|
|
||||||
@@ -36,6 +37,9 @@ vi.mock("../../auto-reply/commands-registry.js", () => {
|
|||||||
if (normalized === "reportcompact") {
|
if (normalized === "reportcompact") {
|
||||||
return reportCompactCommand;
|
return reportCompactCommand;
|
||||||
}
|
}
|
||||||
|
if (normalized === "reportexternal") {
|
||||||
|
return reportExternalCommand;
|
||||||
|
}
|
||||||
if (normalized === "reportlong") {
|
if (normalized === "reportlong") {
|
||||||
return reportLongCommand;
|
return reportLongCommand;
|
||||||
}
|
}
|
||||||
@@ -63,6 +67,12 @@ vi.mock("../../auto-reply/commands-registry.js", () => {
|
|||||||
acceptsArgs: true,
|
acceptsArgs: true,
|
||||||
args: [],
|
args: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "reportexternal",
|
||||||
|
description: "ReportExternal",
|
||||||
|
acceptsArgs: true,
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "reportlong",
|
name: "reportlong",
|
||||||
description: "ReportLong",
|
description: "ReportLong",
|
||||||
@@ -130,6 +140,15 @@ vi.mock("../../auto-reply/commands-registry.js", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (params.command?.key === "reportexternal") {
|
||||||
|
return {
|
||||||
|
arg: { name: "period", description: "period" },
|
||||||
|
choices: Array.from({ length: 140 }, (_v, i) => ({
|
||||||
|
value: `period-${i + 1}`,
|
||||||
|
label: `Period ${i + 1}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (params.command?.key === "unsafeconfirm") {
|
if (params.command?.key === "unsafeconfirm") {
|
||||||
return {
|
return {
|
||||||
arg: { name: "mode_*`~<&>", description: "mode" },
|
arg: { name: "mode_*`~<&>", description: "mode" },
|
||||||
@@ -195,6 +214,7 @@ function findFirstActionsBlock(payload: { blocks?: Array<{ type: string }> }) {
|
|||||||
function createArgMenusHarness() {
|
function createArgMenusHarness() {
|
||||||
const commands = new Map<string, (args: unknown) => Promise<void>>();
|
const commands = new Map<string, (args: unknown) => Promise<void>>();
|
||||||
const actions = new Map<string, (args: unknown) => Promise<void>>();
|
const actions = new Map<string, (args: unknown) => Promise<void>>();
|
||||||
|
const options = new Map<string, (args: unknown) => Promise<void>>();
|
||||||
|
|
||||||
const postEphemeral = vi.fn().mockResolvedValue({ ok: true });
|
const postEphemeral = vi.fn().mockResolvedValue({ ok: true });
|
||||||
const app = {
|
const app = {
|
||||||
@@ -205,6 +225,9 @@ function createArgMenusHarness() {
|
|||||||
action: (id: string, handler: (args: unknown) => Promise<void>) => {
|
action: (id: string, handler: (args: unknown) => Promise<void>) => {
|
||||||
actions.set(id, handler);
|
actions.set(id, handler);
|
||||||
},
|
},
|
||||||
|
options: (id: string, handler: (args: unknown) => Promise<void>) => {
|
||||||
|
options.set(id, handler);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
@@ -240,7 +263,7 @@ function createArgMenusHarness() {
|
|||||||
config: { commands: { native: true, nativeSkills: false } },
|
config: { commands: { native: true, nativeSkills: false } },
|
||||||
} as unknown;
|
} as unknown;
|
||||||
|
|
||||||
return { commands, actions, postEphemeral, ctx, account };
|
return { commands, actions, options, postEphemeral, ctx, account };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Slack native command argument menus", () => {
|
describe("Slack native command argument menus", () => {
|
||||||
@@ -248,9 +271,11 @@ describe("Slack native command argument menus", () => {
|
|||||||
let usageHandler: (args: unknown) => Promise<void>;
|
let usageHandler: (args: unknown) => Promise<void>;
|
||||||
let reportHandler: (args: unknown) => Promise<void>;
|
let reportHandler: (args: unknown) => Promise<void>;
|
||||||
let reportCompactHandler: (args: unknown) => Promise<void>;
|
let reportCompactHandler: (args: unknown) => Promise<void>;
|
||||||
|
let reportExternalHandler: (args: unknown) => Promise<void>;
|
||||||
let reportLongHandler: (args: unknown) => Promise<void>;
|
let reportLongHandler: (args: unknown) => Promise<void>;
|
||||||
let unsafeConfirmHandler: (args: unknown) => Promise<void>;
|
let unsafeConfirmHandler: (args: unknown) => Promise<void>;
|
||||||
let argMenuHandler: (args: unknown) => Promise<void>;
|
let argMenuHandler: (args: unknown) => Promise<void>;
|
||||||
|
let argMenuOptionsHandler: (args: unknown) => Promise<void>;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
harness = createArgMenusHarness();
|
harness = createArgMenusHarness();
|
||||||
@@ -271,6 +296,11 @@ describe("Slack native command argument menus", () => {
|
|||||||
throw new Error("Missing /reportcompact handler");
|
throw new Error("Missing /reportcompact handler");
|
||||||
}
|
}
|
||||||
reportCompactHandler = reportCompact;
|
reportCompactHandler = reportCompact;
|
||||||
|
const reportExternal = harness.commands.get("/reportexternal");
|
||||||
|
if (!reportExternal) {
|
||||||
|
throw new Error("Missing /reportexternal handler");
|
||||||
|
}
|
||||||
|
reportExternalHandler = reportExternal;
|
||||||
const reportLong = harness.commands.get("/reportlong");
|
const reportLong = harness.commands.get("/reportlong");
|
||||||
if (!reportLong) {
|
if (!reportLong) {
|
||||||
throw new Error("Missing /reportlong handler");
|
throw new Error("Missing /reportlong handler");
|
||||||
@@ -287,6 +317,11 @@ describe("Slack native command argument menus", () => {
|
|||||||
throw new Error("Missing arg-menu action handler");
|
throw new Error("Missing arg-menu action handler");
|
||||||
}
|
}
|
||||||
argMenuHandler = argMenu;
|
argMenuHandler = argMenu;
|
||||||
|
const argMenuOptions = harness.options.get("openclaw_cmdarg");
|
||||||
|
if (!argMenuOptions) {
|
||||||
|
throw new Error("Missing arg-menu options handler");
|
||||||
|
}
|
||||||
|
argMenuOptionsHandler = argMenuOptions;
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -498,6 +533,77 @@ describe("Slack native command argument menus", () => {
|
|||||||
expect(call.ctx?.Body).toBe("/reportcompact quarter");
|
expect(call.ctx?.Body).toBe("/reportcompact quarter");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows an external_select menu when choices exceed static_select options max", async () => {
|
||||||
|
const respond = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const ack = vi.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await reportExternalHandler({
|
||||||
|
command: {
|
||||||
|
user_id: "U1",
|
||||||
|
user_name: "Ada",
|
||||||
|
channel_id: "C1",
|
||||||
|
channel_name: "directmessage",
|
||||||
|
text: "",
|
||||||
|
trigger_id: "t1",
|
||||||
|
},
|
||||||
|
ack,
|
||||||
|
respond,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledTimes(1);
|
||||||
|
const payload = respond.mock.calls[0]?.[0] as {
|
||||||
|
blocks?: Array<{ type: string; block_id?: string }>;
|
||||||
|
};
|
||||||
|
const actions = findFirstActionsBlock(payload);
|
||||||
|
const element = actions?.elements?.[0];
|
||||||
|
expect(element?.type).toBe("external_select");
|
||||||
|
expect(element?.action_id).toBe("openclaw_cmdarg");
|
||||||
|
expect(payload.blocks?.find((block) => block.type === "actions")?.block_id).toContain(
|
||||||
|
"openclaw_cmdarg_ext:",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serves filtered options for external_select menus", async () => {
|
||||||
|
const respond = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const ack = vi.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await reportExternalHandler({
|
||||||
|
command: {
|
||||||
|
user_id: "U1",
|
||||||
|
user_name: "Ada",
|
||||||
|
channel_id: "C1",
|
||||||
|
channel_name: "directmessage",
|
||||||
|
text: "",
|
||||||
|
trigger_id: "t1",
|
||||||
|
},
|
||||||
|
ack,
|
||||||
|
respond,
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = respond.mock.calls[0]?.[0] as {
|
||||||
|
blocks?: Array<{ type: string; block_id?: string }>;
|
||||||
|
};
|
||||||
|
const blockId = payload.blocks?.find((block) => block.type === "actions")?.block_id;
|
||||||
|
expect(blockId).toContain("openclaw_cmdarg_ext:");
|
||||||
|
|
||||||
|
const ackOptions = vi.fn().mockResolvedValue(undefined);
|
||||||
|
await argMenuOptionsHandler({
|
||||||
|
ack: ackOptions,
|
||||||
|
body: {
|
||||||
|
user: { id: "U1" },
|
||||||
|
value: "period 12",
|
||||||
|
actions: [{ block_id: blockId }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ackOptions).toHaveBeenCalledTimes(1);
|
||||||
|
const optionsPayload = ackOptions.mock.calls[0]?.[0] as {
|
||||||
|
options?: Array<{ text?: { text?: string }; value?: string }>;
|
||||||
|
};
|
||||||
|
const optionTexts = (optionsPayload.options ?? []).map((option) => option.text?.text ?? "");
|
||||||
|
expect(optionTexts.some((text) => text.includes("Period 12"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects menu clicks from other users", async () => {
|
it("rejects menu clicks from other users", async () => {
|
||||||
const respond = vi.fn().mockResolvedValue(undefined);
|
const respond = vi.fn().mockResolvedValue(undefined);
|
||||||
await argMenuHandler({
|
await argMenuHandler({
|
||||||
|
|||||||
@@ -34,8 +34,16 @@ const SLACK_COMMAND_ARG_OVERFLOW_MIN = 3;
|
|||||||
const SLACK_COMMAND_ARG_OVERFLOW_MAX = 5;
|
const SLACK_COMMAND_ARG_OVERFLOW_MAX = 5;
|
||||||
const SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX = 100;
|
const SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX = 100;
|
||||||
const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75;
|
const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75;
|
||||||
|
const SLACK_COMMAND_ARG_EXTERNAL_PREFIX = "openclaw_cmdarg_ext:";
|
||||||
|
const SLACK_COMMAND_ARG_EXTERNAL_TTL_MS = 10 * 60 * 1000;
|
||||||
const SLACK_HEADER_TEXT_MAX = 150;
|
const SLACK_HEADER_TEXT_MAX = 150;
|
||||||
|
|
||||||
|
type EncodedMenuChoice = { label: string; value: string };
|
||||||
|
const slackExternalArgMenuStore = new Map<
|
||||||
|
string,
|
||||||
|
{ choices: EncodedMenuChoice[]; userId: string; expiresAt: number }
|
||||||
|
>();
|
||||||
|
|
||||||
function truncatePlainText(value: string, max: number): string {
|
function truncatePlainText(value: string, max: number): string {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (trimmed.length <= max) {
|
if (trimmed.length <= max) {
|
||||||
@@ -70,6 +78,36 @@ function buildSlackArgMenuConfirm(params: { command: string; arg: string }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pruneSlackExternalArgMenuStore(now = Date.now()) {
|
||||||
|
for (const [token, entry] of slackExternalArgMenuStore.entries()) {
|
||||||
|
if (entry.expiresAt <= now) {
|
||||||
|
slackExternalArgMenuStore.delete(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeSlackExternalArgMenu(params: {
|
||||||
|
choices: EncodedMenuChoice[];
|
||||||
|
userId: string;
|
||||||
|
}): string {
|
||||||
|
pruneSlackExternalArgMenuStore();
|
||||||
|
const token = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
slackExternalArgMenuStore.set(token, {
|
||||||
|
choices: params.choices,
|
||||||
|
userId: params.userId,
|
||||||
|
expiresAt: Date.now() + SLACK_COMMAND_ARG_EXTERNAL_TTL_MS,
|
||||||
|
});
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSlackExternalArgMenuToken(raw: unknown): string | undefined {
|
||||||
|
if (typeof raw !== "string" || !raw.startsWith(SLACK_COMMAND_ARG_EXTERNAL_PREFIX)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const token = raw.slice(SLACK_COMMAND_ARG_EXTERNAL_PREFIX.length).trim();
|
||||||
|
return token.length > 0 ? token : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
type CommandsRegistry = typeof import("../../auto-reply/commands-registry.js");
|
type CommandsRegistry = typeof import("../../auto-reply/commands-registry.js");
|
||||||
let commandsRegistry: CommandsRegistry | undefined;
|
let commandsRegistry: CommandsRegistry | undefined;
|
||||||
async function getCommandsRegistry(): Promise<CommandsRegistry> {
|
async function getCommandsRegistry(): Promise<CommandsRegistry> {
|
||||||
@@ -139,6 +177,8 @@ function buildSlackCommandArgMenuBlocks(params: {
|
|||||||
arg: string;
|
arg: string;
|
||||||
choices: Array<{ value: string; label: string }>;
|
choices: Array<{ value: string; label: string }>;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
supportsExternalSelect: boolean;
|
||||||
|
createExternalMenuToken: (choices: EncodedMenuChoice[]) => string;
|
||||||
}) {
|
}) {
|
||||||
const encodedChoices = params.choices.map((choice) => ({
|
const encodedChoices = params.choices.map((choice) => ({
|
||||||
label: choice.label,
|
label: choice.label,
|
||||||
@@ -156,6 +196,10 @@ function buildSlackCommandArgMenuBlocks(params: {
|
|||||||
canUseStaticSelect &&
|
canUseStaticSelect &&
|
||||||
encodedChoices.length >= SLACK_COMMAND_ARG_OVERFLOW_MIN &&
|
encodedChoices.length >= SLACK_COMMAND_ARG_OVERFLOW_MIN &&
|
||||||
encodedChoices.length <= SLACK_COMMAND_ARG_OVERFLOW_MAX;
|
encodedChoices.length <= SLACK_COMMAND_ARG_OVERFLOW_MAX;
|
||||||
|
const canUseExternalSelect =
|
||||||
|
params.supportsExternalSelect &&
|
||||||
|
canUseStaticSelect &&
|
||||||
|
encodedChoices.length > SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX;
|
||||||
const rows = canUseOverflow
|
const rows = canUseOverflow
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -173,35 +217,59 @@ function buildSlackCommandArgMenuBlocks(params: {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: encodedChoices.length <= SLACK_COMMAND_ARG_BUTTON_ROW_SIZE || !canUseStaticSelect
|
: canUseExternalSelect
|
||||||
? chunkItems(encodedChoices, SLACK_COMMAND_ARG_BUTTON_ROW_SIZE).map((choices) => ({
|
? [
|
||||||
type: "actions",
|
{
|
||||||
elements: choices.map((choice) => ({
|
type: "actions",
|
||||||
type: "button",
|
block_id: `${SLACK_COMMAND_ARG_EXTERNAL_PREFIX}${params.createExternalMenuToken(
|
||||||
action_id: SLACK_COMMAND_ARG_ACTION_ID,
|
encodedChoices,
|
||||||
text: { type: "plain_text", text: choice.label },
|
)}`,
|
||||||
value: choice.value,
|
elements: [
|
||||||
confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }),
|
{
|
||||||
})),
|
type: "external_select",
|
||||||
}))
|
action_id: SLACK_COMMAND_ARG_ACTION_ID,
|
||||||
: chunkItems(encodedChoices, SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX).map((choices, index) => ({
|
confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }),
|
||||||
type: "actions",
|
min_query_length: 0,
|
||||||
elements: [
|
placeholder: {
|
||||||
{
|
type: "plain_text",
|
||||||
type: "static_select",
|
text: `Search ${params.arg}`,
|
||||||
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: choices.map((choice) => ({
|
],
|
||||||
text: { type: "plain_text", text: choice.label.slice(0, 75) },
|
},
|
||||||
value: choice.value,
|
]
|
||||||
})),
|
: 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: choices.map((choice) => ({
|
||||||
|
text: { type: "plain_text", text: choice.label.slice(0, 75) },
|
||||||
|
value: choice.value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const headerText = truncatePlainText(
|
const headerText = truncatePlainText(
|
||||||
`/${params.command}: choose ${params.arg}`,
|
`/${params.command}: choose ${params.arg}`,
|
||||||
SLACK_HEADER_TEXT_MAX,
|
SLACK_HEADER_TEXT_MAX,
|
||||||
@@ -238,6 +306,7 @@ export async function registerSlackMonitorSlashCommands(params: {
|
|||||||
|
|
||||||
const supportsInteractiveArgMenus =
|
const supportsInteractiveArgMenus =
|
||||||
typeof (ctx.app as { action?: unknown }).action === "function";
|
typeof (ctx.app as { action?: unknown }).action === "function";
|
||||||
|
const supportsExternalArgMenus = typeof (ctx.app as { options?: unknown }).options === "function";
|
||||||
|
|
||||||
const slashCommand = resolveSlackSlashCommandConfig(
|
const slashCommand = resolveSlackSlashCommandConfig(
|
||||||
ctx.slashCommand ?? account.config.slashCommand,
|
ctx.slashCommand ?? account.config.slashCommand,
|
||||||
@@ -454,6 +523,9 @@ export async function registerSlackMonitorSlashCommands(params: {
|
|||||||
arg: menu.arg.name,
|
arg: menu.arg.name,
|
||||||
choices: menu.choices,
|
choices: menu.choices,
|
||||||
userId: command.user_id,
|
userId: command.user_id,
|
||||||
|
supportsExternalSelect: supportsExternalArgMenus,
|
||||||
|
createExternalMenuToken: (choices) =>
|
||||||
|
storeSlackExternalArgMenu({ choices, userId: command.user_id }),
|
||||||
});
|
});
|
||||||
await respond({
|
await respond({
|
||||||
text: title,
|
text: title,
|
||||||
@@ -666,6 +738,57 @@ export async function registerSlackMonitorSlashCommands(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registerArgOptions = () => {
|
||||||
|
const optionsHandler = (
|
||||||
|
ctx.app as unknown as {
|
||||||
|
options?: (
|
||||||
|
actionId: string,
|
||||||
|
handler: (args: {
|
||||||
|
ack: (payload: { options: unknown[] }) => Promise<void>;
|
||||||
|
body: unknown;
|
||||||
|
}) => Promise<void>,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
).options;
|
||||||
|
if (typeof optionsHandler !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
optionsHandler(SLACK_COMMAND_ARG_ACTION_ID, async ({ ack, body }) => {
|
||||||
|
const typedBody = body as {
|
||||||
|
value?: string;
|
||||||
|
user?: { id?: string };
|
||||||
|
actions?: Array<{ block_id?: string }>;
|
||||||
|
block_id?: string;
|
||||||
|
};
|
||||||
|
pruneSlackExternalArgMenuStore();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (typedBody.user?.id && typedBody.user.id !== 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 });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
registerArgOptions();
|
||||||
|
|
||||||
const registerArgAction = (actionId: string) => {
|
const registerArgAction = (actionId: string) => {
|
||||||
(
|
(
|
||||||
ctx.app as unknown as {
|
ctx.app as unknown as {
|
||||||
|
|||||||
Reference in New Issue
Block a user