UNPKG

@slack/bolt

Version:

A framework for building Slack apps, fast.

285 lines 12.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.extractThreadInfo = exports.processAssistantMiddleware = exports.validate = exports.isAssistantMessage = exports.matchesConstraints = exports.isAssistantEvent = exports.enrichAssistantArgs = exports.Assistant = void 0; const AssistantThreadContextStore_1 = require("./AssistantThreadContextStore"); const errors_1 = require("./errors"); const process_1 = __importDefault(require("./middleware/process")); /** Constants */ const ASSISTANT_PAYLOAD_TYPES = new Set(['assistant_thread_started', 'assistant_thread_context_changed', 'message']); class Assistant { threadContextStore; /** 'assistant_thread_started' */ threadStarted; /** 'assistant_thread_context_changed' */ threadContextChanged; /** 'message' */ userMessage; constructor(config) { validate(config); const { threadContextStore = new AssistantThreadContextStore_1.DefaultThreadContextStore(), threadStarted, // When `threadContextChanged` method is not provided, fallback to // AssistantContextStore's save method. If a custom store has also not // been provided, the default save context-via-metadata approach is used. // See DefaultThreadContextStore for details of this implementation. threadContextChanged = (args) => threadContextStore.save(args), userMessage, } = config; this.threadContextStore = threadContextStore; this.threadStarted = Array.isArray(threadStarted) ? threadStarted : [threadStarted]; this.threadContextChanged = Array.isArray(threadContextChanged) ? threadContextChanged : [threadContextChanged]; this.userMessage = Array.isArray(userMessage) ? userMessage : [userMessage]; } getMiddleware() { return async (args) => { if (isAssistantEvent(args) && matchesConstraints(args)) { return this.processEvent(args); } return args.next(); }; } async processEvent(args) { const { payload } = args; const assistantArgs = enrichAssistantArgs(this.threadContextStore, args); const assistantMiddleware = this.getAssistantMiddleware(payload); return processAssistantMiddleware(assistantArgs, assistantMiddleware); } /** * `getAssistantMiddleware()` returns the Assistant instance's middleware */ getAssistantMiddleware(payload) { switch (payload.type) { case 'assistant_thread_started': return this.threadStarted; case 'assistant_thread_context_changed': return this.threadContextChanged; case 'message': return this.userMessage; default: return []; } } } exports.Assistant = Assistant; /** * `enrichAssistantArgs()` takes the event arguments and: * 1. Removes the next() passed in from App-level middleware processing, thus preventing * events from continuing down the global middleware chain to subsequent listeners * 2. Adds assistant-specific utilities (i.e., helper methods) * */ function enrichAssistantArgs(threadContextStore, args) { const { next: _next, ...assistantArgs } = args; const preparedArgs = { ...assistantArgs }; // Do not pass preparedArgs (ie, do not add utilities to get/save) preparedArgs.getThreadContext = () => threadContextStore.get(args); preparedArgs.saveThreadContext = () => threadContextStore.save(args); preparedArgs.say = createSay(preparedArgs); preparedArgs.setStatus = createSetStatus(preparedArgs); preparedArgs.setSuggestedPrompts = createSetSuggestedPrompts(preparedArgs); preparedArgs.setTitle = createSetTitle(preparedArgs); return preparedArgs; } exports.enrichAssistantArgs = enrichAssistantArgs; /** * `isAssistantEvent()` determines if incoming event is a supported * Assistant event type. */ function isAssistantEvent(args) { return ASSISTANT_PAYLOAD_TYPES.has(args.payload.type); } exports.isAssistantEvent = isAssistantEvent; /** * `matchesConstraints()` determines if the incoming event payload * is related to the Assistant. */ function matchesConstraints(args) { return args.payload.type === 'message' ? isAssistantMessage(args.payload) : true; } exports.matchesConstraints = matchesConstraints; /** * `isAssistantMessage()` evaluates if the message payload is associated * with the Assistant container. */ function isAssistantMessage(payload) { const isThreadMessage = 'channel' in payload && 'thread_ts' in payload; const inAssistantContainer = 'channel_type' in payload && payload.channel_type === 'im' && (!('subtype' in payload) || payload.subtype === 'file_share' || payload.subtype === undefined); // TODO: undefined subtype is a limitation of message event, needs fixing (see https://github.com/slackapi/node-slack-sdk/issues/1904) return isThreadMessage && inAssistantContainer; } exports.isAssistantMessage = isAssistantMessage; /** * `validate()` determines if the provided AssistantConfig is a valid configuration. */ function validate(config) { // Ensure assistant config object is passed in if (typeof config !== 'object') { const errorMsg = 'Assistant expects a configuration object as the argument'; throw new errors_1.AssistantInitializationError(errorMsg); } // Check for missing required keys const requiredKeys = ['threadStarted', 'userMessage']; const missingKeys = []; for (const key of requiredKeys) { if (config[key] === undefined) missingKeys.push(key); } if (missingKeys.length > 0) { const errorMsg = `Assistant is missing required keys: ${missingKeys.join(', ')}`; throw new errors_1.AssistantInitializationError(errorMsg); } // Ensure a callback or an array of callbacks is present const requiredFns = ['threadStarted', 'userMessage']; if ('threadContextChanged' in config) requiredFns.push('threadContextChanged'); for (const fn of requiredFns) { if (typeof config[fn] !== 'function' && !Array.isArray(config[fn])) { const errorMsg = `Assistant ${fn} property must be a function or an array of functions`; throw new errors_1.AssistantInitializationError(errorMsg); } } // Validate threadContextStore if (config.threadContextStore) { // Ensure assistant config object is passed in if (typeof config.threadContextStore !== 'object') { const errorMsg = 'Assistant expects threadContextStore to be a configuration object'; throw new errors_1.AssistantInitializationError(errorMsg); } // Check for missing required keys const requiredContextKeys = ['get', 'save']; const missingContextKeys = []; for (const k of requiredContextKeys) { if (config.threadContextStore && config.threadContextStore[k] === undefined) { missingContextKeys.push(k); } } if (missingContextKeys.length > 0) { const errorMsg = `threadContextStore is missing required keys: ${missingContextKeys.join(', ')}`; throw new errors_1.AssistantInitializationError(errorMsg); } // Ensure properties of context store are functions const requiredStoreFns = ['get', 'save']; for (const fn of requiredStoreFns) { if (config.threadContextStore && typeof config.threadContextStore[fn] !== 'function') { const errorMsg = `threadContextStore ${fn} property must be a function`; throw new errors_1.AssistantInitializationError(errorMsg); } } } } exports.validate = validate; /** * `processAssistantMiddleware()` invokes each callback for the given event */ async function processAssistantMiddleware(args, middleware) { const { context, client, logger } = args; const callbacks = [...middleware]; const lastCallback = callbacks.pop(); if (lastCallback !== undefined) { await (0, process_1.default)(callbacks, args, context, client, logger, async () => lastCallback({ ...args, context, client, logger })); } } exports.processAssistantMiddleware = processAssistantMiddleware; /** * Utility functions */ /** * Creates utility `say()` to easily respond to wherever the message * was received. Alias for `postMessage()`. * https://api.slack.com/methods/chat.postMessage */ function createSay(args) { const { client, payload } = args; const { channelId: channel, threadTs: thread_ts, context } = extractThreadInfo(payload); return async (message) => { const threadContext = context.channel_id ? context : await args.getThreadContext(); const postMessageArgument = typeof message === 'string' ? { text: message, channel, thread_ts } : { ...message, channel, thread_ts }; if (threadContext) { postMessageArgument.metadata = { event_type: 'assistant_thread_context', event_payload: threadContext, }; } return client.chat.postMessage(postMessageArgument); }; } /** * Creates utility `setStatus()` to set the status and indicate active processing. * https://api.slack.com/methods/assistant.threads.setStatus */ function createSetStatus(args) { const { client, payload } = args; const { channelId: channel_id, threadTs: thread_ts } = extractThreadInfo(payload); return (status) => client.assistant.threads.setStatus({ channel_id, thread_ts, status, }); } /** * Creates utility `setSuggestedPrompts()` to provides prompts for the user to select from. * https://api.slack.com/methods/assistant.threads.setSuggestedPrompts */ function createSetSuggestedPrompts(args) { const { client, payload } = args; const { channelId: channel_id, threadTs: thread_ts } = extractThreadInfo(payload); return (params) => { const { prompts, title } = params; return client.assistant.threads.setSuggestedPrompts({ channel_id, thread_ts, prompts, title, }); }; } /** * Creates utility `setTitle()` to set the title of the Assistant thread * https://api.slack.com/methods/assistant.threads.setTitle */ function createSetTitle(args) { const { client, payload } = args; const { channelId: channel_id, threadTs: thread_ts } = extractThreadInfo(payload); return (title) => client.assistant.threads.setTitle({ channel_id, thread_ts, title, }); } /** * `extractThreadInfo()` parses an incoming payload and returns relevant * details about the thread */ function extractThreadInfo(payload) { let channelId = ''; let threadTs = ''; let context = {}; // assistant_thread_started, asssistant_thread_context_changed if ('assistant_thread' in payload && 'channel_id' in payload.assistant_thread && 'thread_ts' in payload.assistant_thread) { channelId = payload.assistant_thread.channel_id; threadTs = payload.assistant_thread.thread_ts; context = payload.assistant_thread.context; } // user message in thread if ('channel' in payload && 'thread_ts' in payload && payload.thread_ts !== undefined) { channelId = payload.channel; threadTs = payload.thread_ts; } // throw error if `channel` or `thread_ts` are missing if (!channelId || !threadTs) { const missingProps = []; if (!channelId) missingProps.push('channel_id'); if (!threadTs) missingProps.push('thread_ts'); if (missingProps.length > 0) { const errorMsg = `Assistant message event is missing required properties: ${missingProps.join(', ')}`; throw new errors_1.AssistantMissingPropertyError(errorMsg); } } return { channelId, threadTs, context }; } exports.extractThreadInfo = extractThreadInfo; //# sourceMappingURL=Assistant.js.map