UNPKG

46 kBPlain TextView Raw
1/**
2 * @module botkit
3 */
4/**
5 * Copyright (c) Microsoft Corporation. All rights reserved.
6 * Licensed under the MIT License.
7 */
8import { Activity, MemoryStorage, Storage, ConversationReference, TurnContext, BotAdapter } from 'botbuilder';
9import { Dialog, DialogContext, DialogSet, DialogTurnStatus, WaterfallDialog } from 'botbuilder-dialogs';
10import { BotkitBotFrameworkAdapter } from './adapter';
11import { BotWorker } from './botworker';
12import { BotkitConversationState } from './conversationState';
13import * as path from 'path';
14import * as http from 'http';
15import * as express from 'express';
16import * as bodyParser from 'body-parser';
17import * as Ware from 'ware';
18import * as fs from 'fs';
19import * as Debug from 'debug';
20
21const debug = Debug('botkit');
22
23/**
24 * Defines the options used when instantiating Botkit to create the main app controller with `new Botkit(options)`
25 */
26export interface BotkitConfiguration {
27 /**
28 * Path used to create incoming webhook URI. Defaults to `/api/messages`
29 */
30 webhook_uri?: string;
31
32 /**
33 * Name of the dialogState property in the ConversationState that will be used to automatically track the dialog state. Defaults to `dialogState`.
34 */
35 dialogStateProperty?: string;
36
37 /**
38 * A fully configured BotBuilder Adapter, such as `botbuilder-adapter-slack` or `botbuilder-adapter-web`
39 * The adapter is responsible for translating platform-specific messages into the format understood by Botkit and BotBuilder.
40 */
41 adapter?: any;
42
43 /**
44 * If using the BotFramework service, options included in `adapterConfig` will be passed to the new Adapter when created internally.
45 * See [BotFrameworkAdapterSettings](https://docs.microsoft.com/en-us/javascript/api/botbuilder/botframeworkadaptersettings?view=azure-node-latest&viewFallbackFrom=botbuilder-ts-latest).
46 */
47 adapterConfig?: {[key: string]: any}; // object with stuff in it
48
49 /**
50 * An instance of Express used to define web endpoints. If not specified, one will be created internally.
51 * Note: only use your own Express if you absolutely must for some reason. Otherwise, use `controller.webserver`
52 */
53 webserver?: any;
54
55 /**
56 * An array of middlewares that will be automatically bound to the webserver.
57 * Should be in the form (req, res, next) => {}
58 */
59 webserver_middlewares?: any[];
60
61 /**
62 * A Storage interface compatible with [this specification](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/storage?view=botbuilder-ts-latest)
63 * Defaults to the ephemeral [MemoryStorage](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/memorystorage?view=botbuilder-ts-latest) implementation.
64 */
65 storage?: Storage;
66
67 /**
68 * Disable webserver. If true, Botkit will not create a webserver or expose any webhook endpoints automatically. Defaults to false.
69 */
70 disable_webserver?: boolean;
71
72 /**
73 * Disable messages normally sent to the console during startup.
74 */
75 disable_console?: boolean;
76
77 /**
78 * Limit of the size of incoming JSON payloads parsed by the Express bodyParser. Defaults to '100kb'
79 */
80 jsonLimit?: string;
81
82 /**
83 * Limit of the size of incoming URL encoded payloads parsed by the Express bodyParser. Defaults to '100kb'
84 */
85 urlEncodedLimit?: string;
86
87}
88
89/**
90 * Defines the expected form of a message or event object being handled by Botkit.
91 * Will also contain any additional fields including in the incoming payload.
92 */
93export interface BotkitMessage {
94 /**
95 * The type of event, in most cases defined by the messaging channel or adapter
96 */
97 type: string;
98
99 /**
100 * Text of the message sent by the user (or primary value in case of button click)
101 */
102 text?: string;
103
104 /**
105 * Any value field received from the platform
106 */
107 value?: string;
108
109 /**
110 * Unique identifier of user who sent the message. Typically contains the platform specific user id.
111 */
112 user: string;
113
114 /**
115 * Unique identifier of the room/channel/space in which the message was sent. Typically contains the platform specific designator for that channel.
116 */
117 channel: string;
118
119 /**
120 * A full [ConversationReference](https://docs.microsoft.com/en-us/javascript/api/botframework-schema/conversationreference?view=botbuilder-ts-latest) object that defines the address of the message and all information necessary to send messages back to the originating location.
121 * Can be stored for later use, and used with [bot.changeContext()](#changeContext) to send proactive messages.
122 */
123 reference: ConversationReference;
124
125 /**
126 * The original incoming [BotBuilder Activity](https://docs.microsoft.com/en-us/javascript/api/botframework-schema/activity?view=botbuilder-ts-latest) object as created by the adapter.
127 */
128 incoming_message: Activity;
129
130 /**
131 * Any additional fields found in the incoming payload from the messaging platform.
132 */
133 [key: string]: any;
134}
135
136/**
137 * A handler function passed into `hears()` or `on()` that receives a [BotWorker](#botworker) instance and a [BotkitMessage](#botkitmessage). Should be defined as an async function and/or return a Promise.
138 *
139 * The form of these handlers should be:
140 * ```javascript
141 * async (bot, message) => {
142 * // stuff.
143 * }
144 * ```
145 *
146 * For example:
147 * ```javascript
148 * controller.on('event', async(bot, message) => {
149 * // do somethign using bot and message like...
150 * await bot.reply(message,'Received an event.');
151 * });
152 * ```
153 */
154export interface BotkitHandler {
155 (bot: BotWorker, message: BotkitMessage): Promise<any>;
156}
157
158/**
159 * Defines a trigger, including the type, pattern and handler function to fire if triggered.
160 */
161interface BotkitTrigger {
162 /**
163 * string, regexp or function
164 */
165 type: string;
166 pattern: string | RegExp | { (message: BotkitMessage): Promise<boolean> };
167 handler: BotkitHandler;
168}
169
170/**
171 * An interface for plugins that can contain multiple middlewares as well as an init function.
172 */
173export interface BotkitPlugin {
174 name: string;
175 middlewares?: {
176 [key: string]: any[];
177 };
178 init?: (botkit: Botkit) => void;
179 [key: string]: any; // allow arbitrary additional fields to be added.
180}
181
182/**
183 * Create a new instance of Botkit to define the controller for a conversational app.
184 * To connect Botkit to a chat platform, pass in a fully configured `adapter`.
185 * If one is not specified, Botkit will expose an adapter for the Microsoft Bot Framework.
186 */
187export class Botkit {
188 /**
189 * _config contains the options passed to the constructor.
190 * this property should never be accessed directly - use `getConfig()` instead.
191 */
192 private _config: BotkitConfiguration;
193
194 /**
195 * _events contains the list of all events for which Botkit has registered handlers.
196 * Each key in this object points to an array of handler functions bound to that event.
197 */
198 private _events: {
199 [key: string]: BotkitHandler[];
200 } = {};
201
202 /**
203 * _triggers contains a list of trigger patterns htat Botkit will watch for.
204 * Each key in this object points to an array of patterns and their associated handlers.
205 * Each key represents an event type.
206 */
207 private _triggers: {
208 [key: string]: BotkitTrigger[];
209 } = {};
210
211 /**
212 * _interrupts contains a list of trigger patterns htat Botkit will watch for and fire BEFORE firing any normal triggers.
213 * Each key in this object points to an array of patterns and their associated handlers.
214 * Each key represents an event type.
215 */
216 private _interrupts: {
217 [key: string]: BotkitTrigger[];
218 } = {};
219
220 /**
221 * conversationState is used to track and persist the state of any ongoing conversations.
222 * See https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0&tabs=javascript
223 */
224 private conversationState: BotkitConversationState;
225
226 /**
227 * _deps contains a list of all dependencies that Botkit must load before being ready to operate.
228 * see addDep(), completeDep() and ready()
229 */
230 private _deps: {};
231
232 /**
233 * contains an array of functions that will fire when Botkit has completely booted.
234 */
235 private _bootCompleteHandlers: { (): void }[];
236
237 /**
238 * The current version of Botkit Core
239 */
240 public version: string = require('../package.json').version;
241
242 /**
243 * Middleware endpoints available for plugins and features to extend Botkit.
244 * Endpoints available are: spawn, ingest, receive, send.
245 *
246 * To bind a middleware function to Botkit:
247 * ```javascript
248 * controller.middleware.receive.use(function(bot, message, next) {
249 *
250 * // do something with bot or message
251 *
252 * // always call next, or your bot will freeze!
253 * next();
254 * });
255 * ```
256 */
257 public middleware = {
258 spawn: new Ware(),
259 ingest: new Ware(),
260 send: new Ware(),
261 receive: new Ware(),
262 interpret: new Ware()
263 }
264
265 /**
266 * A list of all the installed plugins.
267 */
268 private plugin_list: string[];
269
270 /**
271 * A place where plugins can extend the controller object with new methods
272 */
273 private _plugins: {
274 [key: string]: any;
275 };
276
277 /**
278 * a BotBuilder storage driver - defaults to MemoryStorage
279 */
280 public storage: Storage;
281
282 /**
283 * An Express webserver
284 */
285 public webserver: any;
286
287 /**
288 * A direct reference to the underlying HTTP server object
289 */
290 public http: any;
291
292 /**
293 * Any BotBuilder-compatible adapter - defaults to a [BotFrameworkAdapter](https://docs.microsoft.com/en-us/javascript/api/botbuilder/botframeworkadapter?view=botbuilder-ts-latest)
294 */
295 public adapter: any; // The base type of this is BotAdapter, but TypeScript doesn't like that we call adapter.processActivity since it is not part of the base class...
296
297 /**
298 * A BotBuilder DialogSet that serves as the top level dialog container for the Botkit app
299 */
300 public dialogSet: DialogSet;
301
302 /**
303 * The path of the main Botkit SDK, used to generate relative paths
304 */
305 public PATH: string;
306
307 /**
308 * Indicates whether or not Botkit has fully booted.
309 */
310 private booted: boolean;
311
312 /**
313 * Create a new Botkit instance and optionally specify a platform-specific adapter.
314 * By default, Botkit will create a [BotFrameworkAdapter](https://docs.microsoft.com/en-us/javascript/api/botbuilder/botframeworkadapter?view=botbuilder-ts-latest).
315 *
316 * ```javascript
317 * const controller = new Botkit({
318 * adapter: some_adapter,
319 * webhook_uri: '/api/messages',
320 * });
321 *
322 * controller.on('message', async(bot, message) => {
323 * // do something!
324 * });
325 * ```
326 *
327 * @param config Configuration for this instance of Botkit
328 */
329 public constructor(config: BotkitConfiguration) {
330 // Set the path where Botkit's core lib is found.
331 this.PATH = __dirname;
332
333 this._config = {
334 webhook_uri: '/api/messages',
335 dialogStateProperty: 'dialogState',
336 disable_webserver: false,
337 jsonLimit: '100kb',
338 urlEncodedLimit: '100kb',
339 ...config
340 };
341
342 // The _deps object contains references to dependencies that may take time to load and be ready.
343 // new _deps are defined in the constructor.
344 // when all _deps are true, the controller.ready function runs and executes all functions in order.
345 this._deps = {};
346 this._bootCompleteHandlers = [];
347 this.booted = false;
348 this.addDep('booted');
349
350 debug('Booting Botkit ', this.version);
351
352 if (!this._config.storage) {
353 // Set up temporary storage for dialog state.
354 this.storage = new MemoryStorage();
355 if (this._config.disable_console !== true) {
356 console.warn('** Your bot is using memory storage and will forget everything when it reboots!');
357 console.warn('** To preserve dialog state, specify a storage adapter in your Botkit config:');
358 console.warn('** const controller = new Botkit({storage: myStorageAdapter});');
359 }
360 } else {
361 this.storage = this._config.storage;
362 }
363
364 this.conversationState = new BotkitConversationState(this.storage);
365
366 const dialogState = this.conversationState.createProperty(this.getConfig('dialogStateProperty'));
367
368 this.dialogSet = new DialogSet(dialogState);
369
370 if (this._config.disable_webserver !== true) {
371 if (!this._config.webserver) {
372 // Create HTTP server
373 this.addDep('webserver');
374
375 this.webserver = express();
376
377 // capture raw body
378 this.webserver.use((req, res, next) => {
379 req.rawBody = '';
380 req.on('data', function(chunk) {
381 req.rawBody += chunk;
382 });
383 next();
384 });
385
386 this.webserver.use(bodyParser.json({ limit: this._config.jsonLimit }));
387 this.webserver.use(bodyParser.urlencoded({ limit: this._config.urlEncodedLimit, extended: true }));
388
389 if (this._config.webserver_middlewares && this._config.webserver_middlewares.length) {
390 this._config.webserver_middlewares.forEach((middleware) => {
391 this.webserver.use(middleware);
392 });
393 }
394
395 this.http = http.createServer(this.webserver);
396
397 this.http.listen(process.env.port || process.env.PORT || 3000, () => {
398 if (this._config.disable_console !== true) {
399 console.log(`Webhook endpoint online: http://localhost:${ process.env.PORT || 3000 }${ this._config.webhook_uri }`);
400 }
401 this.completeDep('webserver');
402 });
403 } else {
404 this.webserver = this._config.webserver;
405 }
406 }
407
408 if (!this._config.adapter) {
409 const adapterConfig = { ...this._config.adapterConfig };
410 debug('Configuring BotFrameworkAdapter:', adapterConfig);
411 this.adapter = new BotkitBotFrameworkAdapter(adapterConfig);
412 if (this.webserver) {
413 if (this._config.disable_console !== true) {
414 console.log('Open this bot in Bot Framework Emulator: bfemulator://livechat.open?botUrl=' + encodeURIComponent(`http://localhost:${ process.env.PORT || 3000 }${ this._config.webhook_uri }`));
415 }
416 }
417 } else {
418 debug('Using pre-configured adapter.');
419 this.adapter = this._config.adapter;
420 }
421
422 // If a webserver has been configured, auto-configure the default webhook url
423 if (this.webserver) {
424 this.configureWebhookEndpoint();
425 }
426
427 // initialize the plugins array.
428 this.plugin_list = [];
429 this._plugins = {};
430
431 // if an adapter has been configured, add it as a plugin.
432 if (this.adapter) {
433 // MAGIC: Treat the adapter as a botkit plugin
434 // which allows them to be carry their own platform-specific behaviors
435 this.usePlugin(this.adapter);
436 }
437
438 this.completeDep('booted');
439 }
440
441 /**
442 * Shutdown the webserver and prepare to terminate the app.
443 * Causes Botkit to first emit a special `shutdown` event, process any bound handlers, and then finally terminate the webserver.
444 * Bind any necessary cleanup helpers to the shutdown event - for example, close the connection to mongo.
445 *
446 * ```javascript
447 * await controller.shutdown();
448 * controller.on('shutdown', async() => {
449 * console.log('Bot is shutting down!');
450 * });
451 * ```
452 */
453 public async shutdown(): Promise<void> {
454 // trigger a special shutdown event
455 await this.trigger('shutdown');
456
457 if (this.http) {
458 this.http.close();
459 }
460 }
461
462 /**
463 * Get a value from the configuration.
464 *
465 * For example:
466 * ```javascript
467 * // get entire config object
468 * let config = controller.getConfig();
469 *
470 * // get a specific value from the config
471 * let webhook_uri = controller.getConfig('webhook_uri');
472 * ```
473 *
474 * @param {string} key The name of a value stored in the configuration
475 * @returns {any} The value stored in the configuration (or null if absent)
476 */
477 public getConfig(key?: string): any {
478 if (key) {
479 return this._config[key];
480 } else {
481 return this._config;
482 }
483 }
484
485 /**
486 * Load a plugin module and bind all included middlewares to their respective endpoints.
487 * @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.
488 */
489 public usePlugin(plugin_or_function: ((botkit: Botkit) => BotkitPlugin) | BotkitPlugin): void {
490 let plugin: BotkitPlugin;
491 if (typeof (plugin_or_function) === 'function') {
492 plugin = plugin_or_function(this);
493 } else {
494 plugin = plugin_or_function;
495 }
496 if (plugin.name) {
497 try {
498 this.registerPlugin(plugin.name, plugin);
499 } catch (err) {
500 console.error('ERROR IN PLUGIN REGISTER', err);
501 }
502 }
503 }
504
505 /**
506 * Called from usePlugin -- do the actual binding of middlewares for a plugin that is being loaded.
507 * @param name name of the plugin
508 * @param endpoints the plugin object that contains middleware endpoint definitions
509 */
510 private registerPlugin(name: string, endpoints: BotkitPlugin): void {
511 if (this._config.disable_console !== true) {
512 console.log('Enabling plugin: ', name);
513 }
514 if (this.plugin_list.indexOf(name) >= 0) {
515 debug('Plugin already enabled:', name);
516 return;
517 }
518 this.plugin_list.push(name);
519
520 if (endpoints.middlewares) {
521 for (const mw in endpoints.middlewares) {
522 for (let e = 0; e < endpoints.middlewares[mw].length; e++) {
523 this.middleware[mw].use(endpoints.middlewares[mw][e]);
524 }
525 }
526 }
527
528 if (endpoints.init) {
529 try {
530 endpoints.init(this);
531 } catch (err) {
532 if (err) {
533 throw new Error(err);
534 }
535 }
536 }
537
538 debug('Plugin Enabled: ', name);
539 }
540
541 /**
542 * (Plugins only) Extend Botkit's controller with new functionality and make it available globally via the controller object.
543 *
544 * ```javascript
545 *
546 * // define the extension interface
547 * let extension = {
548 * stuff: () => { return 'stuff' }
549 * }
550 *
551 * // register the extension
552 * controller.addPluginExtension('foo', extension);
553 *
554 * // call extension
555 * controller.plugins.foo.stuff();
556 *
557 *
558 * ```
559 * @param name name of plugin
560 * @param extension an object containing methods
561 */
562 public addPluginExtension(name: string, extension: any): void {
563 debug('Plugin extension added: controller.' + name);
564 this._plugins[name] = extension;
565 }
566
567 /**
568 * Access plugin extension methods.
569 * After a plugin calls `controller.addPluginExtension('foo', extension_methods)`, the extension will then be available at
570 * `controller.plugins.foo`
571 */
572 public get plugins(): {[key: string]: any} {
573 return this._plugins;
574 }
575
576 /**
577 * Expose a folder to the web as a set of static files.
578 * Useful for plugins that need to bundle additional assets!
579 *
580 * ```javascript
581 * // make content of the local public folder available at http://MYBOTURL/public/myplugin
582 * controller.publicFolder('/public/myplugin', __dirname + '/public);
583 * ```
584 * @param alias the public alias ie /myfiles
585 * @param path the actual path something like `__dirname + '/public'`
586 */
587 public publicFolder(alias, path): void {
588 if (this.webserver) {
589 debug('Make folder public: ', path, 'at alias', alias);
590 this.webserver.use(alias, express.static(path));
591 } else {
592 throw new Error('Cannot create public folder alias when webserver is disabled');
593 }
594 }
595
596 /**
597 * Convert a local path from a plugin folder to a full path relative to the webserver's main views folder.
598 * Allows a plugin to bundle views/layouts and make them available to the webserver's renderer.
599 * @param path_to_view something like path.join(__dirname,'views')
600 */
601 public getLocalView(path_to_view): string {
602 if (this.webserver) {
603 return path.relative(path.join(this.webserver.get('views')), path_to_view);
604 } else {
605 throw new Error('Cannot get local view when webserver is disabled');
606 }
607 }
608
609 /**
610 * (For use by Botkit plugins only) - Add a dependency to Botkit's bootup process that must be marked as completed using `completeDep()`.
611 * Botkit's `controller.ready()` function will not fire until all dependencies have been marked complete.
612 *
613 * For example, a plugin that needs to do an asynchronous task before Botkit proceeds might do:
614 * ```javascript
615 * controller.addDep('my_async_plugin');
616 * somethingAsync().then(function() {
617 * controller.completeDep('my_async_plugin');
618 * });
619 * ```
620 *
621 * @param name {string} The name of the dependency that is being loaded.
622 */
623 public addDep(name: string): void {
624 debug(`Waiting for ${ name }`);
625 this._deps[name] = false;
626 }
627
628 /**
629 * (For use by plugins only) - Mark a bootup dependency as loaded and ready to use
630 * Botkit's `controller.ready()` function will not fire until all dependencies have been marked complete.
631
632 * @param name {string} The name of the dependency that has completed loading.
633 */
634 public completeDep(name: string): boolean {
635 debug(`${ name } ready`);
636
637 this._deps[name] = true;
638
639 for (const key in this._deps) {
640 if (this._deps[key] === false) {
641 return false;
642 }
643 }
644
645 // everything is done!
646 this.signalBootComplete();
647 return true;
648 }
649
650 /**
651 * This function gets called when all of the bootup dependencies are completely loaded.
652 */
653 private signalBootComplete(): void {
654 this.booted = true;
655 for (let h = 0; h < this._bootCompleteHandlers.length; h++) {
656 const handler = this._bootCompleteHandlers[h];
657 handler.call(this);
658 }
659 }
660
661 /**
662 * Use `controller.ready()` to wrap any calls that require components loaded during the bootup process.
663 * This will ensure that the calls will not be made until all of the components have successfully been initialized.
664 *
665 * For example:
666 * ```javascript
667 * controller.ready(() => {
668 *
669 * controller.loadModules(__dirname + '/features');
670 *
671 * });
672 * ```
673 *
674 * @param handler {function} A function to run when Botkit is booted and ready to run.
675 */
676 public ready(handler: () => any): void {
677 if (this.booted) {
678 handler.call(this);
679 } else {
680 this._bootCompleteHandlers.push(handler);
681 }
682 }
683
684 /*
685 * Set up a web endpoint to receive incoming messages,
686 * pass them through a normalization process, and then ingest them for processing.
687 */
688 private configureWebhookEndpoint(): void {
689 if (this.webserver) {
690 this.webserver.post(this._config.webhook_uri, (req, res) => {
691 // Allow the Botbuilder middleware to fire.
692 // this middleware is responsible for turning the incoming payload into a BotBuilder Activity
693 // which we can then use to turn into a BotkitMessage
694 this.adapter.processActivity(req, res, this.handleTurn.bind(this)).catch((err) => {
695 // todo: expose this as a global error handler?
696 console.error('Experienced an error inside the turn handler', err);
697 throw err;
698 });
699 });
700 } else {
701 throw new Error('Cannot configure webhook endpoints when webserver is disabled');
702 }
703 }
704
705 /**
706 * Accepts the result of a BotBuilder adapter's `processActivity()` method and processes it into a Botkit-style message and BotWorker instance
707 * which is then used to test for triggers and emit events.
708 * 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)
709 * @param turnContext {TurnContext} a TurnContext representing an incoming message, typically created by an adapter's `processActivity()` method.
710 */
711 public async handleTurn(turnContext: TurnContext): Promise<any> {
712 debug('INCOMING ACTIVITY:', turnContext.activity);
713
714 // Turn this turnContext into a Botkit message.
715 const message: BotkitMessage = {
716 // ...turnContext.activity,
717 ...turnContext.activity.channelData, // start with all the fields that were in the original incoming payload. NOTE: this is a shallow copy, is that a problem?
718
719 // if Botkit has further classified this message, use that sub-type rather than the Activity type
720 type: (turnContext.activity.channelData && turnContext.activity.channelData.botkitEventType) ? turnContext.activity.channelData.botkitEventType : turnContext.activity.type,
721
722 // normalize the user, text and channel info
723 user: turnContext.activity.from.id,
724 text: turnContext.activity.text,
725 channel: turnContext.activity.conversation.id,
726
727 value: turnContext.activity.value,
728
729 // generate a conversation reference, for replies.
730 // included so people can easily capture it for resuming
731 reference: TurnContext.getConversationReference(turnContext.activity),
732
733 // include the context possible useful.
734 context: turnContext,
735
736 // include the full unmodified record here
737 incoming_message: turnContext.activity
738 };
739
740 // Stash the Botkit message in
741 turnContext.turnState.set('botkitMessage', message);
742
743 // Create a dialog context
744 const dialogContext = await this.dialogSet.createContext(turnContext);
745
746 // Spawn a bot worker with the dialogContext
747 const bot = await this.spawn(dialogContext);
748
749 return new Promise((resolve, reject) => {
750 this.middleware.ingest.run(bot, message, async (err, bot, message) => {
751 if (err) {
752 reject(err);
753 } else {
754 this.middleware.receive.run(bot, message, async (err, bot, message) => {
755 if (err) {
756 reject(err);
757 } else {
758 const interrupt_results = await this.listenForInterrupts(bot, message);
759
760 if (interrupt_results === false) {
761 // Continue dialog if one is present
762 const dialog_results = await dialogContext.continueDialog();
763 if (dialog_results && dialog_results.status === DialogTurnStatus.empty) {
764 await this.processTriggersAndEvents(bot, message);
765 }
766 }
767
768 // make sure changes to the state get persisted after the turn is over.
769 await this.saveState(bot);
770 resolve();
771 }
772 });
773 }
774 });
775 });
776 }
777
778 /**
779 * Save the current conversation state pertaining to a given BotWorker's activities.
780 * Note: this is normally called internally and is only required when state changes happen outside of the normal processing flow.
781 * @param bot {BotWorker} a BotWorker instance created using `controller.spawn()`
782 */
783 public async saveState(bot: BotWorker): Promise<void> {
784 await this.conversationState.saveChanges(bot.getConfig('context'));
785 }
786
787 /**
788 * Ingests a message and evaluates it for triggers, run the receive middleware, and triggers any events.
789 * Note: This is normally called automatically from inside `handleTurn()` and in most cases should not be called directly.
790 * @param bot {BotWorker} An instance of the bot
791 * @param message {BotkitMessage} an incoming message
792 */
793 private async processTriggersAndEvents(bot: BotWorker, message: BotkitMessage): Promise<any> {
794 return new Promise((resolve, reject) => {
795 this.middleware.interpret.run(bot, message, async (err, bot, message) => {
796 if (err) {
797 return reject(err);
798 }
799 const listen_results = await this.listenForTriggers(bot, message);
800
801 if (listen_results !== false) {
802 resolve(listen_results);
803 } else {
804 // Trigger event handlers
805 const trigger_results = await this.trigger(message.type, bot, message);
806
807 resolve(trigger_results);
808 }
809 });
810 });
811 }
812
813 /**
814 * Evaluates an incoming message for triggers created with `controller.hears()` and fires any relevant handler functions.
815 * @param bot {BotWorker} An instance of the bot
816 * @param message {BotkitMessage} an incoming message
817 */
818 private async listenForTriggers(bot: BotWorker, message: BotkitMessage): Promise<any> {
819 if (this._triggers[message.type]) {
820 const triggers = this._triggers[message.type];
821 for (let t = 0; t < triggers.length; t++) {
822 const test_results = await this.testTrigger(triggers[t], message);
823 if (test_results) {
824 debug('Heard pattern: ', triggers[t].pattern);
825 const trigger_results = await triggers[t].handler.call(this, bot, message);
826 return trigger_results;
827 }
828 }
829
830 // nothing has triggered...return false
831 return false;
832 } else {
833 return false;
834 }
835 }
836
837 /**
838 * Evaluates an incoming message for triggers created with `controller.interrupts()` and fires any relevant handler functions.
839 * @param bot {BotWorker} An instance of the bot
840 * @param message {BotkitMessage} an incoming message
841 */
842 private async listenForInterrupts(bot: BotWorker, message: BotkitMessage): Promise<any> {
843 if (this._interrupts[message.type]) {
844 const triggers = this._interrupts[message.type];
845 for (let t = 0; t < triggers.length; t++) {
846 const test_results = await this.testTrigger(triggers[t], message);
847 if (test_results) {
848 debug('Heard interruption: ', triggers[t].pattern);
849 const trigger_results = await triggers[t].handler.call(this, bot, message);
850 return trigger_results;
851 }
852 }
853
854 // nothing has triggered...return false
855 return false;
856 } else {
857 return false;
858 }
859 }
860
861 /**
862 * Evaluates a single trigger and return true if the incoming message matches the conditions
863 * @param trigger {BotkitTrigger} a trigger definition
864 * @param message {BotkitMessage} an incoming message
865 */
866 private async testTrigger(trigger: BotkitTrigger, message: BotkitMessage): Promise<boolean> {
867 if (trigger.type === 'string') {
868 const test = new RegExp(trigger.pattern as string, 'i');
869 if (message.text && message.text.match(test)) {
870 return true;
871 }
872 } else if (trigger.type === 'regexp') {
873 const test = trigger.pattern as RegExp;
874 if (message.text && message.text.match(test)) {
875 message.matches = message.text.match(test);
876 return true;
877 }
878 } else if (trigger.type === 'function') {
879 const test = trigger.pattern as (message) => Promise<boolean>;
880 return await test(message);
881 }
882
883 return false;
884 }
885
886 /**
887 * Instruct your bot to listen for a pattern, and do something when that pattern is heard.
888 * Patterns will be "heard" only if the message is not already handled by an in-progress dialog.
889 * To "hear" patterns _before_ dialogs are processed, use `controller.interrupts()` instead.
890 *
891 * For example:
892 * ```javascript
893 * // listen for a simple keyword
894 * controller.hears('hello','message', async(bot, message) => {
895 * await bot.reply(message,'I heard you say hello.');
896 * });
897 *
898 * // listen for a regular expression
899 * controller.hears(new RegExp(/^[A-Z\s]+$/), 'message', async(bot, message) => {
900 * await bot.reply(message,'I heard a message IN ALL CAPS.');
901 * });
902 *
903 * // listen using a function
904 * controller.hears(async (message) => { return (message.intent === 'hello') }, 'message', async(bot, message) => {
905 * await bot.reply(message,'This message matches the hello intent.');
906 * });
907 * ```
908 * @param patterns {} One or more string, regular expression, or test function
909 * @param events {} A list of event types that should be evaluated for the given patterns
910 * @param handler {BotkitHandler} a function that will be called should the pattern be matched
911 */
912 public hears(patterns: (string | RegExp | { (message: BotkitMessage): Promise<boolean> })[] | RegExp | string | { (message: BotkitMessage): Promise<boolean> }, events: string | string[], handler: BotkitHandler): void {
913 if (!Array.isArray(patterns)) {
914 patterns = [patterns];
915 }
916
917 if (typeof events === 'string') {
918 events = events.split(/,/).map(e => e.trim());
919 }
920
921 debug('Registering hears for ', events);
922
923 for (let p = 0; p < patterns.length; p++) {
924 for (let e = 0; e < events.length; e++) {
925 const event = events[e];
926 const pattern = patterns[p];
927
928 if (!this._triggers[event]) {
929 this._triggers[event] = [];
930 }
931
932 const trigger = {
933 pattern: pattern,
934 handler: handler,
935 type: null
936 };
937
938 if (typeof pattern === 'string') {
939 trigger.type = 'string';
940 } else if (pattern instanceof RegExp) {
941 trigger.type = 'regexp';
942 } else if (typeof pattern === 'function') {
943 trigger.type = 'function';
944 }
945
946 this._triggers[event].push(trigger);
947 }
948 }
949 }
950
951 /**
952 * Instruct your bot to listen for a pattern, and do something when that pattern is heard.
953 * Interruptions work just like "hears" triggers, but fire _before_ the dialog system is engaged,
954 * and thus handlers will interrupt the normal flow of messages through the processing pipeline.
955 *
956 * ```javascript
957 * controller.interrupts('help','message', async(bot, message) => {
958 *
959 * await bot.reply(message,'Before anything else, you need some help!')
960 *
961 * });
962 * ```
963 * @param patterns {} One or more string, regular expression, or test function
964 * @param events {} A list of event types that should be evaluated for the given patterns
965 * @param handler {BotkitHandler} a function that will be called should the pattern be matched
966 */
967 public interrupts(patterns: (string | RegExp | { (message: BotkitMessage): Promise<boolean> })[] | RegExp | RegExp[] | string | { (message: BotkitMessage): Promise<boolean> }, events: string | string[], handler: BotkitHandler): void {
968 if (!Array.isArray(patterns)) {
969 patterns = [patterns];
970 }
971
972 if (typeof events === 'string') {
973 events = events.split(/,/).map(e => e.trim());
974 }
975 debug('Registering hears for ', events);
976
977 for (let p = 0; p < patterns.length; p++) {
978 for (let e = 0; e < events.length; e++) {
979 const event = events[e];
980 const pattern = patterns[p];
981
982 if (!this._interrupts[event]) {
983 this._interrupts[event] = [];
984 }
985
986 const trigger = {
987 pattern: pattern,
988 handler: handler,
989 type: null
990 };
991
992 if (typeof pattern === 'string') {
993 trigger.type = 'string';
994 } else if (pattern instanceof RegExp) {
995 trigger.type = 'regexp';
996 } else if (typeof pattern === 'function') {
997 trigger.type = 'function';
998 }
999
1000 this._interrupts[event].push(trigger);
1001 }
1002 }
1003 }
1004
1005 /**
1006 * Bind a handler function to one or more events.
1007 *
1008 * ```javascript
1009 * controller.on('conversationUpdate', async(bot, message) => {
1010 *
1011 * await bot.reply(message,'I received a conversationUpdate event.');
1012 *
1013 * });
1014 * ```
1015 *
1016 * @param events {} One or more event names
1017 * @param handler {BotkitHandler} a handler function that will fire whenever one of the named events is received.
1018 */
1019 public on(events: string | string[], handler: BotkitHandler): void {
1020 if (typeof events === 'string') {
1021 events = events.split(/,/).map(e => e.trim());
1022 }
1023
1024 debug('Registering handler for: ', events);
1025 events.forEach((event) => {
1026 if (!this._events[event]) {
1027 this._events[event] = [];
1028 }
1029 this._events[event].push(handler);
1030 });
1031 }
1032
1033 /**
1034 * Trigger an event to be fired. This will cause any bound handlers to be executed.
1035 * Note: This is normally used internally, but can be used to emit custom events.
1036 *
1037 * ```javascript
1038 * // fire a custom event
1039 * controller.trigger('my_custom_event', bot, message);
1040 *
1041 * // handle the custom event
1042 * controller.on('my_custom_event', async(bot, message) => {
1043 * //... do something
1044 * });
1045 * ```
1046 *
1047 * @param event {string} the name of the event
1048 * @param bot {BotWorker} a BotWorker instance created using `controller.spawn()`
1049 * @param message {BotkitMessagE} An incoming message or event
1050 */
1051 public async trigger(event: string, bot?: BotWorker, message?: BotkitMessage): Promise<any> {
1052 debug('Trigger event: ', event);
1053 if (this._events[event] && this._events[event].length) {
1054 for (let h = 0; h < this._events[event].length; h++) {
1055 try {
1056 const handler_results = await this._events[event][h].call(bot, bot, message);
1057 if (handler_results === false) {
1058 break;
1059 }
1060 } catch (err) {
1061 console.error('Error in trigger handler', err);
1062 throw Error(err);
1063 }
1064 }
1065 }
1066 }
1067
1068 /**
1069 * Create a platform-specific BotWorker instance that can be used to respond to messages or generate new outbound messages.
1070 * The spawned `bot` contains all information required to process outbound messages and handle dialog state, and may also contain extensions
1071 * for handling platform-specific events or activities.
1072 * @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.
1073 * @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.
1074 */
1075 public async spawn(config?: any, custom_adapter?: BotAdapter): Promise<BotWorker> {
1076 if (config instanceof TurnContext) {
1077 config = {
1078 dialogContext: await this.dialogSet.createContext(config as TurnContext),
1079 context: config as TurnContext,
1080 reference: TurnContext.getConversationReference(config.activity),
1081 activity: config.activity
1082 };
1083 } else if (config instanceof DialogContext) {
1084 config = {
1085 dialogContext: config,
1086 reference: TurnContext.getConversationReference(config.context.activity),
1087 context: config.context,
1088 activity: config.context.activity
1089 };
1090 }
1091
1092 let worker: BotWorker = null;
1093 const adapter = custom_adapter || ((config && config.context && config.context.adapter) ? config.context.adapter : this.adapter);
1094
1095 if (adapter.botkit_worker) {
1096 const CustomBotWorker = adapter.botkit_worker;
1097 worker = new CustomBotWorker(this, config);
1098 } else {
1099 worker = new BotWorker(this, config);
1100 }
1101
1102 // make sure the adapter is available in a standard location.
1103 worker.getConfig().adapter = adapter;
1104
1105 return new Promise((resolve, reject) => {
1106 this.middleware.spawn.run(worker, (err, worker) => {
1107 if (err) {
1108 reject(err);
1109 } else {
1110 resolve(worker);
1111 }
1112 });
1113 });
1114 }
1115
1116 /**
1117 * Load a Botkit feature module
1118 *
1119 * @param p {string} path to module file
1120 */
1121 public loadModule(p: string): void {
1122 debug('Load Module:', p);
1123 // eslint-disable-next-line @typescript-eslint/no-var-requires
1124 const module = require(p);
1125 // Handle both CJS `module.exports` and ESM `export default` syntax.
1126 if (typeof module === 'function') {
1127 module(this);
1128 } else if (module && typeof module.default === 'function') {
1129 module.default(this);
1130 } else {
1131 throw new Error(`Failed to load '${ p }', did you export a function?`);
1132 }
1133 }
1134
1135 /**
1136 * Load all Botkit feature modules located in a given folder.
1137 *
1138 * ```javascript
1139 * controller.ready(() => {
1140 *
1141 * // load all modules from sub-folder features/
1142 * controller.loadModules('./features');
1143 *
1144 * });
1145 * ```
1146 *
1147 * @param p {string} path to a folder of module files
1148 * @param exts {string[]} the extensions that you would like to load (default: ['.js'])
1149 */
1150 public loadModules(p: string, exts: string[] = ['.js']): void {
1151 // load all the .js|.ts files from this path
1152 fs.readdirSync(p).filter((f) => {
1153 return exts.includes(path.extname(f));
1154 }).forEach((file) => {
1155 this.loadModule(path.join(p, file));
1156 });
1157 }
1158
1159 /**
1160 * Add a dialog to the bot, making it accessible via `bot.beginDialog(dialog_id)`
1161 *
1162 * ```javascript
1163 * // Create a dialog -- `BotkitConversation` is just one way to create a dialog
1164 * const my_dialog = new BotkitConversation('my_dialog', controller);
1165 * my_dialog.say('Hello');
1166 *
1167 * // Add the dialog to the Botkit controller
1168 * controller.addDialog(my_dialog);
1169 *
1170 * // Later on, trigger the dialog into action!
1171 * controller.on('message', async(bot, message) => {
1172 * await bot.beginDialog('my_dialog');
1173 * });
1174 * ```
1175 *
1176 * @param dialog A dialog to be added to the bot's dialog set
1177 */
1178 public addDialog(dialog: Dialog): void {
1179 // add the actual dialog
1180 this.dialogSet.add(dialog);
1181
1182 // add a wrapper dialog that will be called by bot.beginDialog
1183 // and is responsible for capturing the parent results
1184 this.dialogSet.add(new WaterfallDialog(dialog.id + ':botkit-wrapper', [
1185 async (step): Promise<any> => {
1186 return step.beginDialog(dialog.id, step.options);
1187 },
1188 async (step): Promise<any> => {
1189 const bot = await this.spawn(step.context);
1190
1191 await this.trigger(dialog.id + ':after', bot, step.result);
1192 return step.endDialog(step.result);
1193 }
1194 ]));
1195 }
1196
1197 /**
1198 * Bind a handler to the end of a dialog.
1199 * NOTE: bot worker cannot use bot.reply(), must use bot.send()
1200 *
1201 * [Learn more about handling end-of-conversation](../docs/conversations.md#handling-end-of-conversation)
1202 * @param dialog the dialog object or the id of the dialog
1203 * @param handler a handler function in the form `async(bot, dialog_results) => {}`
1204 */
1205 public afterDialog(dialog: Dialog | string, handler: BotkitHandler): void {
1206 let id = '';
1207 if (typeof (dialog) === 'string') {
1208 id = dialog as string;
1209 } else {
1210 id = dialog.id;
1211 }
1212
1213 this.on(id + ':after', handler);
1214 }
1215}