mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 07:52:41 +00:00
feat(discord): add allowBots mention gating
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Discord/allowBots mention gating: add `allowBots: "mentions"` to only accept bot-authored messages that mention the bot. Thanks @thewilloftheshadow.
|
||||
- Docs/Web search: remove outdated Brave free-tier wording and replace prescriptive AI ToS guidance with neutral compliance language in Brave setup docs. (#26860) Thanks @HenryLoenwind.
|
||||
- Tools/Diffs guidance loading: move diffs usage guidance from unconditional prompt-hook injection to the plugin companion skill path, reducing unrelated-turn prompt noise while keeping diffs tool behavior unchanged. (#32630) thanks @sircrumpet.
|
||||
- Agents/tool-result truncation: preserve important tail diagnostics by using head+tail truncation for oversized tool results while keeping configurable truncation options. (#20076) thanks @jlwestsr.
|
||||
|
||||
@@ -1082,6 +1082,7 @@ openclaw logs --follow
|
||||
By default bot-authored messages are ignored.
|
||||
|
||||
If you set `channels.discord.allowBots=true`, use strict mention and allowlist rules to avoid loop behavior.
|
||||
Prefer `channels.discord.allowBots="mentions"` to only accept bot messages that mention the bot.
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
- Optional `channels.discord.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
- Use `user:<id>` (DM) or `channel:<id>` (guild channel) for delivery targets; bare numeric IDs are rejected.
|
||||
- Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged name (no `#`). Prefer guild IDs.
|
||||
- Bot-authored messages are ignored by default. `allowBots: true` enables them (own messages still filtered).
|
||||
- Bot-authored messages are ignored by default. `allowBots: true` enables them; use `allowBots: "mentions"` to only accept bot messages that mention the bot (own messages still filtered).
|
||||
- `channels.discord.guilds.<id>.ignoreOtherMentions` (and channel overrides) drops messages that mention another user or role but not the bot (excluding @everyone/@here).
|
||||
- `maxLinesPerMessage` (default 17) splits tall messages even when under 2000 chars.
|
||||
- `channels.discord.threadBindings` controls Discord thread-bound routing:
|
||||
|
||||
@@ -1323,6 +1323,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Allow Discord to write config in response to channel events/commands (default: true).",
|
||||
"channels.discord.token":
|
||||
"Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.",
|
||||
"channels.discord.allowBots":
|
||||
'Allow bot-authored messages to trigger Discord replies (default: false). Set "mentions" to only accept bot messages that mention the bot.',
|
||||
"channels.discord.proxy":
|
||||
"Proxy URL for Discord gateway + API requests (app-id lookup and allowlist resolution). Set per account via channels.discord.accounts.<id>.proxy.",
|
||||
"channels.whatsapp.configWrites":
|
||||
|
||||
@@ -740,6 +740,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.slack.commands.native": "Slack Native Commands",
|
||||
"channels.slack.commands.nativeSkills": "Slack Native Skill Commands",
|
||||
"channels.slack.allowBots": "Slack Allow Bot Messages",
|
||||
"channels.discord.allowBots": "Discord Allow Bot Messages",
|
||||
"channels.discord.token": "Discord Bot Token",
|
||||
"channels.slack.botToken": "Slack Bot Token",
|
||||
"channels.slack.appToken": "Slack App Token",
|
||||
|
||||
@@ -221,8 +221,8 @@ export type DiscordAccountConfig = {
|
||||
token?: string;
|
||||
/** HTTP(S) proxy URL for Discord gateway WebSocket connections. */
|
||||
proxy?: string;
|
||||
/** Allow bot-authored messages to trigger replies (default: false). */
|
||||
allowBots?: boolean;
|
||||
/** Allow bot-authored messages to trigger replies (default: false). Set "mentions" to gate on mentions. */
|
||||
allowBots?: boolean | "mentions";
|
||||
/**
|
||||
* Break-glass override: allow mutable identity matching (names/tags/slugs) in allowlists.
|
||||
* Default behavior is ID-only matching.
|
||||
|
||||
@@ -412,7 +412,7 @@ export const DiscordAccountSchema = z
|
||||
configWrites: z.boolean().optional(),
|
||||
token: SecretInputSchema.optional().register(sensitive),
|
||||
proxy: z.string().optional(),
|
||||
allowBots: z.boolean().optional(),
|
||||
allowBots: z.union([z.boolean(), z.literal("mentions")]).optional(),
|
||||
dangerouslyAllowNameMatching: z.boolean().optional(),
|
||||
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
|
||||
@@ -354,6 +354,148 @@ describe("preflightDiscordMessage", () => {
|
||||
expect(result?.shouldRequireMention).toBe(false);
|
||||
});
|
||||
|
||||
it("drops bot messages without mention when allowBots=mentions", async () => {
|
||||
const channelId = "channel-bot-mentions-off";
|
||||
const guildId = "guild-bot-mentions-off";
|
||||
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-bot-mentions-off",
|
||||
content: "relay chatter",
|
||||
timestamp: new Date().toISOString(),
|
||||
channelId,
|
||||
attachments: [],
|
||||
mentionedUsers: [],
|
||||
mentionedRoles: [],
|
||||
mentionedEveryone: false,
|
||||
author: {
|
||||
id: "relay-bot-1",
|
||||
bot: true,
|
||||
username: "Relay",
|
||||
},
|
||||
} as unknown as import("@buape/carbon").Message;
|
||||
|
||||
const result = await preflightDiscordMessage({
|
||||
cfg: {
|
||||
session: {
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
},
|
||||
} as import("../../config/config.js").OpenClawConfig,
|
||||
discordConfig: {
|
||||
allowBots: "mentions",
|
||||
} 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: guildId,
|
||||
guild: {
|
||||
id: guildId,
|
||||
name: "Guild One",
|
||||
},
|
||||
author: message.author,
|
||||
message,
|
||||
} as unknown as import("./listeners.js").DiscordMessageEvent,
|
||||
client,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("allows bot messages with explicit mention when allowBots=mentions", async () => {
|
||||
const channelId = "channel-bot-mentions-on";
|
||||
const guildId = "guild-bot-mentions-on";
|
||||
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-bot-mentions-on",
|
||||
content: "hi <@openclaw-bot>",
|
||||
timestamp: new Date().toISOString(),
|
||||
channelId,
|
||||
attachments: [],
|
||||
mentionedUsers: [{ id: "openclaw-bot" }],
|
||||
mentionedRoles: [],
|
||||
mentionedEveryone: false,
|
||||
author: {
|
||||
id: "relay-bot-1",
|
||||
bot: true,
|
||||
username: "Relay",
|
||||
},
|
||||
} as unknown as import("@buape/carbon").Message;
|
||||
|
||||
const result = await preflightDiscordMessage({
|
||||
cfg: {
|
||||
session: {
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
},
|
||||
} as import("../../config/config.js").OpenClawConfig,
|
||||
discordConfig: {
|
||||
allowBots: "mentions",
|
||||
} 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: guildId,
|
||||
guild: {
|
||||
id: guildId,
|
||||
name: "Guild One",
|
||||
},
|
||||
author: message.author,
|
||||
message,
|
||||
} as unknown as import("./listeners.js").DiscordMessageEvent,
|
||||
client,
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("drops guild messages that mention another user when ignoreOtherMentions=true", async () => {
|
||||
const channelId = "channel-other-mention-1";
|
||||
const guildId = "guild-other-mention-1";
|
||||
|
||||
@@ -139,7 +139,9 @@ export async function preflightDiscordMessage(
|
||||
return null;
|
||||
}
|
||||
|
||||
const allowBots = params.discordConfig?.allowBots ?? false;
|
||||
const allowBotsSetting = params.discordConfig?.allowBots;
|
||||
const allowBotsMode =
|
||||
allowBotsSetting === "mentions" ? "mentions" : allowBotsSetting === true ? "all" : "off";
|
||||
if (params.botUserId && author.id === params.botUserId) {
|
||||
// Always ignore own messages to prevent self-reply loops
|
||||
return null;
|
||||
@@ -166,7 +168,7 @@ export async function preflightDiscordMessage(
|
||||
});
|
||||
|
||||
if (author.bot) {
|
||||
if (!allowBots && !sender.isPluralKit) {
|
||||
if (allowBotsMode === "off" && !sender.isPluralKit) {
|
||||
logVerbose("discord: drop bot message (allowBots=false)");
|
||||
return null;
|
||||
}
|
||||
@@ -656,6 +658,15 @@ export async function preflightDiscordMessage(
|
||||
}
|
||||
}
|
||||
|
||||
if (author.bot && !sender.isPluralKit && allowBotsMode === "mentions") {
|
||||
const botMentioned = isDirectMessage || wasMentioned || implicitMention;
|
||||
if (!botMentioned) {
|
||||
logDebug(`[discord-preflight] drop: bot message missing mention (allowBots=mentions)`);
|
||||
logVerbose("discord: drop bot message (allowBots=mentions, missing mention)");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const ignoreOtherMentions =
|
||||
channelConfig?.ignoreOtherMentions ?? guildInfo?.ignoreOtherMentions ?? false;
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user