Slack: route modal interactions via private metadata
This commit is contained in:
@@ -202,6 +202,7 @@ describe("registerSlackInteractionEvents", () => {
|
||||
view: {
|
||||
id: "V123",
|
||||
callback_id: "openclaw:deploy_form",
|
||||
private_metadata: JSON.stringify({ channelId: "D123", channelType: "im" }),
|
||||
state: {
|
||||
values: {
|
||||
env_block: {
|
||||
@@ -226,7 +227,10 @@ describe("registerSlackInteractionEvents", () => {
|
||||
});
|
||||
|
||||
expect(ack).toHaveBeenCalled();
|
||||
expect(resolveSessionKey).toHaveBeenCalledWith({});
|
||||
expect(resolveSessionKey).toHaveBeenCalledWith({
|
||||
channelId: "D123",
|
||||
channelType: "im",
|
||||
});
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
|
||||
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
|
||||
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
|
||||
@@ -235,6 +239,7 @@ describe("registerSlackInteractionEvents", () => {
|
||||
callbackId: string;
|
||||
viewId: string;
|
||||
userId: string;
|
||||
routedChannelId?: string;
|
||||
inputs: Array<{ actionId: string; selectedValues?: string[]; inputValue?: string }>;
|
||||
};
|
||||
expect(payload).toMatchObject({
|
||||
@@ -243,6 +248,7 @@ describe("registerSlackInteractionEvents", () => {
|
||||
callbackId: "openclaw:deploy_form",
|
||||
viewId: "V123",
|
||||
userId: "U777",
|
||||
routedChannelId: "D123",
|
||||
});
|
||||
expect(payload.inputs).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -269,7 +275,7 @@ describe("registerSlackInteractionEvents", () => {
|
||||
view: {
|
||||
id: "V900",
|
||||
callback_id: "openclaw:deploy_form",
|
||||
private_metadata: "run:123",
|
||||
private_metadata: JSON.stringify({ sessionKey: "agent:main:slack:channel:C99" }),
|
||||
state: {
|
||||
values: {
|
||||
env_block: {
|
||||
@@ -288,9 +294,12 @@ describe("registerSlackInteractionEvents", () => {
|
||||
});
|
||||
|
||||
expect(ack).toHaveBeenCalled();
|
||||
expect(resolveSessionKey).toHaveBeenCalledWith({});
|
||||
expect(resolveSessionKey).not.toHaveBeenCalled();
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
|
||||
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
|
||||
const [eventText, options] = enqueueSystemEventMock.mock.calls[0] as [
|
||||
string,
|
||||
{ sessionKey?: string },
|
||||
];
|
||||
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
|
||||
interactionType: string;
|
||||
actionId: string;
|
||||
@@ -308,12 +317,13 @@ describe("registerSlackInteractionEvents", () => {
|
||||
viewId: "V900",
|
||||
userId: "U900",
|
||||
isCleared: true,
|
||||
privateMetadata: "run:123",
|
||||
privateMetadata: JSON.stringify({ sessionKey: "agent:main:slack:channel:C99" }),
|
||||
});
|
||||
expect(payload.inputs).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ actionId: "env_select", selectedValues: ["canary"] }),
|
||||
]),
|
||||
);
|
||||
expect(options.sessionKey).toBe("agent:main:slack:channel:C99");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,12 @@ type ModalInputSummary = {
|
||||
inputValue?: string;
|
||||
};
|
||||
|
||||
type ModalPrivateMetadata = {
|
||||
sessionKey?: string;
|
||||
channelId?: string;
|
||||
channelType?: string;
|
||||
};
|
||||
|
||||
function readOptionValues(options: unknown): string[] | undefined {
|
||||
if (!Array.isArray(options)) {
|
||||
return undefined;
|
||||
@@ -146,6 +152,53 @@ function summarizeViewState(values: unknown): ModalInputSummary[] {
|
||||
return entries;
|
||||
}
|
||||
|
||||
function parseModalPrivateMetadata(raw: unknown): ModalPrivateMetadata {
|
||||
if (typeof raw !== "string" || raw.trim().length === 0) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
const sessionKey =
|
||||
typeof parsed.sessionKey === "string" && parsed.sessionKey.trim().length > 0
|
||||
? parsed.sessionKey
|
||||
: undefined;
|
||||
const channelId =
|
||||
typeof parsed.channelId === "string" && parsed.channelId.trim().length > 0
|
||||
? parsed.channelId
|
||||
: undefined;
|
||||
const channelType =
|
||||
typeof parsed.channelType === "string" && parsed.channelType.trim().length > 0
|
||||
? parsed.channelType
|
||||
: undefined;
|
||||
return { sessionKey, channelId, channelType };
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function resolveModalSessionRouting(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
privateMetadata: unknown;
|
||||
}): { sessionKey: string; channelId?: string; channelType?: string } {
|
||||
const metadata = parseModalPrivateMetadata(params.privateMetadata);
|
||||
if (metadata.sessionKey) {
|
||||
return { sessionKey: metadata.sessionKey };
|
||||
}
|
||||
if (metadata.channelId) {
|
||||
return {
|
||||
sessionKey: params.ctx.resolveSlackSystemEventSessionKey({
|
||||
channelId: metadata.channelId,
|
||||
channelType: metadata.channelType,
|
||||
}),
|
||||
channelId: metadata.channelId,
|
||||
channelType: metadata.channelType,
|
||||
};
|
||||
}
|
||||
return {
|
||||
sessionKey: params.ctx.resolveSlackSystemEventSessionKey({}),
|
||||
};
|
||||
}
|
||||
|
||||
export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContext }) {
|
||||
const { ctx } = params;
|
||||
if (typeof ctx.app.action !== "function") {
|
||||
@@ -292,6 +345,7 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
view?: {
|
||||
id?: string;
|
||||
callback_id?: string;
|
||||
private_metadata?: string;
|
||||
state?: { values?: unknown };
|
||||
};
|
||||
};
|
||||
@@ -300,6 +354,10 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
const userId = typedBody.user?.id ?? "unknown";
|
||||
const viewId = typedBody.view?.id;
|
||||
const inputs = summarizeViewState(typedBody.view?.state?.values);
|
||||
const sessionRouting = resolveModalSessionRouting({
|
||||
ctx,
|
||||
privateMetadata: typedBody.view?.private_metadata,
|
||||
});
|
||||
const eventPayload = {
|
||||
interactionType: "view_submission",
|
||||
actionId: `view:${callbackId}`,
|
||||
@@ -307,6 +365,9 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
viewId,
|
||||
userId,
|
||||
teamId: typedBody.team?.id,
|
||||
privateMetadata: typedBody.view?.private_metadata,
|
||||
routedChannelId: sessionRouting.channelId,
|
||||
routedChannelType: sessionRouting.channelType,
|
||||
inputs,
|
||||
};
|
||||
|
||||
@@ -315,7 +376,7 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
);
|
||||
|
||||
enqueueSystemEvent(`Slack interaction: ${JSON.stringify(eventPayload)}`, {
|
||||
sessionKey: ctx.resolveSlackSystemEventSessionKey({}),
|
||||
sessionKey: sessionRouting.sessionKey,
|
||||
contextKey: ["slack:interaction:view", callbackId, viewId, userId]
|
||||
.filter(Boolean)
|
||||
.join(":"),
|
||||
@@ -357,6 +418,10 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
const userId = typedBody.user?.id ?? "unknown";
|
||||
const viewId = typedBody.view?.id;
|
||||
const inputs = summarizeViewState(typedBody.view?.state?.values);
|
||||
const sessionRouting = resolveModalSessionRouting({
|
||||
ctx,
|
||||
privateMetadata: typedBody.view?.private_metadata,
|
||||
});
|
||||
const eventPayload = {
|
||||
interactionType: "view_closed",
|
||||
actionId: `view:${callbackId}`,
|
||||
@@ -366,6 +431,8 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
teamId: typedBody.team?.id,
|
||||
isCleared: typedBody.is_cleared === true,
|
||||
privateMetadata: typedBody.view?.private_metadata,
|
||||
routedChannelId: sessionRouting.channelId,
|
||||
routedChannelType: sessionRouting.channelType,
|
||||
inputs,
|
||||
};
|
||||
|
||||
@@ -376,7 +443,7 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
);
|
||||
|
||||
enqueueSystemEvent(`Slack interaction: ${JSON.stringify(eventPayload)}`, {
|
||||
sessionKey: ctx.resolveSlackSystemEventSessionKey({}),
|
||||
sessionKey: sessionRouting.sessionKey,
|
||||
contextKey: ["slack:interaction:view-closed", callbackId, viewId, userId]
|
||||
.filter(Boolean)
|
||||
.join(":"),
|
||||
|
||||
Reference in New Issue
Block a user