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

Adds view_closed support #276

Merged
merged 14 commits into from
Oct 7, 2019
36 changes: 27 additions & 9 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
onlyEvents,
matchEventType,
matchMessage,
onlyViewSubmits,
matchCallbackId,
onlyViewActions,
} from './middleware/builtin';
import { processMiddleware } from './middleware/process';
import { ConversationStore, conversationContext, MemoryStore } from './conversation-store';
Expand Down Expand Up @@ -89,6 +88,11 @@ export interface ActionConstraints {
callback_id?: string | RegExp;
}

export interface ViewConstraints {
callback_id?: string | RegExp;
type?: 'view_closed' | 'view_submission';
}

export interface ErrorHandler {
(error: CodedError): void;
}
Expand Down Expand Up @@ -304,9 +308,23 @@ export default class App {
);
}

public view(callbackId: string | RegExp, ...listeners: Middleware<SlackViewMiddlewareArgs>[]): void {
public view(
callbackId: string | RegExp,
...listeners: Middleware<SlackViewMiddlewareArgs>[]
): void;
public view(
constraints: ViewConstraints,
...listeners: Middleware<SlackViewMiddlewareArgs>[]
): void;
public view(
callbackIdOrConstraints: string | RegExp | ViewConstraints,
...listeners: Middleware<SlackViewMiddlewareArgs>[]): void {
const constraints: ViewConstraints =
(typeof callbackIdOrConstraints === 'string' || util.types.isRegExp(callbackIdOrConstraints)) ?
{ callback_id: callbackIdOrConstraints, type: 'view_submission' } : callbackIdOrConstraints;

this.listeners.push(
[onlyViewSubmits, matchCallbackId(callbackId), ...listeners] as Middleware<AnyMiddlewareArgs>[],
[onlyViewActions, matchConstraints(constraints), ...listeners] as Middleware<AnyMiddlewareArgs>[],
);
}

Expand Down Expand Up @@ -370,7 +388,7 @@ export default class App {
payload:
(type === IncomingEventType.Event) ?
(bodyArg as SlackEventMiddlewareArgs['body']).event :
(type === IncomingEventType.ViewSubmitAction) ?
(type === IncomingEventType.ViewAction) ?
(bodyArg as SlackViewMiddlewareArgs['body']).view :
(type === IncomingEventType.Action &&
isBlockActionOrInteractiveMessageBody(bodyArg as SlackActionMiddlewareArgs['body'])) ?
Expand Down Expand Up @@ -398,7 +416,7 @@ export default class App {
} else if (type === IncomingEventType.Options) {
const optionListenerArgs = listenerArgs as SlackOptionsMiddlewareArgs<OptionsSource>;
optionListenerArgs.options = optionListenerArgs.payload;
} else if (type === IncomingEventType.ViewSubmitAction) {
} else if (type === IncomingEventType.ViewAction) {
const viewListenerArgs = listenerArgs as SlackViewMiddlewareArgs;
viewListenerArgs.view = viewListenerArgs.payload;
}
Expand Down Expand Up @@ -477,11 +495,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.ViewSubmitAction) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).team.id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewAction) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['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.ViewSubmitAction) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).team.enterprise_id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewAction) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).team.enterprise_id as string :
undefined),
userId:
((type === IncomingEventType.Event) ?
Expand All @@ -490,7 +508,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.ViewSubmitAction) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).user.id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewAction) ? (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
30 changes: 30 additions & 0 deletions src/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ describe('getTypeAndConversation()', () => {
});
});

describe('view types', () => {
// Arrange
const dummyViewBodies = createFakeViews();

dummyViewBodies.forEach((viewBody) => {
it(`should find Action type for ${viewBody.type}`, () => {
// Act
const typeAndConversation = getTypeAndConversation(viewBody);

// Assert
assert(typeAndConversation.type === IncomingEventType.ViewAction);
});
});
});

describe('invalid events', () => {
// Arrange
const fakeEventBody = {
Expand Down Expand Up @@ -150,3 +165,18 @@ function createFakeOptions(conversationId: string): any[] {
},
];
}

function createFakeViews(): any[] {
return [
// Body for a view_submission event
{
type: 'view_submission',
view: { id: 'V123' },
},
// Body for a view_closed event
{
type: 'view_closed',
view: { id: 'V456' },
},
];
}
6 changes: 3 additions & 3 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export enum IncomingEventType {
Action,
Command,
Options,
ViewSubmitAction,
ViewAction,
}

/**
Expand Down Expand Up @@ -54,9 +54,9 @@ export function getTypeAndConversation(body: any): { type?: IncomingEventType, c
conversationId: actionBody.channel !== undefined ? actionBody.channel.id : undefined,
};
}
if (body.type === 'view_submission') {
if (body.type === 'view_submission' || body.type === 'view_closed') {
return {
type: IncomingEventType.ViewSubmitAction,
type: IncomingEventType.ViewAction,
};
}
return {};
Expand Down
134 changes: 66 additions & 68 deletions src/middleware/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import {
BlockElementAction,
ContextMissingPropertyError,
SlackViewMiddlewareArgs,
ViewOutput,
ViewClosedAction,
aoberoi marked this conversation as resolved.
Show resolved Hide resolved
} from '../types';
import { ActionConstraints } from '../App';
import { ActionConstraints, ViewConstraints } from '../App';
import { ErrorCode, errorWithCode } from '../errors';

/**
Expand Down Expand Up @@ -74,107 +74,92 @@ export const onlyEvents: Middleware<AnyMiddlewareArgs & { event?: SlackEvent }>
};

/**
* Middleware that filters out any event that isn't a view_submission
* Middleware that filters out any event that isn't a view_submission or view_closed event
*/
export const onlyViewSubmits: Middleware<AnyMiddlewareArgs & { view?: ViewSubmitAction }> = ({ view, next }) => {
// Filter out anything that isn't a view_submission
if (view === undefined) {
return;
}

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

// TODO: is there a way to consolidate the next two methods without too much type refactoring?
export function matchCallbackId(
callbackId: string | RegExp,
): Middleware<SlackViewMiddlewareArgs> {
return ({ payload, next, context }) => {
let tempMatches: RegExpMatchArray | null;

if (!isCallbackIdentifiedBody(payload)) {
export const onlyViewActions: Middleware<AnyMiddlewareArgs &
{ view?: (ViewSubmitAction | ViewClosedAction) }> = ({ view, next }) => {
// Filter out anything that doesn't have a view
if (view === undefined) {
return;
}
if (typeof callbackId === 'string') {
if (payload.callback_id !== callbackId) {
return;
}
} else {
tempMatches = payload.callback_id.match(callbackId);

if (tempMatches !== null) {
context['callbackIdMatches'] = tempMatches;
} else {
return;
}
}

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

/**
* Middleware that checks for matches given constraints
*/
export function matchConstraints(
aoberoi marked this conversation as resolved.
Show resolved Hide resolved
constraints: ActionConstraints,
): Middleware<SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs> {
constraints: ActionConstraints | ViewConstraints,
): 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
// which matches (and in which order)
let tempMatches: RegExpMatchArray | null;

if (constraints.block_id !== undefined) {
// Narrow type for ActionConstraints
if ('block_id' in constraints || 'action_id' in constraints) {
if (!isBlockPayload(payload)) {
return;
}

if (typeof constraints.block_id === 'string') {
if (payload.block_id !== constraints.block_id) {
return;
// Check block_id
if (constraints.block_id !== undefined) {

if (typeof constraints.block_id === 'string') {
if (payload.block_id !== constraints.block_id) {
return;
}
} else {
tempMatches = payload.block_id.match(constraints.block_id);

if (tempMatches !== null) {
context['blockIdMatches'] = tempMatches;
} else {
return;
}
}
} else {
tempMatches = payload.block_id.match(constraints.block_id);
}

if (tempMatches !== null) {
context['blockIdMatches'] = tempMatches;
// Check action_id
if (constraints.action_id !== undefined) {
if (typeof constraints.action_id === 'string') {
if (payload.action_id !== constraints.action_id) {
return;
}
} else {
return;
tempMatches = payload.action_id.match(constraints.action_id);

if (tempMatches !== null) {
context['actionIdMatches'] = tempMatches;
} else {
return;
}
}
}
}

if (constraints.action_id !== undefined) {
if (!isBlockPayload(payload)) {
return;
}
// Check callback_id
if ('callback_id' in constraints && constraints.callback_id !== undefined) {
let callbackId: string = '';

if (typeof constraints.action_id === 'string') {
if (payload.action_id !== constraints.action_id) {
return;
}
if (isViewBody(body)) {
callbackId = body['view']['callback_id'];
} else {
tempMatches = payload.action_id.match(constraints.action_id);

if (tempMatches !== null) {
context['actionIdMatches'] = tempMatches;
if (isCallbackIdentifiedBody(body)) {
callbackId = body['callback_id'];
} else {
return;
}
}
}

if (constraints.callback_id !== undefined) {
if (!isCallbackIdentifiedBody(body)) {
return;
}
if (typeof constraints.callback_id === 'string') {
if (body.callback_id !== constraints.callback_id) {
if (callbackId !== constraints.callback_id) {
return;
}
} else {
tempMatches = body.callback_id.match(constraints.callback_id);
tempMatches = callbackId.match(constraints.callback_id);

if (tempMatches !== null) {
context['callbackIdMatches'] = tempMatches;
Expand All @@ -184,6 +169,11 @@ export function matchConstraints(
}
}

// Check type
if ('type' in constraints) {
if (body.type !== constraints.type) return;
}

next();
};
}
Expand Down Expand Up @@ -328,24 +318,32 @@ export function directMention(): Middleware<SlackEventMiddlewareArgs<'message'>>
}

function isBlockPayload(
payload: SlackActionMiddlewareArgs['payload'] | SlackOptionsMiddlewareArgs['payload'],
payload:
| SlackActionMiddlewareArgs['payload']
| SlackOptionsMiddlewareArgs['payload']
| SlackViewMiddlewareArgs['payload'],
): payload is BlockElementAction | OptionsRequest<'block_suggestion'> {
return (payload as BlockElementAction | OptionsRequest<'block_suggestion'>).action_id !== undefined;
}

type CallbackIdentifiedBody =
| InteractiveMessage
| DialogSubmitAction
| ViewOutput
| MessageAction
| OptionsRequest<'interactive_message' | 'dialog_suggestion'>;

function isCallbackIdentifiedBody(
body: SlackActionMiddlewareArgs['body'] | SlackOptionsMiddlewareArgs['body'] | SlackViewMiddlewareArgs['payload'],
body: SlackActionMiddlewareArgs['body'] | SlackOptionsMiddlewareArgs['body'],
): body is CallbackIdentifiedBody {
return (body as CallbackIdentifiedBody).callback_id !== undefined;
}

function isViewBody(
body: SlackActionMiddlewareArgs['body'] | SlackOptionsMiddlewareArgs['body'] | SlackViewMiddlewareArgs['body'],
): body is (ViewSubmitAction | ViewClosedAction) {
return (body as ViewSubmitAction).view !== undefined;
}

function isEventArgs(
args: AnyMiddlewareArgs,
): args is SlackEventMiddlewareArgs {
Expand Down
Loading