mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 20:11:23 +00:00
Slack: capture Block Kit modal submissions
This commit is contained in:
@@ -18,12 +18,29 @@ type RegisteredHandler = (args: {
|
||||
respond?: (payload: { text: string; response_type: string }) => Promise<void>;
|
||||
}) => Promise<void>;
|
||||
|
||||
type RegisteredViewHandler = (args: {
|
||||
ack: () => Promise<void>;
|
||||
body: {
|
||||
user?: { id?: string };
|
||||
team?: { id?: string };
|
||||
view?: {
|
||||
id?: string;
|
||||
callback_id?: string;
|
||||
state?: { values?: Record<string, Record<string, Record<string, unknown>>> };
|
||||
};
|
||||
};
|
||||
}) => Promise<void>;
|
||||
|
||||
function createContext() {
|
||||
let handler: RegisteredHandler | null = null;
|
||||
let viewHandler: RegisteredViewHandler | null = null;
|
||||
const app = {
|
||||
action: vi.fn((_matcher: RegExp, next: RegisteredHandler) => {
|
||||
handler = next;
|
||||
}),
|
||||
view: vi.fn((_matcher: RegExp, next: RegisteredViewHandler) => {
|
||||
viewHandler = next;
|
||||
}),
|
||||
client: {
|
||||
chat: {
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -37,7 +54,14 @@ function createContext() {
|
||||
runtime: { log: runtimeLog },
|
||||
resolveSlackSystemEventSessionKey: resolveSessionKey,
|
||||
};
|
||||
return { ctx, app, runtimeLog, resolveSessionKey, getHandler: () => handler };
|
||||
return {
|
||||
ctx,
|
||||
app,
|
||||
runtimeLog,
|
||||
resolveSessionKey,
|
||||
getHandler: () => handler,
|
||||
getViewHandler: () => viewHandler,
|
||||
};
|
||||
}
|
||||
|
||||
describe("registerSlackInteractionEvents", () => {
|
||||
@@ -141,4 +165,70 @@ describe("registerSlackInteractionEvents", () => {
|
||||
expect(payload.selectedValues).toEqual(["canary"]);
|
||||
expect(app.client.chat.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("captures modal submissions and enqueues view submission event", async () => {
|
||||
enqueueSystemEventMock.mockReset();
|
||||
const { ctx, getViewHandler, resolveSessionKey } = createContext();
|
||||
registerSlackInteractionEvents({ ctx: ctx as never });
|
||||
const viewHandler = getViewHandler();
|
||||
expect(viewHandler).toBeTruthy();
|
||||
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
await viewHandler!({
|
||||
ack,
|
||||
body: {
|
||||
user: { id: "U777" },
|
||||
team: { id: "T1" },
|
||||
view: {
|
||||
id: "V123",
|
||||
callback_id: "openclaw:deploy_form",
|
||||
state: {
|
||||
values: {
|
||||
env_block: {
|
||||
env_select: {
|
||||
type: "static_select",
|
||||
selected_option: {
|
||||
text: { type: "plain_text", text: "Production" },
|
||||
value: "prod",
|
||||
},
|
||||
},
|
||||
},
|
||||
notes_block: {
|
||||
notes_input: {
|
||||
type: "plain_text_input",
|
||||
value: "ship now",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(ack).toHaveBeenCalled();
|
||||
expect(resolveSessionKey).toHaveBeenCalledWith({});
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
|
||||
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
|
||||
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
|
||||
interactionType: string;
|
||||
actionId: string;
|
||||
callbackId: string;
|
||||
viewId: string;
|
||||
userId: string;
|
||||
inputs: Array<{ actionId: string; selectedValues?: string[]; inputValue?: string }>;
|
||||
};
|
||||
expect(payload).toMatchObject({
|
||||
interactionType: "view_submission",
|
||||
actionId: "view:openclaw:deploy_form",
|
||||
callbackId: "openclaw:deploy_form",
|
||||
viewId: "V123",
|
||||
userId: "U777",
|
||||
});
|
||||
expect(payload.inputs).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ actionId: "env_select", selectedValues: ["prod"] }),
|
||||
expect.objectContaining({ actionId: "notes_input", inputValue: "ship now" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ type SelectOption = {
|
||||
};
|
||||
|
||||
type InteractionSummary = {
|
||||
interactionType?: "block_action" | "view_submission";
|
||||
actionId: string;
|
||||
blockId?: string;
|
||||
actionType?: string;
|
||||
@@ -33,6 +34,19 @@ type InteractionSummary = {
|
||||
messageTs?: string;
|
||||
};
|
||||
|
||||
type ModalInputSummary = {
|
||||
blockId: string;
|
||||
actionId: string;
|
||||
actionType?: string;
|
||||
value?: string;
|
||||
selectedValues?: string[];
|
||||
selectedLabels?: string[];
|
||||
selectedDate?: string;
|
||||
selectedTime?: string;
|
||||
selectedDateTime?: number;
|
||||
inputValue?: string;
|
||||
};
|
||||
|
||||
function readOptionValues(options: unknown): string[] | undefined {
|
||||
if (!Array.isArray(options)) {
|
||||
return undefined;
|
||||
@@ -108,6 +122,30 @@ function isBulkActionsBlock(block: InteractionMessageBlock): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function summarizeViewState(values: unknown): ModalInputSummary[] {
|
||||
if (!values || typeof values !== "object") {
|
||||
return [];
|
||||
}
|
||||
const entries: ModalInputSummary[] = [];
|
||||
for (const [blockId, blockValue] of Object.entries(values as Record<string, unknown>)) {
|
||||
if (!blockValue || typeof blockValue !== "object") {
|
||||
continue;
|
||||
}
|
||||
for (const [actionId, rawAction] of Object.entries(blockValue as Record<string, unknown>)) {
|
||||
if (!rawAction || typeof rawAction !== "object") {
|
||||
continue;
|
||||
}
|
||||
const actionSummary = summarizeAction(rawAction as Record<string, unknown>);
|
||||
entries.push({
|
||||
blockId,
|
||||
actionId,
|
||||
...actionSummary,
|
||||
});
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContext }) {
|
||||
const { ctx } = params;
|
||||
if (typeof ctx.app.action !== "function") {
|
||||
@@ -144,6 +182,7 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
const messageTs = typedBody.message?.ts;
|
||||
const actionSummary = summarizeAction(typedAction);
|
||||
const eventPayload: InteractionSummary = {
|
||||
interactionType: "block_action",
|
||||
actionId,
|
||||
blockId,
|
||||
...actionSummary,
|
||||
@@ -236,4 +275,51 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (typeof ctx.app.view !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle OpenClaw modal submissions with callback_ids scoped by our prefix.
|
||||
ctx.app.view(
|
||||
new RegExp(`^${OPENCLAW_ACTION_PREFIX}`),
|
||||
async ({ ack, body }: { ack: () => Promise<void>; body: unknown }) => {
|
||||
await ack();
|
||||
|
||||
const typedBody = body as {
|
||||
user?: { id?: string };
|
||||
team?: { id?: string };
|
||||
view?: {
|
||||
id?: string;
|
||||
callback_id?: string;
|
||||
state?: { values?: unknown };
|
||||
};
|
||||
};
|
||||
|
||||
const callbackId = typedBody.view?.callback_id ?? "unknown";
|
||||
const userId = typedBody.user?.id ?? "unknown";
|
||||
const viewId = typedBody.view?.id;
|
||||
const inputs = summarizeViewState(typedBody.view?.state?.values);
|
||||
const eventPayload = {
|
||||
interactionType: "view_submission",
|
||||
actionId: `view:${callbackId}`,
|
||||
callbackId,
|
||||
viewId,
|
||||
userId,
|
||||
teamId: typedBody.team?.id,
|
||||
inputs,
|
||||
};
|
||||
|
||||
ctx.runtime.log?.(
|
||||
`slack:interaction view_submission callback=${callbackId} user=${userId} inputs=${inputs.length}`,
|
||||
);
|
||||
|
||||
enqueueSystemEvent(`Slack interaction: ${JSON.stringify(eventPayload)}`, {
|
||||
sessionKey: ctx.resolveSlackSystemEventSessionKey({}),
|
||||
contextKey: ["slack:interaction:view", callbackId, viewId, userId]
|
||||
.filter(Boolean)
|
||||
.join(":"),
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user