diff --git a/packages/nodes-base/nodes/Slack/SlackTrigger.node.ts b/packages/nodes-base/nodes/Slack/SlackTrigger.node.ts index 3bc68c5a3f92e..2a435ff52020c 100644 --- a/packages/nodes-base/nodes/Slack/SlackTrigger.node.ts +++ b/packages/nodes-base/nodes/Slack/SlackTrigger.node.ts @@ -235,6 +235,20 @@ export class SlackTrigger implements INodeType { description: 'A comma-separated string of encoded user IDs. Choose from the list, or specify IDs using an expression.', }, + { + displayName: 'Emoji Names to Filter', + name: 'reactionEmojis', + type: 'string', + default: '', + placeholder: 'thumbsup, eyes, white_check_mark', + description: + 'Comma-separated list of emoji names to allow (without colons). Leave empty to trigger on any reaction.', + displayOptions: { + show: { + '/trigger': ['reaction_added'], + }, + }, + }, ], }, ], @@ -381,6 +395,20 @@ export class SlackTrigger implements INodeType { } } + // Filter by reaction emoji for reaction_added events + if (eventType === 'reaction_added' && options.reactionEmojis) { + const allowedEmojis = (options.reactionEmojis as string) + .split(',') + .map((e) => e.trim().toLowerCase()) + .filter(Boolean); + if (allowedEmojis.length > 0) { + const reaction = ((req.body.event.reaction as string | undefined) ?? '').toLowerCase(); + if (!allowedEmojis.includes(reaction)) { + return {}; + } + } + } + if (options.resolveIds) { if (req.body.event.user) { if (req.body.event.type === 'reaction_added') { diff --git a/packages/nodes-base/nodes/Slack/test/SlackTrigger.test.ts b/packages/nodes-base/nodes/Slack/test/SlackTrigger.test.ts index 8e1d23e4ae377..e8e85d9ecf957 100644 --- a/packages/nodes-base/nodes/Slack/test/SlackTrigger.test.ts +++ b/packages/nodes-base/nodes/Slack/test/SlackTrigger.test.ts @@ -464,6 +464,151 @@ describe('SlackTrigger Node', () => { }); }); + describe('webhook method - reaction emoji filter', () => { + const reactionRequest = (reaction: string) => ({ + body: { + type: 'event_callback', + event: { + type: 'reaction_added', + user: 'U456', + item: { channel: 'C123', ts: '1234567890.123456' }, + reaction, + }, + }, + }); + + beforeEach(() => { + mockWebhookFunctions.getNodeParameter.mockImplementation( + (paramName: string, defaultValue?: any) => { + switch (paramName) { + case 'trigger': + return ['reaction_added']; + case 'watchWorkspace': + return false; + case 'channelId': + return 'C123'; + case 'downloadFiles': + return false; + case 'options': + return {}; + default: + return defaultValue; + } + }, + ); + }); + + it('should trigger when no emoji filter is set', async () => { + mockWebhookFunctions.getRequestObject.mockReturnValue(reactionRequest('thumbsup') as any); + + const result = await slackTrigger.webhook!.call(mockWebhookFunctions); + + expect(result.workflowData).toBeDefined(); + }); + + it('should trigger when reaction matches the filter', async () => { + mockWebhookFunctions.getRequestObject.mockReturnValue(reactionRequest('thumbsup') as any); + mockWebhookFunctions.getNodeParameter.mockImplementation( + (paramName: string, defaultValue?: any) => { + if (paramName === 'options') return { reactionEmojis: 'thumbsup' }; + if (paramName === 'trigger') return ['reaction_added']; + if (paramName === 'watchWorkspace') return false; + if (paramName === 'channelId') return 'C123'; + return defaultValue; + }, + ); + + const result = await slackTrigger.webhook!.call(mockWebhookFunctions); + + expect(result.workflowData).toBeDefined(); + }); + + it('should not trigger when reaction does not match the filter', async () => { + mockWebhookFunctions.getRequestObject.mockReturnValue(reactionRequest('eyes') as any); + mockWebhookFunctions.getNodeParameter.mockImplementation( + (paramName: string, defaultValue?: any) => { + if (paramName === 'options') return { reactionEmojis: 'thumbsup' }; + if (paramName === 'trigger') return ['reaction_added']; + if (paramName === 'watchWorkspace') return false; + if (paramName === 'channelId') return 'C123'; + return defaultValue; + }, + ); + + const result = await slackTrigger.webhook!.call(mockWebhookFunctions); + + expect(result).toEqual({}); + }); + + it('should support multiple comma-separated emoji names', async () => { + mockWebhookFunctions.getRequestObject.mockReturnValue(reactionRequest('eyes') as any); + mockWebhookFunctions.getNodeParameter.mockImplementation( + (paramName: string, defaultValue?: any) => { + if (paramName === 'options') return { reactionEmojis: 'thumbsup, eyes' }; + if (paramName === 'trigger') return ['reaction_added']; + if (paramName === 'watchWorkspace') return false; + if (paramName === 'channelId') return 'C123'; + return defaultValue; + }, + ); + + const result = await slackTrigger.webhook!.call(mockWebhookFunctions); + + expect(result.workflowData).toBeDefined(); + }); + + it('should match emoji names case-insensitively', async () => { + mockWebhookFunctions.getRequestObject.mockReturnValue(reactionRequest('thumbsup') as any); + mockWebhookFunctions.getNodeParameter.mockImplementation( + (paramName: string, defaultValue?: any) => { + if (paramName === 'options') return { reactionEmojis: 'ThumbsUp' }; + if (paramName === 'trigger') return ['reaction_added']; + if (paramName === 'watchWorkspace') return false; + if (paramName === 'channelId') return 'C123'; + return defaultValue; + }, + ); + + const result = await slackTrigger.webhook!.call(mockWebhookFunctions); + + expect(result.workflowData).toBeDefined(); + }); + + it('should trim whitespace around emoji names', async () => { + mockWebhookFunctions.getRequestObject.mockReturnValue(reactionRequest('thumbsup') as any); + mockWebhookFunctions.getNodeParameter.mockImplementation( + (paramName: string, defaultValue?: any) => { + if (paramName === 'options') return { reactionEmojis: ' thumbsup , eyes ' }; + if (paramName === 'trigger') return ['reaction_added']; + if (paramName === 'watchWorkspace') return false; + if (paramName === 'channelId') return 'C123'; + return defaultValue; + }, + ); + + const result = await slackTrigger.webhook!.call(mockWebhookFunctions); + + expect(result.workflowData).toBeDefined(); + }); + + it('should not trigger when filter has entries but reaction is an empty string', async () => { + mockWebhookFunctions.getRequestObject.mockReturnValue(reactionRequest('') as any); + mockWebhookFunctions.getNodeParameter.mockImplementation( + (paramName: string, defaultValue?: any) => { + if (paramName === 'options') return { reactionEmojis: 'thumbsup' }; + if (paramName === 'trigger') return ['reaction_added']; + if (paramName === 'watchWorkspace') return false; + if (paramName === 'channelId') return 'C123'; + return defaultValue; + }, + ); + + const result = await slackTrigger.webhook!.call(mockWebhookFunctions); + + expect(result).toEqual({}); + }); + }); + describe('webhook method - other event scenarios', () => { it('should handle team_join event (no channel extraction needed)', async () => { const mockRequest = {