8000 Support SlackFunction and Function Localized Interactivity handling by srajiang · Pull Request #1567 · slackapi/bolt-js · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Support SlackFunction and Function Localized Interactivity handling #1567

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

Merged
merged 16 commits into from
Aug 31, 2022
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"url": "https://github.com/slackapi/bolt-js/issues"
},
"dependencies": {
"@slack/deno-slack-sdk": "^0.1.0",
"@slack/deno-slack-sdk": "^1.0.0",
"@slack/logger": "^3.0.0",
"@slack/oauth": "^2.5.1",
"@slack/socket-mode": "^1.3.0",
Expand Down Expand Up @@ -93,7 +93,7 @@
"source-map-support": "^0.5.12",
"ts-node": "^8.1.0",
"tsd": "^0.22.0",
"typescript": "^4.1.0"
"typescript": "4.7.4"
Copy link
Contributor Author
@srajiang srajiang Aug 29, 2022

Choose a reason for hiding this comment

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

Caret let 4.8.2 of typescript into the mix. This version has breaking changes, which were causing ts compiler complaints on npm install of @slack/deno-slack-sdk. Pinning to the latest compatible version.

},
"tsd": {
"directory": "types-tests"
Expand Down
100 changes: 100 additions & 0 deletions src/App-slack-function.spec.ts
< 8000 /tr>
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import 'mocha';
import sinon from 'sinon';
import { assert } from 'chai';
import rewiremock from 'rewiremock';
import { Override, mergeOverrides } from './test-helpers';
import {
Receiver,
ReceiverEvent,
} from './types';
import App from './App';
import importSlackFunctionModule from './SlackFunction.spec';

// Fakes
class FakeReceiver implements Receiver {
private bolt: App | undefined;

public init = (bolt: App) => {
this.bolt = bolt;
};

public start = sinon.fake((...params: any[]): Promise<unknown> => Promise.resolve([...params]));

public stop = sinon.fake((...params: any[]): Promise<unknown> => Promise.resolve([...params]));

public async sendEvent(event: ReceiverEvent): Promise<void> {
return this.bolt?.processEvent(event);
}
}

describe('App SlackFunction middleware', () => {
let fakeReceiver: FakeReceiver;
let dummyAuthorizationResult: { botToken: string; botId: string };

beforeEach(() => {
fakeReceiver = new FakeReceiver();
dummyAuthorizationResult = { botToken: '', botId: '' };
});

let app: App;

beforeEach(async () => {
const MockAppNoOverrides = await importApp();
app = new MockAppNoOverrides({
receiver: fakeReceiver,
authorize: sinon.fake.resolves(dummyAuthorizationResult),
});
});

it('should add a middleware for each SlackFunction passed to app.function', async () => {
const mockFunctionCallbackId = 'reverse_approval';
const { SlackFunction } = await importSlackFunctionModule(withMockValidManifestUtil(mockFunctionCallbackId));
const slackFn = new SlackFunction(mockFunctionCallbackId, () => new Promise((resolve) => resolve()));

const { middleware } = (app as any);

assert.equal(middleware.length, 2);

app.function(slackFn);

assert.equal(middleware.length, 3);
});
});

/* Testing Harness */

// Loading the system under test using overrides
async function importApp(
overrides: Override = mergeOverrides(
withNoopAppMetadata(),
withNoopWebClient(),
),
): Promise<typeof import('./App').default> {
return (await rewiremock.module(() => import('./App'), overrides)).default;
}

// Composable overrides
function withNoopWebClient(): Override {
return {
'@slack/web-api': {
WebClient: class {},
},
};
}

function withNoopAppMetadata(): Override {
return {
'@slack/web-api': {
addAppMetadata: sinon.fake(),
},
};
}

export default function withMockValidManifestUtil(functionCallbackId: string): Override {
const mockManifestOutput = JSON.parse(`{"functions": {"${functionCallbackId}": {}}}`);
return {
'./cli/hook-utils/manifest': {
getManifestData: () => mockManifestOutput,
},
};
}
116 changes: 81 additions & 35 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import processMiddleware from './middleware/process';
import { ConversationStore, conversationContext, MemoryStore } from './conversation-store';
import { WorkflowStep } from './WorkflowStep';
import { Subscription, SubscriptionOptions } from './Subscription';
import { SlackFunction } from './SlackFunction';
import {
CompleteFunction,
SlackFunction,
} from './SlackFunction';
import {
Middleware,
AnyMiddlewareArgs,
Expand Down Expand Up @@ -512,17 +515,6 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
return this;
}

/**
* Register WorkflowStep middleware
*
* @param workflowStep global workflow step middleware function
*/
public step(workflowStep: WorkflowStep): this {
const m = workflowStep.getMiddleware();
this.middleware.push(m);
return this;
}

/**
* Convenience method to call start on the receiver
*
Expand All @@ -548,7 +540,20 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
}

/**
* Register subcription middleware
* Register WorkflowStep middleware
*
* Not to be confused with next-gen platform Workflows + Functions
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adds some documentation to indicate WorkflowStep is an unrelated legacy feature.

*
* @param workflowStep global workflow step middleware function
*/
public step(workflowStep: WorkflowStep): this {
const m = workflowStep.getMiddleware();
this.middleware.push(m);
return this;
}

/**
* Process a subscription event
*
* @param listeners middleware that process and react to subscription payloads
*/
Expand All @@ -564,6 +569,9 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
return this;
}

/**
* Register an event listener
*/
public event<
EventType extends string = string,
MiddlewareCustomContext extends StringIndexed = StringIndexed,
Expand Down Expand Up @@ -610,20 +618,19 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
}

/**
* Register listeners that process and react to a function execution event
* @param title the name of the fn as defined in manifest file
* must match the function defined in manifest file
* @param fn a single function to register
* Register a Slack Function
*
* @param slackFn a main function to register
*
* */
public function(title: string, fn: Middleware<SlackEventMiddlewareArgs>): this {
// TODO: Support for multiple function listeners
const slackFn = new SlackFunction(title, fn);

public function(slackFn: SlackFunction): void {
const m = slackFn.getMiddleware();
this.middleware.push(m);
return this;
}

/**
* Process a message event
*
* @param listeners Middlewares that process and react to a message event
*/
Expand Down Expand Up @@ -700,6 +707,9 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
] as Middleware<AnyMiddlewareArgs>[]);
}

/**
* Process a shortcut event
*/
public shortcut<
Shortcut extends SlackShortcut = SlackShortcut,
MiddlewareCustomContext extends StringIndexed = StringIndexed,
Expand Down Expand Up @@ -745,6 +755,11 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
] as Middleware<AnyMiddlewareArgs>[]);
}

/**
* Process a block_action event
* https://api.slack.com/reference/interaction-payloads/block-actions
*
*/
// NOTE: this is what's called a convenience generic, so that types flow more easily without casting.
// https://web.archive.org/web/20210629110615/https://basarat.gitbook.io/typescript/type-system/generics#motivation-and-samples
public action<
Expand Down Expand Up @@ -923,6 +938,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
const isEnterpriseInstall = isBodyWithTypeEnterpriseInstall(bodyArg, type);
const source = buildSource(type, conversationId, bodyArg, isEnterpriseInstall);

// Get the result of any custom authorize provided
let authorizeResult: AuthorizeResult;
if (type === IncomingEventType.Event && isEventTypeToSkipAuthorize(event.body.event.type)) {
authorizeResult = {
Expand Down Expand Up @@ -967,9 +983,34 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
}
}

/**
* Set the Slack Function Bot Access Token if it exists in event payload to the context.
*
* A bot_access_token will exist in any payload that has been generated
* in the context of a Slack Function execution. This includes function_execution
* events themselves, as well as interactivity payloads (e.g. block_actions, views).
*
* This token must be used for further Slack API calls which are relevant to this
* Slack Function execution in order for any interactivity context data to be
* properly sent by Slack.
*
* Bolt will set this value in the event Context and
* use this token in place of any other token (for example the token
* that the App was configured with) to initialize the Web Client.
*
* Sometimes the bot_access_token is located in the event
* Sometimes it is located directly in the body.
*/
let slackFunctionBotAccessToken = body.bot_access_token;
if (slackFunctionBotAccessToken === undefined && 'event' in body) {
slackFunctionBotAccessToken = body.event.bot_access_token;
}

// Declare the event context
const context: Context = {
...authorizeResult,
...event.customProperties,
slackFunctionBotAccessToken,
isEnterpriseInstall,
retryNum: event.retryNum,
retryReason: event.retryReason,
Expand Down Expand Up @@ -1022,17 +1063,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
break;
}

// NOTE: the following doesn't work because... distributive?
// const listenerArgs: Partial<AnyMiddlewareArgs> = {
const listenerArgs: Pick<AnyMiddlewareArgs, 'body' | 'payload'> & {
/** Say function might be set below */
say?: SayFn;
/** Respond function might be set below */
respond?: RespondFn;
/** Ack function might be set below */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ack?: AckFn<any>;
} = {
const listenerArgs: ListenerArgs = {
body: bodyArg,
payload,
};
Expand Down Expand Up @@ -1083,7 +1114,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
await ack();
}

// Get the client arg
// Get or create the client
let { client } = this;
const token = selectToken(context);

Expand Down Expand Up @@ -1589,9 +1620,11 @@ function isBlockActionOrInteractiveMessageBody(
return (body as SlackActionMiddlewareArgs<BlockAction | InteractiveMessage>['body']).actions !== undefined;
}

// Returns either a bot token or a user token for client, say()
/**
* Returns in order of preference, a bot token, bot access token or user token for client, say()
* */
function selectToken(context: Context): string | undefined {
return context.botToken !== undefined ? context.botToken : context.userToken;
return context.slackFunctionBotAccessToken ?? context.botToken ?? context.userToken;
}

function buildRespondFn(
Expand All @@ -1615,3 +1648,16 @@ function isEventTypeToSkipAuthorize(eventType: string) {
// Instrumentation
// Don't change the position of the following code
addAppMetadata({ name: packageJson.name, version: packageJson.version });

// types
type ListenerArgs = Pick<AnyMiddlewareArgs, 'body' | 'payload'> & {
/** Say function might be set below */
say?: SayFn;
/** Respond function might be set below */
respond?: RespondFn;
/** Ack function might be set below */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ack?: AckFn<any>;
/* Complete might be set below */
complete?: CompleteFunction;
};
Loading
0