Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,25 +620,27 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
): 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<MiddlewareCustomContext extends StringIndexed = StringIndexed>(
pattern: string | RegExp,
pattern: string | RegExp | (string | RegExp)[],
...listeners: MessageEventMiddleware<AppCustomContext & MiddlewareCustomContext>[]
): 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<MiddlewareCustomContext extends StringIndexed = StringIndexed>(
filter: MessageEventMiddleware<AppCustomContext & MiddlewareCustomContext>,
pattern: string | RegExp,
pattern: string | RegExp | (string | RegExp)[],
...listeners: MessageEventMiddleware<AppCustomContext & MiddlewareCustomContext>[]
): void;
/**
Expand All @@ -658,14 +660,28 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
* @param patternsOrMiddleware A mix of patterns and/or middlewares.
*/
public message<MiddlewareCustomContext extends StringIndexed = StringIndexed>(
...patternsOrMiddleware: (string | RegExp | MessageEventMiddleware<AppCustomContext & MiddlewareCustomContext>)[]
...patternsOrMiddleware: (
| string
| RegExp
| (string | RegExp)[]
| MessageEventMiddleware<AppCustomContext & MiddlewareCustomContext>
)[]
): 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<MiddlewareCustomContext extends StringIndexed = StringIndexed>(
...patternsOrMiddleware: (string | RegExp | MessageEventMiddleware<AppCustomContext & MiddlewareCustomContext>)[]
...patternsOrMiddleware: (
| string
| RegExp
| (string | RegExp)[]
| MessageEventMiddleware<AppCustomContext & MiddlewareCustomContext>
)[]
): 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;
Expand Down
35 changes: 21 additions & 14 deletions src/middleware/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SlackEventMiddlewareArgs<'message' | 'app_mention'>> {
// 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();
Expand Down
49 changes: 49 additions & 0 deletions test/types/message.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MessageEvent>(message);
});

// A single RegExp pattern
app.message(/hello/, async ({ message }) => {
expectType<MessageEvent>(message);
});

// An array of string patterns
app.message(['hello', 'hi', 'bonjour'], async ({ message }) => {
expectType<MessageEvent>(message);
});

// An array of RegExp patterns
app.message([/hello/, /hi/], async ({ message }) => {
expectType<MessageEvent>(message);
});

// A mixed array of string and RegExp patterns
app.message(['hello', 'hi', 'bonjour', /\w{2,8}\s*\w{0,7}!?\.?/], async ({ message }) => {
expectType<MessageEvent>(message);
});

// A filter middleware followed by an array pattern
app.message(
async ({ next }) => {
await next();
},
['hello', 'hi'],
async ({ message }) => {
expectType<MessageEvent>(message);
},
);

// A mix of single and array patterns provided as variadic arguments
app.message('hello', /world/, ['hi', 'bonjour'], async ({ message }) => {
expectType<MessageEvent>(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';
}
Expand Down
42 changes: 39 additions & 3 deletions test/unit/middleware/builtin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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 };
Expand Down