fix: harden discord audio preflight mention detection (#32136) (thanks @jnMetaCode)

This commit is contained in:
Peter Steinberger
2026-03-02 22:41:18 +00:00
parent b9b47f5002
commit 0b5d8e5b47
3 changed files with 109 additions and 3 deletions

View File

@@ -1,5 +1,11 @@
import { ChannelType } from "@buape/carbon";
import { beforeEach, describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const transcribeFirstAudioMock = vi.hoisted(() => vi.fn());
vi.mock("../../media-understanding/audio-preflight.js", () => ({
transcribeFirstAudio: (...args: unknown[]) => transcribeFirstAudioMock(...args),
}));
import {
__testing as sessionBindingTesting,
registerSessionBindingAdapter,
@@ -74,6 +80,7 @@ describe("resolvePreflightMentionRequirement", () => {
describe("preflightDiscordMessage", () => {
beforeEach(() => {
sessionBindingTesting.resetSessionBindingAdaptersForTests();
transcribeFirstAudioMock.mockReset();
});
it("bypasses mention gating in bound threads for allowed bot senders", async () => {
@@ -165,6 +172,101 @@ describe("preflightDiscordMessage", () => {
expect(result?.boundSessionKey).toBe(threadBinding.targetSessionKey);
expect(result?.shouldRequireMention).toBe(false);
});
it("uses attachment content_type for guild audio preflight mention detection", async () => {
transcribeFirstAudioMock.mockResolvedValue("hey openclaw");
const channelId = "channel-audio-1";
const client = {
fetchChannel: async (id: string) => {
if (id === channelId) {
return {
id: channelId,
type: ChannelType.GuildText,
name: "general",
};
}
return null;
},
} as unknown as import("@buape/carbon").Client;
const message = {
id: "m-audio-1",
content: "",
timestamp: new Date().toISOString(),
channelId,
attachments: [
{
id: "att-1",
url: "https://cdn.discordapp.com/attachments/voice.ogg",
content_type: "audio/ogg",
filename: "voice.ogg",
},
],
mentionedUsers: [],
mentionedRoles: [],
mentionedEveryone: false,
author: {
id: "user-1",
bot: false,
username: "Alice",
},
} as unknown as import("@buape/carbon").Message;
const result = await preflightDiscordMessage({
cfg: {
session: {
mainKey: "main",
scope: "per-sender",
},
messages: {
groupChat: {
mentionPatterns: ["openclaw"],
},
},
} as import("../../config/config.js").OpenClawConfig,
discordConfig: {} as NonNullable<
import("../../config/config.js").OpenClawConfig["channels"]
>["discord"],
accountId: "default",
token: "token",
runtime: {} as import("../../runtime.js").RuntimeEnv,
botUserId: "openclaw-bot",
guildHistories: new Map(),
historyLimit: 0,
mediaMaxBytes: 1_000_000,
textLimit: 2_000,
replyToMode: "all",
dmEnabled: true,
groupDmEnabled: true,
ackReactionScope: "direct",
groupPolicy: "open",
threadBindings: createNoopThreadBindingManager("default"),
data: {
channel_id: channelId,
guild_id: "guild-1",
guild: {
id: "guild-1",
name: "Guild One",
},
author: message.author,
message,
} as unknown as import("./listeners.js").DiscordMessageEvent,
client,
});
expect(transcribeFirstAudioMock).toHaveBeenCalledTimes(1);
expect(transcribeFirstAudioMock).toHaveBeenCalledWith(
expect.objectContaining({
ctx: expect.objectContaining({
MediaUrls: ["https://cdn.discordapp.com/attachments/voice.ogg"],
MediaTypes: ["audio/ogg"],
}),
}),
);
expect(result).not.toBeNull();
expect(result?.wasMentioned).toBe(true);
});
});
describe("shouldIgnoreBoundThreadWebhookMessage", () => {

View File

@@ -504,11 +504,13 @@ export async function preflightDiscordMessage(
const hasAudioAttachment = message.attachments?.some((att: { content_type?: string }) =>
att.content_type?.startsWith("audio/"),
);
const hasTypedText = Boolean(message.content?.trim());
const needsPreflightTranscription =
!isDirectMessage &&
shouldRequireMention &&
hasAudioAttachment &&
!baseText &&
// `baseText` includes media placeholders; gate on typed text only.
!hasTypedText &&
mentionRegexes.length > 0;
if (needsPreflightTranscription) {
@@ -541,10 +543,11 @@ export async function preflightDiscordMessage(
}
}
const mentionText = hasTypedText ? baseText : "";
const wasMentioned =
!isDirectMessage &&
matchesMentionWithExplicit({
text: baseText,
text: mentionText,
mentionRegexes,
explicit: {
hasAnyMention,