UNPKG

@microsoft/teams-ai

Version:

SDK focused on building AI based applications for Microsoft Teams.

1,005 lines 48.3 kB
"use strict"; /** * @module teams-ai */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Application = void 0; const botbuilder_1 = require("botbuilder"); const AdaptiveCards_1 = require("./AdaptiveCards"); const AI_1 = require("./AI"); const Meetings_1 = require("./Meetings"); const Messages_1 = require("./Messages"); const MessageExtensions_1 = require("./MessageExtensions"); const TaskModules_1 = require("./TaskModules"); const Authentication_1 = require("./authentication/Authentication"); const TurnState_1 = require("./TurnState"); const BotAuthenticationBase_1 = require("./authentication/BotAuthenticationBase"); /** * @private */ const TYPING_TIMER_DELAY = 1000; /** * Application class for routing and processing incoming requests. * @remarks * The Application object replaces the traditional ActivityHandler that a bot would use. It supports * a simpler fluent style of authoring bots versus the inheritance based approach used by the * ActivityHandler class. * * Additionally, it has built-in support for calling into the SDK's AI system and can be used to create * bots that leverage Large Language Models (LLM) and other AI capabilities. * @template TState Optional. Type of the turn state. This allows for strongly typed access to the turn state. */ class Application { _options; _routes = []; _invokeRoutes = []; _adaptiveCards; _meetings; _messages; _messageExtensions; _taskModules; _ai; _beforeTurn = []; _afterTurn = []; _authentication; _adapter; _typingTimer; _startSignIn; /** * Creates a new Application instance. * @param {ApplicationOptions<TState>} options Optional. Options used to configure the application. */ constructor(options) { this._options = { ...options, turnStateFactory: options?.turnStateFactory || (() => new TurnState_1.TurnState()), removeRecipientMention: options?.removeRecipientMention !== undefined ? options.removeRecipientMention : true, startTypingTimer: options?.startTypingTimer !== undefined ? options.startTypingTimer : true, longRunningMessages: options?.longRunningMessages !== undefined ? options.longRunningMessages : false }; // Create Adapter if (this._options.adapter) { this._adapter = this._options.adapter; } // Create AI component if configured with a planner if (this._options.ai) { this._ai = new AI_1.AI(this._options.ai); } // Create OAuthPrompt if configured if (this._options.authentication) { this._startSignIn = createSignInSelector(this._options.authentication.autoSignIn); this._authentication = new Authentication_1.AuthenticationManager(this, this._options.authentication, this._options.storage); } this._adaptiveCards = new AdaptiveCards_1.AdaptiveCards(this); this._messages = new Messages_1.Messages(this); this._messageExtensions = new MessageExtensions_1.MessageExtensions(this); this._meetings = new Meetings_1.Meetings(this); this._taskModules = new TaskModules_1.TaskModules(this); // Validate long running messages configuration if (this._options.longRunningMessages && !this._adapter && !this._options.botAppId) { throw new Error(`The Application.longRunningMessages property is unavailable because no adapter or botAppId was configured.`); } } /** * Fluent interface for accessing Adaptive Card specific features. * @returns {AdaptiveCards<TState>} The AdaptiveCards instance. */ get adaptiveCards() { return this._adaptiveCards; } /** * The bot's adapter. * @returns {BotAdapter} The bot's adapter that is configured for the application. */ get adapter() { if (!this._adapter) { throw new Error(`The Application.adapter property is unavailable because it was not configured when creating the Application.`); } return this._adapter; } /** * Fluent interface for accessing AI specific features. * @remarks * This property is only available if the Application was configured with `ai` options. An * exception will be thrown if you attempt to access it otherwise. * @returns {AI<TState>} The AI instance. */ get ai() { if (!this._ai) { throw new Error(`The Application.ai property is unavailable because no AI options were configured.`); } return this._ai; } /** * @template TState * Fluent interface for accessing Authentication specific features. * @description * This property is only available if the Application was configured with `authentication` options. An * exception will be thrown if you attempt to access it otherwise. * @returns {AuthenticationManager<TState>} The Authentication instance. */ get authentication() { if (!this._authentication) { throw new Error(`The Application.authentication property is unavailable because no authentication options were configured.`); } return this._authentication; } /** * Fluent interface for accessing Messages specific features. * @returns {Messages<TState>} The Messages instance. */ get messages() { return this._messages; } /** * Fluent interface for accessing Message Extensions' specific features. * @returns {MessageExtensions<TState>} The MessageExtensions instance. */ get messageExtensions() { return this._messageExtensions; } /** * Fluent interface for accessing Meetings specific features. * @returns {Meetings<TState>} The Meetings instance. */ get meetings() { return this._meetings; } /** * The application's configured options. * @returns {ApplicationOptions<TState>} The application's configured options. */ get options() { return this._options; } /** * Fluent interface for accessing Task Module specific features. * @returns {TaskModules<TState>} The TaskModules instance. */ get taskModules() { return this._taskModules; } /** * Sets the bot's error handler * @param {Function} handler Function to call when an error is encountered. * @returns {this} The application instance for chaining purposes. */ error(handler) { if (this._adapter) { this._adapter.onTurnError = handler; } return this; } /** * Adds a new route to the application. * @remarks * Developers won't typically need to call this method directly as it's used internally by all * of the fluent interfaces to register routes for their specific activity types. * * Routes will be matched in the order they're added to the application. The first selector to * return `true` when an activity is received will have its handler called. * * Invoke-based activities receive special treatment and are matched separately as they typically * have shorter execution timeouts. * @param {RouteSelector} selector Function thats used to select a route. The function should return true to trigger the route. * @param {RouteHandler<TState>} handler Function to call when the route is triggered. * @param {boolean} isInvokeRoute Optional. Boolean indicating if the RouteSelector is for an activity that uses "invoke" which require special handling. Defaults to `false`. * @returns {this} The application instance for chaining purposes. */ addRoute(selector, handler, isInvokeRoute = false) { if (isInvokeRoute) { this._invokeRoutes.push({ selector, handler }); } else { this._routes.push({ selector, handler }); } return this; } /** * Handles incoming activities of a given type. * @param {string | RegExp | RouteSelector | string[] | RegExp[] | RouteSelector[]} type Name of the activity type to match or a regular expression to match against the incoming activity type. An array of type names or expression can also be passed in. * @param {(context: TurnContext, state: TState) => Promise<void>} handler Function to call when the route is triggered. * @param {TurnContext} handler.context The context object for the turn. * @param {TState} handler.state The state object for the turn. * @returns {this} The application instance for chaining purposes. */ activity(type, handler) { (Array.isArray(type) ? type : [type]).forEach((t) => { const selector = createActivitySelector(t); this.addRoute(selector, handler); }); return this; } /** * Handles conversation update events. * @param {ConversationUpdateEvents} event Name of the conversation update event to handle. * @param {(context: TurnContext, state: TState) => Promise<void>} handler Function to call when the route is triggered. * @param {TurnContext} handler.context The context object for the turn. * @param {TState} handler.state The state object for the turn. * @returns {this} The application instance for chaining purposes. */ conversationUpdate(event, handler) { if (typeof handler !== 'function') { throw new Error(`ConversationUpdate 'handler' for ${event} is ${typeof handler}. Type of 'handler' must be a function.`); } const selector = createConversationUpdateSelector(event); this.addRoute(selector, handler); return this; } messageEventUpdate(event, handler) { if (typeof handler !== 'function') { throw new Error(`MessageUpdate 'handler' for ${event} is ${typeof handler}. Type of 'handler' must be a function.`); } const selector = createMessageEventUpdateSelector(event); this.addRoute(selector, handler); return this; } async continueConversationAsync(context, logic) { if (!this._adapter) { throw new Error(`You must configure the Application with an 'adapter' before calling Application.continueConversationAsync()`); } if (!this.options.botAppId) { console.warn(`Calling Application.continueConversationAsync() without a configured 'botAppId'. In production environments a 'botAppId' is required.`); } // Identify conversation reference const reference = getConversationReference(context); await this.adapter.continueConversationAsync(this._options.botAppId ?? '', reference, logic); } /** * Handles incoming messages with a given keyword. * @remarks * This method provides a simple way to have a bot respond anytime a user sends your bot a * message with a specific word or phrase. * * For example, you can easily clear the current conversation anytime a user sends "/reset": * * ```JavaScript * bot.message('/reset', async (context, state) => { * await state.conversation.delete(); * await context.sendActivity(`I have reset your state.`); * }); * ``` * @param {string | RegExp | RouteSelector | string[] | RegExp[] | RouteSelector[]} keyword Substring of text or a regular expression to match against the text of an incoming message. An array of keywords or expression can also be passed in. * @param {(context: TurnContext, state: TState) => Promise<void>} handler Function to call when the route is triggered. * @returns {this} The application instance for chaining purposes. */ message(keyword, handler) { (Array.isArray(keyword) ? keyword : [keyword]).forEach((k) => { const selector = createMessageSelector(k); this.addRoute(selector, handler); }); return this; } /** * Handles message reaction events. * @param {MessageReactionEvents | MessageReactionEvents[]} event Name of the message reaction event(s) to handle. * @param {(context: TurnContext, state: TState) => Promise<void>} handler Function to call when the route is triggered. * @returns {this} The application instance for chaining purposes. */ messageReactions(event, handler) { const selector = createMessageReactionSelector(event); this.addRoute(selector, handler); return this; } /** * Registers a handler to process when a file consent card is accepted by the user. * @param {(context: TurnContext, state: TState, fileConsentResponse: FileConsentCardResponse) => Promise<void>} handler Function to call when the route is triggered. * @returns {this} The application instance for chaining purposes. */ fileConsentAccept(handler) { const selector = (context) => { return Promise.resolve(context.activity.type === botbuilder_1.ActivityTypes.Invoke && context.activity.name === 'fileConsent/invoke' && context.activity.value?.action === 'accept'); }; const handlerWrapper = async (context, state) => { await handler(context, state, context.activity.value); await context.sendActivity({ type: botbuilder_1.ActivityTypes.InvokeResponse, value: { status: 200 } }); }; this.addRoute(selector, handlerWrapper, true); return this; } /** * Registers a handler to process when a file consent card is declined by the user. * @param {(context: TurnContext, state: TState, fileConsentResponse: FileConsentCardResponse) => Promise<void>} handler Function to call when the route is triggered. * @returns {this} The application instance for chaining purposes. */ fileConsentDecline(handler) { const selector = (context) => { return Promise.resolve(context.activity.type === botbuilder_1.ActivityTypes.Invoke && context.activity.name === 'fileConsent/invoke' && context.activity.value?.action === 'decline'); }; const handlerWrapper = async (context, state) => { await handler(context, state, context.activity.value); await context.sendActivity({ type: botbuilder_1.ActivityTypes.InvokeResponse, value: { status: 200 } }); }; this.addRoute(selector, handlerWrapper, true); return this; } /** * Registers a handler to process when a O365 Connector Card Action activity is received from the user. * @param {(context: TurnContext, state: TState, query: O365ConnectorCardActionQuery) => Promise<void>} handler Function to call when the route is triggered. * @returns {this} The application instance for chaining purposes. */ O365ConnectorCardAction(handler) { const selector = (context) => { return Promise.resolve(context.activity.type === botbuilder_1.ActivityTypes.Invoke && context.activity.name === 'actionableMessage/executeAction'); }; const handlerWrapper = (context, state) => { return handler(context, state, context.activity.value); }; this.addRoute(selector, handlerWrapper, true); return this; } /** * Registers a handler to handoff conversations from one copilot to another. * @param {(context: TurnContext, state: TState, continuation: string) => Promise<void>} handler Function to call when the route is triggered. * @returns {this} The application instance for chaining purposes. */ handoff(handler) { const selector = (context) => { return Promise.resolve(context.activity.type === botbuilder_1.ActivityTypes.Invoke && context.activity.name === 'handoff/action'); }; const handlerWrapper = async (context, state) => { await handler(context, state, context.activity.value.continuation); await context.sendActivity({ type: botbuilder_1.ActivityTypes.InvokeResponse, value: { status: 200 } }); }; this.addRoute(selector, handlerWrapper, true); return this; } /** * Registers a handler for feedbackloop events when a user clicks the thumbsup or thumbsdown button on a response from AI. enable_feedback_loop must be set to true in the AI Module. * @param {(context: TurnContext, state: TState, feedbackLoopData: FeedbackLoopData) => Promise<void>} handler - Function to call when the route is triggered * @returns {this} The application instance for chaining purposes. */ feedbackLoop(handler) { const selector = (context) => { return Promise.resolve(context.activity.type === botbuilder_1.ActivityTypes.Invoke && context.activity.name === 'message/submitAction' && context.activity.value.actionName === 'feedback'); }; const handlerWrapper = async (context, state) => { const feedback = { ...context.activity.value, replyToId: context.activity.replyToId }; await handler(context, state, feedback); await context.sendActivity({ type: botbuilder_1.ActivityTypes.InvokeResponse, value: { status: 200 } }); }; this.addRoute(selector, handlerWrapper, true); return this; } /** * Dispatches an incoming activity to a handler registered with the application. * @remarks * This method should be called from your bot's "turn handler" (its primary message handler) * * ```JavaScript * server.post('/api/messages', async (req, res) => { * await adapter.processActivity(req, res, async (context) => { * await bot.run(context); * }); * }); * ``` * @param {TurnContext} turnContext Context class for the current turn of conversation with the user. * @returns {Promise<boolean>} True if the activity was successfully dispatched to a handler. False if no matching handlers could be found. */ async run(turnContext) { return await this.startLongRunningCall(turnContext, async (context) => { // Start typing indicator timer this.startTypingTimer(context); try { // Remove @mentions if (this._options.removeRecipientMention && context.activity.type == botbuilder_1.ActivityTypes.Message) { context.activity.text = botbuilder_1.TurnContext.removeRecipientMention(context.activity); } // Load turn state const { storage, turnStateFactory } = this._options; const state = turnStateFactory(); await state.load(context, storage); // Sign the user in // If user is in sign in flow, return the authentication setting name let settingName = (0, BotAuthenticationBase_1.userInSignInFlow)(state); if (this._authentication && ((await this._startSignIn?.(context)) || settingName)) { if (!settingName) { // user was not in a sign in flow, but auto-sign in is enabled settingName = this.authentication.default; } // Sets the setting name in the context object. It is used in `signIn/verifyState` & `signIn/tokenExchange` route selectors. (0, BotAuthenticationBase_1.setSettingNameInContextActivityValue)(context, settingName); const response = await this._authentication.signUserIn(context, state, settingName); if (response.status == 'complete') { (0, BotAuthenticationBase_1.deleteUserInSignInFlow)(state); } if (response.status == 'pending') { // Save turn state // - Save authentication status in turn state await state.save(context, storage); return false; } // Invalid activities should be ignored for auth if (response.status == 'error' && response.cause != 'invalidActivity') { (0, BotAuthenticationBase_1.deleteUserInSignInFlow)(state); throw response.error; } } // Call beforeTurn event handlers if (!(await this.callEventHandlers(context, state, this._beforeTurn))) { // Save turn state // - This lets the bot keep track of why it ended the previous turn. It also // allows the dialog system to be used before the AI system is called. await state.save(context, storage); return false; } // Populate {{$temp.input}} if (typeof state.temp.input != 'string') { // Use the received activity text state.temp.input = context.activity.text; } // Download any input files if (Array.isArray(this._options.fileDownloaders) && this._options.fileDownloaders.length > 0) { const inputFiles = state.temp.inputFiles ?? []; for (let i = 0; i < this._options.fileDownloaders.length; i++) { const files = await this._options.fileDownloaders[i].downloadFiles(context, state); inputFiles.push(...files); } state.temp.inputFiles = inputFiles; } // Initialize {{$allOutputs}} if (state.temp.actionOutputs == undefined) { state.temp.actionOutputs = {}; } // Run any RouteSelectors in this._invokeRoutes first if the incoming Teams activity.type is "Invoke". // Invoke Activities from Teams need to be responded to in less than 5 seconds. if (context.activity.type === botbuilder_1.ActivityTypes.Invoke) { for (let i = 0; i < this._invokeRoutes.length; i++) { const route = this._invokeRoutes[i]; if (await route.selector(context)) { // Execute route handler await route.handler(context, state); // Call afterTurn event handlers if (await this.callEventHandlers(context, state, this._afterTurn)) { // Save turn state await state.save(context, storage); } // End dispatch return true; } } } // All other ActivityTypes and any unhandled Invokes are run through the remaining routes. for (let i = 0; i < this._routes.length; i++) { const route = this._routes[i]; if (await route.selector(context)) { // Execute route handler await route.handler(context, state); // Call afterTurn event handlers if (await this.callEventHandlers(context, state, this._afterTurn)) { // Save turn state await state.save(context, storage); } // End dispatch return true; } } // Call AI System if configured if (this._ai && context.activity.type == botbuilder_1.ActivityTypes.Message && context.activity.text) { await this._ai.run(context, state); // Call afterTurn event handlers if (await this.callEventHandlers(context, state, this._afterTurn)) { // Save turn state await state.save(context, storage); } // End dispatch return true; } // Call afterTurn event handlers if (await this.callEventHandlers(context, state, this._afterTurn)) { // Save turn state await state.save(context, storage); } // activity wasn't handled return false; } finally { this.stopTypingTimer(); } }); } async sendProactiveActivity(context, activityOrText, speak, inputHint) { let response; await this.continueConversationAsync(context, async (ctx) => { response = await ctx.sendActivity(activityOrText, speak, inputHint); }); return response; } async getTeamChannels(context) { let teamsChannels = []; // Identify conversation reference const reference = getConversationReference(context); if (reference.conversation?.conversationType === 'channel') { await this.continueConversationAsync(reference, async (ctx) => { const teamId = ctx.activity?.channelData?.team?.id ?? (ctx.activity?.conversation?.name === undefined ? ctx.activity?.conversation?.id : undefined); if (teamId) { teamsChannels = await botbuilder_1.TeamsInfo.getTeamChannels(ctx, teamId); } }); } return teamsChannels; } async getTeamDetails(context) { let teamDetails = undefined; // Identify conversation reference const reference = getConversationReference(context); if (reference.conversation?.conversationType === 'channel') { await this.continueConversationAsync(reference, async (ctx) => { const teamId = ctx.activity?.channelData?.team?.id ?? (ctx.activity?.conversation?.name === undefined ? ctx.activity?.conversation?.id : undefined); if (teamId) { teamDetails = await botbuilder_1.TeamsInfo.getTeamDetails(ctx, teamId); } }); } return teamDetails; } async getPagedMembers(context, pageSize, continuationToken) { let pagedMembers = { members: [], continuationToken: '' }; await this.continueConversationAsync(context, async (ctx) => { // Page sizes with less than 50 are treated as 50, and greater than 500, are capped at 500. pagedMembers = await botbuilder_1.TeamsInfo.getPagedMembers(ctx, pageSize, continuationToken); }); return pagedMembers; } /** * Manually start a timer to periodically send "typing" activities. * @remarks * The timer waits 1000ms to send its initial "typing" activity and then send an additional * "typing" activity every 1000ms. The timer will automatically end once an outgoing activity * has been sent. If the timer is already running or the current activity, is not a "message" * the call is ignored. * @param {TurnContext} context The context for the current turn with the user. */ startTypingTimer(context) { if (context.activity.type == botbuilder_1.ActivityTypes.Message && !this._typingTimer) { // Listen for outgoing activities context.onSendActivities(async (context, activities, next) => { // Listen for any messages to be sent from the bot if (timerRunning) { for (let i = 0; i < activities.length; i++) { if (activities[i].type == botbuilder_1.ActivityTypes.Message || activities[i].channelData?.streamType) { // Stop the timer this.stopTypingTimer(); timerRunning = false; // Wait for the last "typing" activity to finish sending // - This prevents a race condition that results in the typing indicator being stuck on. await lastSend; break; } } } return next(); }); let timerRunning = true; let lastSend = Promise.resolve(); const onTimeout = async () => { try { // Send typing activity lastSend = context.sendActivity({ type: botbuilder_1.ActivityTypes.Typing }); await lastSend; } catch (err) { // Seeing a random proxy violation error from the context object. This is because // we're in the middle of sending an activity on a background thread when the turn ends. // The context object throws when we try to update "this.responded = true". We can just // eat the error but lets make sure our states cleaned up a bit. this._typingTimer = undefined; timerRunning = false; lastSend = Promise.resolve(); } // Restart timer if (timerRunning) { this._typingTimer = setTimeout(onTimeout, TYPING_TIMER_DELAY); } }; this._typingTimer = setTimeout(onTimeout, TYPING_TIMER_DELAY); } } /** * Manually stop the typing timer. * @remarks * If the timer isn't running nothing happens. */ stopTypingTimer() { if (this._typingTimer) { clearTimeout(this._typingTimer); this._typingTimer = undefined; } } /** * Registers a turn event handler. * @remarks * Turn events let you do something before or after a turn is run. Returning false from * `beforeTurn` lets you prevent the turn from running and returning false from `afterTurn` * lets you prevent the bots state from being saved. * * Returning false from `beforeTurn` does result in the bots state being saved which lets you * track the reason why the turn was not processed. It also means you can use `beforeTurn` as * a way to call into the dialog system. For example, you could use the OAuthPrompt to sign the * user in before allowing the AI system to run. * @param {TurnEvents | TurnEvents[]} event - Name of the turn event to handle. * @param {(context: TurnContext, state: TState) => Promise<boolean>} handler - Function to call when the event is triggered. * @returns {this} The application instance for chaining purposes. */ turn(event, handler) { (Array.isArray(event) ? event : [event]).forEach((e) => { switch (event) { case 'beforeTurn': default: this._beforeTurn.push(handler); break; case 'afterTurn': this._afterTurn.push(handler); break; } }); return this; } /** * Adds a handler for Teams' readReceiptInfo event * @param {(context: TurnContext, state: TState, readReceiptInfo: ReadReceiptInfo) => Promise<boolean>} handler Function to call when the event is triggered. * @returns {this} The application instance for chaining purposes. */ teamsReadReceipt(handler) { const selector = (context) => { return Promise.resolve(context.activity.type === botbuilder_1.ActivityTypes.Event && context.activity.channelId === 'msteams' && context.activity.name === 'application/vnd.microsoft/readReceipt'); }; const handlerWrapper = (context, state) => { const readReceiptInfo = context.activity.value; return handler(context, state, readReceiptInfo); }; this.addRoute(selector, handlerWrapper); return this; } /** * Calls the given event handlers with the given context and state. * @param {TurnContext} context - The context for the current turn with the user. * @param {TState} state - The current state of the conversation. * @param {ApplicationEventHandler<TState>[]} handlers - The event handlers to call. * @returns {Promise<boolean>} A Promise that resolves to a boolean indicating whether the event handlers completed successfully. * @private */ async callEventHandlers(context, state, handlers) { for (let i = 0; i < handlers.length; i++) { const continueExecution = await handlers[i](context, state); if (!continueExecution) { return false; } } // Continue execution return true; } /** * Calls the given handler with the given context, either directly or by continuing the conversation * if the message is a long-running message. * @param {TurnContext} context - The context for the current turn with the user. * @param {(context: TurnContext) => Promise<boolean>} handler - The handler function to call. * @returns {Promise<boolean>} A Promise that resolves to a boolean indicating whether the handler completed successfully. * @private */ startLongRunningCall(context, handler) { /** * If the message is a long-running message, continue the conversation * and call the handler with the new context. */ if (context.activity.type == botbuilder_1.ActivityTypes.Message && this._options.longRunningMessages) { return new Promise((resolve, reject) => { this.continueConversationAsync(context, async (ctx) => { try { // Copy original activity to new context for (const key in context.activity) { ctx.activity[key] = context.activity[key]; } // Call handler const result = await handler(ctx); resolve(result); } catch (err) { reject(err); } }); }); } else { // Call handler directly return handler(context); } } /** * If the user is signed in, get the access token. If not, triggers the sign in flow for the provided authentication setting name * and returns. In this case, the bot should end the turn until the sign in flow is completed. * @summary * Use this method to get the access token for a user that is signed in to the bot. * If the user isn't signed in, this method starts the sign-in flow. * The bot should end the turn in this case until the sign-in flow completes and the user is signed in. * @param {TurnContext} context - The context for the current turn with the user. * @param {TState} state - The current state of the conversation. * @param {string} settingName The name of the authentication setting. * @returns {string | undefined} The token for the user if they are signed in, otherwise undefined. */ async getTokenOrStartSignIn(context, state, settingName) { const token = await this.authentication.get(settingName).isUserSignedIn(context); if (token) { (0, BotAuthenticationBase_1.setTokenInState)(state, settingName, token); (0, BotAuthenticationBase_1.deleteUserInSignInFlow)(state); return token; } if (!(0, BotAuthenticationBase_1.userInSignInFlow)(state)) { // set the setting name in the user state so that we know the user is in the sign in flow (0, BotAuthenticationBase_1.setUserInSignInFlow)(state, settingName); } else { (0, BotAuthenticationBase_1.deleteUserInSignInFlow)(state); throw new Error('Invalid state - cannot start sign in when already started'); } const response = await this.authentication.signUserIn(context, state, settingName); if (response.status == 'error') { const message = response.cause == 'invalidActivity' ? `User is not signed in and cannot start sign in flow for this activity: ${response.error}` : `${response.error}`; throw new Error(`Error occured while trying to authenticate user: ${message}`); } if (response.status == 'complete') { (0, BotAuthenticationBase_1.deleteUserInSignInFlow)(state); return state.temp.authTokens[settingName]; } if (response.status == 'pending') { return; } } } exports.Application = Application; /** * @param {string | RegExp | RouteSelector} type The type of activity to match. Can be a string, RegExp, or RouteSelector function. * @returns {RouteSelector} A RouteSelector function that matches the given activity type. * @private */ function createActivitySelector(type) { if (typeof type == 'function') { // Return the passed in selector function return type; } else if (type instanceof RegExp) { // Return a function that matches the activities type using a RegExp return (context) => { return Promise.resolve(context?.activity?.type ? type.test(context.activity.type) : false); }; } else { // Return a function that attempts to match type name const typeName = type.toString().toLocaleLowerCase(); return (context) => { return Promise.resolve(context?.activity?.type ? context.activity.type.toLocaleLowerCase() === typeName : false); }; } } /** * Creates a route selector function that matches a conversation update event. * @param {ConversationUpdateEvents} event The conversation update event to match against. * @returns {RouteSelector} A route selector function that returns true if the activity is a conversation update event and matches the specified event type. * @private */ function createConversationUpdateSelector(event) { switch (event) { case 'channelCreated': case 'channelDeleted': case 'channelRenamed': case 'channelRestored': /** * @param {TurnContext} context The context object for the current turn of conversation. * @returns {Promise<boolean>} A Promise that resolves to a boolean indicating whether the activity is a conversation update event related to channels. */ return (context) => { return Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.ConversationUpdate && context?.activity?.channelData?.eventType == event && context?.activity?.channelData?.channel && context.activity.channelData?.team); }; case 'membersAdded': /** * @param {TurnContext} context The context object for the current turn of conversation. * @returns {Promise<boolean>} A Promise that resolves to a boolean indicating whether the activity is a conversation update event with members added. */ return (context) => { return Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.ConversationUpdate && Array.isArray(context?.activity?.membersAdded) && context.activity.membersAdded.length > 0); }; case 'membersRemoved': /** * @param {TurnContext} context The context object for the current turn of conversation. * @returns {Promise<boolean>} A Promise that resolves to a boolean indicating whether the activity is a conversation update event with members removed. */ return (context) => { return Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.ConversationUpdate && Array.isArray(context?.activity?.membersRemoved) && context.activity.membersRemoved.length > 0); }; case 'teamRenamed': case 'teamDeleted': case 'teamHardDeleted': case 'teamArchived': case 'teamUnarchived': case 'teamRestored': /** * @param {TurnContext} context The context object for the current turn of conversation. * @returns {Promise<boolean>} A Promise that resolves to a boolean indicating whether the activity is a conversation update event related to teams. */ return (context) => { return Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.ConversationUpdate && context?.activity?.channelData?.eventType == event && context?.activity?.channelData?.team); }; default: /** * @param {TurnContext} context The context object for the current turn of conversation. * @returns {Promise<boolean>} A Promise that resolves to a boolean indicating whether the activity is a conversation update event with the specified event type. */ return (context) => { return Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.ConversationUpdate && context?.activity?.channelData?.eventType == event); }; } } /** * Creates a route selector function that matches a message based on a keyword. * @param {string | RegExp | RouteSelector} keyword The keyword to match against the message text. Can be a string, regular expression, or a custom selector function. * @returns {RouteSelector} A route selector function that returns true if the message text matches the keyword. * @private */ function createMessageSelector(keyword) { if (typeof keyword == 'function') { // Return the passed in selector function return keyword; } else if (keyword instanceof RegExp) { // Return a function that matches a messages text using a RegExp return (context) => { if (context?.activity?.type === botbuilder_1.ActivityTypes.Message && context.activity.text) { return Promise.resolve(keyword.test(context.activity.text)); } else { return Promise.resolve(false); } }; } else { // Return a function that attempts to match a messages text using a substring const k = keyword.toString().toLocaleLowerCase(); return (context) => { if (context?.activity?.type === botbuilder_1.ActivityTypes.Message && context.activity.text) { return Promise.resolve(context.activity.text.toLocaleLowerCase().indexOf(k) >= 0); } else { return Promise.resolve(false); } }; } } /** * @private * @param {TeamsMessageEvents} event The type of message event to create a selector for. * @returns {RouteSelector} A selector function that matches the specified message event. */ function createMessageEventUpdateSelector(event) { switch (event) { case 'editMessage': return (context) => { return Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.MessageUpdate && context?.activity?.channelData?.eventType == event); }; case 'softDeleteMessage': return (context) => { return Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.MessageDelete && context?.activity?.channelData?.eventType == event); }; case 'undeleteMessage': return (context) => { return Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.MessageUpdate && context?.activity?.channelData?.eventType == event); }; default: throw new Error(`Invalid TeamsMessageEvent type: ${event}`); } } /** * @param {MessageReactionEvents} event The type of message reaction event to create a selector for. * @returns {RouteSelector} A selector function that matches the specified message reaction event. * @private */ function createMessageReactionSelector(event) { switch (event) { case 'reactionsAdded': default: /** * @param {TurnContext} context The context object for the current turn of the conversation. * @returns {Promise<boolean>} A promise that resolves to true if the context object represents a message reaction event with reactions added, or false otherwise. */ return (context) => { return Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.MessageReaction && Array.isArray(context?.activity?.reactionsAdded) && context.activity.reactionsAdded.length > 0); }; case 'reactionsRemoved': /** * @param {TurnContext} context The context object for the current turn of the conversation. * @returns {Promise<boolean>} A promise that resolves to true if the context object represents a message reaction event with reactions removed, or false otherwise. */ return (context) => { return Promise.resolve(context?.activity?.type == botbuilder_1.ActivityTypes.MessageReaction && Array.isArray(context?.activity?.reactionsRemoved) && context.activity.reactionsRemoved.length > 0); }; } } /** * Returns a selector function that indicates whether the bot should initiate a sign in. * @param {boolean | Selector} startSignIn A boolean or function that indicates whether the bot should initiate a sign in. * @returns {Selector} A selector function that returns true if the bot should initiate a sign in. */ function createSignInSelector(startSignIn) { return (context) => { if (typeof startSignIn === 'function') { return startSignIn(context); } else if (typeof startSignIn === 'boolean') { return Promise.resolve(startSignIn); } else { return Promise.resolve(true); } }; } /** * Retrieves a conversation reference from the given context, activity, or reference. * Overloaded function signatures: * - getConversationReference(context: TurnContext): Partial<ConversationReference> * - getConversationReference(activity: Partial<Activity>): Partial<ConversationReference> * - getConversationReference(reference: Partial<ConversationReference>): Partial<ConversationReference> * @param {TurnContext | Partial<Activity> | Partial<ConversationReference>} context - The context, activity, or reference to extract the conversation reference from. * @returns {Partial<ConversationReference>} The extracted conversation reference. */ function getConversationReference(context) { let reference; if (typeof context.activity == 'object') { reference = botbuilder_1.TurnContext.getConversationReference(context.activity); } else if (typeof context.type == 'string') { reference = botbuilder_1.TurnContext.getConversationReference(context); } else { reference = context; } return reference; } //# sourceMappingURL=Application.js.map