UNPKG

@microsoft/botbuilder-m365

Version:

M365 extensions for Microsoft BotBuilder, Alpha release.

497 lines 24.2 kB
"use strict"; /** * @module botbuilder-m365 */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Application = void 0; const botbuilder_1 = require("botbuilder"); const DefaultTurnStateManager_1 = require("./DefaultTurnStateManager"); const AdaptiveCards_1 = require("./AdaptiveCards"); const MessageExtensions_1 = require("./MessageExtensions"); const AI_1 = require("./AI"); const TaskModules_1 = require("./TaskModules"); const TYPING_TIMER_DELAY = 1000; class Application { constructor(options) { this._routes = []; this._invokeRoutes = []; this._beforeTurn = []; this._afterTurn = []; this._options = Object.assign({ removeRecipientMention: true, startTypingTimer: true }, options); // Create default turn state manager if needed if (!this._options.turnStateManager) { this._options.turnStateManager = new DefaultTurnStateManager_1.DefaultTurnStateManager(); } // Create AI component if configured with a planner if (this._options.ai) { this._ai = new AI_1.AI(this._options.ai); } this._adaptiveCards = new AdaptiveCards_1.AdaptiveCards(this); this._messageExtensions = new MessageExtensions_1.MessageExtensions(this); this._taskModules = new TaskModules_1.TaskModules(this); // Validate long running messages configuration if (this._options.longRunningMessages && (!this._options.adapter || !this._options.botAppId)) { throw new Error(`The Application.longRunningMessages property is unavailable because no adapter or botAppId was configured.`); } } get adaptiveCards() { return this._adaptiveCards; } get ai() { if (!this._ai) { throw new Error(`The Application.ai property is unavailable because no AI options were configured.`); } return this._ai; } get messageExtensions() { return this._messageExtensions; } get options() { return this._options; } get taskModules() { return this._taskModules; } /** * Adds a new route to the application. * * 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. * * @param {RouteSelector} selector Promise to determine if the route should be triggered. * @param {RouteHandler<TurnState>} handler Function to call when the route is triggered. * @param {boolean} isInvokeRoute boolean indicating if the RouteSelector is an invokable Teams activity as part of its routing logic. 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 {Promise<void>} handler Function to call when the route is triggered. * @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 | ConversationUpdateEvents[]} event Name of the conversation update event(s) to handle. * @param {Promise<void>} handler Function to call when the route is triggered. * @returns {this} The application instance for chaining purposes. */ conversationUpdate(event, handler) { (Array.isArray(event) ? event : [event]).forEach((e) => { const selector = createConversationUpdateSelector(e); this.addRoute(selector, handler); }); return this; } continueConversationAsync(context, logic) { var _a; return __awaiter(this, void 0, void 0, function* () { if (!this._options.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 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; } yield this._options.adapter.continueConversationAsync((_a = this._options.botAppId) !== null && _a !== void 0 ? _a : '', reference, logic); }); } /** * Handles incoming messages with a given keyword. * * @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 {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 to handle. * @param {Promise<void>} handler Function to call when the route is triggered. * @returns {this} The application instance for chaining purposes. */ messageReactions(event, handler) { (Array.isArray(event) ? event : [event]).forEach((e) => { const selector = createMessageReactionSelector(e); this.addRoute(selector, handler); }); return this; } /** * Dispatches an incoming activity to a handler registered with the application. * * @param {TurnContext} turnContext Context class for the current turn of conversation with the user. * @returns {boolean} True if the activity was successfully dispatched to a handler. False if no matching handlers could be found. */ run(turnContext) { return __awaiter(this, void 0, void 0, function* () { return yield this.startLongRunningCall(turnContext, (context) => __awaiter(this, void 0, void 0, function* () { // 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, turnStateManager } = this._options; const state = yield turnStateManager.loadState(storage, context); // Call beforeTurn event handlers if (!(yield this.callEventHandlers(context, state, this._beforeTurn))) { return false; } // 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++) { // TODO: fix security/detect-object-injection // eslint-disable-next-line security/detect-object-injection const route = this._invokeRoutes[i]; if (yield route.selector(context)) { // Execute route handler yield route.handler(context, state); // Call afterTurn event handlers if (yield this.callEventHandlers(context, state, this._afterTurn)) { // Save turn state yield turnStateManager.saveState(storage, context, state); } // 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++) { // TODO: // eslint-disable-next-line security/detect-object-injection const route = this._routes[i]; if (yield route.selector(context)) { // Execute route handler yield route.handler(context, state); // Call afterTurn event handlers if (yield this.callEventHandlers(context, state, this._afterTurn)) { // Save turn state yield turnStateManager.saveState(storage, context, state); } // End dispatch return true; } } // Call AI module if configured if (this._ai && context.activity.type == botbuilder_1.ActivityTypes.Message && context.activity.text) { // Begin a new chain of AI calls yield this._ai.chain(context, state); // Call afterTurn event handlers if (yield this.callEventHandlers(context, state, this._afterTurn)) { // Save turn state yield turnStateManager.saveState(storage, context, state); } // End dispatch return true; } // activity wasn't handled return false; } finally { this.stopTypingTimer(); } })); }); } sendProactiveActivity(context, activityOrText, speak, inputHint) { return __awaiter(this, void 0, void 0, function* () { let response; yield this.continueConversationAsync(context, (ctx) => __awaiter(this, void 0, void 0, function* () { response = yield ctx.sendActivity(activityOrText, speak, inputHint); })); return response; }); } /** * Manually start a timer to periodically send "typing" activities. * * * 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((context, activities, next) => { // Listen for any messages to be sent from the bot if (timerRunning) { for (let i = 0; i < activities.length; i++) { // TODO: // eslint-disable-next-line security/detect-object-injection if (activities[i].type == botbuilder_1.ActivityTypes.Message) { // Stop the timer this.stopTypingTimer(); timerRunning = false; break; } } } return next(); }); let timerRunning = true; const onTimeout = () => __awaiter(this, void 0, void 0, function* () { try { // Send typing activity yield context.sendActivity({ type: botbuilder_1.ActivityTypes.Typing }); } 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; } // Restart timer if (timerRunning) { this._typingTimer = setTimeout(onTimeout, TYPING_TIMER_DELAY); } }); this._typingTimer = setTimeout(onTimeout, TYPING_TIMER_DELAY); } } /** * Manually stop the typing timer. * * * If the timer isn't running nothing happens. */ stopTypingTimer() { if (this._typingTimer) { clearTimeout(this._typingTimer); this._typingTimer = undefined; } } /** * Registers a turn event handler. * * @param {TurnEvents | TurnEvents[]} event Name of the turn event to handle. * @param {Promise<void>} 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; } callEventHandlers(context, state, handlers) { return __awaiter(this, void 0, void 0, function* () { for (let i = 0; i < handlers.length; i++) { // TODO: // eslint-disable-next-line security/detect-object-injection const continueExecution = yield handlers[i](context, state); if (!continueExecution) { return false; } } // Continue execution return true; }); } startLongRunningCall(context, handler) { if (context.activity.type == botbuilder_1.ActivityTypes.Message && this._options.longRunningMessages) { return new Promise((resolve, reject) => { this.continueConversationAsync(context, (ctx) => __awaiter(this, void 0, void 0, function* () { try { // Copy original activity to new context for (const key in context.activity) { ctx.activity[key] = context.activity[key]; } // Call handler const result = yield handler(ctx); resolve(result); } catch (err) { reject(err); } })); }); } else { return handler(context); } } } exports.Application = Application; /** * * @param {string | RegExp | RouteSelector} type The activity to match against. * @returns {RouteSelector} A Promise that resolves to true if the event matches the selector. */ 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) => { var _a; return Promise.resolve(((_a = context === null || context === void 0 ? void 0 : context.activity) === null || _a === void 0 ? void 0 : _a.type) ? type.test(context.activity.type) : false); }; } else { // Return a function that attempts to match type name const typeName = type.toString().toLocaleLowerCase(); return (context) => { var _a; return Promise.resolve(((_a = context === null || context === void 0 ? void 0 : context.activity) === null || _a === void 0 ? void 0 : _a.type) ? context.activity.type.toLocaleLowerCase() === typeName : false); }; } } /** * * @param {ConversationUpdateEvents} event The type of event to match against. * @returns {RouteSelector} A promise that resolves to true if the event matches the selector. */ function createConversationUpdateSelector(event) { switch (event) { case 'membersAdded': return (context) => { var _a, _b; return Promise.resolve(((_a = context === null || context === void 0 ? void 0 : context.activity) === null || _a === void 0 ? void 0 : _a.type) == botbuilder_1.ActivityTypes.ConversationUpdate && Array.isArray((_b = context === null || context === void 0 ? void 0 : context.activity) === null || _b === void 0 ? void 0 : _b.membersAdded) && context.activity.membersAdded.length > 0); }; case 'membersRemoved': return (context) => { var _a, _b; return Promise.resolve(((_a = context === null || context === void 0 ? void 0 : context.activity) === null || _a === void 0 ? void 0 : _a.type) == botbuilder_1.ActivityTypes.ConversationUpdate && Array.isArray((_b = context === null || context === void 0 ? void 0 : context.activity) === null || _b === void 0 ? void 0 : _b.membersRemoved) && context.activity.membersRemoved.length > 0); }; default: return (context) => { var _a, _b, _c; return Promise.resolve(((_a = context === null || context === void 0 ? void 0 : context.activity) === null || _a === void 0 ? void 0 : _a.type) == botbuilder_1.ActivityTypes.ConversationUpdate && ((_c = (_b = context === null || context === void 0 ? void 0 : context.activity) === null || _b === void 0 ? void 0 : _b.channelData) === null || _c === void 0 ? void 0 : _c.eventType) == event); }; } } /** * * @param {string | RegExp | RouteSelector} keyword The message keyword to match against. * @returns {RouteSelector} A promise that resolves to true if the event matches the selector. */ 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) => { var _a; if (((_a = context === null || context === void 0 ? void 0 : context.activity) === null || _a === void 0 ? void 0 : _a.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) => { var _a; if (((_a = context === null || context === void 0 ? void 0 : context.activity) === null || _a === void 0 ? void 0 : _a.type) === botbuilder_1.ActivityTypes.Message && context.activity.text) { return Promise.resolve(context.activity.text.toLocaleLowerCase().indexOf(k) >= 0); } else { return Promise.resolve(false); } }; } } /** * * @param {MessageReactionEvents} event The type of reaction event to handle. * @returns {RouteSelector} A Promise that resolves to true if the event matches the selector. */ function createMessageReactionSelector(event) { switch (event) { case 'reactionsAdded': default: return (context) => { var _a, _b; return Promise.resolve(((_a = context === null || context === void 0 ? void 0 : context.activity) === null || _a === void 0 ? void 0 : _a.type) == botbuilder_1.ActivityTypes.MessageReaction && Array.isArray((_b = context === null || context === void 0 ? void 0 : context.activity) === null || _b === void 0 ? void 0 : _b.reactionsAdded) && context.activity.reactionsAdded.length > 0); }; case 'reactionsRemoved': return (context) => { var _a, _b; return Promise.resolve(((_a = context === null || context === void 0 ? void 0 : context.activity) === null || _a === void 0 ? void 0 : _a.type) == botbuilder_1.ActivityTypes.MessageReaction && Array.isArray((_b = context === null || context === void 0 ? void 0 : context.activity) === null || _b === void 0 ? void 0 : _b.reactionsRemoved) && context.activity.reactionsRemoved.length > 0); }; } } //# sourceMappingURL=Application.js.map