diff --git a/src/App.ts b/src/App.ts index fd5dc488f..209f5b881 100644 --- a/src/App.ts +++ b/src/App.ts @@ -620,25 +620,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; /** @@ -658,14 +660,28 @@ 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( - ...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..715215d5d 100644 --- a/src/middleware/builtin.ts +++ b/src/middleware/builtin.ts @@ -228,28 +228,35 @@ 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> { + // 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; - 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; - } - } else { - tempMatches = event.text.match(pattern); - - if (tempMatches !== null) { - context.matches = tempMatches; + // 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 (const candidate of patterns) { + if (typeof candidate === 'string') { + matched = event.text.includes(candidate); } else { - return; + const tempMatches = event.text.match(candidate); + if (tempMatches !== null) { + context.matches = tempMatches; + matched = true; + } } + if (matched) { + break; + } + } + + if (!matched) { + return; } await next(); diff --git a/test/types/message.test-d.ts b/test/types/message.test-d.ts index 94c0177df..0d3558870 100644 --- a/test/types/message.test-d.ts +++ b/test/types/message.test-d.ts @@ -96,6 +96,55 @@ app.message(async ({ message }) => { await Promise.resolve(message); }); +// 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 }) => { + expectType(message); +}); + +// A single RegExp pattern +app.message(/hello/, async ({ 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); +}); + +// A mixed array of string and RegExp patterns +app.message(['hello', 'hi', 'bonjour', /\w{2,8}\s*\w{0,7}!?\.?/], async ({ 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'; } 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 };