1 | ;
|
2 | var __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 | };
|
11 | Object.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 | */
|
19 | const botbuilder_1 = require("botbuilder");
|
20 | const botbuilder_dialogs_1 = require("botbuilder-dialogs");
|
21 | const adapter_1 = require("./adapter");
|
22 | const botworker_1 = require("./botworker");
|
23 | const conversationState_1 = require("./conversationState");
|
24 | const path = require("path");
|
25 | const http = require("http");
|
26 | const express = require("express");
|
27 | const bodyParser = require("body-parser");
|
28 | const Ware = require("ware");
|
29 | const fs = require("fs");
|
30 | const Debug = require("debug");
|
31 | const 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 | */
|
37 | class 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 | }
|
926 | exports.Botkit = Botkit;
|
927 | //# sourceMappingURL=core.js.map |
\ | No newline at end of file |