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.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
|
|
|
|||
|
|
@ -247,6 +247,18 @@ describe("discord guild/channel resolution", () => {
|
|||
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", () => {
|
||||
const guildEntries = makeEntries({
|
||||
"friends-of-openclaw": { slug: "friends-of-openclaw" },
|
||||
|
|
|
|||
|
|
@ -360,6 +360,7 @@ async function ensureAgentComponentInteractionAllowed(params: {
|
|||
}): Promise<{ parentId: string | undefined } | null> {
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: params.interaction.guild ?? undefined,
|
||||
guildId: params.rawGuildId,
|
||||
guildEntries: params.ctx.guildEntries,
|
||||
});
|
||||
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
||||
|
|
@ -1094,6 +1095,7 @@ async function handleDiscordComponentEvent(params: {
|
|||
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: params.interaction.guild ?? undefined,
|
||||
guildId: rawGuildId,
|
||||
guildEntries: params.ctx.guildEntries,
|
||||
});
|
||||
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
||||
|
|
@ -1246,6 +1248,7 @@ async function handleDiscordModalTrigger(params: {
|
|||
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: params.interaction.guild ?? undefined,
|
||||
guildId: rawGuildId,
|
||||
guildEntries: params.ctx.guildEntries,
|
||||
});
|
||||
const channelCtx = resolveDiscordChannelContext(params.interaction);
|
||||
|
|
@ -1696,6 +1699,7 @@ class DiscordComponentModal extends Modal {
|
|||
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: interaction.guild ?? undefined,
|
||||
guildId: rawGuildId,
|
||||
guildEntries: this.ctx.guildEntries,
|
||||
});
|
||||
const channelCtx = resolveDiscordChannelContext(interaction);
|
||||
|
|
|
|||
|
|
@ -321,25 +321,30 @@ export function resolveDiscordCommandAuthorized(params: {
|
|||
|
||||
export function resolveDiscordGuildEntry(params: {
|
||||
guild?: Guild<true> | Guild | null;
|
||||
guildId?: string | null;
|
||||
guildEntries?: Record<string, DiscordGuildEntryResolved>;
|
||||
}): DiscordGuildEntryResolved | null {
|
||||
const guild = params.guild;
|
||||
const entries = params.guildEntries;
|
||||
if (!guild || !entries) {
|
||||
const guildId = params.guildId?.trim() || guild?.id;
|
||||
if (!entries) {
|
||||
return null;
|
||||
}
|
||||
const byId = entries[guild.id];
|
||||
const byId = guildId ? entries[guildId] : undefined;
|
||||
if (byId) {
|
||||
return { ...byId, id: guild.id };
|
||||
return { ...byId, id: guildId };
|
||||
}
|
||||
if (!guild) {
|
||||
return null;
|
||||
}
|
||||
const slug = normalizeDiscordSlug(guild.name ?? "");
|
||||
const bySlug = entries[slug];
|
||||
if (bySlug) {
|
||||
return { ...bySlug, id: guild.id, slug: slug || bySlug.slug };
|
||||
return { ...bySlug, id: guildId ?? guild.id, slug: slug || bySlug.slug };
|
||||
}
|
||||
const wildcard = entries["*"];
|
||||
if (wildcard) {
|
||||
return { ...wildcard, id: guild.id, slug: slug || wildcard.slug };
|
||||
return { ...wildcard, id: guildId ?? guild.id, slug: slug || wildcard.slug };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -430,6 +430,7 @@ async function handleDiscordReactionEvent(
|
|||
const guildInfo = isGuildMessage
|
||||
? resolveDiscordGuildEntry({
|
||||
guild: data.guild ?? undefined,
|
||||
guildId: data.guild_id ?? undefined,
|
||||
guildEntries,
|
||||
})
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -34,14 +34,19 @@ export function createGuildEvent(params: {
|
|||
guildId: string;
|
||||
author: import("@buape/carbon").Message["author"];
|
||||
message: import("@buape/carbon").Message;
|
||||
includeGuildObject?: boolean;
|
||||
}): DiscordMessageEvent {
|
||||
return {
|
||||
channel_id: params.channelId,
|
||||
guild_id: params.guildId,
|
||||
guild: {
|
||||
id: params.guildId,
|
||||
name: "Guild One",
|
||||
},
|
||||
...(params.includeGuildObject === false
|
||||
? {}
|
||||
: {
|
||||
guild: {
|
||||
id: params.guildId,
|
||||
name: "Guild One",
|
||||
},
|
||||
}),
|
||||
author: params.author,
|
||||
message: params.message,
|
||||
} as unknown as DiscordMessageEvent;
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ async function runGuildPreflight(params: {
|
|||
discordConfig: DiscordConfig;
|
||||
cfg?: import("../../config/config.js").OpenClawConfig;
|
||||
guildEntries?: Parameters<typeof preflightDiscordMessage>[0]["guildEntries"];
|
||||
includeGuildObject?: boolean;
|
||||
}) {
|
||||
return preflightDiscordMessage({
|
||||
...createPreflightArgs({
|
||||
|
|
@ -148,6 +149,7 @@ async function runGuildPreflight(params: {
|
|||
guildId: params.guildId,
|
||||
author: params.message.author,
|
||||
message: params.message,
|
||||
includeGuildObject: params.includeGuildObject,
|
||||
}),
|
||||
client: createGuildTextClient(params.channelId),
|
||||
}),
|
||||
|
|
@ -374,6 +376,91 @@ describe("preflightDiscordMessage", () => {
|
|||
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 () => {
|
||||
const channelId = "channel-other-mention-1";
|
||||
const guildId = "guild-other-mention-1";
|
||||
|
|
|
|||
|
|
@ -430,6 +430,7 @@ export async function preflightDiscordMessage(
|
|||
const guildInfo = isGuildMessage
|
||||
? resolveDiscordGuildEntry({
|
||||
guild: params.data.guild ?? undefined,
|
||||
guildId: params.data.guild_id ?? undefined,
|
||||
guildEntries: params.guildEntries,
|
||||
})
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -1355,6 +1355,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||
});
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: interaction.guild ?? undefined,
|
||||
guildId: interaction.guild?.id ?? undefined,
|
||||
guildEntries: discordConfig?.guilds,
|
||||
});
|
||||
let threadParentId: string | undefined;
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ async function authorizeVoiceCommand(
|
|||
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: interaction.guild ?? undefined,
|
||||
guildId: interaction.guild?.id ?? interaction.rawData.guild_id ?? undefined,
|
||||
guildEntries: params.discordConfig.guilds,
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue