diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 85adb3538..20c91db1e 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -19,7 +19,8 @@ table tr:nth-child(even) { } /* changing the links to blue for accessibility */ -p a, .markdown a { +p a, +.markdown a { color: var(--slack-cloud-blue); } @@ -53,7 +54,7 @@ a:hover { /* removing ToC line */ .table-of-contents__left-border { border-left: none !important; -} +} /* increasing name of SDK in sidebar */ .sidebar-title { @@ -64,8 +65,8 @@ a:hover { /* removing sidebar line and adding space to match ToC */ .theme-doc-sidebar-container { - border-right: none !important; - margin-right: 2rem; + border-right: none !important; + margin-right: 2rem; } /* announcement bar up top */ @@ -112,17 +113,17 @@ html[data-theme="dark"] .navbar-github-link::before { /* Docs code bubbles */ [data-theme="light"] { - --code-link-background: #CFE9FE; - --code-link-text: rgb(21, 50, 59); + --code-link-background: #cfe9fe; + --code-link-text: rgb(21, 50, 59); - --method-link-background: #CDEFC4; + --method-link-background: #cdefc4; --method-link-text: rgb(0, 41, 0); - --scope-link-background: #FBF3E0; + --scope-link-background: #fbf3e0; --scope-link-text: rgb(63, 46, 0); - --event-link-background: #FDDDE3; - --event-link-text: rgb(74, 21, 75); + --event-link-background: #fddde3; + --event-link-text: rgb(74, 21, 75); } [data-theme="dark"] { @@ -130,28 +131,28 @@ html[data-theme="dark"] .navbar-github-link::before { --method-link-text: white; --scope-link-text: white; --event-link-text: white; - --code-link-background: #1AB9FF50; - --method-link-background: #41B65850; - --scope-link-background: #FCC00350; - --event-link-background: #E3066A50; + --code-link-background: #1ab9ff50; + --method-link-background: #41b65850; + --scope-link-background: #fcc00350; + --event-link-background: #e3066a50; } a code { background-color: var(--code-link-background); - color: var(--code-link-text); + color: var(--code-link-text); } a[href^="https://api.slack.com/methods"] > code { background-color: var(--method-link-background); - color: var(--method-link-text); + color: var(--method-link-text); } a[href^="https://api.slack.com/scopes"] > code { background-color: var(--scope-link-background); - color: var(--scope-link-text); + color: var(--scope-link-text); } a[href^="https://api.slack.com/events"] > code { background-color: var(--event-link-background); - color: var(--event-link-text); -} \ No newline at end of file + color: var(--event-link-text); +} diff --git a/package.json b/package.json index 0ff1e3110..d67c154bf 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@tsconfig/node18": "^18.2.4", "@types/chai": "^4.1.7", "@types/mocha": "^10.0.1", - "@types/node": "22.8.7", + "@types/node": "22.9.0", "@types/sinon": "^7.0.11", "@types/tsscmp": "^1.0.0", "c8": "^10.1.2", diff --git a/src/App.ts b/src/App.ts index 3084c61a2..f346a11da 100644 --- a/src/App.ts +++ b/src/App.ts @@ -7,11 +7,9 @@ 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'; @@ -31,9 +29,7 @@ import { isEventTypeToSkipAuthorize, } from './helpers'; import { - autoAcknowledge, ignoreSelf as ignoreSelfMiddleware, - isSlackEventMiddlewareArgsOptions, matchCommandName, matchConstraints, matchEventType, @@ -51,6 +47,7 @@ import SocketModeReceiver from './receivers/SocketModeReceiver'; import type { AckFn, ActionConstraints, + AllMiddlewareArgs, AnyMiddlewareArgs, BlockAction, BlockElementAction, @@ -75,7 +72,6 @@ import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs, SlackEventMiddlewareArgs, - SlackEventMiddlewareArgsOptions, SlackOptionsMiddlewareArgs, SlackShortcut, SlackShortcutMiddlewareArgs, @@ -86,7 +82,7 @@ import type { ViewOutput, WorkflowStepEdit, } from './types'; -import { type AllMiddlewareArgs, contextBuiltinKeys } from './types/middleware'; +import { contextBuiltinKeys } from './types'; import { type StringIndexed, isRejected } from './types/utilities'; const packageJson = require('../package.json'); @@ -500,7 +496,7 @@ export default class App * @param m global middleware function */ public use( - m: Middleware, AppCustomContext & MiddlewareCustomContext>, + m: Middleware, ): this { this.middleware.push(m as Middleware); return this; @@ -533,31 +529,10 @@ export default class App /** * Register CustomFunction middleware */ - public function( - callbackId: string, - options: Options, - ...listeners: Middleware>[] - ): this; - public function( - callbackId: string, - ...listeners: Middleware>[] - ): this; - public function( - callbackId: string, - ...optionOrListeners: (Options | Middleware>)[] - ): 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> => { - return !isSlackEventMiddlewareArgsOptions(optionOrListener); - }, - ); - - const fn = new CustomFunction(callbackId, listeners, options); - this.listeners.push(fn.getListeners()); + 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; } @@ -619,7 +594,6 @@ export default class App this.listeners.push([ onlyEvents, matchEventType(eventNameOrPattern), - autoAcknowledge, ..._listeners, ] as Middleware[]); } @@ -688,7 +662,6 @@ export default class App this.listeners.push([ onlyEvents, matchEventType('message'), - autoAcknowledge, ...messageMiddleware, ] as Middleware[]); } @@ -1006,7 +979,7 @@ export default class App // Factory for say() utility const createSay = (channelId: string): SayFn => { - const token = selectToken(context, this.attachFunctionToken); + const token = selectToken(context); return (message) => { let postMessageArguments: ChatPostMessageArguments; if (typeof message === 'string') { @@ -1067,66 +1040,27 @@ export default class App 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; + ack?: AckFn; 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) { - // 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; + const eventListenerArgs = listenerArgs 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; @@ -1154,6 +1088,50 @@ export default class App 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( @@ -1597,15 +1575,7 @@ function isBlockActionOrInteractiveMessageBody( } // Returns either a bot token or a user token for client, say() -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; - } - } +function selectToken(context: Context): string | undefined { return context.botToken !== undefined ? context.botToken : context.userToken; } diff --git a/src/Assistant.ts b/src/Assistant.ts index 14d817d19..687d481de 100644 --- a/src/Assistant.ts +++ b/src/Assistant.ts @@ -9,8 +9,6 @@ import { type AssistantThreadContext, type AssistantThreadContextStore, DefaultThreadContextStore, - type GetThreadContextFn, - type SaveThreadContextFn, } from './AssistantThreadContextStore'; import { AssistantInitializationError, AssistantMissingPropertyError } from './errors'; import processMiddleware from './middleware/process'; @@ -30,14 +28,16 @@ export interface AssistantConfig { * Callback utilities */ interface AssistantUtilityArgs { - getThreadContext: GetThreadContextFn; - saveThreadContext: SaveThreadContextFn; + getThreadContext: GetThreadContextUtilFn; + saveThreadContext: SaveThreadContextUtilFn; say: SayFn; setStatus: SetStatusFn; setSuggestedPrompts: SetSuggestedPromptsFn; setTitle: SetTitleFn; } +type GetThreadContextUtilFn = () => Promise; +type SaveThreadContextUtilFn = () => Promise; type SetStatusFn = (status: string) => Promise; type SetSuggestedPromptsFn = ( @@ -310,7 +310,7 @@ function createSay(args: AllAssistantMiddlewareArgs): SayFn { const { channelId: channel, threadTs: thread_ts, context } = extractThreadInfo(payload); return async (message: Parameters[0]) => { - const threadContext = context.channel_id ? context : await args.getThreadContext(args); + const threadContext = context.channel_id ? context : await args.getThreadContext(); const postMessageArgument: ChatPostMessageArguments = typeof message === 'string' ? { text: message, channel, thread_ts } : { ...message, channel, thread_ts }; diff --git a/src/CustomFunction.ts b/src/CustomFunction.ts index 46563b2c0..b1d38779d 100644 --- a/src/CustomFunction.ts +++ b/src/CustomFunction.ts @@ -1,18 +1,17 @@ import type { FunctionExecutedEvent } from '@slack/types'; -import type { FunctionsCompleteErrorResponse, FunctionsCompleteSuccessResponse, WebClient } from '@slack/web-api'; +import { + type FunctionsCompleteErrorResponse, + type FunctionsCompleteSuccessResponse, + WebClient, + type WebClientOptions, +} from '@slack/web-api'; import { CustomFunctionCompleteFailError, CustomFunctionCompleteSuccessError, CustomFunctionInitializationError, } from './errors'; -import { autoAcknowledge, matchEventType, onlyEvents } from './middleware/builtin'; -import type { - AnyMiddlewareArgs, - Context, - Middleware, - SlackEventMiddlewareArgs, - SlackEventMiddlewareArgsOptions, -} from './types'; +import processMiddleware from './middleware/process'; +import type { AllMiddlewareArgs, AnyMiddlewareArgs, Context, Middleware, SlackEventMiddlewareArgs } from './types'; /** Interfaces */ @@ -21,8 +20,6 @@ interface FunctionCompleteArguments { outputs?: Record; } -/** Types */ - export type FunctionCompleteFn = (params?: FunctionCompleteArguments) => Promise; interface FunctionFailArguments { @@ -31,70 +28,115 @@ interface FunctionFailArguments { export type FunctionFailFn = (params: FunctionFailArguments) => Promise; -export type SlackCustomFunctionMiddlewareArgs< - Options extends SlackEventMiddlewareArgsOptions = { autoAcknowledge: true }, -> = SlackEventMiddlewareArgs<'function_executed', Options> & { +export interface CustomFunctionExecuteMiddlewareArgs extends SlackEventMiddlewareArgs<'function_executed'> { inputs: FunctionExecutedEvent['inputs']; complete: FunctionCompleteFn; fail: FunctionFailFn; -}; - -/* - * Middleware that filters out function scoped events that do not match the provided callback ID - */ -export function matchCallbackId(callbackId: string): Middleware { - return async ({ payload, next }) => { - if (payload.function.callback_id === callbackId) { - await next(); - } - }; } +/** Types */ + +export type SlackCustomFunctionMiddlewareArgs = CustomFunctionExecuteMiddlewareArgs; + +type CustomFunctionExecuteMiddleware = Middleware[]; + +export type CustomFunctionMiddleware = Middleware[]; + +export type AllCustomFunctionMiddlewareArgs< + T extends SlackCustomFunctionMiddlewareArgs = SlackCustomFunctionMiddlewareArgs, +> = T & AllMiddlewareArgs; + +/** Constants */ + +const VALID_PAYLOAD_TYPES = new Set(['function_executed']); + /** Class */ -export class CustomFunction { + +export class CustomFunction { /** Function callback_id */ public callbackId: string; - private listeners: Middleware>[]; + private appWebClientOptions: WebClientOptions; - private options: Options; + private middleware: CustomFunctionMiddleware; - public constructor( - callbackId: string, - listeners: Middleware>[], - options: Options, - ) { - validate(callbackId, listeners); + public constructor(callbackId: string, middleware: CustomFunctionExecuteMiddleware, clientOptions: WebClientOptions) { + validate(callbackId, middleware); + this.appWebClientOptions = clientOptions; this.callbackId = callbackId; - this.listeners = listeners; - this.options = options; + this.middleware = middleware; + } + + public getMiddleware(): Middleware { + return async (args): Promise => { + if (isFunctionEvent(args) && this.matchesConstraints(args)) { + return this.processEvent(args); + } + return args.next(); + }; } - public getListeners(): Middleware[] { - if (this.options.autoAcknowledge) { - return [ - onlyEvents, - matchEventType('function_executed'), - matchCallbackId(this.callbackId), - autoAcknowledge, - ...this.listeners, - ] as Middleware[]; + private matchesConstraints(args: SlackCustomFunctionMiddlewareArgs): boolean { + return args.payload.function.callback_id === this.callbackId; + } + + private async processEvent(args: AllCustomFunctionMiddlewareArgs): Promise { + const functionArgs = enrichFunctionArgs(args, this.appWebClientOptions); + const functionMiddleware = this.getFunctionMiddleware(); + return processFunctionMiddleware(functionArgs, functionMiddleware); + } + + private getFunctionMiddleware(): CustomFunctionMiddleware { + return this.middleware; + } + + /** + * Factory for `complete()` utility + */ + public static createFunctionComplete(context: Context, client: WebClient): FunctionCompleteFn { + const token = selectToken(context); + const { functionExecutionId } = context; + + if (!functionExecutionId) { + const errorMsg = 'No function_execution_id found'; + throw new CustomFunctionCompleteSuccessError(errorMsg); + } + + return (params: Parameters[0] = {}) => + client.functions.completeSuccess({ + token, + outputs: params.outputs || {}, + function_execution_id: functionExecutionId, + }); + } + + /** + * Factory for `fail()` utility + */ + public static createFunctionFail(context: Context, client: WebClient): FunctionFailFn { + const token = selectToken(context); + const { functionExecutionId } = context; + + if (!functionExecutionId) { + const errorMsg = 'No function_execution_id found'; + throw new CustomFunctionCompleteFailError(errorMsg); } - return [ - onlyEvents, - matchEventType('function_executed'), - matchCallbackId(this.callbackId), - ...this.listeners, - ] as Middleware[]; + + return (params: Parameters[0]) => { + const { error } = params ?? {}; + + return client.functions.completeError({ + token, + error, + function_execution_id: functionExecutionId, + }); + }; } } /** Helper Functions */ -export function validate( - callbackId: string, - middleware: Middleware>[], -): void { +export function validate(callbackId: string, middleware: CustomFunctionExecuteMiddleware): void { // Ensure callbackId is valid if (typeof callbackId !== 'string') { const errorMsg = 'CustomFunction expects a callback_id as the first argument'; @@ -119,40 +161,55 @@ export function validate { + const { context, client, logger } = args; + const callbacks = [...middleware] as Middleware[]; + const lastCallback = callbacks.pop(); + + if (lastCallback !== undefined) { + await processMiddleware(callbacks, args, context, client, logger, async () => + lastCallback({ ...args, context, client, logger }), + ); } - - return (params: Parameters[0] = {}) => - client.functions.completeSuccess({ - outputs: params.outputs || {}, - function_execution_id: functionExecutionId, - }); } -/** - * Factory for `fail()` utility - */ -export function createFunctionFail(context: Context, client: WebClient): FunctionFailFn { - const { functionExecutionId } = context; - - if (!functionExecutionId) { - const errorMsg = 'No function_execution_id found'; - throw new CustomFunctionCompleteFailError(errorMsg); - } +export function isFunctionEvent(args: AnyMiddlewareArgs): args is AllCustomFunctionMiddlewareArgs { + return VALID_PAYLOAD_TYPES.has(args.payload.type); +} - return (params: Parameters[0]) => { - const { error } = params ?? {}; +function selectToken(context: Context): string | undefined { + // If attachFunctionToken = false, fallback to botToken or userToken + return context.functionBotAccessToken ? context.functionBotAccessToken : context.botToken || context.userToken; +} - return client.functions.completeError({ - error, - function_execution_id: functionExecutionId, - }); - }; +/** + * `enrichFunctionArgs()` takes in a function's args and: + * 1. removes the next() passed in from App-level middleware processing + * - events will *not* continue down global middleware chain to subsequent listeners + * 2. augments args with step lifecycle-specific properties/utilities + * */ +export function enrichFunctionArgs( + args: AllCustomFunctionMiddlewareArgs, + webClientOptions: WebClientOptions, +): AllCustomFunctionMiddlewareArgs { + const { next: _next, ...functionArgs } = args; + const enrichedArgs = { ...functionArgs }; + const token = selectToken(functionArgs.context); + + // Making calls with a functionBotAccessToken establishes continuity between + // a function_executed event and subsequent interactive events (actions) + const client = new WebClient(token, webClientOptions); + enrichedArgs.client = client; + + // Utility args + enrichedArgs.inputs = enrichedArgs.event.inputs; + enrichedArgs.complete = CustomFunction.createFunctionComplete(enrichedArgs.context, client); + enrichedArgs.fail = CustomFunction.createFunctionFail(enrichedArgs.context, client); + + return enrichedArgs as AllCustomFunctionMiddlewareArgs; // TODO: dangerous casting as it obfuscates missing `next()` } diff --git a/src/middleware/builtin.ts b/src/middleware/builtin.ts index 633730e09..90f00d307 100644 --- a/src/middleware/builtin.ts +++ b/src/middleware/builtin.ts @@ -15,7 +15,6 @@ import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs, SlackEventMiddlewareArgs, - SlackEventMiddlewareArgsOptions, SlackOptionsMiddlewareArgs, SlackShortcutMiddlewareArgs, SlackViewAction, @@ -64,13 +63,6 @@ function isMessageEventArgs(args: AnyMiddlewareArgs): args is SlackEventMiddlewa return isEventArgs(args) && 'message' in args; } -export function isSlackEventMiddlewareArgsOptions< - Options extends SlackEventMiddlewareArgsOptions, - EventMiddlewareArgs extends SlackEventMiddlewareArgs, ->(optionOrListener: Options | Middleware): optionOrListener is Options { - return typeof optionOrListener !== 'function' && 'autoAcknowledge' in optionOrListener; -} - /** * Middleware that filters out any event that isn't an action */ @@ -127,16 +119,6 @@ export const onlyViewActions: Middleware = async (args) => { } }; -/** - * Middleware that auto acknowledges the request received - */ -export const autoAcknowledge: Middleware = async (args) => { - if ('ack' in args && args.ack !== undefined) { - await args.ack(); - } - await args.next(); -}; - /** * Middleware that checks for matches given constraints */ diff --git a/src/types/events/index.ts b/src/types/events/index.ts index 5ff16e244..7b485e5fc 100644 --- a/src/types/events/index.ts +++ b/src/types/events/index.ts @@ -1,18 +1,15 @@ import type { SlackEvent } from '@slack/types'; -import type { AckFn, SayFn, StringIndexed } from '../utilities'; - -export type SlackEventMiddlewareArgsOptions = { autoAcknowledge: boolean }; +import type { SayFn, StringIndexed } from '../utilities'; /** * Arguments which listeners and middleware receive to process an event from Slack's Events API. */ -export type SlackEventMiddlewareArgs< - EventType extends string = string, - Options extends SlackEventMiddlewareArgsOptions = { autoAcknowledge: true }, -> = { +export type SlackEventMiddlewareArgs = { payload: EventFromType; event: EventFromType; body: EnvelopedEvent>; + // Add `ack` as undefined for global middleware in TypeScript TODO: but why? spend some time digging into this + ack?: undefined; } & (EventType extends 'message' ? // If this is a message event, add a `message` property { message: EventFromType } @@ -20,8 +17,7 @@ export type SlackEventMiddlewareArgs< (EventFromType extends { channel: string } | { item: { channel: string } } ? // If this event contains a channel, add a `say` utility function { say: SayFn } - : unknown) & - (Options['autoAcknowledge'] extends true ? unknown : { ack: AckFn }); + : unknown); export interface BaseSlackEvent { type: T; diff --git a/src/types/middleware.ts b/src/types/middleware.ts index 99332f442..8f2bec71f 100644 --- a/src/types/middleware.ts +++ b/src/types/middleware.ts @@ -1,23 +1,21 @@ import type { Logger } from '@slack/logger'; import type { WebClient } from '@slack/web-api'; -import type { SlackCustomFunctionMiddlewareArgs } from '../CustomFunction'; import type { SlackActionMiddlewareArgs } from './actions'; import type { SlackCommandMiddlewareArgs } from './command'; -import type { FunctionInputs, SlackEventMiddlewareArgs, SlackEventMiddlewareArgsOptions } from './events'; +import type { FunctionInputs, SlackEventMiddlewareArgs } from './events'; import type { SlackOptionsMiddlewareArgs } from './options'; import type { SlackShortcutMiddlewareArgs } from './shortcuts'; import type { StringIndexed } from './utilities'; import type { SlackViewMiddlewareArgs } from './view'; // TODO: rename this to AnyListenerArgs, and all the constituent types -export type AnyMiddlewareArgs = - | SlackEventMiddlewareArgs +export type AnyMiddlewareArgs = + | SlackEventMiddlewareArgs | SlackActionMiddlewareArgs | SlackCommandMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs - | SlackShortcutMiddlewareArgs - | SlackCustomFunctionMiddlewareArgs; + | SlackShortcutMiddlewareArgs; export interface AllMiddlewareArgs { context: Context & CustomContext; diff --git a/src/types/utilities.ts b/src/types/utilities.ts index 18e615472..c70f5e692 100644 --- a/src/types/utilities.ts +++ b/src/types/utilities.ts @@ -1,5 +1,4 @@ import type { ChatPostMessageArguments, ChatPostMessageResponse } from '@slack/web-api'; - // TODO: breaking change: remove, unnecessary abstraction, just use Record directly /** * Extend this interface to build a type that is treated as an open set of properties, where each key is a string. diff --git a/test/types/assistant.test-d.ts b/test/types/assistant.test-d.ts new file mode 100644 index 000000000..1a1700056 --- /dev/null +++ b/test/types/assistant.test-d.ts @@ -0,0 +1,75 @@ +import { expectError, expectType } from 'tsd'; +import { type AllAssistantMiddlewareArgs, Assistant } from '../../src/Assistant'; +import type { AssistantThreadContext } from '../../src/AssistantThreadContextStore'; + +// Constructor tests +const asyncNoop = () => Promise.resolve(); + +// missing required properties `threadStarted` and `userMessage` +expectError(new Assistant({})); + +// missing required property `threadStarted` +expectError( + new Assistant({ + userMessage: asyncNoop, + }), +); + +// missing required property `userMessage` +expectError( + new Assistant({ + threadStarted: asyncNoop, + }), +); + +// happy construction +expectType( + new Assistant({ + threadStarted: asyncNoop, + userMessage: asyncNoop, + }), +); + +// threadStarted tests +new Assistant({ + userMessage: asyncNoop, + threadStarted: async ({ saveThreadContext }) => { + expectType(await saveThreadContext()); + return Promise.resolve(); + }, +}); + +// userMessage tests +new Assistant({ + userMessage: async ({ getThreadContext }) => { + expectType(await getThreadContext()); + return Promise.resolve(); + }, + threadStarted: asyncNoop, +}); + +// threadContextChanged tests +new Assistant({ + userMessage: asyncNoop, + threadStarted: asyncNoop, + threadContextChanged: async ({ event }) => { + expectType(event.assistant_thread.context); + return Promise.resolve(); + }, +}); + +// threadContextStore tests +new Assistant({ + threadContextStore: { + get: async (args) => { + expectType(args); + return Promise.resolve({}); + }, + save: async (args) => { + expectType(args); + return Promise.resolve(); + }, + }, + userMessage: asyncNoop, + threadStarted: asyncNoop, +}); diff --git a/test/types/function.test-d.ts b/test/types/function.test-d.ts deleted file mode 100644 index 2d1bb71d4..000000000 --- a/test/types/function.test-d.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expectError, expectNotType, expectType } from 'tsd'; -import App from '../../src/App'; -import type { FunctionCompleteFn, FunctionFailFn } from '../../src/CustomFunction'; -import type { FunctionInputs } from '../../src/types'; -import type { AckFn } from '../../src/types/utilities'; - -const app = new App({ token: 'TOKEN', signingSecret: 'Signing Secret' }); - -// By default `function` handlers auto-acknowledge events so `ack` should not be provided/defined -expectError( - app.function('callback', async ({ ack }) => { - expectNotType>(ack); - }), -); - -// For `function` handlers that are auto-acknowledged, `ack` should not be provided/defined -expectError( - app.function('callback', { autoAcknowledge: true }, async ({ ack }) => { - expectNotType>(ack); - }), -); - -// For `function` handlers that are not auto-acknowledged, `ack` should be provided/defined -app.function('callback', { autoAcknowledge: false }, async ({ ack }) => { - expectType>(ack); -}); - -// By default `function` handlers provide/define the proper arguments -app.function('callback', async ({ inputs, complete, fail }) => { - expectType(inputs); - expectType(complete); - expectType(fail); -}); diff --git a/test/unit/App/middleware.spec.ts b/test/unit/App/middleware.spec.ts index 24ba2383e..4d87fc203 100644 --- a/test/unit/App/middleware.spec.ts +++ b/test/unit/App/middleware.spec.ts @@ -10,7 +10,6 @@ import { type Override, createDummyAppMentionEventMiddlewareArgs, createDummyBlockActionEventMiddlewareArgs, - createDummyCustomFunctionMiddlewareArgs, createDummyMessageEventMiddlewareArgs, createDummyReceiverEvent, createDummyViewSubmissionMiddlewareArgs, @@ -763,43 +762,6 @@ describe('App middleware processing', () => { assert.equal(globalClient, clientArg); }); - - it('should use the xwfp token if the request contains one', async () => { - const MockApp = await importApp(); - const app = new MockApp({ - receiver: fakeReceiver, - authorize: noop, - }); - - let clientArg: WebClient | undefined; - app.use(async ({ client }) => { - clientArg = client; - }); - const testData = createDummyCustomFunctionMiddlewareArgs({ options: { autoAcknowledge: false } }); - await fakeReceiver.sendEvent({ ack: testData.ack, body: testData.body }); - - assert.notTypeOf(clientArg, 'undefined'); - assert.equal(clientArg?.token, 'xwfp-valid'); - }); - - it('should not use xwfp token if the request contains one and attachFunctionToken is false', async () => { - const MockApp = await importApp(); - const app = new MockApp({ - receiver: fakeReceiver, - authorize: noop, - attachFunctionToken: false, - }); - - let clientArg: WebClient | undefined; - app.use(async ({ client }) => { - clientArg = client; - }); - const testData = createDummyCustomFunctionMiddlewareArgs({ options: { autoAcknowledge: false } }); - await fakeReceiver.sendEvent({ ack: testData.ack, body: testData.body }); - - assert.notTypeOf(clientArg, 'undefined'); - assert.equal(clientArg?.token, undefined); - }); }); describe('say()', () => { @@ -1024,7 +986,7 @@ describe('App middleware processing', () => { authorize: sinon.fake.resolves(dummyAuthorizationResult), }); app.use(async ({ ack, next }) => { - if (ack !== noopVoid) { + if (ack) { // this should be called even if app.view listeners do not exist await ack(); return; diff --git a/test/unit/App/routing-function.spec.ts b/test/unit/App/routing-function.spec.ts deleted file mode 100644 index 86bd92cf4..000000000 --- a/test/unit/App/routing-function.spec.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { assert } from 'chai'; -import sinon, { type SinonSpy } from 'sinon'; -import type App from '../../../src/App'; -import { - FakeReceiver, - type Override, - createDummyCustomFunctionMiddlewareArgs, - createFakeLogger, - importApp, - mergeOverrides, - noopMiddleware, - withConversationContext, - withMemoryStore, - withNoopAppMetadata, - withNoopWebClient, -} from '../helpers'; - -function buildOverrides(secondOverrides: Override[]): Override { - return mergeOverrides( - withNoopAppMetadata(), - withNoopWebClient(), - ...secondOverrides, - withMemoryStore(sinon.fake()), - withConversationContext(sinon.fake.returns(noopMiddleware)), - ); -} - -describe('App function() routing', () => { - let fakeReceiver: FakeReceiver; - let fakeHandler: SinonSpy; - const fakeLogger = createFakeLogger(); - let dummyAuthorizationResult: { botToken: string; botId: string }; - let MockApp: Awaited>; - let app: App; - - beforeEach(async () => { - fakeLogger.error.reset(); - fakeReceiver = new FakeReceiver(); - fakeHandler = sinon.fake(); - dummyAuthorizationResult = { botToken: '', botId: '' }; - MockApp = await importApp(buildOverrides([])); - app = new MockApp({ - logger: fakeLogger, - receiver: fakeReceiver, - authorize: sinon.fake.resolves(dummyAuthorizationResult), - }); - }); - describe('for function executed events', () => { - it('should route a function executed event to a handler registered with `function(string)` that matches the callback ID', async () => { - app.function('my_id', fakeHandler); - const args = createDummyCustomFunctionMiddlewareArgs({ - callbackId: 'my_id', - options: { autoAcknowledge: false }, - }); - await fakeReceiver.sendEvent({ - ack: args.ack, - body: args.body, - }); - sinon.assert.called(fakeHandler); - }); - - it('should route a function executed event to a handler with the proper arguments', async () => { - const testInputs = { test: true }; - const testHandler = sinon.spy(async ({ inputs, complete, fail, client }) => { - assert.equal(inputs, testInputs); - assert.typeOf(complete, 'function'); - assert.typeOf(fail, 'function'); - assert.equal(client.token, 'xwfp-valid'); - }); - app.function('my_id', testHandler); - const args = createDummyCustomFunctionMiddlewareArgs({ - callbackId: 'my_id', - inputs: testInputs, - options: { autoAcknowledge: false }, - }); - await fakeReceiver.sendEvent({ - ack: args.ack, - body: args.body, - }); - sinon.assert.called(testHandler); - }); - - it('should route a function executed event to a handler and auto ack by default', async () => { - app.function('my_id', fakeHandler); - const args = createDummyCustomFunctionMiddlewareArgs({ callbackId: 'my_id' }); - let isAck = false; - await fakeReceiver.sendEvent({ - ack: async () => { - isAck = true; - }, - body: args.body, - }); - sinon.assert.called(fakeHandler); - assert.isTrue(isAck); - }); - - it('should route a function executed event to a handler and NOT auto ack if autoAcknowledge is false', async () => { - app.function('my_id', { autoAcknowledge: false }, fakeHandler); - const args = createDummyCustomFunctionMiddlewareArgs({ callbackId: 'my_id' }); - let isAck = false; - await fakeReceiver.sendEvent({ - ack: async () => { - isAck = true; - }, - body: args.body, - }); - sinon.assert.called(fakeHandler); - assert.isFalse(isAck); - }); - }); -}); diff --git a/test/unit/CustomFunction.spec.ts b/test/unit/CustomFunction.spec.ts index ee415cc37..88ca9a0fb 100644 --- a/test/unit/CustomFunction.spec.ts +++ b/test/unit/CustomFunction.spec.ts @@ -1,17 +1,21 @@ import { WebClient } from '@slack/web-api'; import { assert } from 'chai'; +import rewiremock from 'rewiremock'; import sinon from 'sinon'; import { + type AllCustomFunctionMiddlewareArgs, CustomFunction, + type CustomFunctionExecuteMiddlewareArgs, + type CustomFunctionMiddleware, type SlackCustomFunctionMiddlewareArgs, - createFunctionComplete, - createFunctionFail, - matchCallbackId, - validate, } from '../../src/CustomFunction'; import { CustomFunctionInitializationError } from '../../src/errors'; -import { autoAcknowledge, matchEventType, onlyEvents } from '../../src/middleware/builtin'; -import type { Middleware } from '../../src/types'; +import type { AllMiddlewareArgs, Middleware } from '../../src/types'; +import { type Override, createFakeLogger } from './helpers'; + +async function importCustomFunction(overrides: Override = {}): Promise { + return rewiremock.module(() => import('../../src/CustomFunction'), overrides); +} const MOCK_FN = async () => {}; const MOCK_FN_2 = async () => {}; @@ -19,42 +23,65 @@ const MOCK_FN_2 = async () => {}; const MOCK_MIDDLEWARE_SINGLE = [MOCK_FN]; const MOCK_MIDDLEWARE_MULTIPLE = [MOCK_FN, MOCK_FN_2]; -describe('CustomFunction', () => { +describe('CustomFunction class', () => { describe('constructor', () => { it('should accept single function as middleware', async () => { - const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_SINGLE, { autoAcknowledge: true }); + const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_SINGLE, {}); assert.isNotNull(fn); }); it('should accept multiple functions as middleware', async () => { - const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_MULTIPLE, { autoAcknowledge: true }); + const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_MULTIPLE, {}); assert.isNotNull(fn); }); }); - describe('getListeners', () => { - it('should return an ordered array of listeners used to map function events to handlers', async () => { + describe('getMiddleware', () => { + it('should not call next if a function_executed event', async () => { const cbId = 'test_executed_callback_id'; - const fn = new CustomFunction(cbId, MOCK_MIDDLEWARE_SINGLE, { autoAcknowledge: true }); - const listeners = fn.getListeners(); - assert.equal(listeners.length, 5); - assert.equal(listeners[0], onlyEvents); - assert.equal(listeners[1].toString(), matchEventType('function_executed').toString()); - assert.equal(listeners[2].toString(), matchCallbackId(cbId).toString()); - assert.equal(listeners[3], autoAcknowledge); - assert.equal(listeners[4], MOCK_FN); + const fn = new CustomFunction(cbId, MOCK_MIDDLEWARE_SINGLE, {}); + const middleware = fn.getMiddleware(); + const fakeEditArgs = createFakeFunctionExecutedEvent(cbId); + + const fakeNext = sinon.spy(); + fakeEditArgs.next = fakeNext; + + await middleware(fakeEditArgs); + + assert(fakeNext.notCalled, 'next called!'); }); - it('should return a array of listeners without the autoAcknowledge middleware when auto acknowledge is disabled', async () => { - const cbId = 'test_executed_callback_id'; - const fn = new CustomFunction(cbId, MOCK_MIDDLEWARE_SINGLE, { autoAcknowledge: false }); - const listeners = fn.getListeners(); - assert.isFalse(listeners.includes(autoAcknowledge)); + it('should call next if valid custom function but mismatched callback_id', async () => { + const fn = new CustomFunction('bad_executed_callback_id', MOCK_MIDDLEWARE_SINGLE, {}); + const middleware = fn.getMiddleware(); + const fakeEditArgs = createFakeFunctionExecutedEvent(); + + const fakeNext = sinon.spy(); + fakeEditArgs.next = fakeNext; + + await middleware(fakeEditArgs); + + assert(fakeNext.called); + }); + + it('should call next if not a function executed event', async () => { + const fn = new CustomFunction('test_view_callback_id', MOCK_MIDDLEWARE_SINGLE, {}); + const middleware = fn.getMiddleware(); + const fakeViewArgs = createFakeViewEvent() as unknown as SlackCustomFunctionMiddlewareArgs & AllMiddlewareArgs; + + const fakeNext = sinon.spy(); + fakeViewArgs.next = fakeNext; + + await middleware(fakeViewArgs); + + assert(fakeNext.called); }); }); describe('validate', () => { it('should throw an error if callback_id is not valid', async () => { + const { validate } = await importCustomFunction(); + // intentionally casting to string to trigger failure const badId = {} as string; const validationFn = () => validate(badId, MOCK_MIDDLEWARE_SINGLE); @@ -64,8 +91,10 @@ describe('CustomFunction', () => { }); it('should throw an error if middleware is not a function or array', async () => { - // intentionally casting to Middleware[] to trigger failure - const badConfig = '' as unknown as Middleware[]; + const { validate } = await importCustomFunction(); + + // intentionally casting to CustomFunctionMiddleware to trigger failure + const badConfig = '' as unknown as CustomFunctionMiddleware; const validationFn = () => validate('callback_id', badConfig); const expectedMsg = 'CustomFunction expects a function or array of functions as the second argument'; @@ -73,11 +102,10 @@ describe('CustomFunction', () => { }); it('should throw an error if middleware is not a single callback or an array of callbacks', async () => { - // intentionally casting to Middleware[] to trigger failure - const badMiddleware = [ - async () => {}, - 'not-a-function', - ] as unknown as Middleware[]; + const { validate } = await importCustomFunction(); + + // intentionally casting to CustomFunctionMiddleware to trigger failure + const badMiddleware = [async () => {}, 'not-a-function'] as unknown as CustomFunctionMiddleware; const validationFn = () => validate('callback_id', badMiddleware); const expectedMsg = 'All CustomFunction middleware must be functions'; @@ -85,19 +113,66 @@ describe('CustomFunction', () => { }); }); + describe('isFunctionEvent', () => { + it('should return true if recognized function_executed payload type', async () => { + const fakeExecuteArgs = createFakeFunctionExecutedEvent(); + + const { isFunctionEvent } = await importCustomFunction(); + const eventIsFunctionExcuted = isFunctionEvent(fakeExecuteArgs); + + assert.isTrue(eventIsFunctionExcuted); + }); + + it('should return false if not a function_executed payload type', async () => { + const fakeExecutedEvent = createFakeFunctionExecutedEvent(); + // @ts-expect-error expected invalid payload type + fakeExecutedEvent.payload.type = 'invalid_type'; + + const { isFunctionEvent } = await importCustomFunction(); + const eventIsFunctionExecuted = isFunctionEvent(fakeExecutedEvent); + + assert.isFalse(eventIsFunctionExecuted); + }); + }); + + describe('enrichFunctionArgs', () => { + it('should remove next() from all original event args', async () => { + const fakeExecutedEvent = createFakeFunctionExecutedEvent(); + + const { enrichFunctionArgs } = await importCustomFunction(); + const executeFunctionArgs = enrichFunctionArgs(fakeExecutedEvent, {}); + + assert.notExists(executeFunctionArgs.next); + }); + + it('should augment function_executed args with inputs, complete, and fail', async () => { + const fakeArgs = createFakeFunctionExecutedEvent(); + + const { enrichFunctionArgs } = await importCustomFunction(); + const functionArgs = enrichFunctionArgs(fakeArgs, {}); + + assert.exists(functionArgs.inputs); + assert.exists(functionArgs.complete); + assert.exists(functionArgs.fail); + }); + }); + describe('custom function utility functions', () => { describe('`complete` factory function', () => { it('complete should call functions.completeSuccess', async () => { const client = new WebClient('sometoken'); const completeMock = sinon.stub(client.functions, 'completeSuccess').resolves(); - const complete = createFunctionComplete({ isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, client); + const complete = CustomFunction.createFunctionComplete( + { isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, + client, + ); await complete(); assert(completeMock.called, 'client.functions.completeSuccess not called!'); }); it('should throw if no functionExecutionId present on context', () => { const client = new WebClient('sometoken'); assert.throws(() => { - createFunctionComplete({ isEnterpriseInstall: false }, client); + CustomFunction.createFunctionComplete({ isEnterpriseInstall: false }, client); }); }); }); @@ -106,16 +181,119 @@ describe('CustomFunction', () => { it('fail should call functions.completeError', async () => { const client = new WebClient('sometoken'); const completeMock = sinon.stub(client.functions, 'completeError').resolves(); - const complete = createFunctionFail({ isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, client); + const complete = CustomFunction.createFunctionFail( + { isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, + client, + ); await complete({ error: 'boom' }); assert(completeMock.called, 'client.functions.completeError not called!'); }); it('should throw if no functionExecutionId present on context', () => { const client = new WebClient('sometoken'); assert.throws(() => { - createFunctionFail({ isEnterpriseInstall: false }, client); + CustomFunction.createFunctionFail({ isEnterpriseInstall: false }, client); }); }); }); + + it('inputs should map to function payload inputs', async () => { + const fakeExecuteArgs = createFakeFunctionExecutedEvent(); + + const { enrichFunctionArgs } = await importCustomFunction(); + const enrichedArgs = enrichFunctionArgs(fakeExecuteArgs, {}); + + assert.isTrue(enrichedArgs.inputs === fakeExecuteArgs.event.inputs); + }); + }); + + describe('processFunctionMiddleware', () => { + it('should call each callback in user-provided middleware', async () => { + const { ...fakeArgs } = createFakeFunctionExecutedEvent(); + const { processFunctionMiddleware } = await importCustomFunction(); + + const fn1 = sinon.spy((async ({ next: continuation }) => { + await continuation(); + }) as Middleware); + const fn2 = sinon.spy(async () => {}); + const fakeMiddleware = [fn1, fn2] as CustomFunctionMiddleware; + + await processFunctionMiddleware(fakeArgs, fakeMiddleware); + + assert(fn1.called, 'first user-provided middleware not called!'); + assert(fn2.called, 'second user-provided middleware not called!'); + }); }); }); + +function createFakeFunctionExecutedEvent(callbackId?: string): AllCustomFunctionMiddlewareArgs { + const func = { + type: 'function', + id: 'somefunc', + callback_id: callbackId || 'callback_id', + title: 'My dope function', + input_parameters: [], + output_parameters: [], + app_id: 'A1234', + date_created: 123456, + date_deleted: 0, + date_updated: 123456, + }; + const base = { + bot_access_token: 'xoxb-abcd-1234', + event_ts: '123456.789', + function_execution_id: 'Fx1234', + workflow_execution_id: 'Wf1234', + type: 'function_executed', + } as const; + const inputs = { message: 'test123', recipient: 'U012345' }; + const event = { + function: func, + inputs, + ...base, + } as const; + return { + body: { + api_app_id: 'A1234', + event, + event_id: 'E1234', + event_time: 123456, + team_id: 'T1234', + token: 'xoxb-1234', + type: 'event_callback', + }, + client: new WebClient('faketoken'), + complete: () => Promise.resolve({ ok: true }), + context: { + functionBotAccessToken: 'xwfp-123', + functionExecutionId: 'test_executed_callback_id', + isEnterpriseInstall: false, + }, + event, + fail: () => Promise.resolve({ ok: true }), + inputs, + logger: createFakeLogger(), + next: () => Promise.resolve(), + payload: { + function: func, + inputs: { message: 'test123', recipient: 'U012345' }, + ...base, + }, + }; +} + +function createFakeViewEvent() { + return { + body: { + callback_id: 'test_view_callback_id', + trigger_id: 'test_view_trigger_id', + workflow_step: { + workflow_step_edit_id: '', + }, + }, + payload: { + type: 'view_submission', + callback_id: 'test_view_callback_id', + }, + context: {}, + }; +} diff --git a/test/unit/helpers/app.ts b/test/unit/helpers/app.ts index be8ed0a10..0c11335db 100644 --- a/test/unit/helpers/app.ts +++ b/test/unit/helpers/app.ts @@ -58,13 +58,7 @@ export function withNoopWebClient(authTestResponse?: AuthTestResponse): Override test: sinon.fake.resolves(authTestResponse), }; } - : class { - public token?: string; - - public constructor(token?: string, _options?: WebClientOptions) { - this.token = token; - } - }, + : class {}, }, }; } diff --git a/test/unit/helpers/events.ts b/test/unit/helpers/events.ts index d632b6539..9d612024d 100644 --- a/test/unit/helpers/events.ts +++ b/test/unit/helpers/events.ts @@ -17,7 +17,6 @@ import type { AssistantThreadStartedMiddlewareArgs, AssistantUserMessageMiddlewareArgs, } from '../../../src/Assistant'; -import type { SlackCustomFunctionMiddlewareArgs } from '../../../src/CustomFunction'; import type { AckFn, AllMiddlewareArgs, @@ -36,7 +35,6 @@ import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs, SlackEventMiddlewareArgs, - SlackEventMiddlewareArgsOptions, SlackOptionsMiddlewareArgs, SlackShortcutMiddlewareArgs, SlackViewMiddlewareArgs, @@ -210,7 +208,6 @@ export function createDummyAppMentionEventMiddlewareArgs( say, }; } - function enrichDummyAssistantMiddlewareArgs() { return { getThreadContext: sinon.spy(), @@ -275,7 +272,6 @@ export function createDummyAssistantUserMessageEventMiddlewareArgs( ...enrichDummyAssistantMiddlewareArgs(), }; } - interface DummyCommandOverride { command?: string; slashCommand?: SlashCommand; @@ -344,89 +340,6 @@ export function createDummyBlockActionEventMiddlewareArgs( }; } -export function createDummyCustomFunctionMiddlewareArgs< - Options extends SlackEventMiddlewareArgsOptions = { autoAcknowledge: true }, ->( - data: { - callbackId?: string; - inputs?: Record; - options?: Options; - } = { callbackId: 'reverse', inputs: { stringToReverse: 'hello' }, options: { autoAcknowledge: true } as Options }, -): SlackCustomFunctionMiddlewareArgs { - data.callbackId = data.callbackId || 'reverse'; - data.inputs = data.inputs ? data.inputs : { stringToReverse: 'hello' }; - data.options = data.options ? data.options : ({ autoAcknowledge: true } as Options); - const testFunction = { - id: 'Fn111', - callback_id: data.callbackId, - title: data.callbackId, - description: 'Takes a string and reverses it', - type: 'app', - input_parameters: [ - { - type: 'string', - name: 'stringToReverse', - description: 'The string to reverse', - title: 'String To Reverse', - is_required: true, - }, - ], - output_parameters: [ - { - type: 'string', - name: 'reverseString', - description: 'The string in reverse', - title: 'Reverse String', - is_required: true, - }, - ], - app_id: 'A111', - date_updated: 1659054991, - date_deleted: 0, - date_created: 1725987754, - }; - - const event = { - type: 'function_executed', - function: testFunction, - inputs: data.inputs, - function_execution_id: 'Fx111', - workflow_execution_id: 'Wf111', - event_ts: '1659055013.509853', - bot_access_token: 'xwfp-valid', - } as const; - - const body = { - token: 'verification_token', - team_id: 'T111', - api_app_id: 'A111', - event, - event_id: 'Ev111', - event_time: 1659055013, - type: 'event_callback', - } as const; - - if (data.options.autoAcknowledge) { - return { - body, - complete: () => Promise.resolve({ ok: true }), - event, - fail: () => Promise.resolve({ ok: true }), - inputs: data.inputs, - payload: event, - } as SlackCustomFunctionMiddlewareArgs; - } - return { - ack: () => Promise.resolve(), - body, - complete: () => Promise.resolve({ ok: true }), - event, - fail: () => Promise.resolve({ ok: true }), - inputs: data.inputs, - payload: event, - }; -} - interface DummyBlockSuggestionOverride { action_id?: string; block_id?: string; diff --git a/test/unit/middleware/builtin.spec.ts b/test/unit/middleware/builtin.spec.ts index fd8b35643..1567f9ffc 100644 --- a/test/unit/middleware/builtin.spec.ts +++ b/test/unit/middleware/builtin.spec.ts @@ -2,15 +2,9 @@ import { assert } from 'chai'; import rewiremock from 'rewiremock'; import sinon from 'sinon'; -import { expectType } from 'tsd'; import { ErrorCode } from '../../../src/errors'; -import { isSlackEventMiddlewareArgsOptions } from '../../../src/middleware/builtin'; // import { matchCommandName, matchEventType, onlyCommands, onlyEvents, subtype } from '../../../src/middleware/builtin'; -import type { - Context, - /* NextFn, */ SlackEventMiddlewareArgs, - SlackEventMiddlewareArgsOptions, -} from '../../../src/types'; +import type { Context, /* NextFn, */ SlackEventMiddlewareArgs } from '../../../src/types'; import { type Override, createDummyAppHomeOpenedEventMiddlewareArgs, @@ -417,25 +411,4 @@ describe('Built-in global middleware', () => { }); }); }); - - describe(isSlackEventMiddlewareArgsOptions.name, () => { - it('should return true if object is SlackEventMiddlewareArgsOptions', async () => { - const actual = isSlackEventMiddlewareArgsOptions({ autoAcknowledge: true }); - assert.isTrue(actual); - }); - - it('should narrow proper type if object is SlackEventMiddlewareArgsOptions', async () => { - const option = { autoAcknowledge: true }; - if (isSlackEventMiddlewareArgsOptions({ autoAcknowledge: true })) { - expectType(option); - } else { - assert.fail(`${option} should be of type SlackEventMiddlewareArgsOption`); - } - }); - - it('should return false if object is Middleware', async () => { - const actual = isSlackEventMiddlewareArgsOptions(async () => {}); - assert.isFalse(actual); - }); - }); });