diff --git a/src/App.spec.ts b/src/App.spec.ts index 245bd04c0..d94735e9e 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -1099,6 +1099,11 @@ describe('App', () => { app.event('app_home_opened', async ({}) => { /* noop */ }); + + app.event(/app_home_opened|app_mention/, async ({}) => { + /* noop */ + }); + app.message('hello', async ({}) => { /* noop */ }); diff --git a/src/App.ts b/src/App.ts index a3974a9ef..55c6a916d 100644 --- a/src/App.ts +++ b/src/App.ts @@ -33,6 +33,7 @@ import { SlackShortcutMiddlewareArgs, SlackViewMiddlewareArgs, SlackAction, + EventTypePattern, SlackShortcut, Context, SayFn, @@ -410,8 +411,20 @@ export default class App { public event( eventName: EventType, ...listeners: Middleware>[] + ): void; + public event( + eventName: EventType, + ...listeners: Middleware>[] + ): void; + public event( + eventNameOrPattern: EventType, + ...listeners: Middleware>[] ): void { - this.listeners.push([onlyEvents, matchEventType(eventName), ...listeners] as Middleware[]); + this.listeners.push([ + onlyEvents, + matchEventType(eventNameOrPattern), + ...listeners, + ] as Middleware[]); } // TODO: just make a type alias for Middleware> diff --git a/src/middleware/builtin.spec.ts b/src/middleware/builtin.spec.ts index 5ca725c68..1ef2fd0e1 100644 --- a/src/middleware/builtin.spec.ts +++ b/src/middleware/builtin.spec.ts @@ -15,7 +15,7 @@ import { } from '../types'; import { onlyCommands, onlyEvents, matchCommandName, matchEventType, subtype } from './builtin'; import { SlashCommand } from '../types/command'; -import { AppMentionEvent } from '../types/events'; +import { AppMentionEvent, AppHomeOpenedEvent } from '../types/events'; import { GenericMessageEvent } from '../types/events/message-events'; import { WebClient } from '@slack/web-api'; import { Logger } from '@slack/logger'; @@ -532,7 +532,7 @@ describe('onlyEvents', () => { const args: SlackEventMiddlewareArgs<'app_mention'> & { event?: SlackEvent } = { payload: appMentionEvent, event: appMentionEvent, - message: null as never, // a bit hackey to sartisfy TS compiler + message: null as never, // a bit hackey to satisfy TS compiler as 'null' cannot be assigned to type 'never' body: { token: 'token-value', team_id: 'T1234567', @@ -582,7 +582,7 @@ describe('matchEventType', () => { return { payload: appMentionEvent, event: appMentionEvent, - message: null as never, // a bit hackey to sartisfy TS compiler + message: null as never, // a bit hackey to satisfy TS compiler as 'null' cannot be assigned to type 'never' body: { token: 'token-value', team_id: 'T1234567', @@ -597,6 +597,27 @@ describe('matchEventType', () => { }; } + function buildArgsAppHomeOpened(): SlackEventMiddlewareArgs<'app_home_opened'> & { + event?: SlackEvent; + } { + return { + payload: appHomeOpenedEvent, + event: appHomeOpenedEvent, + message: null as never, // a bit hackey to satisfy TS compiler as 'null' cannot be assigned to type 'never' + body: { + token: 'token-value', + team_id: 'T1234567', + api_app_id: 'A1234567', + event: appHomeOpenedEvent, + type: 'event_callback', + event_id: 'event-id-value', + event_time: 123, + authed_users: [], + }, + say: sayNoop, + }; + } + it('should detect valid requests', async () => { const fakeNext = sinon.fake(); await matchEventType('app_mention')({ @@ -609,6 +630,30 @@ describe('matchEventType', () => { assert.isTrue(fakeNext.called); }); + it('should detect valid RegExp requests with app_mention', async () => { + const fakeNext = sinon.fake(); + await matchEventType(/app_mention|app_home_opened/)({ + logger, + client, + next: fakeNext, + context: {}, + ...buildArgs(), + }); + assert.isTrue(fakeNext.called); + }); + + it('should detect valid RegExp requests with app_home_opened', async () => { + const fakeNext = sinon.fake(); + await matchEventType(/app_mention|app_home_opened/)({ + logger, + client, + next: fakeNext, + context: {}, + ...buildArgsAppHomeOpened(), + }); + assert.isTrue(fakeNext.called); + }); + it('should skip other requests', async () => { const fakeNext = sinon.fake(); await matchEventType('app_home_opened')({ @@ -620,6 +665,18 @@ describe('matchEventType', () => { }); assert.isFalse(fakeNext.called); }); + + it('should skip other requests for RegExp', async () => { + const fakeNext = sinon.fake(); + await matchEventType(/foo/)({ + logger, + client, + next: fakeNext, + context: {}, + ...buildArgs(), + }); + assert.isFalse(fakeNext.called); + }); }); describe('subtype', () => { @@ -749,6 +806,21 @@ const appMentionEvent: AppMentionEvent = { thread_ts: '123.123', }; +const appHomeOpenedEvent: AppHomeOpenedEvent = { + type: 'app_home_opened', + user: 'USERNAME', + channel: 'U1234567', + tab: 'home', + view: { + type: 'home', + blocks: [], + clear_on_close: false, + notify_on_close: false, + external_id: '', + }, + event_ts: '123.123', +}; + const botMessageEvent: MessageEvent = { type: 'message', subtype: 'bot_message', diff --git a/src/middleware/builtin.ts b/src/middleware/builtin.ts index 91f921714..6a760db4b 100644 --- a/src/middleware/builtin.ts +++ b/src/middleware/builtin.ts @@ -22,6 +22,7 @@ import { MessageShortcut, BlockElementAction, SlackViewAction, + EventTypePattern, } from '../types'; import { ActionConstraints, ViewConstraints, ShortcutConstraints } from '../App'; import { ContextMissingPropertyError } from '../errors'; @@ -254,16 +255,31 @@ export function matchCommandName(name: string): Middleware { - return async ({ event, next }) => { - // Filter out any events that are not the correct type - if (type !== event.type) { +export function matchEventType(pattern: EventTypePattern): Middleware { + return async ({ event, context, next }) => { + let tempMatches: RegExpMatchArray | null; + if (!('type' in event) || event.type === undefined) { return; } + // Filter out events that don't contain the pattern + if (typeof pattern === 'string') { + if (event.type !== pattern) { + return; + } + } else { + tempMatches = event.type.match(pattern); + + if (tempMatches !== null) { + context['matches'] = tempMatches; + } else { + return; + } + } + // TODO: remove the non-null assertion operator await next!(); }; @@ -287,7 +303,6 @@ export function ignoreSelf(): Middleware { // SlackEventMiddlewareArgs<'message'> without a cast, so the following couple lines do that. if (args.message !== undefined) { const message = args.message as SlackEventMiddlewareArgs<'message'>['message']; - // TODO: revisit this once we have all the message subtypes defined to see if we can do this better with // type narrowing // Look for an event that is identified as a bot message from the same bot ID as this app, and return to skip diff --git a/src/types/events/base-events.ts b/src/types/events/base-events.ts index 31a62acbe..bec46c573 100644 --- a/src/types/events/base-events.ts +++ b/src/types/events/base-events.ts @@ -73,6 +73,8 @@ export type SlackEvent = | WorkflowStepDeletedEvent | WorkflowStepExecuteEvent; +export type EventTypePattern = string | RegExp; + /** * Any event in Slack's Events API *