mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 00:54:53 +00:00
fix(discord): escape component custom id delimiters
This commit is contained in:
@@ -2,17 +2,55 @@ import { parseCustomId, type ComponentParserResult } from "./internal/discord.js
|
||||
|
||||
export const DISCORD_COMPONENT_CUSTOM_ID_KEY = "occomp";
|
||||
export const DISCORD_MODAL_CUSTOM_ID_KEY = "ocmodal";
|
||||
const ENCODED_CUSTOM_ID_VERSION = "1";
|
||||
|
||||
function encodeCustomIdValue(value: string): string {
|
||||
return value.replace(/%/g, "%25").replace(/;/g, "%3B");
|
||||
}
|
||||
|
||||
function needsCustomIdEncoding(value: string): boolean {
|
||||
return /[%;]/.test(value);
|
||||
}
|
||||
|
||||
function decodeCustomIdValue(value: string): string {
|
||||
return value.replace(/%(25|3B)/gi, (match) => (match.toLowerCase() === "%25" ? "%" : ";"));
|
||||
}
|
||||
|
||||
function decodeParsedCustomIdData(
|
||||
data: ComponentParserResult["data"],
|
||||
): ComponentParserResult["data"] {
|
||||
if (data.e !== ENCODED_CUSTOM_ID_VERSION) {
|
||||
return data;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(data).map(([key, value]) => [
|
||||
key,
|
||||
typeof value === "string" ? decodeCustomIdValue(value) : value,
|
||||
]),
|
||||
) as ComponentParserResult["data"];
|
||||
}
|
||||
|
||||
export function buildDiscordComponentCustomId(params: {
|
||||
componentId: string;
|
||||
modalId?: string;
|
||||
}): string {
|
||||
const base = `${DISCORD_COMPONENT_CUSTOM_ID_KEY}:cid=${params.componentId}`;
|
||||
return params.modalId ? `${base};mid=${params.modalId}` : base;
|
||||
const encoded =
|
||||
needsCustomIdEncoding(params.componentId) || needsCustomIdEncoding(params.modalId ?? "");
|
||||
const componentId = encoded ? encodeCustomIdValue(params.componentId) : params.componentId;
|
||||
const base = encoded
|
||||
? `${DISCORD_COMPONENT_CUSTOM_ID_KEY}:e=${ENCODED_CUSTOM_ID_VERSION};cid=${componentId}`
|
||||
: `${DISCORD_COMPONENT_CUSTOM_ID_KEY}:cid=${componentId}`;
|
||||
const modalId = params.modalId;
|
||||
if (!modalId) {
|
||||
return base;
|
||||
}
|
||||
return `${base};mid=${encoded ? encodeCustomIdValue(modalId) : modalId}`;
|
||||
}
|
||||
|
||||
export function buildDiscordModalCustomId(modalId: string): string {
|
||||
return `${DISCORD_MODAL_CUSTOM_ID_KEY}:mid=${modalId}`;
|
||||
return needsCustomIdEncoding(modalId)
|
||||
? `${DISCORD_MODAL_CUSTOM_ID_KEY}:e=${ENCODED_CUSTOM_ID_VERSION};mid=${encodeCustomIdValue(modalId)}`
|
||||
: `${DISCORD_MODAL_CUSTOM_ID_KEY}:mid=${modalId}`;
|
||||
}
|
||||
|
||||
export function parseDiscordComponentCustomId(
|
||||
@@ -22,11 +60,12 @@ export function parseDiscordComponentCustomId(
|
||||
if (parsed.key !== DISCORD_COMPONENT_CUSTOM_ID_KEY) {
|
||||
return null;
|
||||
}
|
||||
const componentId = parsed.data.cid;
|
||||
const data = decodeParsedCustomIdData(parsed.data);
|
||||
const componentId = data.cid;
|
||||
if (typeof componentId !== "string" || !componentId.trim()) {
|
||||
return null;
|
||||
}
|
||||
const modalId = parsed.data.mid;
|
||||
const modalId = data.mid;
|
||||
return {
|
||||
componentId,
|
||||
modalId: typeof modalId === "string" && modalId.trim() ? modalId : undefined,
|
||||
@@ -38,7 +77,8 @@ export function parseDiscordModalCustomId(id: string): string | null {
|
||||
if (parsed.key !== DISCORD_MODAL_CUSTOM_ID_KEY) {
|
||||
return null;
|
||||
}
|
||||
const modalId = parsed.data.mid;
|
||||
const data = decodeParsedCustomIdData(parsed.data);
|
||||
const modalId = data.mid;
|
||||
if (typeof modalId !== "string" || !modalId.trim()) {
|
||||
return null;
|
||||
}
|
||||
@@ -57,7 +97,7 @@ export function parseDiscordComponentCustomIdForInteraction(id: string): Compone
|
||||
if (parsed.key !== DISCORD_COMPONENT_CUSTOM_ID_KEY) {
|
||||
return parsed;
|
||||
}
|
||||
return { key: "*", data: parsed.data };
|
||||
return { key: "*", data: decodeParsedCustomIdData(parsed.data) };
|
||||
}
|
||||
|
||||
export function parseDiscordModalCustomIdForInteraction(id: string): ComponentParserResult {
|
||||
@@ -68,5 +108,5 @@ export function parseDiscordModalCustomIdForInteraction(id: string): ComponentPa
|
||||
if (parsed.key !== DISCORD_MODAL_CUSTOM_ID_KEY) {
|
||||
return parsed;
|
||||
}
|
||||
return { key: "*", data: parsed.data };
|
||||
return { key: "*", data: decodeParsedCustomIdData(parsed.data) };
|
||||
}
|
||||
|
||||
@@ -7,8 +7,14 @@ let resolveDiscordComponentEntry: typeof import("./components-registry.js").reso
|
||||
let resolveDiscordComponentEntryWithPersistence: typeof import("./components-registry.js").resolveDiscordComponentEntryWithPersistence;
|
||||
let resolveDiscordModalEntry: typeof import("./components-registry.js").resolveDiscordModalEntry;
|
||||
let resolveDiscordModalEntryWithPersistence: typeof import("./components-registry.js").resolveDiscordModalEntryWithPersistence;
|
||||
let buildDiscordComponentCustomId: typeof import("./components.js").buildDiscordComponentCustomId;
|
||||
let buildDiscordComponentMessage: typeof import("./components.js").buildDiscordComponentMessage;
|
||||
let buildDiscordComponentMessageFlags: typeof import("./components.js").buildDiscordComponentMessageFlags;
|
||||
let buildDiscordModalCustomId: typeof import("./components.js").buildDiscordModalCustomId;
|
||||
let parseDiscordComponentCustomId: typeof import("./components.js").parseDiscordComponentCustomId;
|
||||
let parseDiscordComponentCustomIdForInteraction: typeof import("./components.js").parseDiscordComponentCustomIdForInteraction;
|
||||
let parseDiscordModalCustomId: typeof import("./components.js").parseDiscordModalCustomId;
|
||||
let parseDiscordModalCustomIdForInteraction: typeof import("./components.js").parseDiscordModalCustomIdForInteraction;
|
||||
let readDiscordComponentSpec: typeof import("./components.js").readDiscordComponentSpec;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -20,11 +26,57 @@ beforeAll(async () => {
|
||||
resolveDiscordModalEntry,
|
||||
resolveDiscordModalEntryWithPersistence,
|
||||
} = await import("./components-registry.js"));
|
||||
({ buildDiscordComponentMessage, buildDiscordComponentMessageFlags, readDiscordComponentSpec } =
|
||||
await import("./components.js"));
|
||||
({
|
||||
buildDiscordComponentCustomId,
|
||||
buildDiscordComponentMessage,
|
||||
buildDiscordComponentMessageFlags,
|
||||
buildDiscordModalCustomId,
|
||||
parseDiscordComponentCustomId,
|
||||
parseDiscordComponentCustomIdForInteraction,
|
||||
parseDiscordModalCustomId,
|
||||
parseDiscordModalCustomIdForInteraction,
|
||||
readDiscordComponentSpec,
|
||||
} = await import("./components.js"));
|
||||
});
|
||||
|
||||
describe("discord components", () => {
|
||||
it("round-trips custom id values that contain separators", () => {
|
||||
const componentId = "button=a;two space%3B";
|
||||
const modalId = "modal=x;y space%3D";
|
||||
|
||||
const componentCustomId = buildDiscordComponentCustomId({ componentId, modalId });
|
||||
expect(componentCustomId).not.toContain(componentId);
|
||||
expect(componentCustomId).toContain("space");
|
||||
expect(parseDiscordComponentCustomId(componentCustomId)).toEqual({ componentId, modalId });
|
||||
expect(parseDiscordComponentCustomIdForInteraction(componentCustomId).data).toMatchObject({
|
||||
cid: componentId,
|
||||
mid: modalId,
|
||||
});
|
||||
|
||||
const modalCustomId = buildDiscordModalCustomId(modalId);
|
||||
expect(modalCustomId).not.toContain(modalId);
|
||||
expect(modalCustomId).toContain("space");
|
||||
expect(parseDiscordModalCustomId(modalCustomId)).toBe(modalId);
|
||||
expect(parseDiscordModalCustomIdForInteraction(modalCustomId).data).toMatchObject({
|
||||
mid: modalId,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps legacy percent-like custom id values raw", () => {
|
||||
expect(buildDiscordComponentCustomId({ componentId: "button_v1" })).toBe(
|
||||
"occomp:cid=button_v1",
|
||||
);
|
||||
expect(buildDiscordComponentCustomId({ componentId: "button=v1" })).toBe(
|
||||
"occomp:cid=button=v1",
|
||||
);
|
||||
expect(buildDiscordModalCustomId("modal_v1")).toBe("ocmodal:mid=modal_v1");
|
||||
expect(buildDiscordModalCustomId("modal=v1")).toBe("ocmodal:mid=modal=v1");
|
||||
expect(parseDiscordComponentCustomId("occomp:cid=button%3Bv1")).toEqual({
|
||||
componentId: "button%3Bv1",
|
||||
});
|
||||
expect(parseDiscordModalCustomId("ocmodal:mid=modal%3Dv1")).toBe("modal%3Dv1");
|
||||
});
|
||||
|
||||
it("builds v2 containers with modal trigger", () => {
|
||||
const spec = readDiscordComponentSpec({
|
||||
text: "Choose a path",
|
||||
|
||||
Reference in New Issue
Block a user