mirror of https://github.com/openclaw/openclaw.git
Discord: fix Group DM component interaction routing and auth (#57763)
* Discord: fix Group DM component interaction routing and auth * Update tests
This commit is contained in:
parent
8fdb19676a
commit
8c83128fc3
|
|
@ -33,6 +33,7 @@ import {
|
|||
isDiscordGroupAllowedByPolicy,
|
||||
normalizeDiscordAllowList,
|
||||
normalizeDiscordSlug,
|
||||
resolveGroupDmAllow,
|
||||
resolveDiscordAllowListMatch,
|
||||
resolveDiscordChannelConfigWithFallback,
|
||||
resolveDiscordGuildEntry,
|
||||
|
|
@ -146,6 +147,7 @@ export function resolveAgentComponentRoute(params: {
|
|||
rawGuildId: string | undefined;
|
||||
memberRoleIds: string[];
|
||||
isDirectMessage: boolean;
|
||||
isGroupDm: boolean;
|
||||
userId: string;
|
||||
channelId: string;
|
||||
parentId: string | undefined;
|
||||
|
|
@ -157,7 +159,7 @@ export function resolveAgentComponentRoute(params: {
|
|||
guildId: params.rawGuildId,
|
||||
memberRoleIds: params.memberRoleIds,
|
||||
peer: {
|
||||
kind: params.isDirectMessage ? "direct" : "channel",
|
||||
kind: params.isDirectMessage ? "direct" : params.isGroupDm ? "group" : "channel",
|
||||
id: params.isDirectMessage ? params.userId : params.channelId,
|
||||
},
|
||||
parentPeer: params.parentId ? { kind: "channel", id: params.parentId } : undefined,
|
||||
|
|
@ -238,7 +240,10 @@ export async function resolveComponentInteractionContext(params: {
|
|||
const username = formatUsername(user);
|
||||
const userId = user.id;
|
||||
const rawGuildId = interaction.rawData.guild_id;
|
||||
const isDirectMessage = !rawGuildId;
|
||||
const channelType = resolveDiscordChannelContext(interaction).channelType;
|
||||
const isGroupDm = channelType === ChannelType.GroupDM;
|
||||
const isDirectMessage =
|
||||
channelType === ChannelType.DM || (!rawGuildId && !isGroupDm && channelType == null);
|
||||
const memberRoleIds = Array.isArray(interaction.rawData.member?.roles)
|
||||
? interaction.rawData.member.roles.map((roleId: string) => String(roleId))
|
||||
: [];
|
||||
|
|
@ -251,6 +256,7 @@ export async function resolveComponentInteractionContext(params: {
|
|||
replyOpts,
|
||||
rawGuildId,
|
||||
isDirectMessage,
|
||||
isGroupDm,
|
||||
memberRoleIds,
|
||||
};
|
||||
}
|
||||
|
|
@ -563,6 +569,47 @@ async function ensureDmComponentAuthorized(params: {
|
|||
return false;
|
||||
}
|
||||
|
||||
async function ensureGroupDmComponentAuthorized(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
channelId: string;
|
||||
componentLabel: string;
|
||||
replyOpts: { ephemeral?: boolean };
|
||||
}) {
|
||||
const { ctx, interaction, channelId, componentLabel, replyOpts } = params;
|
||||
const groupDmEnabled = ctx.discordConfig?.dm?.groupEnabled ?? false;
|
||||
if (!groupDmEnabled) {
|
||||
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (group DMs disabled)`);
|
||||
try {
|
||||
await interaction.reply({
|
||||
content: "Group DM interactions are disabled.",
|
||||
...replyOpts,
|
||||
});
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
|
||||
const channelCtx = resolveDiscordChannelContext(interaction);
|
||||
const allowed = resolveGroupDmAllow({
|
||||
channels: ctx.discordConfig?.dm?.groupChannels,
|
||||
channelId,
|
||||
channelName: channelCtx.channelName,
|
||||
channelSlug: channelCtx.channelSlug,
|
||||
});
|
||||
if (allowed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (not allowlisted)`);
|
||||
try {
|
||||
await interaction.reply({
|
||||
content: `You are not authorized to use this ${componentLabel}.`,
|
||||
...replyOpts,
|
||||
});
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function resolveInteractionContextWithDmAuth(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
|
|
@ -590,6 +637,18 @@ export async function resolveInteractionContextWithDmAuth(params: {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
if (interactionCtx.isGroupDm) {
|
||||
const authorized = await ensureGroupDmComponentAuthorized({
|
||||
ctx: params.ctx,
|
||||
interaction: params.interaction,
|
||||
channelId: interactionCtx.channelId,
|
||||
componentLabel: params.componentLabel,
|
||||
replyOpts: interactionCtx.replyOpts,
|
||||
});
|
||||
if (!authorized) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return interactionCtx;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import { resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/rep
|
|||
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { dispatchReplyWithBufferedBlockDispatcher } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { createReplyReferencePlanner } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { createNonExitingRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { logDebug, logError } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
|
@ -115,6 +114,34 @@ function resolveComponentGroupPolicy(
|
|||
}).groupPolicy;
|
||||
}
|
||||
|
||||
function buildDiscordComponentConversationLabel(params: {
|
||||
interactionCtx: ComponentInteractionContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
channelCtx: DiscordChannelContext;
|
||||
}) {
|
||||
if (params.interactionCtx.isDirectMessage) {
|
||||
return buildDirectLabel(params.interactionCtx.user);
|
||||
}
|
||||
if (params.interactionCtx.isGroupDm) {
|
||||
return `Group DM #${params.channelCtx.channelName ?? params.interactionCtx.channelId} channel id:${params.interactionCtx.channelId}`;
|
||||
}
|
||||
return buildGuildLabel({
|
||||
guild: params.interaction.guild ?? undefined,
|
||||
channelName: params.channelCtx.channelName ?? params.interactionCtx.channelId,
|
||||
channelId: params.interactionCtx.channelId,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveDiscordComponentChatType(interactionCtx: ComponentInteractionContext) {
|
||||
if (interactionCtx.isDirectMessage) {
|
||||
return "direct";
|
||||
}
|
||||
if (interactionCtx.isGroupDm) {
|
||||
return "group";
|
||||
}
|
||||
return "channel";
|
||||
}
|
||||
|
||||
async function dispatchPluginDiscordInteractiveEvent(params: {
|
||||
ctx: AgentComponentContext;
|
||||
interaction: AgentComponentInteraction;
|
||||
|
|
@ -289,29 +316,25 @@ async function dispatchDiscordComponentEvent(params: {
|
|||
}): Promise<void> {
|
||||
const { ctx, interaction, interactionCtx, channelCtx, guildInfo, eventText } = params;
|
||||
const runtime = ctx.runtime ?? createNonExitingRuntime();
|
||||
const route = resolveAgentRoute({
|
||||
cfg: ctx.cfg,
|
||||
channel: "discord",
|
||||
accountId: ctx.accountId,
|
||||
guildId: interactionCtx.rawGuildId,
|
||||
const route = resolveAgentComponentRoute({
|
||||
ctx,
|
||||
rawGuildId: interactionCtx.rawGuildId,
|
||||
memberRoleIds: interactionCtx.memberRoleIds,
|
||||
peer: {
|
||||
kind: interactionCtx.isDirectMessage ? "direct" : "channel",
|
||||
id: interactionCtx.isDirectMessage ? interactionCtx.userId : interactionCtx.channelId,
|
||||
},
|
||||
parentPeer: channelCtx.parentId ? { kind: "channel", id: channelCtx.parentId } : undefined,
|
||||
isDirectMessage: interactionCtx.isDirectMessage,
|
||||
isGroupDm: interactionCtx.isGroupDm,
|
||||
userId: interactionCtx.userId,
|
||||
channelId: interactionCtx.channelId,
|
||||
parentId: channelCtx.parentId,
|
||||
});
|
||||
const sessionKey = params.routeOverrides?.sessionKey ?? route.sessionKey;
|
||||
const agentId = params.routeOverrides?.agentId ?? route.agentId;
|
||||
const accountId = params.routeOverrides?.accountId ?? route.accountId;
|
||||
|
||||
const fromLabel = interactionCtx.isDirectMessage
|
||||
? buildDirectLabel(interactionCtx.user)
|
||||
: buildGuildLabel({
|
||||
guild: interaction.guild ?? undefined,
|
||||
channelName: channelCtx.channelName ?? interactionCtx.channelId,
|
||||
channelId: interactionCtx.channelId,
|
||||
});
|
||||
const fromLabel = buildDiscordComponentConversationLabel({
|
||||
interactionCtx,
|
||||
interaction,
|
||||
channelCtx,
|
||||
});
|
||||
const chatType = resolveDiscordComponentChatType(interactionCtx);
|
||||
const senderName = interactionCtx.user.globalName ?? interactionCtx.user.username;
|
||||
const senderUsername = interactionCtx.user.username;
|
||||
const senderTag = formatDiscordUserTag(interactionCtx.user);
|
||||
|
|
@ -369,7 +392,7 @@ async function dispatchDiscordComponentEvent(params: {
|
|||
from: fromLabel,
|
||||
timestamp,
|
||||
body: eventText,
|
||||
chatType: interactionCtx.isDirectMessage ? "direct" : "channel",
|
||||
chatType,
|
||||
senderLabel: senderName,
|
||||
previousTimestamp,
|
||||
envelope: envelopeOptions,
|
||||
|
|
@ -382,11 +405,13 @@ async function dispatchDiscordComponentEvent(params: {
|
|||
CommandBody: eventText,
|
||||
From: interactionCtx.isDirectMessage
|
||||
? `discord:${interactionCtx.userId}`
|
||||
: `discord:channel:${interactionCtx.channelId}`,
|
||||
: interactionCtx.isGroupDm
|
||||
? `discord:group:${interactionCtx.channelId}`
|
||||
: `discord:channel:${interactionCtx.channelId}`,
|
||||
To: `channel:${interactionCtx.channelId}`,
|
||||
SessionKey: sessionKey,
|
||||
AccountId: accountId,
|
||||
ChatType: interactionCtx.isDirectMessage ? "direct" : "channel",
|
||||
ChatType: chatType,
|
||||
ConversationLabel: fromLabel,
|
||||
SenderName: senderName,
|
||||
SenderId: interactionCtx.userId,
|
||||
|
|
@ -694,6 +719,7 @@ async function handleDiscordModalTrigger(params: {
|
|||
interaction: ButtonInteraction;
|
||||
data: ComponentData;
|
||||
label: string;
|
||||
interactionCtx?: ComponentInteractionContext;
|
||||
}): Promise<void> {
|
||||
const parsed = parseDiscordComponentData(
|
||||
params.data,
|
||||
|
|
@ -737,13 +763,15 @@ async function handleDiscordModalTrigger(params: {
|
|||
return;
|
||||
}
|
||||
|
||||
const interactionCtx = await resolveInteractionContextWithDmAuth({
|
||||
ctx: params.ctx,
|
||||
interaction: params.interaction,
|
||||
label: params.label,
|
||||
componentLabel: "form",
|
||||
defer: false,
|
||||
});
|
||||
const interactionCtx =
|
||||
params.interactionCtx ??
|
||||
(await resolveInteractionContextWithDmAuth({
|
||||
ctx: params.ctx,
|
||||
interaction: params.interaction,
|
||||
label: params.label,
|
||||
componentLabel: "form",
|
||||
defer: false,
|
||||
}));
|
||||
if (!interactionCtx) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -870,6 +898,7 @@ export class AgentComponentButton extends Button {
|
|||
replyOpts,
|
||||
rawGuildId,
|
||||
isDirectMessage,
|
||||
isGroupDm,
|
||||
memberRoleIds,
|
||||
} = interactionCtx;
|
||||
|
||||
|
|
@ -896,6 +925,7 @@ export class AgentComponentButton extends Button {
|
|||
rawGuildId,
|
||||
memberRoleIds,
|
||||
isDirectMessage,
|
||||
isGroupDm,
|
||||
userId,
|
||||
channelId,
|
||||
parentId,
|
||||
|
|
@ -960,6 +990,7 @@ export class AgentSelectMenu extends StringSelectMenu {
|
|||
replyOpts,
|
||||
rawGuildId,
|
||||
isDirectMessage,
|
||||
isGroupDm,
|
||||
memberRoleIds,
|
||||
} = interactionCtx;
|
||||
|
||||
|
|
@ -989,6 +1020,7 @@ export class AgentSelectMenu extends StringSelectMenu {
|
|||
rawGuildId,
|
||||
memberRoleIds,
|
||||
isDirectMessage,
|
||||
isGroupDm,
|
||||
userId,
|
||||
channelId,
|
||||
parentId,
|
||||
|
|
@ -1022,11 +1054,22 @@ class DiscordComponentButton extends Button {
|
|||
async run(interaction: ButtonInteraction, data: ComponentData): Promise<void> {
|
||||
const parsed = parseDiscordComponentData(data, resolveInteractionCustomId(interaction));
|
||||
if (parsed?.modalId) {
|
||||
const interactionCtx = await resolveInteractionContextWithDmAuth({
|
||||
ctx: this.ctx,
|
||||
interaction,
|
||||
label: "discord component button",
|
||||
componentLabel: "form",
|
||||
defer: false,
|
||||
});
|
||||
if (!interactionCtx) {
|
||||
return;
|
||||
}
|
||||
await handleDiscordModalTrigger({
|
||||
ctx: this.ctx,
|
||||
interaction,
|
||||
data,
|
||||
label: "discord component modal",
|
||||
interactionCtx,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ButtonInteraction, ComponentData, StringSelectMenuInteraction } from "@buape/carbon";
|
||||
import { ChannelType } from "discord-api-types/v10";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import * as conversationRuntime from "openclaw/plugin-sdk/conversation-runtime";
|
||||
|
|
@ -12,6 +13,7 @@ import {
|
|||
resetDiscordComponentRuntimeMocks,
|
||||
upsertPairingRequestMock,
|
||||
} from "../test-support/component-runtime.js";
|
||||
import { resolveComponentInteractionContext } from "./agent-components-helpers.js";
|
||||
import { createAgentComponentButton, createAgentSelectMenu } from "./agent-components.js";
|
||||
|
||||
describe("agent components", () => {
|
||||
|
|
@ -21,6 +23,12 @@ describe("agent components", () => {
|
|||
accountId: "default",
|
||||
peer: { kind: "direct", id: "123456789" },
|
||||
});
|
||||
const defaultGroupDmSessionKey = buildAgentSessionKey({
|
||||
agentId: "main",
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
peer: { kind: "group", id: "group-dm-channel" },
|
||||
});
|
||||
|
||||
const createCfg = (): OpenClawConfig => ({}) as OpenClawConfig;
|
||||
|
||||
|
|
@ -60,6 +68,35 @@ describe("agent components", () => {
|
|||
};
|
||||
};
|
||||
|
||||
const createBaseGroupDmInteraction = (overrides: Record<string, unknown> = {}) => {
|
||||
const reply = vi.fn().mockResolvedValue(undefined);
|
||||
const defer = vi.fn().mockResolvedValue(undefined);
|
||||
const interaction = {
|
||||
rawData: { channel_id: "group-dm-channel" },
|
||||
channel: {
|
||||
id: "group-dm-channel",
|
||||
type: ChannelType.GroupDM,
|
||||
name: "incident-room",
|
||||
},
|
||||
user: { id: "123456789", username: "Alice", discriminator: "1234" },
|
||||
defer,
|
||||
reply,
|
||||
...overrides,
|
||||
};
|
||||
return { interaction, defer, reply };
|
||||
};
|
||||
|
||||
const createGroupDmButtonInteraction = (overrides: Partial<ButtonInteraction> = {}) => {
|
||||
const { interaction, defer, reply } = createBaseGroupDmInteraction(
|
||||
overrides as Record<string, unknown>,
|
||||
);
|
||||
return {
|
||||
interaction: interaction as unknown as ButtonInteraction,
|
||||
defer,
|
||||
reply,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
resetDiscordComponentRuntimeMocks();
|
||||
resetSystemEventsForTest();
|
||||
|
|
@ -117,6 +154,76 @@ describe("agent components", () => {
|
|||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("classifies Group DM component interactions separately from direct messages", async () => {
|
||||
const { interaction, defer } = createGroupDmButtonInteraction();
|
||||
|
||||
const ctx = await resolveComponentInteractionContext({
|
||||
interaction,
|
||||
label: "group-dm-test",
|
||||
defer: false,
|
||||
});
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(ctx).toMatchObject({
|
||||
channelId: "group-dm-channel",
|
||||
isDirectMessage: false,
|
||||
isGroupDm: true,
|
||||
rawGuildId: undefined,
|
||||
userId: "123456789",
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks Group DM interactions that are not allowlisted even when dmPolicy is open", async () => {
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "open",
|
||||
discordConfig: {
|
||||
dm: {
|
||||
groupEnabled: true,
|
||||
groupChannels: ["other-group-dm"],
|
||||
},
|
||||
} as DiscordAccountConfig,
|
||||
});
|
||||
const { interaction, defer, reply } = createGroupDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({
|
||||
content: "You are not authorized to use this button.",
|
||||
ephemeral: true,
|
||||
});
|
||||
expect(peekSystemEvents(defaultGroupDmSessionKey)).toEqual([]);
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes allowlisted Group DM interactions to the group session without applying DM policy", async () => {
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "disabled",
|
||||
discordConfig: {
|
||||
dm: {
|
||||
groupEnabled: true,
|
||||
groupChannels: ["group-dm-channel"],
|
||||
},
|
||||
} as DiscordAccountConfig,
|
||||
});
|
||||
const { interaction, defer, reply } = createGroupDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(peekSystemEvents(defaultGroupDmSessionKey)).toEqual([
|
||||
"[Discord component: hello clicked by Alice#1234 (123456789)]",
|
||||
]);
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("authorizes DM interactions from pairing-store entries in pairing mode", async () => {
|
||||
readAllowFromStoreMock.mockResolvedValue(["123456789"]);
|
||||
const button = createAgentComponentButton({
|
||||
|
|
|
|||
|
|
@ -701,6 +701,76 @@ describe("discord component interactions", () => {
|
|||
expect(dispatchReplyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("marks built-in Group DM component fallbacks with group metadata", async () => {
|
||||
registerDiscordComponentEntries({
|
||||
entries: [createButtonEntry()],
|
||||
modals: [],
|
||||
});
|
||||
|
||||
const button = createDiscordComponentButton(
|
||||
createComponentContext({
|
||||
discordConfig: createDiscordConfig({
|
||||
dm: {
|
||||
groupEnabled: true,
|
||||
groupChannels: ["group-dm-1"],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const { interaction, reply } = createComponentButtonInteraction({
|
||||
rawData: {
|
||||
channel_id: "group-dm-1",
|
||||
id: "interaction-group-dm-fallback",
|
||||
} as unknown as ButtonInteraction["rawData"],
|
||||
channel: {
|
||||
id: "group-dm-1",
|
||||
type: ChannelType.GroupDM,
|
||||
name: "incident-room",
|
||||
} as unknown as ButtonInteraction["channel"],
|
||||
});
|
||||
|
||||
await button.run(interaction, { cid: "btn_1" } as ComponentData);
|
||||
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(dispatchReplyMock).toHaveBeenCalledTimes(1);
|
||||
expect(lastDispatchCtx).toMatchObject({
|
||||
From: "discord:group:group-dm-1",
|
||||
ChatType: "group",
|
||||
ConversationLabel: "Group DM #incident-room channel id:group-dm-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks Group DM modal triggers before showing the modal", async () => {
|
||||
registerDiscordComponentEntries({
|
||||
entries: [createButtonEntry({ kind: "modal-trigger", modalId: "mdl_1" })],
|
||||
modals: [createModalEntry()],
|
||||
});
|
||||
|
||||
const button = createDiscordComponentButton(createComponentContext());
|
||||
const showModal = vi.fn().mockResolvedValue(undefined);
|
||||
const { interaction, reply } = createComponentButtonInteraction({
|
||||
rawData: {
|
||||
channel_id: "group-dm-1",
|
||||
id: "interaction-group-dm-modal-trigger",
|
||||
} as unknown as ButtonInteraction["rawData"],
|
||||
channel: {
|
||||
id: "group-dm-1",
|
||||
type: ChannelType.GroupDM,
|
||||
name: "incident-room",
|
||||
} as unknown as ButtonInteraction["channel"],
|
||||
showModal,
|
||||
});
|
||||
|
||||
await button.run(interaction, { cid: "btn_1", mid: "mdl_1" } as ComponentData);
|
||||
|
||||
expect(reply).toHaveBeenCalledWith({
|
||||
content: "Group DM interactions are disabled.",
|
||||
ephemeral: true,
|
||||
});
|
||||
expect(showModal).not.toHaveBeenCalled();
|
||||
expect(dispatchReplyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not fall through to Claw when a plugin Discord interaction already replied", async () => {
|
||||
registerDiscordComponentEntries({
|
||||
entries: [createButtonEntry({ callbackData: "codex:approve" })],
|
||||
|
|
|
|||
Loading…
Reference in New Issue