Skip to content

Commit

Permalink
Add support for remote functions (#2026)
Browse files Browse the repository at this point in the history
Co-authored-by: Ethan Zimbelman <[email protected]>
Co-authored-by: @zimeg <[email protected]>
Co-authored-by: Fil Maj <[email protected]>
Co-authored-by: Filip Maj <[email protected]>
  • Loading branch information
5 people authored Aug 14, 2024
1 parent 01c174d commit 88e897a
Show file tree
Hide file tree
Showing 15 changed files with 755 additions and 31 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/samples.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# This workflow runs a TypeScript compilation against slack sample apps built on top of bolt-js
name: Samples Integration Type-checking

on:
push:
branches: [main]
pull_request:

jobs:
samples:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
sample:
- slack-samples/bolt-ts-starter-template

steps:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Checkout bolt-js
uses: actions/checkout@v4
with:
path: ./bolt-js
- name: Install and link bolt-js
working-directory: ./bolt-js
run: npm i && npm link .
- name: Checkout ${{ matrix.sample }}
uses: actions/checkout@v4
with:
repository: ${{ matrix.sample }}
path: ./sample
- name: Install sample dependencies and link bolt-js
working-directory: ./sample
run: npm i && npm link @slack/bolt
- name: Compile sample
working-directory: ./sample
run: npx tsc

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@slack/bolt",
"version": "3.20.0",
"version": "3.20.0-customFunctionBeta.1",
"description": "A framework for building Slack apps, fast.",
"author": "Slack Technologies, LLC",
"license": "MIT",
Expand Down Expand Up @@ -30,7 +30,8 @@
"build:clean": "shx rm -rf ./dist ./coverage ./.nyc_output",
"lint": "eslint --fix --ext .ts src",
"mocha": "TS_NODE_PROJECT=tsconfig.json nyc mocha --config .mocharc.json \"src/**/*.spec.ts\"",
"test": "npm run lint && npm run mocha && npm run test:types",
"test": "npm run build && npm run lint && npm run mocha && npm run test:types",
"test:coverage": "npm run mocha && nyc report --reporter=text",
"test:types": "tsd",
"watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build"
},
Expand All @@ -44,7 +45,7 @@
"@slack/oauth": "^2.6.2",
"@slack/socket-mode": "^1.3.3",
"@slack/types": "^2.11.0",
"@slack/web-api": "^6.11.2",
"@slack/web-api": "^6.12.0",
"@types/express": "^4.16.1",
"@types/promise.allsettled": "^1.0.3",
"@types/tsscmp": "^1.0.0",
Expand Down
72 changes: 71 additions & 1 deletion src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@ import {
SlashCommand,
WorkflowStepEdit,
SlackOptions,
FunctionInputs,
} from './types';
import { IncomingEventType, getTypeAndConversation, assertNever, isBodyWithTypeEnterpriseInstall, isEventTypeToSkipAuthorize } from './helpers';
import { CodedError, asCodedError, AppInitializationError, MultipleListenerError, ErrorCode, InvalidCustomPropertyError } from './errors';
import { AllMiddlewareArgs, contextBuiltinKeys } from './types/middleware';
import { StringIndexed } from './types/helpers';
// eslint-disable-next-line import/order
import allSettled = require('promise.allsettled'); // eslint-disable-line @typescript-eslint/no-require-imports
import { FunctionCompleteFn, FunctionFailFn, CustomFunction, CustomFunctionMiddleware } from './CustomFunction';
// eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-commonjs
const packageJson = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires

Expand Down Expand Up @@ -108,6 +110,7 @@ export interface AppOptions {
tokenVerificationEnabled?: boolean;
deferInitialization?: boolean;
extendedErrorHandler?: boolean;
attachFunctionToken?: boolean;
}

export { LogLevel, Logger } from '@slack/logger';
Expand Down Expand Up @@ -268,6 +271,8 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>

private initialized: boolean;

private attachFunctionToken: boolean;

public constructor({
signingSecret = undefined,
endpoints = undefined,
Expand Down Expand Up @@ -300,6 +305,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
tokenVerificationEnabled = true,
extendedErrorHandler = false,
deferInitialization = false,
attachFunctionToken = true,
}: AppOptions = {}) {
/* ------------------------ Developer mode ----------------------------- */
this.developerMode = developerMode;
Expand Down Expand Up @@ -332,6 +338,9 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
this.errorHandler = defaultErrorHandler(this.logger) as AnyErrorHandler;
this.extendedErrorHandler = extendedErrorHandler;

// Override token with functionBotAccessToken in function-related handlers
this.attachFunctionToken = attachFunctionToken;

/* ------------------------ Set client options ------------------------*/
this.clientOptions = clientOptions !== undefined ? clientOptions : {};
if (agent !== undefined && this.clientOptions.agent === undefined) {
Expand Down Expand Up @@ -494,6 +503,10 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
}
}

public get webClientOptions(): WebClientOptions {
return this.clientOptions;
}

/**
* Register a new middleware, processed in the order registered.
*
Expand All @@ -517,6 +530,16 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
return this;
}

/**
* Register CustomFunction middleware
*/
public function(callbackId: string, ...listeners: CustomFunctionMiddleware): this {
const fn = new CustomFunction(callbackId, listeners, this.webClientOptions);
const m = fn.getMiddleware();
this.middleware.push(m);
return this;
}

/**
* Convenience method to call start on the receiver
*
Expand Down Expand Up @@ -946,6 +969,18 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
retryReason: event.retryReason,
};

// Extract function-related information and augment context
const { functionExecutionId, functionBotAccessToken, functionInputs } = extractFunctionContext(body);
if (functionExecutionId) {
context.functionExecutionId = functionExecutionId;
if (functionInputs) { context.functionInputs = functionInputs; }
}

// Attach and make available the JIT/function-related token on context
if (this.attachFunctionToken) {
if (functionBotAccessToken) { context.functionBotAccessToken = functionBotAccessToken; }
}

// Factory for say() utility
const createSay = (channelId: string): SayFn => {
const token = selectToken(context);
Expand Down Expand Up @@ -1001,6 +1036,9 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
/** Ack function might be set below */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ack?: AckFn<any>;
complete?: FunctionCompleteFn;
fail?: FunctionFailFn;
inputs?: FunctionInputs;
} = {
body: bodyArg,
payload,
Expand Down Expand Up @@ -1054,7 +1092,18 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>

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

// If functionBotAccessToken exists on context, the incoming event is function-related *and* the
// user has `attachFunctionToken` enabled. In that case, subsequent calls with the client should
// use the function-related/JIT token in lieu of the botToken or userToken.
const token = context.functionBotAccessToken ? context.functionBotAccessToken : selectToken(context);

// Add complete() and fail() utilities for function-related interactivity
if (type === IncomingEventType.Action && context.functionExecutionId !== undefined) {
listenerArgs.complete = CustomFunction.createFunctionComplete(context, client);
listenerArgs.fail = CustomFunction.createFunctionFail(context, client);
listenerArgs.inputs = context.functionInputs;
}

if (token !== undefined) {
let pool;
Expand Down Expand Up @@ -1562,6 +1611,27 @@ function escapeHtml(input: string | undefined | null): string {
return '';
}

function extractFunctionContext(body: StringIndexed) {
let functionExecutionId;
let functionBotAccessToken;
let functionInputs;

// function_executed event
if (body.event && body.event.type === 'function_executed' && body.event.function_execution_id) {
functionExecutionId = body.event.function_execution_id;
functionBotAccessToken = body.event.bot_access_token;
}

// interactivity (block_actions)
if (body.function_data) {
functionExecutionId = body.function_data.execution_id;
functionBotAccessToken = body.bot_access_token;
functionInputs = body.function_data.inputs;
}

return { functionExecutionId, functionBotAccessToken, functionInputs };
}

// ----------------------------
// Instrumentation
// Don't change the position of the following code
Expand Down
Loading

0 comments on commit 88e897a

Please sign in to comment.