Skip to content

Commit

Permalink
feat: expose auto acknowledgement flag on the function handler (#2283)
Browse files Browse the repository at this point in the history
Co-authored-by: Kazuhiro Sera <[email protected]>
Co-authored-by: Filip Maj <[email protected]>
Co-authored-by: Fil Maj <[email protected]>
  • Loading branch information
4 people authored Nov 5, 2024
1 parent 6670c37 commit 7048251
Show file tree
Hide file tree
Showing 13 changed files with 545 additions and 423 deletions.
142 changes: 86 additions & 56 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import axios, { type AxiosInstance, type AxiosResponse } from 'axios';
import type { Assistant } from './Assistant';
import {
CustomFunction,
type CustomFunctionMiddleware,
type FunctionCompleteFn,
type FunctionFailFn,
type SlackCustomFunctionMiddlewareArgs,
createFunctionComplete,
createFunctionFail,
} from './CustomFunction';
import type { WorkflowStep } from './WorkflowStep';
import { type ConversationStore, MemoryStore, conversationContext } from './conversation-store';
Expand All @@ -29,7 +31,9 @@ import {
isEventTypeToSkipAuthorize,
} from './helpers';
import {
autoAcknowledge,
ignoreSelf as ignoreSelfMiddleware,
isSlackEventMiddlewareArgsOptions,
matchCommandName,
matchConstraints,
matchEventType,
Expand All @@ -47,7 +51,6 @@ import SocketModeReceiver from './receivers/SocketModeReceiver';
import type {
AckFn,
ActionConstraints,
AllMiddlewareArgs,
AnyMiddlewareArgs,
BlockAction,
BlockElementAction,
Expand All @@ -72,6 +75,7 @@ import type {
SlackActionMiddlewareArgs,
SlackCommandMiddlewareArgs,
SlackEventMiddlewareArgs,
SlackEventMiddlewareArgsOptions,
SlackOptionsMiddlewareArgs,
SlackShortcut,
SlackShortcutMiddlewareArgs,
Expand All @@ -82,7 +86,7 @@ import type {
ViewOutput,
WorkflowStepEdit,
} from './types';
import { contextBuiltinKeys } from './types';
import { type AllMiddlewareArgs, contextBuiltinKeys } from './types/middleware';
import { type StringIndexed, isRejected } from './types/utilities';
const packageJson = require('../package.json');

Expand Down Expand Up @@ -496,7 +500,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
* @param m global middleware function
*/
public use<MiddlewareCustomContext extends StringIndexed = StringIndexed>(
m: Middleware<AnyMiddlewareArgs, AppCustomContext & MiddlewareCustomContext>,
m: Middleware<AnyMiddlewareArgs<{ autoAcknowledge: false }>, AppCustomContext & MiddlewareCustomContext>,
): this {
this.middleware.push(m as Middleware<AnyMiddlewareArgs>);
return this;
Expand Down Expand Up @@ -529,10 +533,31 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
/**
* 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);
public function<Options extends SlackEventMiddlewareArgsOptions = { autoAcknowledge: true }>(
callbackId: string,
options: Options,
...listeners: Middleware<SlackCustomFunctionMiddlewareArgs<Options>>[]
): this;
public function<Options extends SlackEventMiddlewareArgsOptions = { autoAcknowledge: true }>(
callbackId: string,
...listeners: Middleware<SlackCustomFunctionMiddlewareArgs<Options>>[]
): this;
public function<Options extends SlackEventMiddlewareArgsOptions = { autoAcknowledge: true }>(
callbackId: string,
...optionOrListeners: (Options | Middleware<SlackCustomFunctionMiddlewareArgs<Options>>)[]
): this {
// TODO: fix this casting; edge case is if dev specifically sets AutoAck generic as false, this true assignment is invalid according to TS.
const options = isSlackEventMiddlewareArgsOptions(optionOrListeners[0])
? optionOrListeners[0]
: ({ autoAcknowledge: true } as Options);
const listeners = optionOrListeners.filter(
(optionOrListener): optionOrListener is Middleware<SlackCustomFunctionMiddlewareArgs<Options>> => {
return !isSlackEventMiddlewareArgsOptions(optionOrListener);
},
);

const fn = new CustomFunction<Options>(callbackId, listeners, options);
this.listeners.push(fn.getListeners());
return this;
}

Expand Down Expand Up @@ -594,6 +619,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
this.listeners.push([
onlyEvents,
matchEventType(eventNameOrPattern),
autoAcknowledge,
..._listeners,
] as Middleware<AnyMiddlewareArgs>[]);
}
Expand Down Expand Up @@ -662,6 +688,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
this.listeners.push([
onlyEvents,
matchEventType('message'),
autoAcknowledge,
...messageMiddleware,
] as Middleware<AnyMiddlewareArgs>[]);
}
Expand Down Expand Up @@ -979,7 +1006,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>

// Factory for say() utility
const createSay = (channelId: string): SayFn => {
const token = selectToken(context);
const token = selectToken(context, this.attachFunctionToken);
return (message) => {
let postMessageArguments: ChatPostMessageArguments;
if (typeof message === 'string') {
Expand Down Expand Up @@ -1040,27 +1067,66 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
respond?: RespondFn;
/** Ack function might be set below */
// biome-ignore lint/suspicious/noExplicitAny: different kinds of acks accept different arguments, TODO: revisit this to see if we can type better
ack?: AckFn<any>;
ack: AckFn<any>;
complete?: FunctionCompleteFn;
fail?: FunctionFailFn;
inputs?: FunctionInputs;
} = {
body: bodyArg,
ack,
payload,
};

// Get the client arg
let { client } = this;

const token = selectToken(context, this.attachFunctionToken);

if (token !== undefined) {
let pool: WebClientPool | undefined = undefined;
const clientOptionsCopy = { ...this.clientOptions };
if (authorizeResult.teamId !== undefined) {
pool = this.clients[authorizeResult.teamId];
if (pool === undefined) {
pool = this.clients[authorizeResult.teamId] = new WebClientPool();
}
// Add teamId to clientOptions so it can be automatically added to web-api calls
clientOptionsCopy.teamId = authorizeResult.teamId;
} else if (authorizeResult.enterpriseId !== undefined) {
pool = this.clients[authorizeResult.enterpriseId];
if (pool === undefined) {
pool = this.clients[authorizeResult.enterpriseId] = new WebClientPool();
}
}
if (pool !== undefined) {
client = pool.getOrCreate(token, clientOptionsCopy);
}
}

// 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
// Set aliases
if (type === IncomingEventType.Event) {
const eventListenerArgs = listenerArgs as SlackEventMiddlewareArgs;
// TODO: assigning eventListenerArgs by reference to set properties of listenerArgs is error prone, there should be a better way to do this!
const eventListenerArgs = listenerArgs as unknown as SlackEventMiddlewareArgs;
eventListenerArgs.event = eventListenerArgs.payload;
if (eventListenerArgs.event.type === 'message') {
const messageEventListenerArgs = eventListenerArgs as SlackEventMiddlewareArgs<'message'>;
messageEventListenerArgs.message = messageEventListenerArgs.payload;
}
if (eventListenerArgs.event.type === 'function_executed') {
listenerArgs.complete = createFunctionComplete(context, client);
listenerArgs.fail = createFunctionFail(context, client);
listenerArgs.inputs = eventListenerArgs.event.inputs;
}
} else if (type === IncomingEventType.Action) {
const actionListenerArgs = listenerArgs as SlackActionMiddlewareArgs;
actionListenerArgs.action = actionListenerArgs.payload;
// Add complete() and fail() utilities for function-related interactivity
if (context.functionExecutionId !== undefined) {
listenerArgs.complete = createFunctionComplete(context, client);
listenerArgs.fail = createFunctionFail(context, client);
listenerArgs.inputs = context.functionInputs;
}
} else if (type === IncomingEventType.Command) {
const commandListenerArgs = listenerArgs as SlackCommandMiddlewareArgs;
commandListenerArgs.command = commandListenerArgs.payload;
Expand Down Expand Up @@ -1088,50 +1154,6 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
listenerArgs.respond = buildRespondFn(this.axios, body.response_urls[0].response_url);
}

// Set ack() utility
if (type !== IncomingEventType.Event) {
listenerArgs.ack = ack;
} else {
// Events API requests are acknowledged right away, since there's no data expected
await ack();
}

// Get the client arg
let { client } = this;

// 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: WebClientPool | undefined = undefined;
const clientOptionsCopy = { ...this.clientOptions };
if (authorizeResult.teamId !== undefined) {
pool = this.clients[authorizeResult.teamId];
if (pool === undefined) {
pool = this.clients[authorizeResult.teamId] = new WebClientPool();
}
// Add teamId to clientOptions so it can be automatically added to web-api calls
clientOptionsCopy.teamId = authorizeResult.teamId;
} else if (authorizeResult.enterpriseId !== undefined) {
pool = this.clients[authorizeResult.enterpriseId];
if (pool === undefined) {
pool = this.clients[authorizeResult.enterpriseId] = new WebClientPool();
}
}
if (pool !== undefined) {
client = pool.getOrCreate(token, clientOptionsCopy);
}
}

// Dispatch event through the global middleware chain
try {
await processMiddleware(
Expand Down Expand Up @@ -1575,7 +1597,15 @@ function isBlockActionOrInteractiveMessageBody(
}

// Returns either a bot token or a user token for client, say()
function selectToken(context: Context): string | undefined {
function selectToken(context: Context, attachFunctionToken: boolean): string | undefined {
if (attachFunctionToken) {
// 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.
if (context.functionBotAccessToken) {
return context.functionBotAccessToken;
}
}
return context.botToken !== undefined ? context.botToken : context.userToken;
}

Expand Down
Loading

0 comments on commit 7048251

Please sign in to comment.