From 61ca43d19313e92631674a208b14c321bd26584b Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Thu, 23 Apr 2026 12:23:12 +0000 Subject: [PATCH] fix(discord): resolve user ID from DM interactions for approval clicks Discord puts the clicking user at interaction.member.user for guild interactions but interaction.user for DM interactions. The Gateway handler only checked interaction.member, so DM button clicks resolved to an empty user ID and were silently rejected as unauthorized. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/channels/chat-sdk-bridge.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/channels/chat-sdk-bridge.ts b/src/channels/chat-sdk-bridge.ts index 5c120e0..6c9f802 100644 --- a/src/channels/chat-sdk-bridge.ts +++ b/src/channels/chat-sdk-bridge.ts @@ -105,7 +105,7 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter let setupConfig: ChannelSetup; let gatewayAbort: AbortController | null = null; - async function messageToInbound(message: ChatMessage, isMention: boolean): Promise { + async function messageToInbound(message: ChatMessage, isMention: boolean, isGroup?: boolean): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const serialized = message.toJSON() as Record; @@ -162,6 +162,7 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter content: serialized, timestamp: message.metadata.dateSent.toISOString(), isMention, + isGroup, }; } @@ -195,13 +196,13 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter // wirings still fire on in-thread mentions. chat.onSubscribedMessage(async (thread, message) => { const channelId = adapter.channelIdFromThreadId(thread.id); - await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message, message.isMention === true)); + await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message, message.isMention === true, true)); }); // @mention in an unsubscribed thread — SDK-confirmed bot mention. chat.onNewMention(async (thread, message) => { const channelId = adapter.channelIdFromThreadId(thread.id); - await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message, true)); + await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message, true, true)); }); // DMs — by definition addressed to the bot. Thread id flows through @@ -216,7 +217,7 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter sender: (message.author as any)?.fullName ?? (message.author as any)?.userId ?? 'unknown', threadId: thread.id, }); - await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message, true)); + await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message, true, false)); }); // Plain messages in unsubscribed threads. @@ -231,7 +232,7 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter // flood gate. chat.onNewMessage(/./, async (thread, message) => { const channelId = adapter.channelIdFromThreadId(thread.id); - await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message, false)); + await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message, false, true)); }); // Handle button clicks (ask_user_question) @@ -501,7 +502,10 @@ async function handleForwardedEvent( // type 3 = MessageComponent (button/select) if (interaction.type === 3) { const customId = (interaction.data as Record)?.custom_id as string; - const user = (interaction.member as Record)?.user as Record | undefined; + // In guilds the clicker is at interaction.member.user; in DMs it's interaction.user directly. + const user = + ((interaction.member as Record)?.user as Record | undefined) ?? + (interaction.user as Record | undefined); const interactionId = interaction.id as string; const interactionToken = interaction.token as string;