diff --git a/.changeset/slack-reaction-display-names.md b/.changeset/slack-reaction-display-names.md new file mode 100644 index 00000000..e4a70e2c --- /dev/null +++ b/.changeset/slack-reaction-display-names.md @@ -0,0 +1,9 @@ +--- +"@chat-adapter/slack": patch +--- + +fix(slack): resolve reaction user display names + +Slack reaction events now resolve the reacting user's display name and real name +through the existing cached user lookup path. If lookup fails, the adapter falls +back to the Slack user ID. diff --git a/packages/adapter-slack/src/index.test.ts b/packages/adapter-slack/src/index.test.ts index 5fd91287..512dbfe1 100644 --- a/packages/adapter-slack/src/index.test.ts +++ b/packages/adapter-slack/src/index.test.ts @@ -650,10 +650,16 @@ describe("handleWebhook - event_callback", () => { messages: [{ ts: replyTs, thread_ts: parentTs }], }) ); + mockClientMethod( + adapter, + "users.info", + vi.fn().mockResolvedValue({ + ok: true, + user: { name: "user", profile: { display_name: "User" } }, + }) + ); - const mockChat = { - processReaction: vi.fn(), - } as unknown as ChatInstance; + const mockChat = createMockChatInstance(createMockState()); (adapter as unknown as { chat: ChatInstance }).chat = mockChat; const body = JSON.stringify({ @@ -683,6 +689,62 @@ describe("handleWebhook - event_callback", () => { undefined ); }); + + it("resolves reaction user display name", async () => { + mockClientMethod( + adapter, + "conversations.replies", + vi.fn().mockResolvedValue({ + ok: true, + messages: [{ ts: "1234567890.123456" }], + }) + ); + const usersInfoMock = vi.fn().mockResolvedValue({ + ok: true, + user: { + id: "U123", + name: "alice", + real_name: "Alice Example", + profile: { display_name: "Alice", real_name: "Alice Example" }, + }, + }); + mockClientMethod(adapter, "users.info", usersInfoMock); + + const mockChat = createMockChatInstance(createMockState()); + (adapter as unknown as { chat: ChatInstance }).chat = mockChat; + + const body = JSON.stringify({ + type: "event_callback", + event: { + type: "reaction_added", + user: "U123", + reaction: "thumbsup", + item: { + type: "message", + channel: "C456", + ts: "1234567890.123456", + }, + }, + }); + const request = createWebhookRequest(body, secret); + + await adapter.handleWebhook(request); + await new Promise((r) => setTimeout(r, 50)); + + expect(usersInfoMock).toHaveBeenCalledWith( + expect.objectContaining({ user: "U123" }) + ); + expect(mockChat.processReaction).toHaveBeenCalledWith( + expect.objectContaining({ + user: expect.objectContaining({ + userId: "U123", + userName: "Alice", + fullName: "Alice Example", + }), + }), + undefined + ); + }); }); // ============================================================================ diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index 71ca7a77..4026706b 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -2306,6 +2306,10 @@ export class SlackAdapter implements Adapter { (this._botUserId !== null && event.user === this._botUserId) || (this._botId !== null && event.user === this._botId); + const userInfo = await this.lookupUser(event.user); + const userName = userInfo?.displayName ?? event.user; + const fullName = userInfo?.realName ?? userName; + // Build reaction event const reactionEvent: Omit = { emoji: normalizedEmoji, @@ -2313,9 +2317,9 @@ export class SlackAdapter implements Adapter { added: event.type === "reaction_added", user: { userId: event.user, - userName: event.user, // Will be resolved below if possible - fullName: event.user, - isBot: false, // Users add reactions, not bots typically + userName, + fullName, + isBot: userInfo?.isBot ?? false, isMe, }, messageId,