Skip to content

Commit

Permalink
Extend app.function to register action + view handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
srajiang committed Aug 26, 2022
1 parent d7ac7c4 commit d40ec6e
Show file tree
Hide file tree
Showing 17 changed files with 1,073 additions and 164 deletions.
2 changes: 1 addition & 1 deletion 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": "^0.2.0",
"@slack/logger": "^3.0.0",
"@slack/oauth": "^2.5.1",
"@slack/socket-mode": "^1.3.0",
Expand Down
100 changes: 100 additions & 0 deletions src/App-slack-function.spec.ts
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,
},
};
}
82 changes: 45 additions & 37 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 @@ -540,17 +543,17 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>

/**
* Register WorkflowStep middleware
*
*
* Not to be confused with next-gen platform Workflows + Functions
*
* @param workflowStep global workflow step middleware function
*/
public step(workflowStep: WorkflowStep): this {
public step(workflowStep: WorkflowStep): this {
const m = workflowStep.getMiddleware();
this.middleware.push(m);
return this;
}

/**
* Process a subscription event
*
Expand Down Expand Up @@ -617,27 +620,22 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
}

/**
* Register a Slack Function handler
*
* and other function-scoped
* interactivity handlers
* (block_actions, view interaction payloads)
*
*
* Register a Slack Function
*
* @param callbackId the id of the function as defined in manifest
*
* @param fn a function to register
*
* @param slackFn a main function to register
*
* */
public function(callbackId: string, fn: Middleware<SlackEventMiddlewareArgs>): this {
const slackFn = new SlackFunction(callbackId, fn);

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

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

/**
/**
* Process a shortcut event
*/
public shortcut<
Expand Down Expand Up @@ -761,10 +759,10 @@ 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
Expand Down Expand Up @@ -989,8 +987,15 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
}
}

// set bot access token if it exists
let botAccessToken = body.bot_access_token ?? body.event.bot_access_token;
/**
* Set the Bot Access Token if it exists in event payload
* Sometimes the bot_access_token is located in the event
* Sometimes it is located directly in the body.
*/
let botAccessToken = body.bot_access_token;
if (botAccessToken === undefined && 'event' in body) {
botAccessToken = body.event.bot_access_token;
}

// Declare the event context
const context: Context = {
Expand Down Expand Up @@ -1049,17 +1054,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 @@ -1110,7 +1105,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 @@ -1616,11 +1611,11 @@ function isBlockActionOrInteractiveMessageBody(
return (body as SlackActionMiddlewareArgs<BlockAction | InteractiveMessage>['body']).actions !== undefined;
}

/**
/**
* Returns a bot token, bot access token or user token for client, say()
* */
function selectToken(context: Context): string | undefined {
return context.botAccessToken ?? context.botToken ?? context.userToken;
return context.botAccessToken ?? context.botToken ?? context.userToken;
}

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

// types
export 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;
};
2 changes: 2 additions & 0 deletions src/Manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Schema,
ManifestSchema,
DefineOAuth2Provider,
DefineDatastore,
} from '@slack/deno-slack-sdk';

export const Manifest = (definition: SlackManifestType): ManifestSchema => {
Expand All @@ -22,6 +23,7 @@ export {
Schema,
SlackManifest,
DefineType,
DefineDatastore,
};

export type {
Expand Down

0 comments on commit d40ec6e

Please sign in to comment.