1 | /**
|
2 | * @module botkit
|
3 | */
|
4 | /**
|
5 | * Copyright (c) Microsoft Corporation. All rights reserved.
|
6 | * Licensed under the MIT License.
|
7 | */
|
8 | import { Activity, MemoryStorage, Storage, ConversationReference, TurnContext, BotAdapter } from 'botbuilder';
|
9 | import { Dialog, DialogContext, DialogSet, DialogTurnStatus, WaterfallDialog } from 'botbuilder-dialogs';
|
10 | import { BotkitBotFrameworkAdapter } from './adapter';
|
11 | import { BotWorker } from './botworker';
|
12 | import { BotkitConversationState } from './conversationState';
|
13 | import * as path from 'path';
|
14 | import * as http from 'http';
|
15 | import * as express from 'express';
|
16 | import * as bodyParser from 'body-parser';
|
17 | import * as Ware from 'ware';
|
18 | import * as fs from 'fs';
|
19 | import * as Debug from 'debug';
|
20 |
|
21 | const debug = Debug('botkit');
|
22 |
|
23 | /**
|
24 | * Defines the options used when instantiating Botkit to create the main app controller with `new Botkit(options)`
|
25 | */
|
26 | export 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 | */
|
93 | export 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 | */
|
154 | export 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 | */
|
161 | interface 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 | */
|
173 | export 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 | */
|
187 | export 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<void>((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 | }
|