From 7d69d61ef6ef8124516924bdff3b515e039b1e2a Mon Sep 17 00:00:00 2001 From: yilmazhasan Date: Mon, 22 Jun 2026 15:59:13 +0300 Subject: [PATCH 1/3] Issue 281 take diff from PR 761 --- src/App.ts | 13 +++++++-- src/middleware/builtin.ts | 39 ++++++++++++++++++-------- test/types/message.test-d.ts | 15 ++++++++++ test/unit/middleware/builtin.spec.ts | 42 ++++++++++++++++++++++++++-- 4 files changed, 93 insertions(+), 16 deletions(-) diff --git a/src/App.ts b/src/App.ts index 8a7a91616..92a9dea72 100644 --- a/src/App.ts +++ b/src/App.ts @@ -691,10 +691,19 @@ export default class App ): void; // TODO: expose a type parameter for overriding the MessageEvent type (just like shortcut() and action() does) https://github.com/slackapi/bolt-js/issues/796 public message( - ...patternsOrMiddleware: (string | RegExp | MessageEventMiddleware)[] + ...patternsOrMiddleware: ( + | string + | RegExp + | (string | RegExp)[] + | MessageEventMiddleware + )[] ): void { const messageMiddleware = patternsOrMiddleware.map((patternOrMiddleware) => { - if (typeof patternOrMiddleware === 'string' || util.types.isRegExp(patternOrMiddleware)) { + if ( + typeof patternOrMiddleware === 'string' || + util.types.isRegExp(patternOrMiddleware) || + Array.isArray(patternOrMiddleware) + ) { return matchMessage(patternOrMiddleware); } return patternOrMiddleware; diff --git a/src/middleware/builtin.ts b/src/middleware/builtin.ts index c6c0635b8..065ab1dd1 100644 --- a/src/middleware/builtin.ts +++ b/src/middleware/builtin.ts @@ -1,3 +1,4 @@ +import util from 'node:util'; import type { ActionConstraints, OptionsConstraints, ShortcutConstraints, ViewConstraints } from '../App'; import { ContextMissingPropertyError } from '../errors'; import type { @@ -228,30 +229,46 @@ export function matchConstraints( * Middleware that filters out messages that don't match pattern */ export function matchMessage( - pattern: string | RegExp, + pattern: string | RegExp | (string | RegExp)[], ): Middleware> { return async ({ event, context, next }) => { - let tempMatches: RegExpMatchArray | null; + let tempMatches: RegExpMatchArray | null = null; + let patternArray: (string | RegExp)[] = []; if (!('text' in event) || event.text === undefined) { return; } - // Filter out messages or app mentions that don't contain the pattern - if (typeof pattern === 'string') { - if (!event.text.includes(pattern)) { - return; - } + // Normalize a single pattern into an array of patterns + if (typeof pattern === 'string' || util.types.isRegExp(pattern)) { + patternArray = [pattern]; } else { - tempMatches = event.text.match(pattern); + patternArray = pattern; + } - if (tempMatches !== null) { - context.matches = tempMatches; + // Filter out messages or app mentions that don't contain any of the patterns + let matched = false; + for (let i = 0; i < patternArray.length; i += 1) { + const patternArrayItem = patternArray[i]; + if (typeof patternArrayItem === 'string') { + if (event.text.includes(patternArrayItem)) { + matched = true; + break; + } } else { - return; + tempMatches = event.text.match(patternArrayItem); + if (tempMatches !== null) { + context.matches = tempMatches; + matched = true; + break; + } } } + if (matched === false) { + return; + } + await next(); }; } diff --git a/test/types/message.test-d.ts b/test/types/message.test-d.ts index 94c0177df..4d5a3234e 100644 --- a/test/types/message.test-d.ts +++ b/test/types/message.test-d.ts @@ -96,6 +96,21 @@ app.message(async ({ message }) => { await Promise.resolve(message); }); +// Patterns: a single string pattern should be accepted +app.message('hello', async ({ message }) => { + await Promise.resolve(message); +}); + +// Patterns: a single RegExp pattern should be accepted +app.message(/hello/, async ({ message }) => { + await Promise.resolve(message); +}); + +// Patterns: an array of string | RegExp patterns should be accepted (issue #281) +app.message(['hello', 'hi', 'bonjour', /\w{2,8}\s*\w{0,7}!?\.?/], async ({ message }) => { + await Promise.resolve(message); +}); + interface MyContext { doesnt: 'matter'; } diff --git a/test/unit/middleware/builtin.spec.ts b/test/unit/middleware/builtin.spec.ts index 9badf9368..a4d6407d4 100644 --- a/test/unit/middleware/builtin.spec.ts +++ b/test/unit/middleware/builtin.spec.ts @@ -38,7 +38,7 @@ describe('Built-in global middleware', () => { }); describe('matchMessage()', () => { function matchesPatternTestCase( - pattern: string | RegExp, + pattern: string | RegExp | (string | RegExp)[], event: SlackEventMiddlewareArgs<'message' | 'app_mention'>, ): Mocha.AsyncFunc { return async () => { @@ -50,7 +50,7 @@ describe('Built-in global middleware', () => { sinon.assert.calledOnce(args.next); // The following assertion(s) check behavior that is only targeted at RegExp patterns - if (typeof pattern !== 'string') { + if (typeof pattern !== 'string' && !Array.isArray(pattern)) { if (ctx.matches !== undefined) { assert.lengthOf(ctx.matches, 1); } else { @@ -61,7 +61,7 @@ describe('Built-in global middleware', () => { } function notMatchesPatternTestCase( - pattern: string | RegExp, + pattern: string | RegExp | (string | RegExp)[], event: SlackEventMiddlewareArgs<'message' | 'app_mention'>, ): Mocha.AsyncFunc { return async () => { @@ -149,6 +149,42 @@ describe('Built-in global middleware', () => { ); }); + describe('using an array pattern', () => { + const pattern = ['foo', /bar/]; + const matchingText = 'foobaz'; + const nonMatchingText = 'apple'; + it( + 'should match message events with a pattern that matches', + matchesPatternTestCase(pattern, createDummyMessageEventMiddlewareArgs({ text: matchingText })), + ); + it( + 'should match app_mention events with a pattern that matches', + matchesPatternTestCase(pattern, createDummyAppMentionEventMiddlewareArgs({ text: matchingText })), + ); + it( + 'should filter out message events with a pattern that does not match', + notMatchesPatternTestCase(pattern, createDummyMessageEventMiddlewareArgs({ text: nonMatchingText })), + ); + it( + 'should filter out app_mention events with a pattern that does not match', + notMatchesPatternTestCase(pattern, createDummyAppMentionEventMiddlewareArgs({ text: nonMatchingText })), + ); + it( + 'should filter out message events which do not have text (block kit)', + notMatchesPatternTestCase( + pattern, + createDummyMessageEventMiddlewareArgs({ + text: '', + blocks: [ + { + type: 'divider', + }, + ], + }), + ), + ); + }); + describe('directMention()', () => { it('should bail when the context does not provide a bot user ID', async () => { const ctx = { ...dummyContext }; From 52d814dc55e1b0011aa1c78e0bf3891888944328 Mon Sep 17 00:00:00 2001 From: yilmazhasan Date: Mon, 22 Jun 2026 15:59:25 +0300 Subject: [PATCH 2/3] Improve PR changes --- src/App.ts | 19 +++++++++++++------ src/middleware/builtin.ts | 34 ++++++++++++---------------------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/App.ts b/src/App.ts index 92a9dea72..71fc5cd52 100644 --- a/src/App.ts +++ b/src/App.ts @@ -649,25 +649,27 @@ export default class App ): void; /** * - * @param pattern Used for filtering out messages that don't match. + * @param pattern Used for filtering out messages that don't match. Accepts a single string or RegExp, or an + * array of strings/RegExps in which case the message matches when it matches any one of the provided patterns. * Strings match via {@link String.prototype.includes}. * @param listeners Middlewares that process and react to the message events that matched the provided patterns. */ public message( - pattern: string | RegExp, + pattern: string | RegExp | (string | RegExp)[], ...listeners: MessageEventMiddleware[] ): void; /** * * @param filter Middleware that can filter out messages. Generally this is done by returning before * calling {@link AllMiddlewareArgs.next} if there is no match. See {@link directMention} for an example. - * @param pattern Used for filtering out messages that don't match the pattern. Strings match - * via {@link String.prototype.includes}. + * @param pattern Used for filtering out messages that don't match the pattern. Accepts a single string or + * RegExp, or an array of strings/RegExps in which case the message matches when it matches any one of them. + * Strings match via {@link String.prototype.includes}. * @param listeners Middlewares that process and react to the message events that matched the provided pattern. */ public message( filter: MessageEventMiddleware, - pattern: string | RegExp, + pattern: string | RegExp | (string | RegExp)[], ...listeners: MessageEventMiddleware[] ): void; /** @@ -687,7 +689,12 @@ export default class App * @param patternsOrMiddleware A mix of patterns and/or middlewares. */ public message( - ...patternsOrMiddleware: (string | RegExp | MessageEventMiddleware)[] + ...patternsOrMiddleware: ( + | string + | RegExp + | (string | RegExp)[] + | MessageEventMiddleware + )[] ): void; // TODO: expose a type parameter for overriding the MessageEvent type (just like shortcut() and action() does) https://github.com/slackapi/bolt-js/issues/796 public message( diff --git a/src/middleware/builtin.ts b/src/middleware/builtin.ts index 065ab1dd1..715215d5d 100644 --- a/src/middleware/builtin.ts +++ b/src/middleware/builtin.ts @@ -1,4 +1,3 @@ -import util from 'node:util'; import type { ActionConstraints, OptionsConstraints, ShortcutConstraints, ViewConstraints } from '../App'; import { ContextMissingPropertyError } from '../errors'; import type { @@ -231,41 +230,32 @@ export function matchConstraints( export function matchMessage( pattern: string | RegExp | (string | RegExp)[], ): Middleware> { + // Normalize a single pattern into an array of patterns once, at registration time + const patterns = Array.isArray(pattern) ? pattern : [pattern]; return async ({ event, context, next }) => { - let tempMatches: RegExpMatchArray | null = null; - let patternArray: (string | RegExp)[] = []; - if (!('text' in event) || event.text === undefined) { return; } - // Normalize a single pattern into an array of patterns - if (typeof pattern === 'string' || util.types.isRegExp(pattern)) { - patternArray = [pattern]; - } else { - patternArray = pattern; - } - - // Filter out messages or app mentions that don't contain any of the patterns + // Filter out messages or app mentions that don't match any of the patterns. The first matching + // pattern wins; for a matching RegExp its capture groups are exposed via context.matches. let matched = false; - for (let i = 0; i < patternArray.length; i += 1) { - const patternArrayItem = patternArray[i]; - if (typeof patternArrayItem === 'string') { - if (event.text.includes(patternArrayItem)) { - matched = true; - break; - } + for (const candidate of patterns) { + if (typeof candidate === 'string') { + matched = event.text.includes(candidate); } else { - tempMatches = event.text.match(patternArrayItem); + const tempMatches = event.text.match(candidate); if (tempMatches !== null) { context.matches = tempMatches; matched = true; - break; } } + if (matched) { + break; + } } - if (matched === false) { + if (!matched) { return; } From af9ec16be58c03b8b55e28089015ed811eceae9b Mon Sep 17 00:00:00 2001 From: yilmazhasan Date: Mon, 22 Jun 2026 15:59:35 +0300 Subject: [PATCH 3/3] Add type tests --- test/types/message.test-d.ts | 46 +++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/test/types/message.test-d.ts b/test/types/message.test-d.ts index 4d5a3234e..0d3558870 100644 --- a/test/types/message.test-d.ts +++ b/test/types/message.test-d.ts @@ -96,21 +96,55 @@ app.message(async ({ message }) => { await Promise.resolve(message); }); -// Patterns: a single string pattern should be accepted +// Pattern arguments to message(). These validate the supported pattern shapes type-check and that the +// message argument is still correctly inferred. Array support: https://github.com/slackapi/bolt-js/issues/281 + +// A single string pattern app.message('hello', async ({ message }) => { - await Promise.resolve(message); + expectType(message); }); -// Patterns: a single RegExp pattern should be accepted +// A single RegExp pattern app.message(/hello/, async ({ message }) => { - await Promise.resolve(message); + expectType(message); +}); + +// An array of string patterns +app.message(['hello', 'hi', 'bonjour'], async ({ message }) => { + expectType(message); +}); + +// An array of RegExp patterns +app.message([/hello/, /hi/], async ({ message }) => { + expectType(message); }); -// Patterns: an array of string | RegExp patterns should be accepted (issue #281) +// A mixed array of string and RegExp patterns app.message(['hello', 'hi', 'bonjour', /\w{2,8}\s*\w{0,7}!?\.?/], async ({ message }) => { - await Promise.resolve(message); + expectType(message); }); +// A filter middleware followed by an array pattern +app.message( + async ({ next }) => { + await next(); + }, + ['hello', 'hi'], + async ({ message }) => { + expectType(message); + }, +); + +// A mix of single and array patterns provided as variadic arguments +app.message('hello', /world/, ['hi', 'bonjour'], async ({ message }) => { + expectType(message); +}); + +// Patterns must be strings or RegExps; other element types are rejected +expectError(app.message(123, async () => {})); +expectError(app.message([123], async () => {})); +expectError(app.message([true, 'hello'], async () => {})); + interface MyContext { doesnt: 'matter'; }