mirror of https://github.com/openclaw/openclaw.git
fix: harden discord guild allowlist resolution
This commit is contained in:
parent
5c73ed62d5
commit
10afde99c1
|
|
@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai
|
||||||
- Dashboard/chat UI: render oversized plain-text replies as normal paragraphs instead of capped gray code blocks, so long desktop chat responses stay readable without tab-switching refreshes.
|
- Dashboard/chat UI: render oversized plain-text replies as normal paragraphs instead of capped gray code blocks, so long desktop chat responses stay readable without tab-switching refreshes.
|
||||||
- Gateway/Control UI: restore the operator-only device-auth bypass and classify browser connect failures so origin and device-identity problems no longer show up as auth errors in the Control UI and web chat. (#45512) thanks @sallyom.
|
- Gateway/Control UI: restore the operator-only device-auth bypass and classify browser connect failures so origin and device-identity problems no longer show up as auth errors in the Control UI and web chat. (#45512) thanks @sallyom.
|
||||||
- macOS/voice wake: stop crashing wake-word command extraction when speech segment ranges come from a different transcript instance.
|
- macOS/voice wake: stop crashing wake-word command extraction when speech segment ranges come from a different transcript instance.
|
||||||
|
- Discord/allowlists: honor raw `guild_id` when hydrated guild objects are missing so allowlisted channels and threads like `#maintainers` no longer get false-dropped before channel allowlist checks.
|
||||||
|
|
||||||
## 2026.3.12
|
## 2026.3.12
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,18 @@ describe("discord guild/channel resolution", () => {
|
||||||
expect(resolved?.slug).toBe("friends-of-openclaw");
|
expect(resolved?.slug).toBe("friends-of-openclaw");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("resolves guild entry by raw guild id when guild object is missing", () => {
|
||||||
|
const guildEntries = makeEntries({
|
||||||
|
"123": { slug: "friends-of-openclaw" },
|
||||||
|
});
|
||||||
|
const resolved = resolveDiscordGuildEntry({
|
||||||
|
guildId: "123",
|
||||||
|
guildEntries,
|
||||||
|
});
|
||||||
|
expect(resolved?.id).toBe("123");
|
||||||
|
expect(resolved?.slug).toBe("friends-of-openclaw");
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves guild entry by slug key", () => {
|
it("resolves guild entry by slug key", () => {
|
||||||
const guildEntries = makeEntries({
|
const guildEntries = makeEntries({
|
||||||
"friends-of-openclaw": { slug: "friends-of-openclaw" },
|
"friends-of-openclaw": { slug: "friends-of-openclaw" },
|
||||||
|
|
|
||||||
|
|
@ -360,6 +360,7 @@ async function ensureAgentComponentInteractionAllowed(params: {
|
||||||
}): Promise<{ parentId: string | undefined } | null> {
|
}): Promise<{ parentId: string | undefined } | null> {
|
||||||
const guildInfo = resolveDiscordGuildEntry({
|
const guildInfo = resolveDiscordGuildEntry({
|
||||||
guild: params.interaction.guild ?? undefined,
|
guild: params.interaction.guild ?? undefined,
|
||||||
|
guildId: params.rawGuildId,
|
||||||
guildEntries: params.ctx.guildEntries,
|
guildEntries: params.ctx.guildEntries,
|
||||||
});
|
});
|
||||||
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
||||||
|
|
@ -1094,6 +1095,7 @@ async function handleDiscordComponentEvent(params: {
|
||||||
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
|
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
|
||||||
const guildInfo = resolveDiscordGuildEntry({
|
const guildInfo = resolveDiscordGuildEntry({
|
||||||
guild: params.interaction.guild ?? undefined,
|
guild: params.interaction.guild ?? undefined,
|
||||||
|
guildId: rawGuildId,
|
||||||
guildEntries: params.ctx.guildEntries,
|
guildEntries: params.ctx.guildEntries,
|
||||||
});
|
});
|
||||||
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
||||||
|
|
@ -1246,6 +1248,7 @@ async function handleDiscordModalTrigger(params: {
|
||||||
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
|
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
|
||||||
const guildInfo = resolveDiscordGuildEntry({
|
const guildInfo = resolveDiscordGuildEntry({
|
||||||
guild: params.interaction.guild ?? undefined,
|
guild: params.interaction.guild ?? undefined,
|
||||||
|
guildId: rawGuildId,
|
||||||
guildEntries: params.ctx.guildEntries,
|
guildEntries: params.ctx.guildEntries,
|
||||||
});
|
});
|
||||||
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
||||||
|
|
@ -1696,6 +1699,7 @@ class DiscordComponentModal extends Modal {
|
||||||
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
|
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
|
||||||
const guildInfo = resolveDiscordGuildEntry({
|
const guildInfo = resolveDiscordGuildEntry({
|
||||||
guild: interaction.guild ?? undefined,
|
guild: interaction.guild ?? undefined,
|
||||||
|
guildId: rawGuildId,
|
||||||
guildEntries: this.ctx.guildEntries,
|
guildEntries: this.ctx.guildEntries,
|
||||||
});
|
});
|
||||||
const channelCtx = resolveDiscordChannelContext(interaction);
|
const channelCtx = resolveDiscordChannelContext(interaction);
|
||||||
|
|
|
||||||
|
|
@ -321,25 +321,30 @@ export function resolveDiscordCommandAuthorized(params: {
|
||||||
|
|
||||||
export function resolveDiscordGuildEntry(params: {
|
export function resolveDiscordGuildEntry(params: {
|
||||||
guild?: Guild<true> | Guild | null;
|
guild?: Guild<true> | Guild | null;
|
||||||
|
guildId?: string | null;
|
||||||
guildEntries?: Record<string, DiscordGuildEntryResolved>;
|
guildEntries?: Record<string, DiscordGuildEntryResolved>;
|
||||||
}): DiscordGuildEntryResolved | null {
|
}): DiscordGuildEntryResolved | null {
|
||||||
const guild = params.guild;
|
const guild = params.guild;
|
||||||
const entries = params.guildEntries;
|
const entries = params.guildEntries;
|
||||||
if (!guild || !entries) {
|
const guildId = params.guildId?.trim() || guild?.id;
|
||||||
|
if (!entries) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const byId = entries[guild.id];
|
const byId = guildId ? entries[guildId] : undefined;
|
||||||
if (byId) {
|
if (byId) {
|
||||||
return { ...byId, id: guild.id };
|
return { ...byId, id: guildId };
|
||||||
|
}
|
||||||
|
if (!guild) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
const slug = normalizeDiscordSlug(guild.name ?? "");
|
const slug = normalizeDiscordSlug(guild.name ?? "");
|
||||||
const bySlug = entries[slug];
|
const bySlug = entries[slug];
|
||||||
if (bySlug) {
|
if (bySlug) {
|
||||||
return { ...bySlug, id: guild.id, slug: slug || bySlug.slug };
|
return { ...bySlug, id: guildId ?? guild.id, slug: slug || bySlug.slug };
|
||||||
}
|
}
|
||||||
const wildcard = entries["*"];
|
const wildcard = entries["*"];
|
||||||
if (wildcard) {
|
if (wildcard) {
|
||||||
return { ...wildcard, id: guild.id, slug: slug || wildcard.slug };
|
return { ...wildcard, id: guildId ?? guild.id, slug: slug || wildcard.slug };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,7 @@ async function handleDiscordReactionEvent(
|
||||||
const guildInfo = isGuildMessage
|
const guildInfo = isGuildMessage
|
||||||
? resolveDiscordGuildEntry({
|
? resolveDiscordGuildEntry({
|
||||||
guild: data.guild ?? undefined,
|
guild: data.guild ?? undefined,
|
||||||
|
guildId: data.guild_id ?? undefined,
|
||||||
guildEntries,
|
guildEntries,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,19 @@ export function createGuildEvent(params: {
|
||||||
guildId: string;
|
guildId: string;
|
||||||
author: import("@buape/carbon").Message["author"];
|
author: import("@buape/carbon").Message["author"];
|
||||||
message: import("@buape/carbon").Message;
|
message: import("@buape/carbon").Message;
|
||||||
|
includeGuildObject?: boolean;
|
||||||
}): DiscordMessageEvent {
|
}): DiscordMessageEvent {
|
||||||
return {
|
return {
|
||||||
channel_id: params.channelId,
|
channel_id: params.channelId,
|
||||||
guild_id: params.guildId,
|
guild_id: params.guildId,
|
||||||
guild: {
|
...(params.includeGuildObject === false
|
||||||
id: params.guildId,
|
? {}
|
||||||
name: "Guild One",
|
: {
|
||||||
},
|
guild: {
|
||||||
|
id: params.guildId,
|
||||||
|
name: "Guild One",
|
||||||
|
},
|
||||||
|
}),
|
||||||
author: params.author,
|
author: params.author,
|
||||||
message: params.message,
|
message: params.message,
|
||||||
} as unknown as DiscordMessageEvent;
|
} as unknown as DiscordMessageEvent;
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ async function runGuildPreflight(params: {
|
||||||
discordConfig: DiscordConfig;
|
discordConfig: DiscordConfig;
|
||||||
cfg?: import("../../config/config.js").OpenClawConfig;
|
cfg?: import("../../config/config.js").OpenClawConfig;
|
||||||
guildEntries?: Parameters<typeof preflightDiscordMessage>[0]["guildEntries"];
|
guildEntries?: Parameters<typeof preflightDiscordMessage>[0]["guildEntries"];
|
||||||
|
includeGuildObject?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return preflightDiscordMessage({
|
return preflightDiscordMessage({
|
||||||
...createPreflightArgs({
|
...createPreflightArgs({
|
||||||
|
|
@ -148,6 +149,7 @@ async function runGuildPreflight(params: {
|
||||||
guildId: params.guildId,
|
guildId: params.guildId,
|
||||||
author: params.message.author,
|
author: params.message.author,
|
||||||
message: params.message,
|
message: params.message,
|
||||||
|
includeGuildObject: params.includeGuildObject,
|
||||||
}),
|
}),
|
||||||
client: createGuildTextClient(params.channelId),
|
client: createGuildTextClient(params.channelId),
|
||||||
}),
|
}),
|
||||||
|
|
@ -374,6 +376,91 @@ describe("preflightDiscordMessage", () => {
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts allowlisted guild messages when guild object is missing", async () => {
|
||||||
|
const message = createDiscordMessage({
|
||||||
|
id: "m-guild-id-only",
|
||||||
|
channelId: "ch-1",
|
||||||
|
content: "hello from maintainers",
|
||||||
|
author: {
|
||||||
|
id: "user-1",
|
||||||
|
bot: false,
|
||||||
|
username: "Peter",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await runGuildPreflight({
|
||||||
|
channelId: "ch-1",
|
||||||
|
guildId: "guild-1",
|
||||||
|
message,
|
||||||
|
discordConfig: {} as DiscordConfig,
|
||||||
|
guildEntries: {
|
||||||
|
"guild-1": {
|
||||||
|
channels: {
|
||||||
|
"ch-1": {
|
||||||
|
allow: true,
|
||||||
|
requireMention: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
includeGuildObject: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.guildInfo?.id).toBe("guild-1");
|
||||||
|
expect(result?.channelConfig?.allowed).toBe(true);
|
||||||
|
expect(result?.shouldRequireMention).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("inherits parent thread allowlist when guild object is missing", async () => {
|
||||||
|
const threadId = "thread-1";
|
||||||
|
const parentId = "parent-1";
|
||||||
|
const message = createDiscordMessage({
|
||||||
|
id: "m-thread-id-only",
|
||||||
|
channelId: threadId,
|
||||||
|
content: "thread hello",
|
||||||
|
author: {
|
||||||
|
id: "user-1",
|
||||||
|
bot: false,
|
||||||
|
username: "Peter",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await preflightDiscordMessage({
|
||||||
|
...createPreflightArgs({
|
||||||
|
cfg: DEFAULT_PREFLIGHT_CFG,
|
||||||
|
discordConfig: {} as DiscordConfig,
|
||||||
|
data: createGuildEvent({
|
||||||
|
channelId: threadId,
|
||||||
|
guildId: "guild-1",
|
||||||
|
author: message.author,
|
||||||
|
message,
|
||||||
|
includeGuildObject: false,
|
||||||
|
}),
|
||||||
|
client: createThreadClient({
|
||||||
|
threadId,
|
||||||
|
parentId,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
guildEntries: {
|
||||||
|
"guild-1": {
|
||||||
|
channels: {
|
||||||
|
[parentId]: {
|
||||||
|
allow: true,
|
||||||
|
requireMention: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.guildInfo?.id).toBe("guild-1");
|
||||||
|
expect(result?.threadParentId).toBe(parentId);
|
||||||
|
expect(result?.channelConfig?.allowed).toBe(true);
|
||||||
|
expect(result?.shouldRequireMention).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("drops guild messages that mention another user when ignoreOtherMentions=true", async () => {
|
it("drops guild messages that mention another user when ignoreOtherMentions=true", async () => {
|
||||||
const channelId = "channel-other-mention-1";
|
const channelId = "channel-other-mention-1";
|
||||||
const guildId = "guild-other-mention-1";
|
const guildId = "guild-other-mention-1";
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,7 @@ export async function preflightDiscordMessage(
|
||||||
const guildInfo = isGuildMessage
|
const guildInfo = isGuildMessage
|
||||||
? resolveDiscordGuildEntry({
|
? resolveDiscordGuildEntry({
|
||||||
guild: params.data.guild ?? undefined,
|
guild: params.data.guild ?? undefined,
|
||||||
|
guildId: params.data.guild_id ?? undefined,
|
||||||
guildEntries: params.guildEntries,
|
guildEntries: params.guildEntries,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
|
||||||
|
|
@ -1355,6 +1355,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||||
});
|
});
|
||||||
const guildInfo = resolveDiscordGuildEntry({
|
const guildInfo = resolveDiscordGuildEntry({
|
||||||
guild: interaction.guild ?? undefined,
|
guild: interaction.guild ?? undefined,
|
||||||
|
guildId: interaction.guild?.id ?? undefined,
|
||||||
guildEntries: discordConfig?.guilds,
|
guildEntries: discordConfig?.guilds,
|
||||||
});
|
});
|
||||||
let threadParentId: string | undefined;
|
let threadParentId: string | undefined;
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ async function authorizeVoiceCommand(
|
||||||
|
|
||||||
const guildInfo = resolveDiscordGuildEntry({
|
const guildInfo = resolveDiscordGuildEntry({
|
||||||
guild: interaction.guild ?? undefined,
|
guild: interaction.guild ?? undefined,
|
||||||
|
guildId: interaction.guild?.id ?? interaction.rawData.guild_id ?? undefined,
|
||||||
guildEntries: params.discordConfig.guilds,
|
guildEntries: params.discordConfig.guilds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue