Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Shortcut Method #427

Merged
merged 5 commits into from
Mar 18, 2020
Merged
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
55 changes: 45 additions & 10 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
onlyCommands,
matchCommandName,
onlyOptions,
onlyShortcuts,
onlyEvents,
matchEventType,
matchMessage,
Expand All @@ -23,8 +24,10 @@ import {
SlackCommandMiddlewareArgs,
SlackEventMiddlewareArgs,
SlackOptionsMiddlewareArgs,
SlackShortcutMiddlewareArgs,
SlackViewMiddlewareArgs,
SlackAction,
SlackShortcut,
Context,
SayFn,
AckFn,
Expand Down Expand Up @@ -93,6 +96,11 @@ export interface ActionConstraints<A extends SlackAction = SlackAction> {
callback_id?: Extract<A, { callback_id?: string }> extends any ? (string | RegExp) : never;
}

export interface ShortcutConstraints<S extends SlackShortcut = SlackShortcut> {
type?: S['type'];
callback_id?: string | RegExp;
}

export interface ViewConstraints {
callback_id?: string | RegExp;
type?: 'view_closed' | 'view_submission';
Expand Down Expand Up @@ -296,6 +304,27 @@ export default class App {
);
}

public shortcut<Shortcut extends SlackShortcut = SlackShortcut>(
callbackId: string | RegExp,
...listeners: Middleware<SlackShortcutMiddlewareArgs<Shortcut>>[]
): void;
public shortcut<Shortcut extends SlackShortcut = SlackShortcut>(
constraints: ShortcutConstraints,
...listeners: Middleware<SlackShortcutMiddlewareArgs<Shortcut>>[]
): void;
public shortcut<Shortcut extends SlackShortcut = SlackShortcut>(
callbackIdOrConstraints: string | RegExp | ShortcutConstraints,
...listeners: Middleware<SlackShortcutMiddlewareArgs<Shortcut>>[]
): void {
const constraints: ShortcutConstraints =
(typeof callbackIdOrConstraints === 'string' || util.types.isRegExp(callbackIdOrConstraints)) ?
{ callback_id: callbackIdOrConstraints } : callbackIdOrConstraints;

this.listeners.push(
[onlyShortcuts, matchConstraints(constraints), ...listeners] as Middleware<AnyMiddlewareArgs>[],
);
}

// NOTE: this is what's called a convenience generic, so that types flow more easily without casting.
// https://basarat.gitbooks.io/typescript/docs/types/generics.html#design-pattern-convenience-generic
public action<Action extends SlackAction = SlackAction>(
Expand Down Expand Up @@ -405,6 +434,7 @@ export default class App {
* Handles events from the receiver
*/
private async onIncomingEvent({ body, ack, respond }: ReceiverEvent): Promise<void> {

// TODO: when generating errors (such as in the say utility) it may become useful to capture the current context,
// or even all of the args, as properties of the error. This would give error handling code some ability to deal
// with "finally" type error situations.
Expand Down Expand Up @@ -474,13 +504,15 @@ export default class App {
(bodyArg as SlackEventMiddlewareArgs['body']).event :
(type === IncomingEventType.ViewAction) ?
(bodyArg as SlackViewMiddlewareArgs['body']).view :
(type === IncomingEventType.Action &&
isBlockActionOrInteractiveMessageBody(bodyArg as SlackActionMiddlewareArgs['body'])) ?
(bodyArg as SlackActionMiddlewareArgs<BlockAction | InteractiveMessage>['body']).actions[0] :
(bodyArg as (
Exclude<AnyMiddlewareArgs, SlackEventMiddlewareArgs | SlackActionMiddlewareArgs |
SlackViewMiddlewareArgs> | SlackActionMiddlewareArgs<Exclude<SlackAction, BlockAction |
InteractiveMessage>>
(type === IncomingEventType.Shortcut) ?
(bodyArg as SlackShortcutMiddlewareArgs['body']) :
(type === IncomingEventType.Action &&
isBlockActionOrInteractiveMessageBody(bodyArg as SlackActionMiddlewareArgs['body'])) ?
(bodyArg as SlackActionMiddlewareArgs<BlockAction | InteractiveMessage>['body']).actions[0] :
(bodyArg as (
Exclude<AnyMiddlewareArgs, SlackEventMiddlewareArgs | SlackActionMiddlewareArgs |
SlackViewMiddlewareArgs> | SlackActionMiddlewareArgs<Exclude<SlackAction, BlockAction |
InteractiveMessage>>
)['body']),
};

Expand All @@ -504,6 +536,9 @@ export default class App {
} else if (type === IncomingEventType.ViewAction) {
const viewListenerArgs = listenerArgs as SlackViewMiddlewareArgs;
viewListenerArgs.view = viewListenerArgs.payload;
} else if (type === IncomingEventType.Shortcut) {
const shortcutListenerArgs = listenerArgs as SlackShortcutMiddlewareArgs;
shortcutListenerArgs.shortcut = shortcutListenerArgs.payload;
}

// Set say() utility
Expand Down Expand Up @@ -586,11 +621,11 @@ function buildSource(
const source: AuthorizeSourceData = {
teamId:
((type === IncomingEventType.Event || type === IncomingEventType.Command) ? (body as (SlackEventMiddlewareArgs | SlackCommandMiddlewareArgs)['body']).team_id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewAction) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).team.id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewAction || type === IncomingEventType.Shortcut) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs | SlackShortcutMiddlewareArgs)['body']).team.id as string :
assertNever(type)),
enterpriseId:
((type === IncomingEventType.Event || type === IncomingEventType.Command) ? (body as (SlackEventMiddlewareArgs | SlackCommandMiddlewareArgs)['body']).enterprise_id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewAction) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).team.enterprise_id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewAction || type === IncomingEventType.Shortcut) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs | SlackShortcutMiddlewareArgs)['body']).team.enterprise_id as string :
undefined),
userId:
((type === IncomingEventType.Event) ?
Expand All @@ -599,7 +634,7 @@ function buildSource(
((body as SlackEventMiddlewareArgs['body']).event.channel !== undefined && (body as SlackEventMiddlewareArgs['body']).event.channel.creator !== undefined) ? (body as SlackEventMiddlewareArgs['body']).event.channel.creator as string :
((body as SlackEventMiddlewareArgs['body']).event.subteam !== undefined && (body as SlackEventMiddlewareArgs['body']).event.subteam.created_by !== undefined) ? (body as SlackEventMiddlewareArgs['body']).event.subteam.created_by as string :
undefined) :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewAction) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).user.id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewAction || type === IncomingEventType.Shortcut) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).user.id as string :
(type === IncomingEventType.Command) ? (body as SlackCommandMiddlewareArgs['body']).user_id as string :
undefined),
conversationId: channelId,
Expand Down
6 changes: 6 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum IncomingEventType {
Command,
Options,
ViewAction,
Shortcut,
}

/**
Expand Down Expand Up @@ -54,6 +55,11 @@ export function getTypeAndConversation(body: any): { type?: IncomingEventType, c
conversationId: actionBody.channel !== undefined ? actionBody.channel.id : undefined,
};
}
if (body.type === 'shortcut') {
return {
type: IncomingEventType.Shortcut,
};
}
if (body.type === 'view_submission' || body.type === 'view_closed') {
return {
type: IncomingEventType.ViewAction,
Expand Down
23 changes: 20 additions & 3 deletions src/middleware/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,24 @@ import {
SlackCommandMiddlewareArgs,
SlackEventMiddlewareArgs,
SlackOptionsMiddlewareArgs,
SlackShortcutMiddlewareArgs,
SlackViewMiddlewareArgs,
SlackEvent,
SlackAction,
SlackShortcut,
SlashCommand,
ViewSubmitAction,
ViewClosedAction,
OptionsRequest,
InteractiveMessage,
DialogSubmitAction,
GlobalShortcut,
MessageAction,
BlockElementAction,
ContextMissingPropertyError,
SlackViewAction,
} from '../types';
import { ActionConstraints, ViewConstraints } from '../App';
import { ActionConstraints, ViewConstraints, ShortcutConstraints } from '../App';
import { ErrorCode, errorWithCode } from '../errors';

/**
Expand All @@ -35,6 +38,19 @@ export const onlyActions: Middleware<AnyMiddlewareArgs & { action?: SlackAction
next();
};

/**
* Middleware that filters out any event that isn't a shortcut
*/
export const onlyShortcuts: Middleware<AnyMiddlewareArgs & { shortcut?: SlackShortcut }> = ({ shortcut, next }) => {
// Filter out any non-shortcuts
if (shortcut === undefined) {
return;
}

// It matches so we should continue down this middleware listener chain
next();
};

/**
* Middleware that filters out any event that isn't a command
*/
Expand Down Expand Up @@ -92,7 +108,7 @@ export const onlyViewActions: Middleware<AnyMiddlewareArgs &
* Middleware that checks for matches given constraints
*/
export function matchConstraints(
constraints: ActionConstraints | ViewConstraints,
constraints: ActionConstraints | ViewConstraints | ShortcutConstraints,
): Middleware<SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs> {
return ({ payload, body, next, context }) => {
// TODO: is putting matches in an array actually helpful? there's no way to know which of the regexps contributed
Expand Down Expand Up @@ -338,10 +354,11 @@ type CallbackIdentifiedBody =
| InteractiveMessage
| DialogSubmitAction
| MessageAction
| GlobalShortcut
| OptionsRequest<'interactive_message' | 'dialog_suggestion'>;

function isCallbackIdentifiedBody(
body: SlackActionMiddlewareArgs['body'] | SlackOptionsMiddlewareArgs['body'],
body: SlackActionMiddlewareArgs['body'] | SlackOptionsMiddlewareArgs['body'] | SlackShortcutMiddlewareArgs['body'],
): body is CallbackIdentifiedBody {
return (body as CallbackIdentifiedBody).callback_id !== undefined;
}
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './events';
export * from './options';
export * from './view';
export * from './receiver';
export * from './shortcuts';
3 changes: 2 additions & 1 deletion src/types/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { SlackEventMiddlewareArgs } from './events';
import { SlackActionMiddlewareArgs } from './actions';
import { SlackCommandMiddlewareArgs } from './command';
import { SlackOptionsMiddlewareArgs } from './options';
import { SlackShortcutMiddlewareArgs } from './shortcuts';
import { SlackViewMiddlewareArgs } from './view';
import { CodedError, ErrorCode } from '../errors';
import { WebClient } from '@slack/web-api';
import { Logger } from '@slack/logger';

export type AnyMiddlewareArgs =
SlackEventMiddlewareArgs | SlackActionMiddlewareArgs | SlackCommandMiddlewareArgs |
SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs;
SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs | SlackShortcutMiddlewareArgs;

export interface PostProcessFn {
(error: Error | undefined, done: (error?: Error) => void): unknown;
Expand Down
24 changes: 24 additions & 0 deletions src/types/shortcuts/global-shortcut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* A Slack global shortcut wrapped in the standard metadata.
*
* This describes the entire JSON-encoded body of a request from Slack global shortcuts.
*/
export interface GlobalShortcut {
type: 'shortcut';
callback_id: string;
trigger_id: string;
user: {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated based on feedback from @shaydewael

id: string;
username: string;
team_id: string;
name: string;
};
team: {
id: string;
domain: string;
enterprise_id?: string;
enterprise_name?: string;
};
token: string;
action_ts: string;
}
22 changes: 22 additions & 0 deletions src/types/shortcuts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export * from '../shortcuts/global-shortcut';

import { GlobalShortcut } from '../shortcuts/global-shortcut';
import { SayFn, RespondFn, AckFn } from '../utilities';

/**
* All known shortcuts from Slack.
*
*/
export type SlackShortcut = GlobalShortcut;

/**
* Arguments which listeners and middleware receive to process an shorcut from Slack.
*/
export interface SlackShortcutMiddlewareArgs<Shortcut extends SlackShortcut = SlackShortcut> {
payload: Shortcut;
shortcut: this['payload'];
body: this['payload'];
say: SayFn;
respond: RespondFn;
ack: AckFn<void>;
}