mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
fix(discord): prevent accidental mentions inside unfinished code (#129338)
* fix(discord): protect mentions inside unfinished inline code * fix(discord): respect escaped inline-code delimiters
This commit is contained in:
committed by
GitHub
parent
18234ea964
commit
712a97d2ab
@@ -85,19 +85,44 @@ describe("rewriteDiscordKnownMentions", () => {
|
||||
expect(rewritten).toBe("hello @unknown @everyone @here");
|
||||
});
|
||||
|
||||
it("does not rewrite mentions inside markdown code spans", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "balanced inline and fenced code",
|
||||
input: "inline `@alice` fence ```\n@alice\n``` text @alice",
|
||||
expected: "inline `@alice` fence ```\n@alice\n``` text <@123456789>",
|
||||
},
|
||||
{
|
||||
name: "unterminated single-backtick code",
|
||||
input: "outside @alice then `inside @alice",
|
||||
expected: "outside <@123456789> then `inside @alice",
|
||||
},
|
||||
{
|
||||
name: "unterminated double-backtick code",
|
||||
input: "outside @alice then ``inside @alice",
|
||||
expected: "outside <@123456789> then ``inside @alice",
|
||||
},
|
||||
{
|
||||
name: "escaped literal backticks",
|
||||
input: "literal \\` outside @alice",
|
||||
expected: "literal \\` outside <@123456789>",
|
||||
},
|
||||
{
|
||||
name: "backticks after an even number of backslashes",
|
||||
input: "literal \\\\` inside @alice",
|
||||
expected: "literal \\\\` inside @alice",
|
||||
},
|
||||
{
|
||||
name: "escaped backticks before real unterminated code",
|
||||
input: "literal \\` outside @alice then `inside @alice",
|
||||
expected: "literal \\` outside <@123456789> then `inside @alice",
|
||||
},
|
||||
])("does not rewrite mentions inside $name", ({ input, expected }) => {
|
||||
rememberDiscordDirectoryUser({
|
||||
accountId: "default",
|
||||
userId: "123456789",
|
||||
handles: ["alice"],
|
||||
});
|
||||
const rewritten = rewriteDiscordKnownMentions(
|
||||
"inline `@alice` fence ```\n@alice\n``` text @alice",
|
||||
{
|
||||
accountId: "default",
|
||||
},
|
||||
);
|
||||
expect(rewritten).toBe("inline `@alice` fence ```\n@alice\n``` text <@123456789>");
|
||||
expect(rewriteDiscordKnownMentions(input, { accountId: "default" })).toBe(expected);
|
||||
});
|
||||
|
||||
it("does not end longer code fences at triple-backtick literals inside the body", () => {
|
||||
|
||||
@@ -155,26 +155,18 @@ function findNextMarkdownCodeSegment(
|
||||
text: string,
|
||||
startIndex: number,
|
||||
): { startIndex: number; endIndex: number } | null {
|
||||
let searchIndex = startIndex;
|
||||
while (searchIndex < text.length) {
|
||||
const segmentStart = text.indexOf("`", searchIndex);
|
||||
if (segmentStart === -1) {
|
||||
return null;
|
||||
}
|
||||
const runLength = countBacktickRun(text, segmentStart);
|
||||
const inlineEndIndex = findSameLineBacktickRun(text, segmentStart + runLength, runLength);
|
||||
if (inlineEndIndex !== null) {
|
||||
return { startIndex: segmentStart, endIndex: inlineEndIndex };
|
||||
}
|
||||
if (runLength >= 3) {
|
||||
return {
|
||||
startIndex: segmentStart,
|
||||
endIndex: findFenceEnd(text, segmentStart, runLength),
|
||||
};
|
||||
}
|
||||
searchIndex = segmentStart + runLength;
|
||||
const segmentOffset = text.slice(startIndex).search(/(?<=(?:^|[^\\])(?:\\\\)*)`/);
|
||||
if (segmentOffset === -1) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
const segmentStart = startIndex + segmentOffset;
|
||||
const runLength = countBacktickRun(text, segmentStart);
|
||||
return {
|
||||
startIndex: segmentStart,
|
||||
endIndex:
|
||||
findSameLineBacktickRun(text, segmentStart + runLength, runLength) ??
|
||||
(runLength >= 3 ? findFenceEnd(text, segmentStart, runLength) : text.length),
|
||||
};
|
||||
}
|
||||
|
||||
export function rewriteDiscordKnownMentions(
|
||||
|
||||
@@ -680,27 +680,35 @@ describe("sendMessageDiscord", () => {
|
||||
expect(body).not.toHaveProperty("flags");
|
||||
});
|
||||
|
||||
it("rewrites cached @username mentions to id-based mentions", async () => {
|
||||
rememberDiscordDirectoryUser({
|
||||
accountId: "default",
|
||||
userId: "123456789012345678",
|
||||
handles: ["Alice"],
|
||||
});
|
||||
const { rest, postMock, getMock } = makeDiscordRest();
|
||||
getMock.mockResolvedValueOnce({ type: ChannelType.GuildText });
|
||||
postMock.mockResolvedValue({
|
||||
id: "msg1",
|
||||
channel_id: "789",
|
||||
});
|
||||
await sendMessageDiscord("channel:789", "ping @Alice", {
|
||||
rest,
|
||||
token: "t",
|
||||
cfg: DISCORD_TEST_CFG,
|
||||
accountId: "default",
|
||||
});
|
||||
expectRestRoute(postMock, 0, Routes.channelMessages("789"));
|
||||
expect(requireRestBody(postMock).content).toBe("ping <@123456789012345678>");
|
||||
});
|
||||
it.each([
|
||||
{ input: "ping @Alice", expected: "ping <@123456789012345678>" },
|
||||
{ input: "Run `notify @Alice", expected: "Run `notify @Alice" },
|
||||
{ input: "literal \\` ping @Alice", expected: "literal \\` ping <@123456789012345678>" },
|
||||
{ input: "literal \\\\` inside @Alice", expected: "literal \\\\` inside @Alice" },
|
||||
])(
|
||||
"rewrites cached @username mentions only outside code: $input",
|
||||
async ({ input, expected }) => {
|
||||
rememberDiscordDirectoryUser({
|
||||
accountId: "default",
|
||||
userId: "123456789012345678",
|
||||
handles: ["Alice"],
|
||||
});
|
||||
const { rest, postMock, getMock } = makeDiscordRest();
|
||||
getMock.mockResolvedValueOnce({ type: ChannelType.GuildText });
|
||||
postMock.mockResolvedValue({
|
||||
id: "msg1",
|
||||
channel_id: "789",
|
||||
});
|
||||
await sendMessageDiscord("channel:789", input, {
|
||||
rest,
|
||||
token: "t",
|
||||
cfg: DISCORD_TEST_CFG,
|
||||
accountId: "default",
|
||||
});
|
||||
expectRestRoute(postMock, 0, Routes.channelMessages("789"));
|
||||
expect(requireRestBody(postMock).content).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it("rewrites configured @username aliases to id-based mentions", async () => {
|
||||
const { rest, postMock, getMock } = makeDiscordRest();
|
||||
|
||||
@@ -153,6 +153,37 @@ describe("sendWebhookMessageDiscord proxy support", () => {
|
||||
globalFetchMock.mockRestore();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ input: "Run `notify @OpsLead", expected: "Run `notify @OpsLead" },
|
||||
{
|
||||
input: "literal \\` ping @OpsLead",
|
||||
expected: "literal \\` ping <@123456789012345678>",
|
||||
},
|
||||
])("only rewrites webhook mentions outside inline code: $input", async ({ input, expected }) => {
|
||||
const globalFetchMock = vi
|
||||
.spyOn(globalThis, "fetch")
|
||||
.mockResolvedValue(new Response(JSON.stringify({ id: "msg-code" }), { status: 200 }));
|
||||
|
||||
await sendWebhookMessageDiscord(input, {
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "Bot test-token",
|
||||
mentionAliases: { opslead: "123456789012345678" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
accountId: "default",
|
||||
webhookId: "123",
|
||||
webhookToken: "abc",
|
||||
wait: true,
|
||||
});
|
||||
|
||||
expect(globalFetchMock.mock.calls[0]?.[1]?.body).toContain(
|
||||
`"content":${JSON.stringify(expected)}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts Discord's no-body webhook response when wait is false", async () => {
|
||||
const response = new Response(null, { status: 204 });
|
||||
const jsonSpy = vi.spyOn(response, "json");
|
||||
|
||||
Reference in New Issue
Block a user