UNPKG

@slack/bolt

Version:

A framework for building Slack apps, fast.

216 lines 8.99 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.hasStringProperty = exports.isRecord = exports.extractEventChannelId = exports.extractEventTs = exports.extractEventThreadTs = exports.assertNever = exports.isEventTypeToSkipAuthorize = exports.isBodyWithTypeEnterpriseInstall = exports.getTypeAndConversation = exports.IncomingEventType = void 0; /** * Internal data type for capturing the class of event processed in App#onIncomingEvent() */ var IncomingEventType; (function (IncomingEventType) { IncomingEventType[IncomingEventType["Event"] = 0] = "Event"; IncomingEventType[IncomingEventType["Action"] = 1] = "Action"; IncomingEventType[IncomingEventType["Command"] = 2] = "Command"; IncomingEventType[IncomingEventType["Options"] = 3] = "Options"; IncomingEventType[IncomingEventType["ViewAction"] = 4] = "ViewAction"; IncomingEventType[IncomingEventType["Shortcut"] = 5] = "Shortcut"; })(IncomingEventType || (exports.IncomingEventType = IncomingEventType = {})); // ---------------------------- // For skipping authorize with event const eventTypesToSkipAuthorize = ['app_uninstalled', 'tokens_revoked']; /** * Helper which finds the type and channel (if any) that any specific incoming event is related to. * * This is analogous to WhenEventHasChannelContext and the conditional type that checks SlackAction for a channel * context. */ // biome-ignore lint/suspicious/noExplicitAny: response bodies can be anything function getTypeAndConversation(body) { if (body.event !== undefined) { const { event } = body; // Find conversationId const conversationId = (() => { let foundConversationId; if ('channel' in event) { if (typeof event?.channel === 'string') { foundConversationId = event.channel; } else if (typeof event?.channel === 'object' && 'id' in event.channel) { foundConversationId = event.channel.id; } } if ('channel_id' in event) { foundConversationId = event.channel_id; } if ('item' in event && 'channel' in event.item) { // no channel for reaction_added, reaction_removed, star_added, or star_removed with file or file_comment items foundConversationId = event.item.channel; } // Using non-null assertion (!) because the alternative is to use `foundConversation: (string | undefined)`, which // impedes the very useful type checker help above that ensures the value is only defined to strings, not // undefined. This is safe when used in combination with the || operator with a default value. // biome-ignore lint/style/noNonNullAssertion: TODO: revisit this and use the types return foundConversationId || undefined; })(); return { conversationId, type: IncomingEventType.Event, }; } if (body.command !== undefined) { return { type: IncomingEventType.Command, conversationId: body.channel_id, }; } if (body.name !== undefined || body.type === 'block_suggestion') { const optionsBody = body; return { type: IncomingEventType.Options, conversationId: optionsBody.channel !== undefined ? optionsBody.channel.id : undefined, }; } // TODO: remove workflow_step stuff in v5 if (body.actions !== undefined || body.type === 'dialog_submission' || body.type === 'workflow_step_edit') { const actionBody = body; return { type: IncomingEventType.Action, conversationId: actionBody.channel !== undefined ? actionBody.channel.id : undefined, }; } if (body.type === 'shortcut') { return { type: IncomingEventType.Shortcut, }; } if (body.type === 'message_action') { const shortcutBody = body; return { type: IncomingEventType.Shortcut, conversationId: shortcutBody.channel !== undefined ? shortcutBody.channel.id : undefined, }; } if (body.type === 'view_submission' || body.type === 'view_closed') { return { type: IncomingEventType.ViewAction, }; } return {}; } exports.getTypeAndConversation = getTypeAndConversation; /** * Helper which determines if the body of a request is enterprise install. * * Providing the type is optional but if you do the execution will be faster */ function isBodyWithTypeEnterpriseInstall(body, type) { const _type = type !== undefined ? type : getTypeAndConversation(body).type; if (_type === IncomingEventType.Event) { const bodyAsEvent = body; if (Array.isArray(bodyAsEvent.authorizations) && bodyAsEvent.authorizations[0] !== undefined) { return !!bodyAsEvent.authorizations[0].is_enterprise_install; } } // command payloads have this property set as a string if (typeof body.is_enterprise_install === 'string') { return body.is_enterprise_install === 'true'; } // all remaining types have a boolean property if (body.is_enterprise_install !== undefined) { return body.is_enterprise_install; } // as a fallback we assume it's a single team installation (but this should never happen) return false; } exports.isBodyWithTypeEnterpriseInstall = isBodyWithTypeEnterpriseInstall; /** * Helper which determines if the event type will skip Authorize. * * Token revocation use cases * https://github.com/slackapi/bolt-js/issues/674 */ function isEventTypeToSkipAuthorize(event) { return eventTypesToSkipAuthorize.includes(event.body.event?.type); } exports.isEventTypeToSkipAuthorize = isEventTypeToSkipAuthorize; /** Helper that should never be called, but is useful for exhaustiveness checking in conditional branches */ function assertNever(x) { throw new Error(`Unexpected object: ${x}`); } exports.assertNever = assertNever; /** * Extracts thread_ts from the event payload, checking common locations where it may appear. */ function extractEventThreadTs(event) { if (hasStringProperty(event, 'thread_ts')) { return event.thread_ts; } if ('assistant_thread' in event && hasStringProperty(event.assistant_thread, 'thread_ts')) { return event.assistant_thread.thread_ts; } if ('message' in event && hasStringProperty(event.message, 'thread_ts')) { return event.message.thread_ts; } if ('previous_message' in event && hasStringProperty(event.previous_message, 'thread_ts')) { return event.previous_message.thread_ts; } return undefined; } exports.extractEventThreadTs = extractEventThreadTs; /** * Extracts ts from the event payload. */ function extractEventTs(event) { if (hasStringProperty(event, 'ts')) { return event.ts; } return undefined; } exports.extractEventTs = extractEventTs; /** * Extracts the channel ID from the event payload, checking common locations where it may appear. * * TODO: When ready use this in getTypeAndConversation * Note: this intentionally prefers channel (string) > channel (object.id) > channel_id > item.channel > assistant_thread.channel_id, * which differs from getTypeAndConversation where channel_id overwrites channel. Align when consolidating. */ function extractEventChannelId(event) { if (hasStringProperty(event, 'channel')) { return event.channel; } if ('channel' in event && hasStringProperty(event.channel, 'id')) { return event.channel.id; } if (hasStringProperty(event, 'channel_id')) { return event.channel_id; } if ('item' in event && hasStringProperty(event.item, 'channel')) { return event.item.channel; } if ('assistant_thread' in event && hasStringProperty(event.assistant_thread, 'channel_id')) { return event.assistant_thread.channel_id; } return undefined; } exports.extractEventChannelId = extractEventChannelId; /** * Type guard that narrows an unknown value to a record (non-null object). * @example * isRecord({ key: 'value' }) // true * isRecord(null) // false * isRecord('string') // false */ function isRecord(value) { return typeof value === 'object' && value !== null; } exports.isRecord = isRecord; /** * Type guard that checks whether an object contains a specific key with a string value. * @example * hasStringProperty({ channel: 'C123' }, 'channel') // true * hasStringProperty({ count: 42 }, 'count') // false (not a string) * hasStringProperty({}, 'channel') // false (key missing) */ function hasStringProperty(obj, key) { return isRecord(obj) && key in obj && typeof obj[key] === 'string'; } exports.hasStringProperty = hasStringProperty; //# sourceMappingURL=helpers.js.map