mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-02 11:08:07 +00:00
Slack: add modal private metadata utilities
This commit is contained in:
55
src/slack/modal-metadata.test.ts
Normal file
55
src/slack/modal-metadata.test.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
encodeSlackModalPrivateMetadata,
|
||||||
|
parseSlackModalPrivateMetadata,
|
||||||
|
} from "./modal-metadata.js";
|
||||||
|
|
||||||
|
describe("parseSlackModalPrivateMetadata", () => {
|
||||||
|
it("returns empty object for missing or invalid values", () => {
|
||||||
|
expect(parseSlackModalPrivateMetadata(undefined)).toEqual({});
|
||||||
|
expect(parseSlackModalPrivateMetadata("")).toEqual({});
|
||||||
|
expect(parseSlackModalPrivateMetadata("{bad-json")).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses known metadata fields", () => {
|
||||||
|
expect(
|
||||||
|
parseSlackModalPrivateMetadata(
|
||||||
|
JSON.stringify({
|
||||||
|
sessionKey: "agent:main:slack:channel:C1",
|
||||||
|
channelId: "D123",
|
||||||
|
channelType: "im",
|
||||||
|
ignored: "x",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
sessionKey: "agent:main:slack:channel:C1",
|
||||||
|
channelId: "D123",
|
||||||
|
channelType: "im",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encodeSlackModalPrivateMetadata", () => {
|
||||||
|
it("encodes only known non-empty fields", () => {
|
||||||
|
expect(
|
||||||
|
JSON.parse(
|
||||||
|
encodeSlackModalPrivateMetadata({
|
||||||
|
sessionKey: "agent:main:slack:channel:C1",
|
||||||
|
channelId: "",
|
||||||
|
channelType: "im",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
sessionKey: "agent:main:slack:channel:C1",
|
||||||
|
channelType: "im",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when encoded payload exceeds Slack metadata limit", () => {
|
||||||
|
expect(() =>
|
||||||
|
encodeSlackModalPrivateMetadata({
|
||||||
|
sessionKey: `agent:main:${"x".repeat(4000)}`,
|
||||||
|
}),
|
||||||
|
).toThrow(/cannot exceed 3000 chars/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
42
src/slack/modal-metadata.ts
Normal file
42
src/slack/modal-metadata.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export type SlackModalPrivateMetadata = {
|
||||||
|
sessionKey?: string;
|
||||||
|
channelId?: string;
|
||||||
|
channelType?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SLACK_PRIVATE_METADATA_MAX = 3000;
|
||||||
|
|
||||||
|
function normalizeString(value: unknown) {
|
||||||
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSlackModalPrivateMetadata(raw: unknown): SlackModalPrivateMetadata {
|
||||||
|
if (typeof raw !== "string" || raw.trim().length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
sessionKey: normalizeString(parsed.sessionKey),
|
||||||
|
channelId: normalizeString(parsed.channelId),
|
||||||
|
channelType: normalizeString(parsed.channelType),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeSlackModalPrivateMetadata(input: SlackModalPrivateMetadata): string {
|
||||||
|
const payload: SlackModalPrivateMetadata = {
|
||||||
|
...(input.sessionKey ? { sessionKey: input.sessionKey } : {}),
|
||||||
|
...(input.channelId ? { channelId: input.channelId } : {}),
|
||||||
|
...(input.channelType ? { channelType: input.channelType } : {}),
|
||||||
|
};
|
||||||
|
const encoded = JSON.stringify(payload);
|
||||||
|
if (encoded.length > SLACK_PRIVATE_METADATA_MAX) {
|
||||||
|
throw new Error(
|
||||||
|
`Slack modal private_metadata cannot exceed ${SLACK_PRIVATE_METADATA_MAX} chars`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import type { SlackActionMiddlewareArgs } from "@slack/bolt";
|
|||||||
import type { Block, KnownBlock } from "@slack/web-api";
|
import type { Block, KnownBlock } from "@slack/web-api";
|
||||||
import type { SlackMonitorContext } from "../context.js";
|
import type { SlackMonitorContext } from "../context.js";
|
||||||
import { enqueueSystemEvent } from "../../../infra/system-events.js";
|
import { enqueueSystemEvent } from "../../../infra/system-events.js";
|
||||||
|
import { parseSlackModalPrivateMetadata } from "../../modal-metadata.js";
|
||||||
|
|
||||||
// Prefix for OpenClaw-generated action IDs to scope our handler
|
// Prefix for OpenClaw-generated action IDs to scope our handler
|
||||||
const OPENCLAW_ACTION_PREFIX = "openclaw:";
|
const OPENCLAW_ACTION_PREFIX = "openclaw:";
|
||||||
@@ -47,12 +48,6 @@ type ModalInputSummary = {
|
|||||||
inputValue?: string;
|
inputValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ModalPrivateMetadata = {
|
|
||||||
sessionKey?: string;
|
|
||||||
channelId?: string;
|
|
||||||
channelType?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function readOptionValues(options: unknown): string[] | undefined {
|
function readOptionValues(options: unknown): string[] | undefined {
|
||||||
if (!Array.isArray(options)) {
|
if (!Array.isArray(options)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -152,35 +147,11 @@ function summarizeViewState(values: unknown): ModalInputSummary[] {
|
|||||||
return entries;
|
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: {
|
function resolveModalSessionRouting(params: {
|
||||||
ctx: SlackMonitorContext;
|
ctx: SlackMonitorContext;
|
||||||
privateMetadata: unknown;
|
privateMetadata: unknown;
|
||||||
}): { sessionKey: string; channelId?: string; channelType?: string } {
|
}): { sessionKey: string; channelId?: string; channelType?: string } {
|
||||||
const metadata = parseModalPrivateMetadata(params.privateMetadata);
|
const metadata = parseSlackModalPrivateMetadata(params.privateMetadata);
|
||||||
if (metadata.sessionKey) {
|
if (metadata.sessionKey) {
|
||||||
return { sessionKey: metadata.sessionKey };
|
return { sessionKey: metadata.sessionKey };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user