Skip to content

Commit 7048251

Browse files
WilliamBergaminseratchFilip Majfilmaj
authored
feat: expose auto acknowledgement flag on the function handler (#2283)
Co-authored-by: Kazuhiro Sera <seratch@gmail.com> Co-authored-by: Filip Maj <fmaj@slack-corp.com> Co-authored-by: Fil Maj <maj.fil@gmail.com>
1 parent 6670c37 commit 7048251

File tree

13 files changed

+545
-423
lines changed

13 files changed

+545
-423
lines changed

src/App.ts

Lines changed: 86 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import axios, { type AxiosInstance, type AxiosResponse } from 'axios';
77
import type { Assistant } from './Assistant';
88
import {
99
CustomFunction,
10-
type CustomFunctionMiddleware,
1110
type FunctionCompleteFn,
1211
type FunctionFailFn,
12+
type SlackCustomFunctionMiddlewareArgs,
13+
createFunctionComplete,
14+
createFunctionFail,
1315
} from './CustomFunction';
1416
import type { WorkflowStep } from './WorkflowStep';
1517
import { type ConversationStore, MemoryStore, conversationContext } from './conversation-store';
@@ -29,7 +31,9 @@ import {
2931
isEventTypeToSkipAuthorize,
3032
} from './helpers';
3133
import {
34+
autoAcknowledge,
3235
ignoreSelf as ignoreSelfMiddleware,
36+
isSlackEventMiddlewareArgsOptions,
3337
matchCommandName,
3438
matchConstraints,
3539
matchEventType,
@@ -47,7 +51,6 @@ import SocketModeReceiver from './receivers/SocketModeReceiver';
4751
import type {
4852
AckFn,
4953
ActionConstraints,
50-
AllMiddlewareArgs,
5154
AnyMiddlewareArgs,
5255
BlockAction,
5356
BlockElementAction,
@@ -72,6 +75,7 @@ import type {
7275
SlackActionMiddlewareArgs,
7376
SlackCommandMiddlewareArgs,
7477
SlackEventMiddlewareArgs,
78+
SlackEventMiddlewareArgsOptions,
7579
SlackOptionsMiddlewareArgs,
7680
SlackShortcut,
7781
SlackShortcutMiddlewareArgs,
@@ -82,7 +86,7 @@ import type {
8286
ViewOutput,
8387
WorkflowStepEdit,
8488
} from './types';
85-
import { contextBuiltinKeys } from './types';
89+
import { type AllMiddlewareArgs, contextBuiltinKeys } from './types/middleware';
8690
import { type StringIndexed, isRejected } from './types/utilities';
8791
const packageJson = require('../package.json');
8892

@@ -496,7 +500,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
496500
* @param m global middleware function
497501
*/
498502
public use<MiddlewareCustomContext extends StringIndexed = StringIndexed>(
499-
m: Middleware<AnyMiddlewareArgs, AppCustomContext & MiddlewareCustomContext>,
503+
m: Middleware<AnyMiddlewareArgs<{ autoAcknowledge: false }>, AppCustomContext & MiddlewareCustomContext>,
500504
): this {
501505
this.middleware.push(m as Middleware<AnyMiddlewareArgs>);
502506
return this;
@@ -529,10 +533,31 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
529533
/**
530534
* Register CustomFunction middleware
531535
*/
532-
public function(callbackId: string, ...listeners: CustomFunctionMiddleware): this {
533-
const fn = new CustomFunction(callbackId, listeners, this.webClientOptions);
534-
const m = fn.getMiddleware();
535-
this.middleware.push(m);
536+
public function<Options extends SlackEventMiddlewareArgsOptions = { autoAcknowledge: true }>(
537+
callbackId: string,
538+
options: Options,
539+
...listeners: Middleware<SlackCustomFunctionMiddlewareArgs<Options>>[]
540+
): this;
541+
public function<Options extends SlackEventMiddlewareArgsOptions = { autoAcknowledge: true }>(
542+
callbackId: string,
543+
...listeners: Middleware<SlackCustomFunctionMiddlewareArgs<Options>>[]
544+
): this;
545+
public function<Options extends SlackEventMiddlewareArgsOptions = { autoAcknowledge: true }>(
546+
callbackId: string,
547+
...optionOrListeners: (Options | Middleware<SlackCustomFunctionMiddlewareArgs<Options>>)[]
548+
): this {
549+
// TODO: fix this casting; edge case is if dev specifically sets AutoAck generic as false, this true assignment is invalid according to TS.
550+
const options = isSlackEventMiddlewareArgsOptions(optionOrListeners[0])
551+
? optionOrListeners[0]
552+
: ({ autoAcknowledge: true } as Options);
553+
const listeners = optionOrListeners.filter(
554+
(optionOrListener): optionOrListener is Middleware<SlackCustomFunctionMiddlewareArgs<Options>> => {
555+
return !isSlackEventMiddlewareArgsOptions(optionOrListener);
556+
},
557+
);
558+
559+
const fn = new CustomFunction<Options>(callbackId, listeners, options);
560+
this.listeners.push(fn.getListeners());
536561
return this;
537562
}
538563

@@ -594,6 +619,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
594619
this.listeners.push([
595620
onlyEvents,
596621
matchEventType(eventNameOrPattern),
622+
autoAcknowledge,
597623
..._listeners,
598624
] as Middleware<AnyMiddlewareArgs>[]);
599625
}
@@ -662,6 +688,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
662688
this.listeners.push([
663689
onlyEvents,
664690
matchEventType('message'),
691+
autoAcknowledge,
665692
...messageMiddleware,
666693
] as Middleware<AnyMiddlewareArgs>[]);
667694
}
@@ -979,7 +1006,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
9791006

9801007
// Factory for say() utility
9811008
const createSay = (channelId: string): SayFn => {
982-
const token = selectToken(context);
1009+
const token = selectToken(context, this.attachFunctionToken);
9831010
return (message) => {
9841011
let postMessageArguments: ChatPostMessageArguments;
9851012
if (typeof message === 'string') {
@@ -1040,27 +1067,66 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
10401067
respond?: RespondFn;
10411068
/** Ack function might be set below */
10421069
// biome-ignore lint/suspicious/noExplicitAny: different kinds of acks accept different arguments, TODO: revisit this to see if we can type better
1043-
ack?: AckFn<any>;
1070+
ack: AckFn<any>;
10441071
complete?: FunctionCompleteFn;
10451072
fail?: FunctionFailFn;
10461073
inputs?: FunctionInputs;
10471074
} = {
10481075
body: bodyArg,
1076+
ack,
10491077
payload,
10501078
};
10511079

1080+
// Get the client arg
1081+
let { client } = this;
1082+
1083+
const token = selectToken(context, this.attachFunctionToken);
1084+
1085+
if (token !== undefined) {
1086+
let pool: WebClientPool | undefined = undefined;
1087+
const clientOptionsCopy = { ...this.clientOptions };
1088+
if (authorizeResult.teamId !== undefined) {
1089+
pool = this.clients[authorizeResult.teamId];
1090+
if (pool === undefined) {
1091+
pool = this.clients[authorizeResult.teamId] = new WebClientPool();
1092+
}
1093+
// Add teamId to clientOptions so it can be automatically added to web-api calls
1094+
clientOptionsCopy.teamId = authorizeResult.teamId;
1095+
} else if (authorizeResult.enterpriseId !== undefined) {
1096+
pool = this.clients[authorizeResult.enterpriseId];
1097+
if (pool === undefined) {
1098+
pool = this.clients[authorizeResult.enterpriseId] = new WebClientPool();
1099+
}
1100+
}
1101+
if (pool !== undefined) {
1102+
client = pool.getOrCreate(token, clientOptionsCopy);
1103+
}
1104+
}
1105+
10521106
// TODO: can we instead use type predicates in these switch cases to allow for narrowing of the body simultaneously? we have isEvent, isView, isShortcut, isAction already in types/utilities / helpers
10531107
// Set aliases
10541108
if (type === IncomingEventType.Event) {
1055-
const eventListenerArgs = listenerArgs as SlackEventMiddlewareArgs;
1109+
// TODO: assigning eventListenerArgs by reference to set properties of listenerArgs is error prone, there should be a better way to do this!
1110+
const eventListenerArgs = listenerArgs as unknown as SlackEventMiddlewareArgs;
10561111
eventListenerArgs.event = eventListenerArgs.payload;
10571112
if (eventListenerArgs.event.type === 'message') {
10581113
const messageEventListenerArgs = eventListenerArgs as SlackEventMiddlewareArgs<'message'>;
10591114
messageEventListenerArgs.message = messageEventListenerArgs.payload;
10601115
}
1116+
if (eventListenerArgs.event.type === 'function_executed') {
1117+
listenerArgs.complete = createFunctionComplete(context, client);
1118+
listenerArgs.fail = createFunctionFail(context, client);
1119+
listenerArgs.inputs = eventListenerArgs.event.inputs;
1120+
}
10611121
} else if (type === IncomingEventType.Action) {
10621122
const actionListenerArgs = listenerArgs as SlackActionMiddlewareArgs;
10631123
actionListenerArgs.action = actionListenerArgs.payload;
1124+
// Add complete() and fail() utilities for function-related interactivity
1125+
if (context.functionExecutionId !== undefined) {
1126+
listenerArgs.complete = createFunctionComplete(context, client);
1127+
listenerArgs.fail = createFunctionFail(context, client);
1128+
listenerArgs.inputs = context.functionInputs;
1129+
}
10641130
} else if (type === IncomingEventType.Command) {
10651131
const commandListenerArgs = listenerArgs as SlackCommandMiddlewareArgs;
10661132
commandListenerArgs.command = commandListenerArgs.payload;
@@ -1088,50 +1154,6 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
10881154
listenerArgs.respond = buildRespondFn(this.axios, body.response_urls[0].response_url);
10891155
}
10901156

1091-
// Set ack() utility
1092-
if (type !== IncomingEventType.Event) {
1093-
listenerArgs.ack = ack;
1094-
} else {
1095-
// Events API requests are acknowledged right away, since there's no data expected
1096-
await ack();
1097-
}
1098-
1099-
// Get the client arg
1100-
let { client } = this;
1101-
1102-
// If functionBotAccessToken exists on context, the incoming event is function-related *and* the
1103-
// user has `attachFunctionToken` enabled. In that case, subsequent calls with the client should
1104-
// use the function-related/JIT token in lieu of the botToken or userToken.
1105-
const token = context.functionBotAccessToken ? context.functionBotAccessToken : selectToken(context);
1106-
1107-
// Add complete() and fail() utilities for function-related interactivity
1108-
if (type === IncomingEventType.Action && context.functionExecutionId !== undefined) {
1109-
listenerArgs.complete = CustomFunction.createFunctionComplete(context, client);
1110-
listenerArgs.fail = CustomFunction.createFunctionFail(context, client);
1111-
listenerArgs.inputs = context.functionInputs;
1112-
}
1113-
1114-
if (token !== undefined) {
1115-
let pool: WebClientPool | undefined = undefined;
1116-
const clientOptionsCopy = { ...this.clientOptions };
1117-
if (authorizeResult.teamId !== undefined) {
1118-
pool = this.clients[authorizeResult.teamId];
1119-
if (pool === undefined) {
1120-
pool = this.clients[authorizeResult.teamId] = new WebClientPool();
1121-
}
1122-
// Add teamId to clientOptions so it can be automatically added to web-api calls
1123-
clientOptionsCopy.teamId = authorizeResult.teamId;
1124-
} else if (authorizeResult.enterpriseId !== undefined) {
1125-
pool = this.clients[authorizeResult.enterpriseId];
1126-
if (pool === undefined) {
1127-
pool = this.clients[authorizeResult.enterpriseId] = new WebClientPool();
1128-
}
1129-
}
1130-
if (pool !== undefined) {
1131-
client = pool.getOrCreate(token, clientOptionsCopy);
1132-
}
1133-
}
1134-
11351157
// Dispatch event through the global middleware chain
11361158
try {
11371159
await processMiddleware(
@@ -1575,7 +1597,15 @@ function isBlockActionOrInteractiveMessageBody(
15751597
}
15761598

15771599
// Returns either a bot token or a user token for client, say()
1578-
function selectToken(context: Context): string | undefined {
1600+
function selectToken(context: Context, attachFunctionToken: boolean): string | undefined {
1601+
if (attachFunctionToken) {
1602+
// If functionBotAccessToken exists on context, the incoming event is function-related *and* the
1603+
// user has `attachFunctionToken` enabled. In that case, subsequent calls with the client should
1604+
// use the function-related/JIT token in lieu of the botToken or userToken.
1605+
if (context.functionBotAccessToken) {
1606+
return context.functionBotAccessToken;
1607+
}
1608+
}
15791609
return context.botToken !== undefined ? context.botToken : context.userToken;
15801610
}
15811611

0 commit comments

Comments
 (0)