Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Add local Botkit CMS plugin to load a local scripts file #1851

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions packages/botkit-plugin-cms/src/cms-local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* @module botkit-plugin-cms
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { Botkit, BotkitConversation, BotkitMessage, BotkitPlugin, BotWorker } from 'botkit';
import { CmsPluginCore } from './plugin-core';

const debug = require('debug')('botkit:cms-local');

/**
* A plugin for Botkit that provides access to local scripts in [Botkit CMS](https://github.com/howdyai/botkit-cms) format,
* including the ability to load script content into a DialogSet and bind before, after and onChange handlers to those dynamically imported dialogs by name.
*
* ```javascript
* const cms = require("botkit-cms")();
* controller.usePlugin(new BotkitCmsLocalPlugin({
* cms,
* path: `${__dirname}/scripts.json`,
* }));
*
* // use the local cms to test dialog triggers
* controller.on("message", async (bot, message) => {
* const results = await controller.plugins.cms.testTrigger(bot, message);
* return results === false;
* });
* ```
*/
export class BotkitCmsLocalPlugin extends CmsPluginCore implements BotkitPlugin {
private _config: LocalCmsOptions;
protected _controller: Botkit;

public name = 'Botkit CMS local';

/**
* Constructor
* @param config
*/
public constructor(config: LocalCmsOptions) {
super();

this._config = config;

if (!this._config.path) {
throw new Error('Scripts paths must be set to use Botkit CMS local plugin.');
}
if (!this._config.cms) {
throw new Error('CMS must be set to use Botkit CMS local plugin.');
}
}

/**
* Botkit plugin init function
* @param controller
*/
public init(controller: Botkit): void {
this._controller = controller;
this._controller.addDep('cms');

controller.addPluginExtension('cms', this);

this._config.cms.loadScriptsFromFile(this._config.path).then((scripts) => {
scripts.forEach((script) => {
// map threads from array to object
const threads = {};
script.script.forEach((thread) => {
threads[thread.topic] = thread.script.map(this.mapFields);
});

const dialog = new BotkitConversation(script.command, this._controller);
dialog.script = threads;
this._controller.addDialog(dialog);
});

debug('Dialogs loaded from Botkit CMS local file');
this._controller.completeDep('cms');
}).catch((err) => {
console.error('Error loading Botkit CMS local scripts!');
console.error(`****************************************************************\n${ err }\n****************************************************************`);
});
}

/**
* Evaluate if the message's text triggers a dialog from the CMS. Returns a promise
* with the command object if found, or rejects if not found.
*
* @param text
*/
public evaluateTrigger(text: string): Promise<any> {
return this._config.cms.evaluateTriggers(text);
};

/**
* Uses the Botkit CMS trigger API to test an incoming message against a list of predefined triggers.
* If a trigger is matched, the appropriate dialog will begin immediately.
*
* @param bot The current bot worker instance
* @param message An incoming message to be interpreted
* @returns Returns false if a dialog is NOT triggered, otherwise returns void.
*/
public testTrigger(bot: BotWorker, message: BotkitMessage): Promise<any> {
debug('Testing Botkit CMS trigger with: ' + message.text);
return this.evaluateTrigger(message.text).then(function(command) {
if (command.command) {
debug('Trigger found, beginning dialog ' + command.command);
return bot.beginDialog(command.command);
}
}).catch(function(error) {
if (typeof (error) === 'undefined') {
return false;
}
throw error;
});
};

/**
* Get all scripts, optionally filtering by a tag
* @param tag
*/
public getScripts(tag?: string): Promise<any[]> {
return this._config.cms.getScripts(tag);
};

/**
* Load script from CMS by id
* @param id
*/
public async getScriptById(id: string): Promise<any> {
try {
return await this._config.cms.getScriptById(id);
} catch (error) {
if (typeof (error) === 'undefined') {
// Script was just not found
return null;
}
throw error;
}
};

/**
* Load script from CMS by command
* @param command
*/
public async getScript(command: string): Promise<any> {
try {
return await this._config.cms.getScript(command);
} catch (error) {
if (typeof (error) === 'undefined') {
// Script was just not found
return null;
}
throw error;
}
};
}

export interface LocalCmsOptions {
path: string;
cms: any;
}
164 changes: 8 additions & 156 deletions packages/botkit-plugin-cms/src/cms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
* Licensed under the MIT License.
*/

import { Botkit, BotkitDialogWrapper, BotkitMessage, BotWorker, BotkitConversation } from 'botkit';
import { Botkit, BotkitDialogWrapper, BotkitMessage, BotWorker, BotkitConversation, BotkitPlugin } from 'botkit';
import * as request from 'request';
import * as Debug from 'debug';
import * as url from 'url';
import { CmsPluginCore } from './plugin-core';

const debug = Debug('botkit:cms');

Expand All @@ -29,16 +30,18 @@ const debug = Debug('botkit:cms');
* });
* ```
*/
export class BotkitCMSHelper {
export class BotkitCMSHelper extends CmsPluginCore implements BotkitPlugin {
private _config: any;
private _controller: Botkit;
protected _controller: Botkit;

/**
* Botkit Plugin name
*/
public name = 'Botkit CMS';

public constructor(config: CMSOptions) {
super();

this._config = config;
if (config.controller) {
this._controller = this._config.controller;
Expand Down Expand Up @@ -138,7 +141,7 @@ export class BotkitCMSHelper {

/**
* Load all script content from the configured CMS instance into a DialogSet and prepare them to be used.
* @param dialogSet A DialogSet into which the dialogs should be loaded. In most cases, this is `controller.dialogSet`, allowing Botkit to access these dialogs through `bot.beginDialog()`.
* @param botkit The Botkit controller instance
*/
public async loadAllScripts(botkit: Botkit): Promise<void> {
const scripts = await this.getScripts();
Expand All @@ -156,86 +159,11 @@ export class BotkitCMSHelper {
});
}

/**
* Map some less-than-ideal legacy fields to better places
*/
private mapFields(line): void {
// Create the channelData field where any channel-specific stuff goes
if (!line.channelData) {
line.channelData = {};
}

// TODO: Port over all the other mappings

// move slack attachments
if (line.attachments) {
line.channelData.attachments = line.attachments;
}

// we might have a facebook attachment in fb_attachments
if (line.fb_attachment) {
const attachment = line.fb_attachment;
if (attachment.template_type) {
if (attachment.template_type === 'button') {
attachment.text = line.text[0];
}
line.channelData.attachment = {
type: 'template',
payload: attachment
};
} else if (attachment.type) {
line.channelData.attachment = attachment;
}

// blank text, not allowed with attachment
line.text = null;

// remove blank button array if specified
if (line.channelData.attachment.payload.elements) {
for (let e = 0; e < line.channelData.attachment.payload.elements.length; e++) {
if (!line.channelData.attachment.payload.elements[e].buttons || !line.channelData.attachment.payload.elements[e].buttons.length) {
delete (line.channelData.attachment.payload.elements[e].buttons);
}
}
}

delete (line.fb_attachment);
}

// Copy quick replies to channelData.
// This gives support for both "native" quick replies AND facebook quick replies
if (line.quick_replies) {
line.channelData.quick_replies = line.quick_replies;
}

// handle teams attachments
if (line.platforms && line.platforms.teams) {
if (line.platforms.teams.attachments) {
line.attachments = line.platforms.teams.attachments.map((a) => {
a.content = { ...a };
a.contentType = 'application/vnd.microsoft.card.' + a.type;
return a;
});
}
delete (line.platforms.teams);
}

// handle additional custom fields defined in Botkit-CMS
if (line.meta) {
for (let a = 0; a < line.meta.length; a++) {
line.channelData[line.meta[a].key] = line.meta[a].value;
}
delete (line.meta);
}

return line;
}

/**
* Uses the Botkit CMS trigger API to test an incoming message against a list of predefined triggers.
* If a trigger is matched, the appropriate dialog will begin immediately.
* @param bot The current bot worker instance
* @param message An incoming message to be interpretted
* @param message An incoming message to be interpreted
* @returns Returns false if a dialog is NOT triggered, otherwise returns void.
*/
public async testTrigger(bot: BotWorker, message: Partial<BotkitMessage>): Promise<any> {
Expand All @@ -245,82 +173,6 @@ export class BotkitCMSHelper {
}
return false;
}

/**
* Bind a handler function that will fire before a given script and thread begin.
* Provides a way to use BotkitConversation.before() on dialogs loaded dynamically via the CMS api instead of being created in code.
*
* ```javascript
* controller.cms.before('my_script','my_thread', async(convo, bot) => {
*
* // do stuff
* console.log('starting my_thread as part of my_script');
* // other stuff including convo.setVar convo.gotoThread
*
* });
* ```
*
* @param script_name The name of the script to bind to
* @param thread_name The name of a thread within the script to bind to
* @param handler A handler function in the form async(convo, bot) => {}
*/
public before(script_name: string, thread_name: string, handler: (convo: BotkitDialogWrapper, bot: BotWorker) => Promise<void>): void {
const dialog = this._controller.dialogSet.find(script_name) as BotkitConversation;
if (dialog) {
dialog.before(thread_name, handler);
} else {
throw new Error('Could not find dialog: ' + script_name);
}
}

/**
* Bind a handler function that will fire when a given variable is set within a a given script.
* Provides a way to use BotkitConversation.onChange() on dialogs loaded dynamically via the CMS api instead of being created in code.
*
* ```javascript
* controller.plugins.cms.onChange('my_script','my_variable', async(new_value, convo, bot) => {
*
* console.log('A new value got set for my_variable inside my_script: ', new_value);
*
* });
* ```
*
* @param script_name The name of the script to bind to
* @param variable_name The name of a variable within the script to bind to
* @param handler A handler function in the form async(value, convo, bot) => {}
*/
public onChange(script_name: string, variable_name: string, handler: (value: any, convo: BotkitDialogWrapper, bot: BotWorker) => Promise<void>): void {
const dialog = this._controller.dialogSet.find(script_name) as BotkitConversation;
if (dialog) {
dialog.onChange(variable_name, handler);
} else {
throw new Error('Could not find dialog: ' + script_name);
}
}

/**
* Bind a handler function that will fire after a given dialog ends.
* Provides a way to use BotkitConversation.after() on dialogs loaded dynamically via the CMS api instead of being created in code.
*
* ```javascript
* controller.plugins.cms.after('my_script', async(results, bot) => {
*
* console.log('my_script just ended! here are the results', results);
*
* });
* ```
*
* @param script_name The name of the script to bind to
* @param handler A handler function in the form async(results, bot) => {}
*/
public after(script_name: string, handler: (results: any, bot: BotWorker) => Promise<void>): void {
const dialog = this._controller.dialogSet.find(script_name) as BotkitConversation;
if (dialog) {
dialog.after(handler);
} else {
throw new Error('Could not find dialog: ' + script_name);
}
}
}

export interface CMSOptions {
Expand Down
1 change: 1 addition & 0 deletions packages/botkit-plugin-cms/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
*/

export * from './cms';
export * from './cms-local';
Loading