mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 10:07:41 +00:00
refactor(discord): dedupe component context + reaction timing
This commit is contained in:
@@ -71,6 +71,70 @@ function resolveDiscordChannelContext(
|
|||||||
return { channelName, channelSlug, channelType, isThread, parentId, parentName, parentSlug };
|
return { channelName, channelSlug, channelType, isThread, parentId, parentName, parentSlug };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveComponentInteractionContext(params: {
|
||||||
|
interaction: AgentComponentInteraction;
|
||||||
|
label: string;
|
||||||
|
}): Promise<{
|
||||||
|
channelId: string;
|
||||||
|
user: DiscordUser;
|
||||||
|
username: string;
|
||||||
|
userId: string;
|
||||||
|
replyOpts: { ephemeral?: boolean };
|
||||||
|
rawGuildId: string | undefined;
|
||||||
|
isDirectMessage: boolean;
|
||||||
|
memberRoleIds: string[];
|
||||||
|
} | null> {
|
||||||
|
const { interaction, label } = params;
|
||||||
|
|
||||||
|
// Use interaction's actual channel_id (trusted source from Discord)
|
||||||
|
// This prevents channel spoofing attacks
|
||||||
|
const channelId = interaction.rawData.channel_id;
|
||||||
|
if (!channelId) {
|
||||||
|
logError(`${label}: missing channel_id in interaction`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = interaction.user;
|
||||||
|
if (!user) {
|
||||||
|
logError(`${label}: missing user in interaction`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let didDefer = false;
|
||||||
|
// Defer immediately to satisfy Discord's 3-second interaction ACK requirement.
|
||||||
|
// We use an ephemeral deferred reply so subsequent interaction.reply() calls
|
||||||
|
// can safely edit the original deferred response.
|
||||||
|
try {
|
||||||
|
await interaction.defer({ ephemeral: true });
|
||||||
|
didDefer = true;
|
||||||
|
} catch (err) {
|
||||||
|
logError(`${label}: failed to defer interaction: ${String(err)}`);
|
||||||
|
}
|
||||||
|
const replyOpts = didDefer ? {} : { ephemeral: true };
|
||||||
|
|
||||||
|
const username = formatUsername(user);
|
||||||
|
const userId = user.id;
|
||||||
|
|
||||||
|
// P1 FIX: Use rawData.guild_id as source of truth - interaction.guild can be null
|
||||||
|
// when guild is not cached even though guild_id is present in rawData
|
||||||
|
const rawGuildId = interaction.rawData.guild_id;
|
||||||
|
const isDirectMessage = !rawGuildId;
|
||||||
|
const memberRoleIds = Array.isArray(interaction.rawData.member?.roles)
|
||||||
|
? interaction.rawData.member.roles.map((roleId: string) => String(roleId))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
channelId,
|
||||||
|
user,
|
||||||
|
username,
|
||||||
|
userId,
|
||||||
|
replyOpts,
|
||||||
|
rawGuildId,
|
||||||
|
isDirectMessage,
|
||||||
|
memberRoleIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureGuildComponentMemberAllowed(params: {
|
async function ensureGuildComponentMemberAllowed(params: {
|
||||||
interaction: AgentComponentInteraction;
|
interaction: AgentComponentInteraction;
|
||||||
guildInfo: ReturnType<typeof resolveDiscordGuildEntry>;
|
guildInfo: ReturnType<typeof resolveDiscordGuildEntry>;
|
||||||
@@ -314,43 +378,23 @@ export class AgentComponentButton extends Button {
|
|||||||
|
|
||||||
const { componentId } = parsed;
|
const { componentId } = parsed;
|
||||||
|
|
||||||
// P1 FIX: Use interaction's actual channel_id instead of trusting customId
|
const interactionCtx = await resolveComponentInteractionContext({
|
||||||
// This prevents channel ID spoofing attacks where an attacker crafts a button
|
interaction,
|
||||||
// with a different channelId to inject events into other sessions
|
label: "agent button",
|
||||||
const channelId = interaction.rawData.channel_id;
|
});
|
||||||
if (!channelId) {
|
if (!interactionCtx) {
|
||||||
logError("agent button: missing channel_id in interaction");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const {
|
||||||
const user = interaction.user;
|
channelId,
|
||||||
if (!user) {
|
user,
|
||||||
logError("agent button: missing user in interaction");
|
username,
|
||||||
return;
|
userId,
|
||||||
}
|
replyOpts,
|
||||||
|
rawGuildId,
|
||||||
let didDefer = false;
|
isDirectMessage,
|
||||||
// Defer immediately to satisfy Discord's 3-second interaction ACK requirement.
|
memberRoleIds,
|
||||||
// We use an ephemeral deferred reply so subsequent interaction.reply() calls
|
} = interactionCtx;
|
||||||
// can safely edit the original deferred response.
|
|
||||||
try {
|
|
||||||
await interaction.defer({ ephemeral: true });
|
|
||||||
didDefer = true;
|
|
||||||
} catch (err) {
|
|
||||||
logError(`agent button: failed to defer interaction: ${String(err)}`);
|
|
||||||
}
|
|
||||||
const replyOpts = didDefer ? {} : { ephemeral: true };
|
|
||||||
|
|
||||||
const username = formatUsername(user);
|
|
||||||
const userId = user.id;
|
|
||||||
|
|
||||||
// P1 FIX: Use rawData.guild_id as source of truth - interaction.guild can be null
|
|
||||||
// when guild is not cached even though guild_id is present in rawData
|
|
||||||
const rawGuildId = interaction.rawData.guild_id;
|
|
||||||
const isDirectMessage = !rawGuildId;
|
|
||||||
const memberRoleIds = Array.isArray(interaction.rawData.member?.roles)
|
|
||||||
? interaction.rawData.member.roles.map((roleId: string) => String(roleId))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (isDirectMessage) {
|
if (isDirectMessage) {
|
||||||
const authorized = await ensureDmComponentAuthorized({
|
const authorized = await ensureDmComponentAuthorized({
|
||||||
@@ -452,42 +496,23 @@ export class AgentSelectMenu extends StringSelectMenu {
|
|||||||
|
|
||||||
const { componentId } = parsed;
|
const { componentId } = parsed;
|
||||||
|
|
||||||
// Use interaction's actual channel_id (trusted source from Discord)
|
const interactionCtx = await resolveComponentInteractionContext({
|
||||||
// This prevents channel spoofing attacks
|
interaction,
|
||||||
const channelId = interaction.rawData.channel_id;
|
label: "agent select",
|
||||||
if (!channelId) {
|
});
|
||||||
logError("agent select: missing channel_id in interaction");
|
if (!interactionCtx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const {
|
||||||
const user = interaction.user;
|
channelId,
|
||||||
if (!user) {
|
user,
|
||||||
logError("agent select: missing user in interaction");
|
username,
|
||||||
return;
|
userId,
|
||||||
}
|
replyOpts,
|
||||||
|
rawGuildId,
|
||||||
let didDefer = false;
|
isDirectMessage,
|
||||||
// Defer immediately to satisfy Discord's 3-second interaction ACK requirement.
|
memberRoleIds,
|
||||||
// We use an ephemeral deferred reply so subsequent interaction.reply() calls
|
} = interactionCtx;
|
||||||
// can safely edit the original deferred response.
|
|
||||||
try {
|
|
||||||
await interaction.defer({ ephemeral: true });
|
|
||||||
didDefer = true;
|
|
||||||
} catch (err) {
|
|
||||||
logError(`agent select: failed to defer interaction: ${String(err)}`);
|
|
||||||
}
|
|
||||||
const replyOpts = didDefer ? {} : { ephemeral: true };
|
|
||||||
|
|
||||||
const username = formatUsername(user);
|
|
||||||
const userId = user.id;
|
|
||||||
|
|
||||||
// P1 FIX: Use rawData.guild_id as source of truth - interaction.guild can be null
|
|
||||||
// when guild is not cached even though guild_id is present in rawData
|
|
||||||
const rawGuildId = interaction.rawData.guild_id;
|
|
||||||
const isDirectMessage = !rawGuildId;
|
|
||||||
const memberRoleIds = Array.isArray(interaction.rawData.member?.roles)
|
|
||||||
? interaction.rawData.member.roles.map((roleId: string) => String(roleId))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (isDirectMessage) {
|
if (isDirectMessage) {
|
||||||
const authorized = await ensureDmComponentAuthorized({
|
const authorized = await ensureDmComponentAuthorized({
|
||||||
|
|||||||
@@ -108,26 +108,14 @@ export class DiscordReactionListener extends MessageReactionAddListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handle(data: DiscordReactionEvent, client: Client) {
|
async handle(data: DiscordReactionEvent, client: Client) {
|
||||||
const startedAt = Date.now();
|
await runDiscordReactionHandler({
|
||||||
try {
|
data,
|
||||||
await handleDiscordReactionEvent({
|
client,
|
||||||
data,
|
action: "added",
|
||||||
client,
|
handlerParams: this.params,
|
||||||
action: "added",
|
listener: this.constructor.name,
|
||||||
cfg: this.params.cfg,
|
event: this.type,
|
||||||
accountId: this.params.accountId,
|
});
|
||||||
botUserId: this.params.botUserId,
|
|
||||||
guildEntries: this.params.guildEntries,
|
|
||||||
logger: this.params.logger,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
logSlowDiscordListener({
|
|
||||||
logger: this.params.logger,
|
|
||||||
listener: this.constructor.name,
|
|
||||||
event: this.type,
|
|
||||||
durationMs: Date.now() - startedAt,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,26 +134,51 @@ export class DiscordReactionRemoveListener extends MessageReactionRemoveListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handle(data: DiscordReactionEvent, client: Client) {
|
async handle(data: DiscordReactionEvent, client: Client) {
|
||||||
const startedAt = Date.now();
|
await runDiscordReactionHandler({
|
||||||
try {
|
data,
|
||||||
await handleDiscordReactionEvent({
|
client,
|
||||||
data,
|
action: "removed",
|
||||||
client,
|
handlerParams: this.params,
|
||||||
action: "removed",
|
listener: this.constructor.name,
|
||||||
cfg: this.params.cfg,
|
event: this.type,
|
||||||
accountId: this.params.accountId,
|
});
|
||||||
botUserId: this.params.botUserId,
|
}
|
||||||
guildEntries: this.params.guildEntries,
|
}
|
||||||
logger: this.params.logger,
|
|
||||||
});
|
async function runDiscordReactionHandler(params: {
|
||||||
} finally {
|
data: DiscordReactionEvent;
|
||||||
logSlowDiscordListener({
|
client: Client;
|
||||||
logger: this.params.logger,
|
action: "added" | "removed";
|
||||||
listener: this.constructor.name,
|
handlerParams: {
|
||||||
event: this.type,
|
cfg: LoadedConfig;
|
||||||
durationMs: Date.now() - startedAt,
|
accountId: string;
|
||||||
});
|
runtime: RuntimeEnv;
|
||||||
}
|
botUserId?: string;
|
||||||
|
guildEntries?: Record<string, import("./allow-list.js").DiscordGuildEntryResolved>;
|
||||||
|
logger: Logger;
|
||||||
|
};
|
||||||
|
listener: string;
|
||||||
|
event: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
try {
|
||||||
|
await handleDiscordReactionEvent({
|
||||||
|
data: params.data,
|
||||||
|
client: params.client,
|
||||||
|
action: params.action,
|
||||||
|
cfg: params.handlerParams.cfg,
|
||||||
|
accountId: params.handlerParams.accountId,
|
||||||
|
botUserId: params.handlerParams.botUserId,
|
||||||
|
guildEntries: params.handlerParams.guildEntries,
|
||||||
|
logger: params.handlerParams.logger,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
logSlowDiscordListener({
|
||||||
|
logger: params.handlerParams.logger,
|
||||||
|
listener: params.listener,
|
||||||
|
event: params.event,
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user