-
Notifications
You must be signed in to change notification settings - Fork 410
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
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
ac525e9
Add run script hook
srajiang 5e8d03e
Update Function (#1536)
srajiang 07f3a4f
merge
srajiang bbddd25
Override token for function and function interactivity events
srajiang aa25971
completeSuccess() and completeError() --> complete()
srajiang 668e388
Extend actions types to include function context and function interac…
srajiang b6501ef
Extend app.function to register action + view handlers
srajiang 86fba82
Pin typescript version
srajiang ef261ac
Change node import type
srajiang 766f615
Improve comment value
srajiang 64039b2
Add test for runHandler error
srajiang 2bfbcbb
Remove DefineDatastore from exports
srajiang e3b3ce2
Update FunctionExectionContext to reflect payload changes
srajiang 83b45a6
botAccessToken renamed slackFunctionBotAccessToken
srajiang e2f2fd4
Tweak language
srajiang de0b2c2
Bump deno to major
srajiang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | < 8000 /tr>||
}; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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 | ||
* | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
*/ | ||
|
@@ -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, | ||
|
@@ -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 | ||
*/ | ||
|
@@ -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, | ||
|
@@ -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< | ||
|
@@ -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 = { | ||
|
@@ -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. | ||
srajiang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
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, | ||
|
@@ -1022,17 +1063,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed> | |
break; | ||
} | ||
|
||
// NOTE: the following doesn't work because... distributive? | ||
srajiang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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, | ||
}; | ||
|
@@ -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); | ||
|
||
|
@@ -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( | ||
|
@@ -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 */ | ||
srajiang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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; | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.