Skip to content
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

feat: expose auto acknowledgement as a per-custom-function option #2333

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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 @@
isEventTypeToSkipAuthorize,
} from './helpers';
import {
autoAcknowledge,
ignoreSelf as ignoreSelfMiddleware,
isSlackEventMiddlewareArgsOptions,
matchCommandName,
matchConstraints,
matchEventType,
Expand All @@ -47,7 +51,6 @@
import type {
AckFn,
ActionConstraints,
AllMiddlewareArgs,
AnyMiddlewareArgs,
BlockAction,
BlockElementAction,
Expand All @@ -72,6 +75,7 @@
SlackActionMiddlewareArgs,
SlackCommandMiddlewareArgs,
SlackEventMiddlewareArgs,
SlackEventMiddlewareArgsOptions,
SlackOptionsMiddlewareArgs,
SlackShortcut,
SlackShortcutMiddlewareArgs,
Expand All @@ -82,7 +86,7 @@
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 @@
* @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 @@
/**
* 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 @@
this.listeners.push([
onlyEvents,
matchEventType(eventNameOrPattern),
autoAcknowledge,
..._listeners,
] as Middleware<AnyMiddlewareArgs>[]);
}
Expand Down Expand Up @@ -662,6 +688,7 @@
this.listeners.push([
onlyEvents,
matchEventType('message'),
autoAcknowledge,
...messageMiddleware,
] as Middleware<AnyMiddlewareArgs>[]);
}
Expand Down Expand Up @@ -979,7 +1006,7 @@

// 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 @@
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();
}
}

Check warning on line 1100 in src/App.ts

View check run for this annotation

Codecov / codecov/patch

src/App.ts#L1096-L1100

Added lines #L1096 - L1100 were not covered by tests
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;
}

Check warning on line 1129 in src/App.ts

View check run for this annotation

Codecov / codecov/patch

src/App.ts#L1126-L1129

Added lines #L1126 - L1129 were not covered by tests
} else if (type === IncomingEventType.Command) {
const commandListenerArgs = listenerArgs as SlackCommandMiddlewareArgs;
commandListenerArgs.command = commandListenerArgs.payload;
Expand Down Expand Up @@ -1088,50 +1154,6 @@
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 @@
}

// 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