@microsoft/botbuilder-m365
Version:
M365 extensions for Microsoft BotBuilder, Alpha release.
497 lines • 24.2 kB
JavaScript
"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