Skip to content

Commit

Permalink
Fix #894 Unable to build options request objects in TypeScript (#900)
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch authored Apr 29, 2021
1 parent a5ac8fd commit c88aeb7
Show file tree
Hide file tree
Showing 5 changed files with 350 additions and 77 deletions.
14 changes: 9 additions & 5 deletions src/middleware/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
SlashCommand,
ViewSubmitAction,
ViewClosedAction,
OptionsRequest,
SlackOptions,
BlockSuggestion,
InteractiveMessageSuggestion,
DialogSuggestion,
InteractiveMessage,
DialogSubmitAction,
GlobalShortcut,
Expand Down Expand Up @@ -76,7 +79,7 @@ export const onlyCommands: Middleware<AnyMiddlewareArgs & { command?: SlashComma
/**
* Middleware that filters out any event that isn't an options
*/
export const onlyOptions: Middleware<AnyMiddlewareArgs & { options?: OptionsRequest }> = async ({ options, next }) => {
export const onlyOptions: Middleware<AnyMiddlewareArgs & { options?: SlackOptions }> = async ({ options, next }) => {
// Filter out any non-options requests
if (options === undefined) {
return;
Expand Down Expand Up @@ -385,16 +388,17 @@ function isBlockPayload(
| SlackActionMiddlewareArgs['payload']
| SlackOptionsMiddlewareArgs['payload']
| SlackViewMiddlewareArgs['payload'],
): payload is BlockElementAction | OptionsRequest<'block_suggestion'> {
return (payload as BlockElementAction | OptionsRequest<'block_suggestion'>).action_id !== undefined;
): payload is BlockElementAction | BlockSuggestion {
return (payload as BlockElementAction | BlockSuggestion).action_id !== undefined;
}

type CallbackIdentifiedBody =
| InteractiveMessage
| DialogSubmitAction
| MessageShortcut
| GlobalShortcut
| OptionsRequest<'interactive_message' | 'dialog_suggestion'>;
| InteractiveMessageSuggestion
| DialogSuggestion;

function isCallbackIdentifiedBody(
body: SlackActionMiddlewareArgs['body'] | SlackOptionsMiddlewareArgs['body'] | SlackShortcutMiddlewareArgs['body'],
Expand Down
130 changes: 130 additions & 0 deletions src/types/options/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// tslint:disable:no-implicit-dependencies
import { assert } from 'chai';
import { BlockSuggestion, DialogSuggestion, InteractiveMessageSuggestion } from './index';

describe('options types', () => {
it('should be compatible with block_suggestion payloads', () => {
const payload: BlockSuggestion = {
type: 'block_suggestion',
user: {
id: 'W111',
name: 'primary-owner',
team_id: 'T111',
},
container: { type: 'view', view_id: 'V111' },
api_app_id: 'A111',
token: 'verification_token',
block_id: 'block-id-value',
action_id: 'action-id-value',
value: 'search word',
team: {
id: 'T111',
domain: 'workspace-domain',
enterprise_id: 'E111',
enterprise_name: 'Sandbox Org',
},
view: {
id: 'V111',
team_id: 'T111',
type: 'modal',
blocks: [
{
type: 'input',
block_id: '5ar+',
label: { type: 'plain_text', text: 'Label' },
optional: false,
element: { type: 'plain_text_input', action_id: 'i5IpR' },
},
{
type: 'input',
block_id: 'block-id-value',
label: { type: 'plain_text', text: 'Search' },
optional: false,
element: {
type: 'external_select',
action_id: 'action-id-value',
placeholder: { type: 'plain_text', text: 'Select an item' },
},
},
{
type: 'input',
block_id: 'xxx',
label: { type: 'plain_text', text: 'Search (multi)' },
optional: false,
element: {
type: 'multi_external_select',
action_id: 'yyy',
placeholder: { type: 'plain_text', text: 'Select an item' },
},
},
],
private_metadata: '',
callback_id: 'view-id',
state: { values: {} },
hash: '111.xxx',
title: { type: 'plain_text', text: 'My App' },
clear_on_close: false,
notify_on_close: false,
close: { type: 'plain_text', text: 'Cancel' },
submit: { type: 'plain_text', text: 'Submit' },
root_view_id: 'V111',
previous_view_id: null,
app_id: 'A111',
external_id: '',
app_installed_team_id: 'T111',
bot_id: 'B111',
},
};
assert.equal(payload.action_id, 'action-id-value');
assert.equal(payload.value, 'search word');
});

it('should be compatible with interactive_message payloads', () => {
const payload: InteractiveMessageSuggestion = {
name: 'bugs_list',
value: 'bot',
callback_id: 'select_remote_1234',
type: 'interactive_message',
team: {
id: 'T012AB0A1',
domain: 'pocket-calculator',
},
channel: {
id: 'C012AB3CD',
name: 'general',
},
user: {
id: 'U012A1BCJ',
name: 'bugcatcher',
},
action_ts: '1481670445.010908',
message_ts: '1481670439.000007',
attachment_id: '1',
token: 'verification_token_string',
};
assert.equal(payload.callback_id, 'select_remote_1234');
assert.equal(payload.value, 'bot');
});

it('should be compatible with dialog_suggestion payloads', () => {
const payload: DialogSuggestion = {
type: 'dialog_suggestion',
token: 'verification_token',
action_ts: '1596603332.676855',
team: {
id: 'T111',
domain: 'workspace-domain',
enterprise_id: 'E111',
enterprise_name: 'Sandbox Org',
},
user: { id: 'W111', name: 'primary-owner', team_id: 'T111' },
channel: { id: 'C111', name: 'test-channel' },
name: 'types',
value: 'search keyword',
callback_id: 'dialog-callback-id',
state: 'Limo',
};
assert.equal(payload.callback_id, 'dialog-callback-id');
assert.equal(payload.value, 'search keyword');
});
});
179 changes: 152 additions & 27 deletions src/types/options/index.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,51 @@
import { Option } from '@slack/types';
import { StringIndexed, XOR } from '../helpers';
import { AckFn } from '../utilities';
import { ViewOutput } from '../view/index';

/**
* Arguments which listeners and middleware receive to process an options request from Slack
*/
export interface SlackOptionsMiddlewareArgs<Source extends OptionsSource = OptionsSource> {
payload: OptionsRequest<Source>;
payload: OptionsPayloadFromType<Source>;
body: this['payload'];
options: this['payload'];
ack: OptionsAckFn<Source>;
}

/**
* A request for options for a select menu with an external data source, wrapped in the standard metadata. The menu
* can have a source of Slack's Block Kit external select elements, dialogs, or legacy interactive components.
*
* This describes the entire JSON-encoded body of a request.
* All sources from which Slack sends options requests.
*/
export interface OptionsRequest<Source extends OptionsSource = OptionsSource> extends StringIndexed {
export type OptionsSource = 'interactive_message' | 'dialog_suggestion' | 'block_suggestion';

export type SlackOptions = BlockSuggestion | InteractiveMessageSuggestion | DialogSuggestion;

export interface BasicOptionsPayload<Type extends string = string> {
type: Type;
value: string;
type: Source;
}

type OptionsPayloadFromType<T extends string> = KnownOptionsPayloadFromType<T> extends never
? BasicOptionsPayload<T>
: KnownOptionsPayloadFromType<T>;

type KnownOptionsPayloadFromType<T extends string> = Extract<SlackOptions, { type: T }>;

/**
* external data source in blocks
*/
export interface BlockSuggestion extends StringIndexed {
type: 'block_suggestion';
block_id: string;
action_id: string;
value: string;

api_app_id: string;
team: {
id: string;
domain: string;
enterprise_id?: string; // undocumented
enterprise_name?: string; // undocumented
enterprise_id?: string;
enterprise_name?: string;
} | null;
channel?: {
id: string;
Expand All @@ -34,25 +54,48 @@ export interface OptionsRequest<Source extends OptionsSource = OptionsSource> ex
user: {
id: string;
name: string;
team_id?: string; // undocumented
team_id?: string;
};
token: string;

name: Source extends 'interactive_message' | 'dialog_suggestion' ? string : never;
callback_id: Source extends 'interactive_message' | 'dialog_suggestion' ? string : never;
action_ts: Source extends 'interactive_message' | 'dialog_suggestion' ? string : never;

message_ts: Source extends 'interactive_message' ? string : never;
attachment_id: Source extends 'interactive_message' ? string : never;

api_app_id: Source extends 'block_suggestion' ? string : never;
action_id: Source extends 'block_suggestion' ? string : never;
block_id: Source extends 'block_suggestion' ? string : never;
container: Source extends 'block_suggestion' ? StringIndexed : never;
token: string; // legacy verification token
container: StringIndexed;
// exists for blocks in either a modal or a home tab
view?: ViewOutput;
// exists for enterprise installs
is_enterprise_install?: boolean;
enterprise?: {
id: string;
name: string;
};
}

// this appears in the block_suggestions schema, but we're not sure when its present or what its type would be
app_unfurl?: any;
/**
* external data source in attachments
*/
export interface InteractiveMessageSuggestion extends StringIndexed {
type: 'interactive_message';
name: string;
value: string;
callback_id: string;
action_ts: string;
message_ts: string;
attachment_id: string;

team: {
id: string;
domain: string;
enterprise_id?: string;
enterprise_name?: string;
} | null;
channel?: {
id: string;
name: string;
};
user: {
id: string;
name: string;
team_id?: string;
};
token: string; // legacy verification token
// exists for enterprise installs
is_enterprise_install?: boolean;
enterprise?: {
Expand All @@ -62,9 +105,38 @@ export interface OptionsRequest<Source extends OptionsSource = OptionsSource> ex
}

/**
* All sources from which Slack sends options requests.
* external data source in dialogs
*/
export type OptionsSource = 'interactive_message' | 'dialog_suggestion' | 'block_suggestion';
export interface DialogSuggestion extends StringIndexed {
type: 'dialog_suggestion';
name: string;
value: string;
callback_id: string;
action_ts: string;

team: {
id: string;
domain: string;
enterprise_id?: string;
enterprise_name?: string;
} | null;
channel?: {
id: string;
name: string;
};
user: {
id: string;
name: string;
team_id?: string;
};
token: string; // legacy verification token
// exists for enterprise installs
is_enterprise_install?: boolean;
enterprise?: {
id: string;
name: string;
};
}

/**
* Type function which given an options source `Source` returns a corresponding type for the `ack()` function. The
Expand Down Expand Up @@ -96,3 +168,56 @@ export interface OptionGroups<Options> {
label: string;
} & Options)[];
}

// Don't delete the following interface for backward-compatibility
// We may remove it in v4 or newer

/**
* A request for options for a select menu with an external data source, wrapped in the standard metadata. The menu
* can have a source of Slack's Block Kit external select elements, dialogs, or legacy interactive components.
*
* This describes the entire JSON-encoded body of a request.
* @deprecated You can use more specific types such as BlockSuggestionPayload
*/
export interface OptionsRequest<Source extends OptionsSource = OptionsSource> extends StringIndexed {
value: string;
type: Source;
team: {
id: string;
domain: string;
enterprise_id?: string; // undocumented
enterprise_name?: string; // undocumented
} | null;
channel?: {
id: string;
name: string;
};
user: {
id: string;
name: string;
team_id?: string; // undocumented
};
token: string;

name: Source extends 'interactive_message' | 'dialog_suggestion' ? string : never;
callback_id: Source extends 'interactive_message' | 'dialog_suggestion' ? string : never;
action_ts: Source extends 'interactive_message' | 'dialog_suggestion' ? string : never;

message_ts: Source extends 'interactive_message' ? string : never;
attachment_id: Source extends 'interactive_message' ? string : never;

api_app_id: Source extends 'block_suggestion' ? string : never;
action_id: Source extends 'block_suggestion' ? string : never;
block_id: Source extends 'block_suggestion' ? string : never;
container: Source extends 'block_suggestion' ? StringIndexed : never;

// this appears in the block_suggestions schema, but we're not sure when its present or what its type would be
app_unfurl?: any;

// exists for enterprise installs
is_enterprise_install?: boolean;
enterprise?: {
id: string;
name: string;
};
}
Loading

0 comments on commit c88aeb7

Please sign in to comment.