UNPKG

39.2 kBJavaScriptView Raw
1"use strict";
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 return new (P || (P = Promise))(function (resolve, reject) {
5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 step((generator = generator.apply(thisArg, _arguments || [])).next());
9 });
10};
11Object.defineProperty(exports, "__esModule", { value: true });
12exports.Botkit = void 0;
13/**
14 * @module botkit
15 */
16/**
17 * Copyright (c) Microsoft Corporation. All rights reserved.
18 * Licensed under the MIT License.
19 */
20const botbuilder_1 = require("botbuilder");
21const botbuilder_dialogs_1 = require("botbuilder-dialogs");
22const adapter_1 = require("./adapter");
23const botworker_1 = require("./botworker");
24const conversationState_1 = require("./conversationState");
25const path = require("path");
26const http = require("http");
27const express = require("express");
28const bodyParser = require("body-parser");
29const Ware = require("ware");
30const fs = require("fs");
31const Debug = require("debug");
32const debug = Debug('botkit');
33/**
34 * Create a new instance of Botkit to define the controller for a conversational app.
35 * To connect Botkit to a chat platform, pass in a fully configured `adapter`.
36 * If one is not specified, Botkit will expose an adapter for the Microsoft Bot Framework.
37 */
38class Botkit {
39 /**
40 * Create a new Botkit instance and optionally specify a platform-specific adapter.
41 * By default, Botkit will create a [BotFrameworkAdapter](https://docs.microsoft.com/en-us/javascript/api/botbuilder/botframeworkadapter?view=botbuilder-ts-latest).
42 *
43 * ```javascript
44 * const controller = new Botkit({
45 * adapter: some_adapter,
46 * webhook_uri: '/api/messages',
47 * });
48 *
49 * controller.on('message', async(bot, message) => {
50 * // do something!
51 * });
52 * ```
53 *
54 * @param config Configuration for this instance of Botkit
55 */
56 constructor(config) {
57 /**
58 * _events contains the list of all events for which Botkit has registered handlers.
59 * Each key in this object points to an array of handler functions bound to that event.
60 */
61 this._events = {};
62 /**
63 * _triggers contains a list of trigger patterns htat Botkit will watch for.
64 * Each key in this object points to an array of patterns and their associated handlers.
65 * Each key represents an event type.
66 */
67 this._triggers = {};
68 /**
69 * _interrupts contains a list of trigger patterns htat Botkit will watch for and fire BEFORE firing any normal triggers.
70 * Each key in this object points to an array of patterns and their associated handlers.
71 * Each key represents an event type.
72 */
73 this._interrupts = {};
74 /**
75 * The current version of Botkit Core
76 */
77 this.version = require('../package.json').version;
78 /**
79 * Middleware endpoints available for plugins and features to extend Botkit.
80 * Endpoints available are: spawn, ingest, receive, send.
81 *
82 * To bind a middleware function to Botkit:
83 * ```javascript
84 * controller.middleware.receive.use(function(bot, message, next) {
85 *
86 * // do something with bot or message
87 *
88 * // always call next, or your bot will freeze!
89 * next();
90 * });
91 * ```
92 */
93 this.middleware = {
94 spawn: new Ware(),
95 ingest: new Ware(),
96 send: new Ware(),
97 receive: new Ware(),
98 interpret: new Ware()
99 };
100 // Set the path where Botkit's core lib is found.
101 this.PATH = __dirname;
102 this._config = Object.assign({ webhook_uri: '/api/messages', dialogStateProperty: 'dialogState', disable_webserver: false, jsonLimit: '100kb', urlEncodedLimit: '100kb' }, config);
103 // The _deps object contains references to dependencies that may take time to load and be ready.
104 // new _deps are defined in the constructor.
105 // when all _deps are true, the controller.ready function runs and executes all functions in order.
106 this._deps = {};
107 this._bootCompleteHandlers = [];
108 this.booted = false;
109 this.addDep('booted');
110 debug('Booting Botkit ', this.version);
111 if (!this._config.storage) {
112 // Set up temporary storage for dialog state.
113 this.storage = new botbuilder_1.MemoryStorage();
114 if (this._config.disable_console !== true) {
115 console.warn('** Your bot is using memory storage and will forget everything when it reboots!');
116 console.warn('** To preserve dialog state, specify a storage adapter in your Botkit config:');
117 console.warn('** const controller = new Botkit({storage: myStorageAdapter});');
118 }
119 }
120 else {
121 this.storage = this._config.storage;
122 }
123 this.conversationState = new conversationState_1.BotkitConversationState(this.storage);
124 const dialogState = this.conversationState.createProperty(this.getConfig('dialogStateProperty'));
125 this.dialogSet = new botbuilder_dialogs_1.DialogSet(dialogState);
126 if (this._config.disable_webserver !== true) {
127 if (!this._config.webserver) {
128 // Create HTTP server
129 this.addDep('webserver');
130 this.webserver = express();
131 // capture raw body
132 this.webserver.use((req, res, next) => {
133 req.rawBody = '';
134 req.on('data', function (chunk) {
135 req.rawBody += chunk;
136 });
137 next();
138 });
139 this.webserver.use(bodyParser.json({ limit: this._config.jsonLimit }));
140 this.webserver.use(bodyParser.urlencoded({ limit: this._config.urlEncodedLimit, extended: true }));
141 if (this._config.webserver_middlewares && this._config.webserver_middlewares.length) {
142 this._config.webserver_middlewares.forEach((middleware) => {
143 this.webserver.use(middleware);
144 });
145 }
146 this.http = http.createServer(this.webserver);
147 this.http.listen(process.env.port || process.env.PORT || 3000, () => {
148 if (this._config.disable_console !== true) {
149 console.log(`Webhook endpoint online: http://localhost:${process.env.PORT || 3000}${this._config.webhook_uri}`);
150 }
151 this.completeDep('webserver');
152 });
153 }
154 else {
155 this.webserver = this._config.webserver;
156 }
157 }
158 if (!this._config.adapter) {
159 const adapterConfig = Object.assign({}, this._config.adapterConfig);
160 debug('Configuring BotFrameworkAdapter:', adapterConfig);
161 this.adapter = new adapter_1.BotkitBotFrameworkAdapter(adapterConfig);
162 if (this.webserver) {
163 if (this._config.disable_console !== true) {
164 console.log('Open this bot in Bot Framework Emulator: bfemulator://livechat.open?botUrl=' + encodeURIComponent(`http://localhost:${process.env.PORT || 3000}${this._config.webhook_uri}`));
165 }
166 }
167 }
168 else {
169 debug('Using pre-configured adapter.');
170 this.adapter = this._config.adapter;
171 }
172 // If a webserver has been configured, auto-configure the default webhook url
173 if (this.webserver) {
174 this.configureWebhookEndpoint();
175 }
176 // initialize the plugins array.
177 this.plugin_list = [];
178 this._plugins = {};
179 // if an adapter has been configured, add it as a plugin.
180 if (this.adapter) {
181 // MAGIC: Treat the adapter as a botkit plugin
182 // which allows them to be carry their own platform-specific behaviors
183 this.usePlugin(this.adapter);
184 }
185 this.completeDep('booted');
186 }
187 /**
188 * Shutdown the webserver and prepare to terminate the app.
189 * Causes Botkit to first emit a special `shutdown` event, process any bound handlers, and then finally terminate the webserver.
190 * Bind any necessary cleanup helpers to the shutdown event - for example, close the connection to mongo.
191 *
192 * ```javascript
193 * await controller.shutdown();
194 * controller.on('shutdown', async() => {
195 * console.log('Bot is shutting down!');
196 * });
197 * ```
198 */
199 shutdown() {
200 return __awaiter(this, void 0, void 0, function* () {
201 // trigger a special shutdown event
202 yield this.trigger('shutdown');
203 if (this.http) {
204 this.http.close();
205 }
206 });
207 }
208 /**
209 * Get a value from the configuration.
210 *
211 * For example:
212 * ```javascript
213 * // get entire config object
214 * let config = controller.getConfig();
215 *
216 * // get a specific value from the config
217 * let webhook_uri = controller.getConfig('webhook_uri');
218 * ```
219 *
220 * @param {string} key The name of a value stored in the configuration
221 * @returns {any} The value stored in the configuration (or null if absent)
222 */
223 getConfig(key) {
224 if (key) {
225 return this._config[key];
226 }
227 else {
228 return this._config;
229 }
230 }
231 /**
232 * Load a plugin module and bind all included middlewares to their respective endpoints.
233 * @param plugin_or_function A plugin module in the form of function(botkit) {...} that returns {name, middlewares, init} or an object in the same form.
234 */
235 usePlugin(plugin_or_function) {
236 let plugin;
237 if (typeof (plugin_or_function) === 'function') {
238 plugin = plugin_or_function(this);
239 }
240 else {
241 plugin = plugin_or_function;
242 }
243 if (plugin.name) {
244 try {
245 this.registerPlugin(plugin.name, plugin);
246 }
247 catch (err) {
248 console.error('ERROR IN PLUGIN REGISTER', err);
249 }
250 }
251 }
252 /**
253 * Called from usePlugin -- do the actual binding of middlewares for a plugin that is being loaded.
254 * @param name name of the plugin
255 * @param endpoints the plugin object that contains middleware endpoint definitions
256 */
257 registerPlugin(name, endpoints) {
258 if (this._config.disable_console !== true) {
259 console.log('Enabling plugin: ', name);
260 }
261 if (this.plugin_list.indexOf(name) >= 0) {
262 debug('Plugin already enabled:', name);
263 return;
264 }
265 this.plugin_list.push(name);
266 if (endpoints.middlewares) {
267 for (const mw in endpoints.middlewares) {
268 for (let e = 0; e < endpoints.middlewares[mw].length; e++) {
269 this.middleware[mw].use(endpoints.middlewares[mw][e]);
270 }
271 }
272 }
273 if (endpoints.init) {
274 try {
275 endpoints.init(this);
276 }
277 catch (err) {
278 if (err) {
279 throw new Error(err);
280 }
281 }
282 }
283 debug('Plugin Enabled: ', name);
284 }
285 /**
286 * (Plugins only) Extend Botkit's controller with new functionality and make it available globally via the controller object.
287 *
288 * ```javascript
289 *
290 * // define the extension interface
291 * let extension = {
292 * stuff: () => { return 'stuff' }
293 * }
294 *
295 * // register the extension
296 * controller.addPluginExtension('foo', extension);
297 *
298 * // call extension
299 * controller.plugins.foo.stuff();
300 *
301 *
302 * ```
303 * @param name name of plugin
304 * @param extension an object containing methods
305 */
306 addPluginExtension(name, extension) {
307 debug('Plugin extension added: controller.' + name);
308 this._plugins[name] = extension;
309 }
310 /**
311 * Access plugin extension methods.
312 * After a plugin calls `controller.addPluginExtension('foo', extension_methods)`, the extension will then be available at
313 * `controller.plugins.foo`
314 */
315 get plugins() {
316 return this._plugins;
317 }
318 /**
319 * Expose a folder to the web as a set of static files.
320 * Useful for plugins that need to bundle additional assets!
321 *
322 * ```javascript
323 * // make content of the local public folder available at http://MYBOTURL/public/myplugin
324 * controller.publicFolder('/public/myplugin', __dirname + '/public);
325 * ```
326 * @param alias the public alias ie /myfiles
327 * @param path the actual path something like `__dirname + '/public'`
328 */
329 publicFolder(alias, path) {
330 if (this.webserver) {
331 debug('Make folder public: ', path, 'at alias', alias);
332 this.webserver.use(alias, express.static(path));
333 }
334 else {
335 throw new Error('Cannot create public folder alias when webserver is disabled');
336 }
337 }
338 /**
339 * Convert a local path from a plugin folder to a full path relative to the webserver's main views folder.
340 * Allows a plugin to bundle views/layouts and make them available to the webserver's renderer.
341 * @param path_to_view something like path.join(__dirname,'views')
342 */
343 getLocalView(path_to_view) {
344 if (this.webserver) {
345 return path.relative(path.join(this.webserver.get('views')), path_to_view);
346 }
347 else {
348 throw new Error('Cannot get local view when webserver is disabled');
349 }
350 }
351 /**
352 * (For use by Botkit plugins only) - Add a dependency to Botkit's bootup process that must be marked as completed using `completeDep()`.
353 * Botkit's `controller.ready()` function will not fire until all dependencies have been marked complete.
354 *
355 * For example, a plugin that needs to do an asynchronous task before Botkit proceeds might do:
356 * ```javascript
357 * controller.addDep('my_async_plugin');
358 * somethingAsync().then(function() {
359 * controller.completeDep('my_async_plugin');
360 * });
361 * ```
362 *
363 * @param name {string} The name of the dependency that is being loaded.
364 */
365 addDep(name) {
366 debug(`Waiting for ${name}`);
367 this._deps[name] = false;
368 }
369 /**
370 * (For use by plugins only) - Mark a bootup dependency as loaded and ready to use
371 * Botkit's `controller.ready()` function will not fire until all dependencies have been marked complete.
372
373 * @param name {string} The name of the dependency that has completed loading.
374 */
375 completeDep(name) {
376 debug(`${name} ready`);
377 this._deps[name] = true;
378 for (const key in this._deps) {
379 if (this._deps[key] === false) {
380 return false;
381 }
382 }
383 // everything is done!
384 this.signalBootComplete();
385 return true;
386 }
387 /**
388 * This function gets called when all of the bootup dependencies are completely loaded.
389 */
390 signalBootComplete() {
391 this.booted = true;
392 for (let h = 0; h < this._bootCompleteHandlers.length; h++) {
393 const handler = this._bootCompleteHandlers[h];
394 handler.call(this);
395 }
396 }
397 /**
398 * Use `controller.ready()` to wrap any calls that require components loaded during the bootup process.
399 * This will ensure that the calls will not be made until all of the components have successfully been initialized.
400 *
401 * For example:
402 * ```javascript
403 * controller.ready(() => {
404 *
405 * controller.loadModules(__dirname + '/features');
406 *
407 * });
408 * ```
409 *
410 * @param handler {function} A function to run when Botkit is booted and ready to run.
411 */
412 ready(handler) {
413 if (this.booted) {
414 handler.call(this);
415 }
416 else {
417 this._bootCompleteHandlers.push(handler);
418 }
419 }
420 /*
421 * Set up a web endpoint to receive incoming messages,
422 * pass them through a normalization process, and then ingest them for processing.
423 */
424 configureWebhookEndpoint() {
425 if (this.webserver) {
426 this.webserver.post(this._config.webhook_uri, (req, res) => {
427 // Allow the Botbuilder middleware to fire.
428 // this middleware is responsible for turning the incoming payload into a BotBuilder Activity
429 // which we can then use to turn into a BotkitMessage
430 this.adapter.processActivity(req, res, this.handleTurn.bind(this)).catch((err) => {
431 // todo: expose this as a global error handler?
432 console.error('Experienced an error inside the turn handler', err);
433 throw err;
434 });
435 });
436 }
437 else {
438 throw new Error('Cannot configure webhook endpoints when webserver is disabled');
439 }
440 }
441 /**
442 * Accepts the result of a BotBuilder adapter's `processActivity()` method and processes it into a Botkit-style message and BotWorker instance
443 * which is then used to test for triggers and emit events.
444 * NOTE: This method should only be used in custom adapters that receive messages through mechanisms other than the main webhook endpoint (such as those received via websocket, for example)
445 * @param turnContext {TurnContext} a TurnContext representing an incoming message, typically created by an adapter's `processActivity()` method.
446 */
447 handleTurn(turnContext) {
448 return __awaiter(this, void 0, void 0, function* () {
449 debug('INCOMING ACTIVITY:', turnContext.activity);
450 // Turn this turnContext into a Botkit message.
451 const message = Object.assign(Object.assign({}, turnContext.activity.channelData), {
452 // if Botkit has further classified this message, use that sub-type rather than the Activity type
453 type: (turnContext.activity.channelData && turnContext.activity.channelData.botkitEventType) ? turnContext.activity.channelData.botkitEventType : turnContext.activity.type,
454 // normalize the user, text and channel info
455 user: turnContext.activity.from.id, text: turnContext.activity.text, channel: turnContext.activity.conversation.id, value: turnContext.activity.value,
456 // generate a conversation reference, for replies.
457 // included so people can easily capture it for resuming
458 reference: botbuilder_1.TurnContext.getConversationReference(turnContext.activity),
459 // include the context possible useful.
460 context: turnContext,
461 // include the full unmodified record here
462 incoming_message: turnContext.activity });
463 // Stash the Botkit message in
464 turnContext.turnState.set('botkitMessage', message);
465 // Create a dialog context
466 const dialogContext = yield this.dialogSet.createContext(turnContext);
467 // Spawn a bot worker with the dialogContext
468 const bot = yield this.spawn(dialogContext);
469 return new Promise((resolve, reject) => {
470 this.middleware.ingest.run(bot, message, (err, bot, message) => __awaiter(this, void 0, void 0, function* () {
471 if (err) {
472 reject(err);
473 }
474 else {
475 this.middleware.receive.run(bot, message, (err, bot, message) => __awaiter(this, void 0, void 0, function* () {
476 if (err) {
477 reject(err);
478 }
479 else {
480 const interrupt_results = yield this.listenForInterrupts(bot, message);
481 if (interrupt_results === false) {
482 // Continue dialog if one is present
483 const dialog_results = yield dialogContext.continueDialog();
484 if (dialog_results && dialog_results.status === botbuilder_dialogs_1.DialogTurnStatus.empty) {
485 yield this.processTriggersAndEvents(bot, message);
486 }
487 }
488 // make sure changes to the state get persisted after the turn is over.
489 yield this.saveState(bot);
490 resolve();
491 }
492 }));
493 }
494 }));
495 });
496 });
497 }
498 /**
499 * Save the current conversation state pertaining to a given BotWorker's activities.
500 * Note: this is normally called internally and is only required when state changes happen outside of the normal processing flow.
501 * @param bot {BotWorker} a BotWorker instance created using `controller.spawn()`
502 */
503 saveState(bot) {
504 return __awaiter(this, void 0, void 0, function* () {
505 yield this.conversationState.saveChanges(bot.getConfig('context'));
506 });
507 }
508 /**
509 * Ingests a message and evaluates it for triggers, run the receive middleware, and triggers any events.
510 * Note: This is normally called automatically from inside `handleTurn()` and in most cases should not be called directly.
511 * @param bot {BotWorker} An instance of the bot
512 * @param message {BotkitMessage} an incoming message
513 */
514 processTriggersAndEvents(bot, message) {
515 return __awaiter(this, void 0, void 0, function* () {
516 return new Promise((resolve, reject) => {
517 this.middleware.interpret.run(bot, message, (err, bot, message) => __awaiter(this, void 0, void 0, function* () {
518 if (err) {
519 return reject(err);
520 }
521 const listen_results = yield this.listenForTriggers(bot, message);
522 if (listen_results !== false) {
523 resolve(listen_results);
524 }
525 else {
526 // Trigger event handlers
527 const trigger_results = yield this.trigger(message.type, bot, message);
528 resolve(trigger_results);
529 }
530 }));
531 });
532 });
533 }
534 /**
535 * Evaluates an incoming message for triggers created with `controller.hears()` and fires any relevant handler functions.
536 * @param bot {BotWorker} An instance of the bot
537 * @param message {BotkitMessage} an incoming message
538 */
539 listenForTriggers(bot, message) {
540 return __awaiter(this, void 0, void 0, function* () {
541 if (this._triggers[message.type]) {
542 const triggers = this._triggers[message.type];
543 for (let t = 0; t < triggers.length; t++) {
544 const test_results = yield this.testTrigger(triggers[t], message);
545 if (test_results) {
546 debug('Heard pattern: ', triggers[t].pattern);
547 const trigger_results = yield triggers[t].handler.call(this, bot, message);
548 return trigger_results;
549 }
550 }
551 // nothing has triggered...return false
552 return false;
553 }
554 else {
555 return false;
556 }
557 });
558 }
559 /**
560 * Evaluates an incoming message for triggers created with `controller.interrupts()` and fires any relevant handler functions.
561 * @param bot {BotWorker} An instance of the bot
562 * @param message {BotkitMessage} an incoming message
563 */
564 listenForInterrupts(bot, message) {
565 return __awaiter(this, void 0, void 0, function* () {
566 if (this._interrupts[message.type]) {
567 const triggers = this._interrupts[message.type];
568 for (let t = 0; t < triggers.length; t++) {
569 const test_results = yield this.testTrigger(triggers[t], message);
570 if (test_results) {
571 debug('Heard interruption: ', triggers[t].pattern);
572 const trigger_results = yield triggers[t].handler.call(this, bot, message);
573 return trigger_results;
574 }
575 }
576 // nothing has triggered...return false
577 return false;
578 }
579 else {
580 return false;
581 }
582 });
583 }
584 /**
585 * Evaluates a single trigger and return true if the incoming message matches the conditions
586 * @param trigger {BotkitTrigger} a trigger definition
587 * @param message {BotkitMessage} an incoming message
588 */
589 testTrigger(trigger, message) {
590 return __awaiter(this, void 0, void 0, function* () {
591 if (trigger.type === 'string') {
592 const test = new RegExp(trigger.pattern, 'i');
593 if (message.text && message.text.match(test)) {
594 return true;
595 }
596 }
597 else if (trigger.type === 'regexp') {
598 const test = trigger.pattern;
599 if (message.text && message.text.match(test)) {
600 message.matches = message.text.match(test);
601 return true;
602 }
603 }
604 else if (trigger.type === 'function') {
605 const test = trigger.pattern;
606 return yield test(message);
607 }
608 return false;
609 });
610 }
611 /**
612 * Instruct your bot to listen for a pattern, and do something when that pattern is heard.
613 * Patterns will be "heard" only if the message is not already handled by an in-progress dialog.
614 * To "hear" patterns _before_ dialogs are processed, use `controller.interrupts()` instead.
615 *
616 * For example:
617 * ```javascript
618 * // listen for a simple keyword
619 * controller.hears('hello','message', async(bot, message) => {
620 * await bot.reply(message,'I heard you say hello.');
621 * });
622 *
623 * // listen for a regular expression
624 * controller.hears(new RegExp(/^[A-Z\s]+$/), 'message', async(bot, message) => {
625 * await bot.reply(message,'I heard a message IN ALL CAPS.');
626 * });
627 *
628 * // listen using a function
629 * controller.hears(async (message) => { return (message.intent === 'hello') }, 'message', async(bot, message) => {
630 * await bot.reply(message,'This message matches the hello intent.');
631 * });
632 * ```
633 * @param patterns {} One or more string, regular expression, or test function
634 * @param events {} A list of event types that should be evaluated for the given patterns
635 * @param handler {BotkitHandler} a function that will be called should the pattern be matched
636 */
637 hears(patterns, events, handler) {
638 if (!Array.isArray(patterns)) {
639 patterns = [patterns];
640 }
641 if (typeof events === 'string') {
642 events = events.split(/,/).map(e => e.trim());
643 }
644 debug('Registering hears for ', events);
645 for (let p = 0; p < patterns.length; p++) {
646 for (let e = 0; e < events.length; e++) {
647 const event = events[e];
648 const pattern = patterns[p];
649 if (!this._triggers[event]) {
650 this._triggers[event] = [];
651 }
652 const trigger = {
653 pattern: pattern,
654 handler: handler,
655 type: null
656 };
657 if (typeof pattern === 'string') {
658 trigger.type = 'string';
659 }
660 else if (pattern instanceof RegExp) {
661 trigger.type = 'regexp';
662 }
663 else if (typeof pattern === 'function') {
664 trigger.type = 'function';
665 }
666 this._triggers[event].push(trigger);
667 }
668 }
669 }
670 /**
671 * Instruct your bot to listen for a pattern, and do something when that pattern is heard.
672 * Interruptions work just like "hears" triggers, but fire _before_ the dialog system is engaged,
673 * and thus handlers will interrupt the normal flow of messages through the processing pipeline.
674 *
675 * ```javascript
676 * controller.interrupts('help','message', async(bot, message) => {
677 *
678 * await bot.reply(message,'Before anything else, you need some help!')
679 *
680 * });
681 * ```
682 * @param patterns {} One or more string, regular expression, or test function
683 * @param events {} A list of event types that should be evaluated for the given patterns
684 * @param handler {BotkitHandler} a function that will be called should the pattern be matched
685 */
686 interrupts(patterns, events, handler) {
687 if (!Array.isArray(patterns)) {
688 patterns = [patterns];
689 }
690 if (typeof events === 'string') {
691 events = events.split(/,/).map(e => e.trim());
692 }
693 debug('Registering hears for ', events);
694 for (let p = 0; p < patterns.length; p++) {
695 for (let e = 0; e < events.length; e++) {
696 const event = events[e];
697 const pattern = patterns[p];
698 if (!this._interrupts[event]) {
699 this._interrupts[event] = [];
700 }
701 const trigger = {
702 pattern: pattern,
703 handler: handler,
704 type: null
705 };
706 if (typeof pattern === 'string') {
707 trigger.type = 'string';
708 }
709 else if (pattern instanceof RegExp) {
710 trigger.type = 'regexp';
711 }
712 else if (typeof pattern === 'function') {
713 trigger.type = 'function';
714 }
715 this._interrupts[event].push(trigger);
716 }
717 }
718 }
719 /**
720 * Bind a handler function to one or more events.
721 *
722 * ```javascript
723 * controller.on('conversationUpdate', async(bot, message) => {
724 *
725 * await bot.reply(message,'I received a conversationUpdate event.');
726 *
727 * });
728 * ```
729 *
730 * @param events {} One or more event names
731 * @param handler {BotkitHandler} a handler function that will fire whenever one of the named events is received.
732 */
733 on(events, handler) {
734 if (typeof events === 'string') {
735 events = events.split(/,/).map(e => e.trim());
736 }
737 debug('Registering handler for: ', events);
738 events.forEach((event) => {
739 if (!this._events[event]) {
740 this._events[event] = [];
741 }
742 this._events[event].push(handler);
743 });
744 }
745 /**
746 * Trigger an event to be fired. This will cause any bound handlers to be executed.
747 * Note: This is normally used internally, but can be used to emit custom events.
748 *
749 * ```javascript
750 * // fire a custom event
751 * controller.trigger('my_custom_event', bot, message);
752 *
753 * // handle the custom event
754 * controller.on('my_custom_event', async(bot, message) => {
755 * //... do something
756 * });
757 * ```
758 *
759 * @param event {string} the name of the event
760 * @param bot {BotWorker} a BotWorker instance created using `controller.spawn()`
761 * @param message {BotkitMessagE} An incoming message or event
762 */
763 trigger(event, bot, message) {
764 return __awaiter(this, void 0, void 0, function* () {
765 debug('Trigger event: ', event);
766 if (this._events[event] && this._events[event].length) {
767 for (let h = 0; h < this._events[event].length; h++) {
768 try {
769 const handler_results = yield this._events[event][h].call(bot, bot, message);
770 if (handler_results === false) {
771 break;
772 }
773 }
774 catch (err) {
775 console.error('Error in trigger handler', err);
776 throw Error(err);
777 }
778 }
779 }
780 });
781 }
782 /**
783 * Create a platform-specific BotWorker instance that can be used to respond to messages or generate new outbound messages.
784 * The spawned `bot` contains all information required to process outbound messages and handle dialog state, and may also contain extensions
785 * for handling platform-specific events or activities.
786 * @param config {any} Preferably receives a DialogContext, though can also receive a TurnContext. If excluded, must call `bot.changeContext(reference)` before calling any other method.
787 * @param adapter {BotAdapter} An optional reference to a specific adapter from which the bot will be spawned. If not specified, will use the adapter from which the configuration object originates. Required for spawning proactive bots in a multi-adapter scenario.
788 */
789 spawn(config, custom_adapter) {
790 return __awaiter(this, void 0, void 0, function* () {
791 if (config instanceof botbuilder_1.TurnContext) {
792 config = {
793 dialogContext: yield this.dialogSet.createContext(config),
794 context: config,
795 reference: botbuilder_1.TurnContext.getConversationReference(config.activity),
796 activity: config.activity
797 };
798 }
799 else if (config instanceof botbuilder_dialogs_1.DialogContext) {
800 config = {
801 dialogContext: config,
802 reference: botbuilder_1.TurnContext.getConversationReference(config.context.activity),
803 context: config.context,
804 activity: config.context.activity
805 };
806 }
807 let worker = null;
808 const adapter = custom_adapter || ((config && config.context && config.context.adapter) ? config.context.adapter : this.adapter);
809 if (adapter.botkit_worker) {
810 const CustomBotWorker = adapter.botkit_worker;
811 worker = new CustomBotWorker(this, config);
812 }
813 else {
814 worker = new botworker_1.BotWorker(this, config);
815 }
816 // make sure the adapter is available in a standard location.
817 worker.getConfig().adapter = adapter;
818 return new Promise((resolve, reject) => {
819 this.middleware.spawn.run(worker, (err, worker) => {
820 if (err) {
821 reject(err);
822 }
823 else {
824 resolve(worker);
825 }
826 });
827 });
828 });
829 }
830 /**
831 * Load a Botkit feature module
832 *
833 * @param p {string} path to module file
834 */
835 loadModule(p) {
836 debug('Load Module:', p);
837 // eslint-disable-next-line @typescript-eslint/no-var-requires
838 const module = require(p);
839 // Handle both CJS `module.exports` and ESM `export default` syntax.
840 if (typeof module === 'function') {
841 module(this);
842 }
843 else if (module && typeof module.default === 'function') {
844 module.default(this);
845 }
846 else {
847 throw new Error(`Failed to load '${p}', did you export a function?`);
848 }
849 }
850 /**
851 * Load all Botkit feature modules located in a given folder.
852 *
853 * ```javascript
854 * controller.ready(() => {
855 *
856 * // load all modules from sub-folder features/
857 * controller.loadModules('./features');
858 *
859 * });
860 * ```
861 *
862 * @param p {string} path to a folder of module files
863 * @param exts {string[]} the extensions that you would like to load (default: ['.js'])
864 */
865 loadModules(p, exts = ['.js']) {
866 // load all the .js|.ts files from this path
867 fs.readdirSync(p).filter((f) => {
868 return exts.includes(path.extname(f));
869 }).forEach((file) => {
870 this.loadModule(path.join(p, file));
871 });
872 }
873 /**
874 * Add a dialog to the bot, making it accessible via `bot.beginDialog(dialog_id)`
875 *
876 * ```javascript
877 * // Create a dialog -- `BotkitConversation` is just one way to create a dialog
878 * const my_dialog = new BotkitConversation('my_dialog', controller);
879 * my_dialog.say('Hello');
880 *
881 * // Add the dialog to the Botkit controller
882 * controller.addDialog(my_dialog);
883 *
884 * // Later on, trigger the dialog into action!
885 * controller.on('message', async(bot, message) => {
886 * await bot.beginDialog('my_dialog');
887 * });
888 * ```
889 *
890 * @param dialog A dialog to be added to the bot's dialog set
891 */
892 addDialog(dialog) {
893 // add the actual dialog
894 this.dialogSet.add(dialog);
895 // add a wrapper dialog that will be called by bot.beginDialog
896 // and is responsible for capturing the parent results
897 this.dialogSet.add(new botbuilder_dialogs_1.WaterfallDialog(dialog.id + ':botkit-wrapper', [
898 (step) => __awaiter(this, void 0, void 0, function* () {
899 return step.beginDialog(dialog.id, step.options);
900 }),
901 (step) => __awaiter(this, void 0, void 0, function* () {
902 const bot = yield this.spawn(step.context);
903 yield this.trigger(dialog.id + ':after', bot, step.result);
904 return step.endDialog(step.result);
905 })
906 ]));
907 }
908 /**
909 * Bind a handler to the end of a dialog.
910 * NOTE: bot worker cannot use bot.reply(), must use bot.send()
911 *
912 * [Learn more about handling end-of-conversation](../docs/conversations.md#handling-end-of-conversation)
913 * @param dialog the dialog object or the id of the dialog
914 * @param handler a handler function in the form `async(bot, dialog_results) => {}`
915 */
916 afterDialog(dialog, handler) {
917 let id = '';
918 if (typeof (dialog) === 'string') {
919 id = dialog;
920 }
921 else {
922 id = dialog.id;
923 }
924 this.on(id + ':after', handler);
925 }
926}
927exports.Botkit = Botkit;
928//# sourceMappingURL=core.js.map
\No newline at end of file