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