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

Fix #894 Unable to build options request objects in TypeScript #900

Merged
merged 2 commits into from
Apr 29, 2021
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
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;
Copy link
Member Author

Choose a reason for hiding this comment

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

This may sound a bit unnatural but this is consistent with other union types (e.g., SlackAction, SlackEvent, SlackShortcut, SlackViewAction)


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

type OptionsPayloadFromType<T extends string> = KnownOptionsPayloadFromType<T> extends never
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the same approach with Events API payload handling.

? BasicOptionsPayload<T>
: KnownOptionsPayloadFromType<T>;

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

/**
* external data source in blocks
*/
export interface BlockSuggestion extends StringIndexed {
Copy link
Member Author

Choose a reason for hiding this comment

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

I wanted to remove StringIndexed but it's not backward compatible.

type: 'block_suggestion';
block_id: string;
action_id: string;
value: string;

api_app_id: string;
team: {
id: string;
domain: string;
enterprise_id?: string; // undocumented
Copy link
Member Author

Choose a reason for hiding this comment

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

"undocumented " here could be outdated comments

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