├── .env_sample ├── .eslintrc.json ├── .gitignore ├── ATTRIBUTIONS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── components ├── express_webserver.js └── routes │ ├── admin.js │ └── api.js ├── docs └── screenshots │ └── dialog.png ├── gulpfile.js ├── index.js ├── js ├── app.js ├── controller.editBot.js ├── controller.editor.js └── sdk.js ├── npm-debug.log ├── package-lock.json ├── package.json ├── public ├── bot_avatar.png ├── css │ └── new.css └── js │ ├── partials │ ├── ciscospark_attachments.html │ ├── conditional.html │ ├── facebook_attachments.html │ ├── facebook_button_attachment.html │ ├── facebook_generic_attachment.html │ ├── facebook_media_attachment.html │ ├── meta_attachments.html │ ├── properties_incoming.html │ ├── properties_none.html │ ├── properties_outgoing.html │ ├── slack_attachments.html │ ├── teams_attachments.html │ ├── teams_attachments_file.html │ ├── teams_attachments_hero.html │ ├── teams_attachments_thumbnail.html │ └── web_attachments.html │ └── scripts.js ├── readme.md ├── sample_scripts.json ├── sass ├── _ed.scss ├── _skeleton.scss ├── _vars.scss ├── features │ ├── _input.scss │ ├── _inspector.scss │ ├── _modal.scss │ ├── _sidebar.scss │ └── _text-tokens.scss ├── foundation │ ├── _buttons.scss │ ├── _flex-page.scss │ └── _reset.scss └── new.scss ├── src ├── api.js └── botkit_mutagen.js └── views ├── config.hbs ├── edit.hbs ├── index.hbs ├── instructions.hbs ├── layouts └── layout.hbs └── partials ├── editor ├── branches.hbs ├── description.hbs ├── message_list.hbs ├── modal_branch.hbs ├── modal_duplicate.hbs ├── modal_export.hbs ├── modal_info.hbs ├── modal_menu.hbs └── modal_triggers.hbs ├── foot.hbs ├── head.hbs ├── header.hbs ├── importexport ├── modal_create.hbs ├── modal_export.hbs └── modal_import.hbs ├── modal.hbs └── nav.hbs /.env_sample: -------------------------------------------------------------------------------- 1 | # Chat platform 2 | # PLATFORM=[slack,teams,ciscospark,web,facebook] 3 | 4 | # authentication tokens for Bot users 5 | # TOKENS="token-123 token-345" 6 | 7 | # Admin users for UI 8 | # USERS="username:password username2:password2 username3:password3" 9 | 10 | # Bot Name 11 | # BOT_NAME=Marvin 12 | 13 | # LUIS Endpoint 14 | # URL to published LUIS Endpoint in the form of https://.api.cognitive.microsoft.com/luis/v2.0/apps/?subscription-key=&verbose=true&timezoneOffset=-360&q= 15 | # Get this from LUIS.ai Keys and Endpoint settings 16 | # LUIS_ENDPOINT= 17 | 18 | # LUIS App Version 19 | # Defaults to 0.1, update if you manage multiple LUIS app versions 20 | # LUIS_VERSION=0.1 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-with": 2, 4 | "max-len": [ 5 | 2, 6 | 250 7 | ], 8 | "indent": [ 9 | 2, 10 | 4, 11 | { 12 | "SwitchCase": 1 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .data/scripts.json 2 | .DS_Store 3 | node_modules/ 4 | public/js/bower_components/ 5 | .env 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.3 2 | 3 | * Bug fixes 4 | * Changed location of the sample_scripts.json file 5 | * Clarified some docs 6 | 7 | # 0.0.2 8 | 9 | First public release 10 | 11 | # 0.0.1 12 | 13 | Initial version -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Instructions for Contributing Code 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 6 | 7 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | ## Code of Conduct 12 | 13 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright XOXCO, Inc, http://xoxco.com, http://howdy.ai 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /components/express_webserver.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | var querystring = require('querystring'); 4 | var hbs = require('express-hbs'); 5 | var basicAuth = require('express-basic-auth') 6 | 7 | module.exports = function(admin_creds) { 8 | 9 | 10 | var webserver = express(); 11 | webserver.use(bodyParser.json()); 12 | webserver.use(bodyParser.urlencoded({ extended: true })); 13 | 14 | webserver.use(express.static(__dirname + '/../public')); 15 | 16 | webserver.engine('hbs', hbs.express4({ 17 | partialsDir: __dirname + '/../views/partials' 18 | })); 19 | 20 | webserver.set('view engine', 'hbs'); 21 | webserver.set('views', __dirname + '/../views/'); 22 | 23 | var authFunction = basicAuth({ 24 | users: admin_creds, 25 | challenge: true, 26 | }); 27 | 28 | webserver.use(function(req, res, next) { 29 | if (req.url.match(/^\/admin/)) { 30 | authFunction(req, res, next); 31 | } else { 32 | next(); 33 | } 34 | }); 35 | 36 | 37 | webserver.listen(process.env.PORT || 3000, null, function() { 38 | 39 | console.log('Express webserver up on port ' + (process.env.PORT || 3000)); 40 | console.log('Login: http://localhost:' + (process.env.PORT || 3000) + '/admin/'); 41 | console.log('API Root: http://localhost:' + (process.env.PORT || 3000) + '/'); 42 | 43 | }); 44 | 45 | return webserver; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /components/routes/admin.js: -------------------------------------------------------------------------------- 1 | 2 | const uuidv4 = require('uuid/v4'); 3 | const request = require('request'); 4 | const async = require('async'); 5 | 6 | module.exports = function(webserver, api) { 7 | 8 | webserver.get('/admin', function(req, res) { 9 | res.render('index',{ 10 | layout: 'layouts/layout', 11 | }); 12 | }); 13 | 14 | webserver.get('/admin/edit/:name', function(req, res) { 15 | res.render('edit',{ 16 | layout: 'layouts/layout', 17 | platform: process.env.PLATFORM || 'web', 18 | command_id: req.params.name, 19 | }); 20 | }); 21 | 22 | webserver.post('/admin/save', function(req, res) { 23 | var update = req.body; 24 | 25 | api.getScripts().then(function(scripts) { 26 | var found = false; 27 | for (var s = 0; s < scripts.length; s++) { 28 | if (scripts[s].id === update.id) { 29 | found = s; 30 | } 31 | } 32 | 33 | if (update.is_fallback) { 34 | scripts.forEach(function(script) { 35 | script.is_fallback = false; 36 | }); 37 | } 38 | 39 | if (found === false) { 40 | if (!update.id && update._id) { 41 | update.id = update._id; 42 | } else if (!update.id) { 43 | update.id = uuidv4(); 44 | } 45 | update.modified = new Date(); 46 | scripts.push(update); 47 | api.writeScriptsToFile(scripts).then(function() { 48 | res.json({ 49 | success: true, 50 | data: update, 51 | }); 52 | }); 53 | 54 | } else if (new Date(scripts[found].modified) > new Date(update.modified)) { 55 | 56 | // if the version in the database was more recently modified, reject this update! 57 | res.json({ 58 | success: false, 59 | message: 'Script was modified more recently, please refresh your browser to load the latest', 60 | }); 61 | 62 | } else { 63 | 64 | var original_name = scripts[found].command; 65 | 66 | scripts[found] = update; 67 | scripts[found].modified = new Date(); 68 | 69 | if (update.command != original_name) { 70 | handleRenamed(scripts, original_name, update.command).then(function() { 71 | api.writeScriptsToFile(scripts).then(function() { 72 | res.json({ 73 | success: true, 74 | data: update, 75 | }); 76 | }); 77 | }); 78 | } else { 79 | api.writeScriptsToFile(scripts).then(function() { 80 | res.json({ 81 | success: true, 82 | data: update, 83 | }); 84 | }); 85 | } 86 | } 87 | }); 88 | 89 | }); 90 | 91 | 92 | function handleRenamed(scripts, original_name, new_name) { 93 | return new Promise(function(resolve, reject) { 94 | async.each(scripts, function(command, next) { 95 | updateExecScript(command, original_name, new_name, next); 96 | }, function() { 97 | resolve(); 98 | }) 99 | }); 100 | } 101 | 102 | function updateExecScript(command, original_name, new_name, next) { 103 | // need to look at command.script[*].script[*].action 104 | // need to look at command.script[*].script[*].collect.options[*].action 105 | var dirty = false; 106 | for (var t = 0; t < command.script.length; t++) { 107 | for (var m = 0; m < command.script[t].script.length; m++) { 108 | if (command.script[t].script[m].action == 'execute_script' && command.script[t].script[m].execute && command.script[t].script[m].execute.script == original_name) { 109 | command.script[t].script[m].execute.script = new_name; 110 | dirty = true; 111 | } 112 | 113 | if (command.script[t].script[m].collect && command.script[t].script[m].collect.options) { 114 | for (var o = 0; o < command.script[t].script[m].collect.options.length; o++) { 115 | if (command.script[t].script[m].collect.options[o].action=='execute_script' && command.script[t].script[m].collect.options[o].execute && command.script[t].script[m].collect.options[o].execute.script == original_name) { 116 | command.script[t].script[m].collect.options[o].execute.script = new_name; 117 | dirty = true; 118 | } 119 | } 120 | } 121 | } 122 | } 123 | 124 | if (dirty) { 125 | command.modified = new Date(); 126 | next(); 127 | } else { 128 | next(); 129 | } 130 | } 131 | 132 | 133 | 134 | // receives: command, user 135 | webserver.post('/admin/api/script', function(req, res) { 136 | if (req.body.command) { 137 | api.getScript(req.body.command).then(function(script) { 138 | res.json({success: 'ok', data: script}); 139 | }).catch(function(err) { 140 | if (err) { 141 | console.error('Error in getScript',err); 142 | } 143 | res.json({}); 144 | }) 145 | } else if (req.body.id) { 146 | api.getScriptById(req.body.id).then(function(script) { 147 | res.json(script); 148 | }).catch(function(err) { 149 | if (err) { 150 | console.error('Error in getScript',err); 151 | } 152 | res.json({}); 153 | }) 154 | } 155 | }); 156 | 157 | 158 | // receives: command, user 159 | webserver.get('/admin/api/scripts', function(req, res) { 160 | api.getScripts(req.query.tag).then(function(scripts) { 161 | res.json({success: true, data: scripts}); 162 | }).catch(function(err) { 163 | if (err) { 164 | console.error('Error in getScripts',err); 165 | } 166 | res.json({}); 167 | }) 168 | }); 169 | 170 | function parseLUISEndpoint(endpoint) { 171 | 172 | // endpoint in form of 173 | // https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/?subscription-key=&verbose=true&timezoneOffset=-360&q= 174 | 175 | const config = { 176 | region: 'westus', 177 | app: '', 178 | key: '', 179 | } 180 | 181 | config.region = endpoint.match(/https\:\/\/(.*?)\.api/)[1]; 182 | config.app = endpoint.match(/\/apps\/(.*?)\?/)[1]; 183 | config.key = endpoint.match(/subscription\-key\=(.*?)\&/)[1]; 184 | config.version = process.env.LUIS_VERSION || "0.1"; 185 | 186 | return config; 187 | 188 | } 189 | 190 | webserver.get('/admin/api/luisIntents', function(req, res) { 191 | if (process.env.LUIS_ENDPOINT) { 192 | 193 | const luisConfig = parseLUISEndpoint(process.env.LUIS_ENDPOINT); 194 | var url = `https://${ luisConfig.region }.api.cognitive.microsoft.com/luis/api/v2.0/apps/${ luisConfig.app }/versions/${ luisConfig.version }/intents?skip=0&take=500`; 195 | 196 | var options = { 197 | method: 'GET', 198 | url: url, 199 | headers: { 200 | 'Ocp-Apim-Subscription-Key': luisConfig.key 201 | } 202 | }; 203 | request(options, function(err, resp, body) { 204 | if (err) { 205 | console.error('Error commnicating with LUIS:', err); 206 | res.json({success: true, data: []}); 207 | } else { 208 | var intents = []; 209 | try { 210 | intents = JSON.parse(body); 211 | } catch(err) { 212 | console.error('Error parsing LUIS intents:', err); 213 | } 214 | res.json({success: true, data: intents}); 215 | } 216 | }); 217 | 218 | } else { 219 | res.json({success: true, data: []}); 220 | } 221 | 222 | }); 223 | 224 | webserver.delete('/admin/api/scripts/:id', function(req, res) { 225 | api.getScripts().then(function(scripts) { 226 | 227 | // delete script out of list. 228 | scripts = scripts.filter((script) => { return (script.id !== req.params.id) }); 229 | 230 | // write scripts back to file. 231 | api.writeScriptsToFile(scripts).then(function() { 232 | res.json({ 233 | success: true, 234 | data: scripts, 235 | }); 236 | }); 237 | 238 | }).catch(function(err) { 239 | if (err) { 240 | console.error('Error in getScripts',err); 241 | } 242 | res.json({}); 243 | }) 244 | }); 245 | 246 | webserver.get('/admin/config', function(req, res) { 247 | 248 | var allowed_tokens = process.env.TOKENS ? process.env.TOKENS.split(/\s+/) : []; 249 | 250 | var package_version = require('../../package.json').version; 251 | 252 | res.render('config',{ 253 | layout: 'layouts/layout', 254 | token: allowed_tokens[0], 255 | tokens: allowed_tokens.join("\n"), 256 | token_count: allowed_tokens.length, 257 | multiple_tokens: (allowed_tokens.length > 1), 258 | version: package_version, 259 | url: req.protocol + "://" + req.headers.host 260 | }); 261 | }) 262 | 263 | 264 | } 265 | -------------------------------------------------------------------------------- /components/routes/api.js: -------------------------------------------------------------------------------- 1 | module.exports = function(webserver, api) { 2 | 3 | var allowed_tokens = process.env.TOKENS ? process.env.TOKENS.split(/\s+/) : []; 4 | 5 | if (!allowed_tokens.length) { 6 | throw new Error('Define at least one API access token in the TOKENS environment variable'); 7 | } 8 | 9 | if (allowed_tokens && allowed_tokens.length) { 10 | // require an access token 11 | webserver.use(function(req, res, next) { 12 | 13 | if (req.url.match(/^\/api\//)) { 14 | if (!req.query.access_token) { 15 | res.status(403); 16 | res.send('Access denied'); 17 | } else { 18 | 19 | // test access_token against allowed tokens 20 | if (allowed_tokens.indexOf(req.query.access_token) >= 0) { 21 | next(); 22 | } else { 23 | res.status(403); 24 | res.send('Invalid access token'); 25 | } 26 | } 27 | } else { 28 | next(); 29 | } 30 | }) 31 | } 32 | 33 | 34 | /* define the bot-facing API */ 35 | // receives: triggers, user 36 | webserver.post('/api/v1/commands/triggers', function(req, res) { 37 | // look for triggers 38 | api.evaluateTriggers(req.body.triggers).then(function(results) { 39 | results.id = results.command; 40 | res.json(results); 41 | }).catch(function() { 42 | res.json({}); 43 | }) 44 | 45 | }); 46 | 47 | // receives: command, user 48 | webserver.post('/api/v1/commands/name', function(req, res) { 49 | api.getScript(req.body.command).then(function(script) { 50 | res.json(script); 51 | }).catch(function(err) { 52 | if (err) { 53 | console.error('Error in getScript',err); 54 | } 55 | res.json({}); 56 | }) 57 | }); 58 | 59 | // receives: command, user 60 | webserver.post('/api/v1/commands/id', function(req, res) { 61 | api.getScriptById(req.body.id).then(function(script) { 62 | res.json(script); 63 | }).catch(function(err) { 64 | if (err) { 65 | console.error('Error in getScript',err); 66 | } 67 | res.json({}); 68 | }) 69 | }); 70 | 71 | 72 | // receives: command, user 73 | webserver.get('/api/v1/commands/list', function(req, res) { 74 | api.getScripts(req.query.tag).then(function(scripts) { 75 | res.json(scripts); 76 | }).catch(function(err) { 77 | if (err) { 78 | console.error('Error in getScripts',err); 79 | } 80 | res.json({}); 81 | }) 82 | }); 83 | 84 | 85 | webserver.get('/api/v2/bots/identify', function(req, res) { 86 | res.json({ 87 | name: process.env.BOT_NAME || 'Botkit Bot', 88 | platforms: [{type:(process.env.PLATFORM || 'web')}] 89 | }) 90 | }); 91 | 92 | 93 | // receives: command, user 94 | webserver.post('/api/v1/stats', function(req, res) { 95 | 96 | res.json({}); 97 | 98 | }); 99 | 100 | 101 | 102 | } 103 | -------------------------------------------------------------------------------- /docs/screenshots/dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/howdyai/botkit-cms/23128e26982d3818423911cb15d18b377da0389b/docs/screenshots/dialog.png -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // -----------------------------------------------------------/ 2 | // gulpfile.js 3 | // HowdyPro web site 4 | // howdy.ai 5 | // Change Log: 6 | // ** Repaired the gulp watch to reload on changes to js and sass files. 7 | // ** Added livereload plugin to refresh the page in the browser (note: livereload extension needs to be enabled in chrome) 8 | // ** Added gulp task to run unit tests and unit test coverate report per mocha unit test support 9 | // -----------------------------------------------------------/ 10 | // -----------------------------------------------------------/ 11 | // Dependencies 12 | // -----------------------------------------------------------/ 13 | var gulp = require('gulp'), 14 | concat = require('gulp-concat'), 15 | sass = require('gulp-sass'), 16 | gutil = require('gulp-util'), 17 | package = require('./package.json'); 18 | 19 | var banner = function() { 20 | return '/*! ' + package.name + ' - v' + package.version + ' - ' + gutil.date(new Date(), "yyyy-mm-dd") + 21 | ' [copyright: ' + package.copyright + ']' + ' */'; 22 | }; 23 | 24 | function logData() { 25 | gutil.log( 26 | gutil.colors.bgGreen( 27 | gutil.colors.white( 28 | gutil.colors.bold( 29 | gutil.colors.white.apply(this, arguments) 30 | ) 31 | ) 32 | ) 33 | ); 34 | } 35 | 36 | function ready() { 37 | gutil.log( 38 | gutil.colors.bgMagenta( 39 | gutil.colors.white( 40 | gutil.colors.bold('[ STATUS: READY ]') 41 | ) 42 | ) 43 | ); 44 | } 45 | 46 | // -----------------------------------------------------------/ 47 | // Get Sassy 48 | // -----------------------------------------------------------/ 49 | gulp.task('sass', function() { 50 | logData('Compiling Sass...'); 51 | return gulp.src('./sass/*.scss') 52 | .pipe(sass({ 53 | outputStyle: 'compressed' 54 | }).on('error', sass.logError)) 55 | .pipe(gulp.dest('./public/css/')); 56 | }); 57 | // -----------------------------------------------------------/ 58 | // Get JSesesussy 59 | // Copy JS files to public scripts distribution folder 60 | // -----------------------------------------------------------/ 61 | // copy JS scripts 62 | // gulp.task('copy-js', function() { 63 | // logData('Copying JS assets...'); 64 | // return gulp.src([]) 65 | // .pipe(gulp.dest('./public/js/')); 66 | // }); 67 | // copy partials to public scripts 68 | gulp.task('copy-partials', function() { 69 | logData('Copying HTML partials...'); 70 | return gulp.src(['./botkit-scriptui/*.html']) 71 | .pipe(gulp.dest('./public/js/partials/')); 72 | }); 73 | 74 | // copy partials to public scripts 75 | gulp.task('copy-more-partials', function() { 76 | logData('Copying HTML partials...'); 77 | return gulp.src(['./views/partials/*.html']) 78 | .pipe(gulp.dest('./public/partials/')); 79 | }); 80 | 81 | // concat controllers to public scripts 82 | gulp.task('controller-concat', function() { 83 | logData('Concatenating and Copying Controllers...'); 84 | return gulp.src(['./js/*.js', './botkit-scriptui/*.js']) 85 | .pipe(concat('scripts.js')) 86 | .pipe(gulp.dest('./public/js/')); 87 | }); 88 | // -----------------------------------------------------------/ 89 | // Rebuild all the things 90 | // -----------------------------------------------------------/ 91 | gulp.task('default', gulp.series('controller-concat', 'copy-partials','copy-more-partials', 'sass')); 92 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | var api = require(__dirname + '/src/api.js')() 3 | 4 | if (!process.env.USERS) { 5 | throw new Error('Specify at least one username:password combo in the USERS environment variable') 6 | } 7 | 8 | var admin_creds = api.parseAdminUsers(process.env.USERS); 9 | 10 | // load scripts from file 11 | api.loadScriptsFromFile(__dirname + '/.data/scripts.json', __dirname + '/sample_scripts.json').catch(function(err) { 12 | console.log('Could not load scripts from file:', err); 13 | process.exit(1); 14 | }).then(function(scripts) { 15 | // verify that we can now write back to the file. 16 | api.writeScriptsToFile(scripts).catch(function(err) { 17 | console.error(err); 18 | process.exit(1); 19 | }); 20 | }).catch(function(err) { 21 | console.error(err); 22 | process.exit(1); 23 | }); 24 | 25 | // Set up an Express-powered webserver to expose oauth and webhook endpoints 26 | var webserver = require(__dirname + '/components/express_webserver.js')(admin_creds); 27 | 28 | require(__dirname + '/components/routes/admin.js')(webserver, api); 29 | require(__dirname + '/components/routes/api.js')(webserver, api); 30 | 31 | webserver.get('/', function(req, res) { 32 | res.render('instructions',{layout: null}); 33 | }); 34 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | /* this is the main app.js file */ 2 | 3 | var app = angular.module('howdyPro', ['ngCookies','ngclipboard','dndLists','monospaced.elastic']); 4 | 5 | app.config(function($interpolateProvider) { 6 | $interpolateProvider.startSymbol('{%'); 7 | $interpolateProvider.endSymbol('%}'); 8 | }).config( [ 9 | '$compileProvider', 10 | function( $compileProvider ) 11 | { 12 | $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|data):/); 13 | // Angular before v1.2 uses $compileProvider.urlSanitizationWhitelist(...) 14 | } 15 | ]); 16 | 17 | 18 | app.directive('compile', ['$compile', function ($compile) { 19 | return function(scope, element, attrs) { 20 | scope.$watch( 21 | function(scope) { 22 | return scope.$eval(attrs.compile); 23 | }, 24 | function(value) { 25 | element.html(value); 26 | $compile(element.contents())(scope); 27 | } 28 | )}; 29 | }]); 30 | 31 | 32 | function truncateString(value, max, wordwise, tail) { 33 | if (!value) return ''; 34 | 35 | max = parseInt(max, 10); 36 | if (!max) return value; 37 | if (value.length <= max) return value; 38 | 39 | value = value.substr(0, max); 40 | if (wordwise) { 41 | var lastspace = value.lastIndexOf(' '); 42 | if (lastspace !== -1) { 43 | //Also remove . and , so its gives a cleaner result. 44 | if (value.charAt(lastspace-1) === '.' || value.charAt(lastspace-1) === ',') { 45 | lastspace = lastspace - 1; 46 | } 47 | value = value.substr(0, lastspace); 48 | } 49 | } 50 | 51 | return value + (tail || ' …'); 52 | } 53 | 54 | 55 | app.filter('truncateString', function () { 56 | return function (value, max, wordwise, tail) { 57 | return truncateString(value, max, wordwise, tail); 58 | } 59 | }); 60 | 61 | // copied from https://stackoverflow.com/questions/16630471/how-can-i-invoke-encodeuricomponent-from-angularjs-template 62 | app.filter('encodeURIComponent', function() { 63 | return window.encodeURIComponent; 64 | }); 65 | 66 | app.controller('app', ['$scope', '$http', '$sce', '$cookies','sdk', '$location', function($scope, $http, $sce, $cookies, sdk, $location) { 67 | 68 | 69 | 70 | $scope.ui = { 71 | modalVisible: false, 72 | modalText: '', 73 | saved: false, 74 | commandsExpanded: false, 75 | inspectorExpanded: true, 76 | confirmText: null, 77 | confirmShow: false, 78 | }; 79 | 80 | $scope.goto = function(url) { 81 | 82 | window.location = url; 83 | 84 | } 85 | 86 | $scope.open = function(url) { 87 | 88 | window.open(url); 89 | 90 | } 91 | 92 | 93 | 94 | $scope.saved = function() { 95 | $scope.ui.saved = true; 96 | setTimeout(function() { 97 | $scope.ui.saved = false; 98 | },1000); 99 | } 100 | 101 | $scope.confirmation = function(message, do_not_clear) { 102 | $scope.ui.confirmText = $sce.trustAsHtml(message); 103 | $scope.ui.confirmShow = true; 104 | $scope.$apply(); 105 | if (!do_not_clear) { 106 | setTimeout(function() { 107 | $scope.ui.confirmShow = false; 108 | $scope.$apply(); 109 | }, 3000); 110 | } 111 | } 112 | 113 | $scope.getCookie = function(key) { 114 | return $cookies.get(key); 115 | } 116 | 117 | $scope.setCookie = function(key,val) { 118 | var now = new Date(); 119 | now.setDate(now.getDate() + 365); 120 | 121 | var cookie_domain = '.botkit.ai'; 122 | if (!$location.host().match(/botkit\.ai/)) { 123 | cookie_domain = $location.host(); 124 | } 125 | 126 | $cookies.put(key,val,{ 127 | expires: now, 128 | path: '/', 129 | domain: cookie_domain, 130 | }); 131 | } 132 | 133 | $scope.showModal = function(text) { 134 | $scope.ui.modalText = $sce.trustAsHtml(text); 135 | $scope.ui.modalVisible = true; 136 | $scope.$apply(); 137 | }; 138 | 139 | $scope.hideModal = function() { 140 | $scope.ui.modalVisible = false; 141 | }; 142 | 143 | $scope.showUpgrade = function(reason) { 144 | 145 | $scope.ui.showUpgrade = true; 146 | $scope.$apply(); 147 | 148 | } 149 | 150 | $scope.dismissUpgrade = function() { 151 | $scope.ui.showUpgrade = false; 152 | } 153 | 154 | $scope.handleAjaxError = function(error) { 155 | var error_text = ''; 156 | console.log('HANDLE AJAX',error); 157 | if (error.message) { 158 | error_text = error.message; 159 | } else if (error.error) { 160 | error_text = error.error; 161 | } else if (error.data && error.data.error && error.data.error) { 162 | // details, name, message, stack, status, statusCode 163 | // details contains a bunch of info, down to the field 164 | error_text = error.data.error; 165 | } else if (error.statusText) { 166 | error_text = error.statusText; 167 | } else { 168 | if (typeof(error) == 'string') { 169 | error_text = error; 170 | } else { 171 | error_text = JSON.stringify(error); 172 | } 173 | } 174 | 175 | $scope.showModal('ERROR: ' + error_text); 176 | 177 | }; 178 | 179 | 180 | 181 | $scope.toggle = function(obj,field) { 182 | if (obj[field] === true || obj[field]=='true') { 183 | delete(obj[field]); 184 | } else { 185 | obj[field] = true; 186 | } 187 | }; 188 | 189 | $scope.truncateString = truncateString; 190 | 191 | if ($cookies.get('customer')) { 192 | $scope.customer = $cookies.get('customer'); 193 | } 194 | 195 | 196 | 197 | }]); 198 | 199 | function getParameterByName(name, url) { 200 | if (!url) { 201 | url = window.location.href; 202 | } 203 | name = name.replace(/[\[\]]/g, "\\$&"); 204 | var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), 205 | results = regex.exec(url); 206 | if (!results) return null; 207 | if (!results[2]) return ''; 208 | return decodeURIComponent(results[2].replace(/\+/g, " ")); 209 | } 210 | -------------------------------------------------------------------------------- /js/controller.editBot.js: -------------------------------------------------------------------------------- 1 | 2 | app.controller('editBot', ['$scope', '$cookies', 'sdk', '$rootScope', '$window', function($scope, $cookies, sdk, $rootScope, $window) { 3 | 4 | $scope.bot = { 5 | name: '', 6 | description: '', 7 | platform: null, 8 | loading: true, 9 | settings: {}, 10 | }; 11 | 12 | $scope.commands = []; 13 | $scope.manifest_download = null; 14 | 15 | // confirmation button for luis page 16 | var default_button_text = 'Update LUIS.ai Settings'; 17 | 18 | $scope.ui.copy = 'Copy'; 19 | $scope.ui.button_text = default_button_text; 20 | 21 | 22 | // manifest builder options 23 | $scope.ui.autoconfigure_website = true; 24 | $scope.ui.autoconfigure_tabs = true; 25 | $scope.ui.autoconfigure_base = null; 26 | 27 | $scope.filter_by_tag = function(tag, event) { 28 | $scope.ui.filter = tag; 29 | event.preventDefault(); 30 | }; 31 | 32 | $scope.clipped = function(e) { 33 | $scope.ui.copy = 'Copied'; 34 | setTimeout(function() { 35 | $scope.ui.copy = 'Copy'; 36 | $scope.$apply(); 37 | }, 3000); 38 | }; 39 | 40 | $scope.clipError = function(e) { 41 | $scope.handleAjaxError(e); 42 | }; 43 | 44 | $scope.go = function(url) { 45 | window.open(url); 46 | }; 47 | 48 | $scope.getCommands = function() { 49 | sdk.getCommandsByBot().then(function(commands) { 50 | $scope.commands = commands.sort(function(a, b) { 51 | // sort by modified date!! 52 | if (new Date(a.modified) < new Date(b.modified)) return 1; 53 | if (new Date(b.modified) > new Date(a.modified)) return -1; 54 | return 0; 55 | }); 56 | $scope.$broadcast('commands_loaded'); 57 | $scope.$apply(); 58 | }); 59 | }; 60 | 61 | $scope.getCommands(); 62 | }]); 63 | 64 | 65 | app.controller('botCommands', ['$scope', 'sdk', function($scope, sdk) { 66 | 67 | 68 | $scope.bot_id = 1; 69 | 70 | $scope.ui.external_url = ''; 71 | 72 | $scope.parsed_commands = []; 73 | $scope.imported_commands = []; 74 | $scope.import_already_exists = []; 75 | 76 | $scope.ui = { 77 | filter: '', 78 | export_mode: false, 79 | copy: 'Copy', 80 | import_button_text: 'Import', 81 | sortField: '-modified', 82 | }; 83 | 84 | $scope.command = { 85 | trigger: '', 86 | description: '', 87 | }; 88 | 89 | $scope.toggleSort = function(sortby) { 90 | console.log('toggle sort',sortby); 91 | if ($scope.ui.sortField == sortby) { 92 | $scope.ui.sortField = '-' + sortby; 93 | } else if ($scope.ui.sortField == '-' + sortby) { 94 | $scope.ui.sortField = sortby; 95 | } else { 96 | $scope.ui.sortField = sortby; 97 | } 98 | console.log($scope.ui.sortField); 99 | } 100 | 101 | $scope.filter_by_tag = function(tag, event){ 102 | // console.log('tag: ', tag); 103 | $scope.ui.filter = tag; 104 | event.preventDefault(); 105 | }; 106 | 107 | $scope.clipped = function(e) { 108 | $scope.ui.copy = 'Copied'; 109 | setTimeout(function() { 110 | $scope.ui.copy = 'Copy'; 111 | $scope.$apply(); 112 | },3000); 113 | }; 114 | 115 | $scope.clipError = function(e) { 116 | $scope.handleAjaxError(e); 117 | }; 118 | 119 | $scope.filterRow = function(row) { 120 | var pattern = new RegExp($scope.ui.filter,'i'); 121 | 122 | //search by tags 123 | if(row.tags){ 124 | var in_tags = row.tags.filter(function(t){ 125 | return t === $scope.ui.filter; 126 | }); 127 | if(in_tags.length != 0){ 128 | return true; 129 | } 130 | } 131 | 132 | // search name and description first 133 | if (row.command.match(pattern)) { 134 | return true; 135 | } 136 | if (row.description.match(pattern)) { 137 | return true; 138 | } 139 | 140 | if (!$scope.ui.searchScript) { 141 | return; 142 | } 143 | 144 | for (var t = 0; t < row.triggers.length; t++) { 145 | if (row.triggers[t].pattern.match(pattern)) { 146 | return true; 147 | } 148 | } 149 | for (var t = 0; t < row.script.length; t++) { 150 | if (row.script[t].topic.match(pattern)) { 151 | return true; 152 | } 153 | for (var m = 0; m < row.script[t].script.length; m++) { 154 | if (row.script[t].script[m].text) { 155 | for (var v = 0; v < row.script[t].script[m].text.length; v++) { 156 | if (row.script[t].script[m].text[v].match(pattern)) { 157 | return true; 158 | } 159 | } 160 | } 161 | 162 | // FIX THIS 163 | // we should do some sort of recursive search of attachment values... 164 | 165 | if (row.script[t].script[m].action && row.script[t].script[m].action.match(pattern)) { 166 | return true; 167 | } 168 | 169 | } 170 | 171 | } 172 | 173 | 174 | return false; 175 | }; 176 | 177 | $scope.updateImportButton = function() { 178 | var import_count = 0; 179 | var update_count = 0; 180 | for (var x = 0; x < $scope.commands_for_import.length; x++) { 181 | if (!$scope.commands_for_import[x].exclude_from_import) { 182 | if ($scope.commands_for_import[x].already_exist) { 183 | update_count++; 184 | } else { 185 | import_count++; 186 | } 187 | } 188 | } 189 | 190 | var texts = []; 191 | if (import_count > 0) { 192 | texts.push('Import ' + import_count); 193 | } 194 | if (update_count > 0) { 195 | texts.push('Update ' + update_count); 196 | } 197 | 198 | if (texts.length) { 199 | $scope.ui.import_button_text = texts.join(' and '); 200 | } else { 201 | $scope.ui.import_button_text = 'Import'; 202 | } 203 | 204 | } 205 | 206 | $scope.getExternalUrl = function(){ 207 | sdk.getExternalUrl(String($scope.ui.external_url)).then(function(response){ 208 | var data = response.data; 209 | $scope.commands_for_import = []; 210 | try{ 211 | // var parsed = JSON.parse(response.data); 212 | data.forEach(function(c){ 213 | 214 | 215 | if (!c.command || !c.script || !c.script.length) { 216 | throw new Error('Invalid script discovered in imported JSON.'); 217 | } 218 | 219 | var demuxed = c; 220 | demuxed.botId = $scope.bot_id; 221 | demuxed.deleted = false; 222 | var existencia = $scope.commands.filter(function(c){ 223 | return c.command === demuxed.command; 224 | }); 225 | if(existencia.length > 0){ 226 | demuxed.already_exist = true; 227 | }else { 228 | demuxed.already_exist = false; 229 | } 230 | $scope.commands_for_import.push(demuxed); 231 | $scope.updateImportButton(); 232 | }); 233 | } 234 | catch(err){ 235 | // we should do some sort of ajaxy error handling thing here maybe. 236 | return $scope.handleAjaxError('Data at URL does not appear to be valid scripts'); 237 | 238 | } 239 | }); 240 | }; 241 | 242 | $scope.import_unparsed = function(){ 243 | $scope.commands_for_import = []; 244 | 245 | try{ 246 | var parsed = JSON.parse($scope.ui.un_parsed_commands); 247 | parsed.forEach(function(c){ 248 | 249 | if (!c.command || !c.script || !c.script.length) { 250 | throw new Error('Invalid script discovered in imported JSON.'); 251 | } 252 | 253 | var demuxed = c; 254 | demuxed.botId = $scope.bot_id; 255 | demuxed.deleted = false; 256 | var existencia = $scope.commands.filter(function(c){ 257 | return c.command === demuxed.command; 258 | }); 259 | if(existencia.length > 0){ 260 | demuxed.already_exist = true; 261 | }else { 262 | demuxed.already_exist = false; 263 | } 264 | $scope.commands_for_import.push(demuxed); 265 | $scope.updateImportButton(); 266 | }); 267 | } 268 | catch(err){ 269 | // we should do some sort of ajaxy error handling thing here maybe. 270 | console.log('err: ', err); 271 | $scope.handleAjaxError(err); 272 | } 273 | }; 274 | 275 | $scope.makeImportApi = function(){ 276 | 277 | $scope.parseImport(); 278 | 279 | if ($scope.imported_commands.length) { 280 | if (!confirm('Import or update ' + $scope.imported_commands.length + ' scripts?')) { 281 | return false; 282 | } 283 | } else { 284 | $scope.ui.import_mode = false; 285 | } 286 | 287 | $scope.import_already_exists = []; 288 | 289 | async.each($scope.imported_commands, function(command, next) { 290 | 291 | var existencia = $scope.commands.filter(function(c){ 292 | return c.command === command.command; 293 | }); 294 | if(existencia.length > 0){ 295 | // get the id and do an update instead 296 | var upd_cmd = command; 297 | var to_update = existencia.filter(function(e){ 298 | return e.command === upd_cmd.command; 299 | }); 300 | upd_cmd._id = to_update[0]._id; 301 | upd_cmd.id = to_update[0].id; 302 | sdk.saveCommand(upd_cmd).then(function() { 303 | next(); 304 | }).catch(function(err) { 305 | next(err); 306 | }); 307 | }else { 308 | sdk.saveCommand(command).then(function(command) { 309 | next(); 310 | }).catch(function(err) { 311 | next(err); 312 | }); 313 | } 314 | 315 | }, function(err) { 316 | 317 | $scope.getCommands(); 318 | if (err) { 319 | $scope.handleAjaxError(err); 320 | } else { 321 | $scope.ui.import_mode = false; 322 | $scope.confirmation('Import successful!'); 323 | } 324 | $scope.imported_commands = []; 325 | }); 326 | }; 327 | 328 | $scope.add_to_import = function(command){ 329 | command.exclude_from_import = !command.exclude_from_import; 330 | $scope.updateImportButton(); 331 | }; 332 | 333 | $scope.parseImport = function() { 334 | $scope.imported_commands = []; 335 | for (var x = 0; x < $scope.commands_for_import.length; x++) { 336 | if (!$scope.commands_for_import[x].exclude_from_import) { 337 | $scope.imported_commands.push($scope.commands_for_import[x]); 338 | } 339 | } 340 | }; 341 | 342 | $scope.toggleExportMode = function() { 343 | $scope.ui.export_mode = !$scope.ui.export_mode; 344 | for (var x = 0; x < $scope.commands.length; x++) { 345 | $scope.commands[x].exclude_from_export = false; 346 | } 347 | $scope.ui.export_count = $scope.commands.length; 348 | } 349 | 350 | $scope.toggleImportMode = function() { 351 | $scope.ui.import_button_text = 'Import'; 352 | $scope.ui.import_mode = !$scope.ui.import_mode; 353 | $scope.commands_for_import = []; 354 | } 355 | 356 | 357 | $scope.add_to_export = function(command){ 358 | command.exclude_from_export = !command.exclude_from_export; 359 | var count = 0; 360 | for (var x = 0; x < $scope.commands.length; x++) { 361 | if (!$scope.commands[x].exclude_from_export) { 362 | count++; 363 | } 364 | } 365 | $scope.ui.export_count = count; 366 | }; 367 | 368 | $scope.clearExport = function() { 369 | $scope.ui.export_display = false; 370 | $scope.ui.export_mode = false; 371 | } 372 | 373 | $scope.createExport = function() { 374 | $scope.parsed_commands = []; 375 | for (var x = 0; x < $scope.commands.length; x++) { 376 | if (!$scope.commands[x].exclude_from_export) { 377 | var muxed = {}; 378 | muxed.command = $scope.commands[x].command; 379 | muxed.description = $scope.commands[x].description; 380 | muxed.script = $scope.commands[x].script; 381 | muxed.triggers = $scope.commands[x].triggers; 382 | muxed.variables = $scope.commands[x].variables; 383 | muxed.tags = $scope.commands[x].tags; 384 | $scope.parsed_commands.push(muxed); 385 | } 386 | } 387 | 388 | $scope.ui.exported_text = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify($scope.parsed_commands,0,4)); 389 | 390 | $scope.ui.export_display = true; 391 | 392 | }; 393 | 394 | 395 | $scope.deleteCommand = function(command) { 396 | 397 | // WAIT! Is this the fallback script? 398 | if (command.is_fallback) { 399 | if (!confirm('Deleting this script will disable your bot\'s fallback behavior. Are you sure you want to continue?')) { 400 | return false; 401 | } 402 | } else { 403 | if (!confirm('Delete this command?')) { 404 | return false; 405 | } 406 | } 407 | 408 | sdk.removeCommand($scope.bot_id, command).then(function() { 409 | $scope.getCommands(); 410 | }).catch(function(err) { 411 | $scope.handleAjaxError(err); 412 | }); 413 | 414 | }; 415 | 416 | 417 | $scope.createScriptModal = function() { 418 | $scope.ui.modal_create = true; 419 | } 420 | 421 | $scope.setAsFallback = function(command) { 422 | command.is_fallback = true; 423 | sdk.saveCommand(command).then(function() { 424 | // update UI 425 | $scope.commands.forEach(function(c) { 426 | if (c.id !== command.id) { 427 | c.is_fallback = false; 428 | } 429 | }); 430 | $scope.$apply(); 431 | }).catch(function(err) { 432 | $scope.handleAjaxError(err); 433 | }); 434 | } 435 | 436 | $scope.addCommand = function() { 437 | 438 | // make sure this command name does not already exist 439 | for (var c = 0; c < $scope.commands.length; c++) { 440 | if ($scope.commands[c].command.toLowerCase() == $scope.command.trigger.toLowerCase()) { 441 | return $scope.handleAjaxError('A script with the name "' + $scope.command.trigger + '" already exists. Please choose another name.'); 442 | } 443 | } 444 | 445 | var new_command = { 446 | command: $scope.command.trigger, 447 | botId: $scope.bot_id, 448 | description: $scope.command.description, 449 | triggers: [ 450 | { 451 | type:"string", 452 | pattern: $scope.command.trigger, 453 | } 454 | ], 455 | variables: [ 456 | { 457 | "name":"question_1", 458 | "type":"string" 459 | }, 460 | { 461 | "name":"question_2", 462 | "type":"string" 463 | }, 464 | { 465 | "name":"question_3", 466 | "type":"string" 467 | } 468 | ], 469 | script: [{ 470 | "topic":"default", 471 | "script":[ 472 | { 473 | "text": ["This is the " + $scope.command.trigger + " script. Customize me!"], 474 | }, 475 | { 476 | "action": "complete", 477 | } 478 | ] 479 | }, 480 | { 481 | "topic":"on_timeout", 482 | "script":[ 483 | { 484 | "text": ["Looks like you got distracted. We can continue later."], 485 | }, 486 | { 487 | "action": "timeout", 488 | } 489 | ] 490 | } 491 | ] 492 | }; 493 | 494 | sdk.saveCommand(new_command).then(function(command) { 495 | 496 | // clear and reset the UI 497 | $scope.command.trigger = ''; 498 | $scope.command.description = ''; 499 | $scope.add_command.$setPristine(); 500 | 501 | // it would be nice to refocus the trigger field here 502 | // FIX THIS if possible (not angular friendly) 503 | 504 | $scope.commands.unshift(command); 505 | $scope.ui.modal_create = false; 506 | 507 | $scope.$apply(); 508 | 509 | }).catch(function(err) { 510 | $scope.handleAjaxError(err); 511 | }); 512 | 513 | }; 514 | 515 | $scope.getCommands(); 516 | 517 | }]); 518 | -------------------------------------------------------------------------------- /js/sdk.js: -------------------------------------------------------------------------------- 1 | // sdk.js 2 | 3 | angular.module('howdyPro').factory('sdk', ['$http', '$q', function($http, $q) { 4 | //====================================================================== 5 | // variables 6 | //====================================================================== 7 | var sdk = {}; 8 | 9 | const VERBOSE_LOGGING = false; 10 | const REQUEST_OK_SC = 200; 11 | 12 | 13 | function handleError(err) { 14 | console.log('*** HANDLING ERROR'); 15 | console.error(err); 16 | } 17 | //====================================================================== 18 | // Verbose Logging function 19 | //====================================================================== 20 | function _log(msg, args) { 21 | if (VERBOSE_LOGGING) { 22 | if (!args) { 23 | console.log(msg); 24 | } else { 25 | if (Array.isArray(args)) { 26 | args.forEach(function(arg) { 27 | console.log(msg + ': ', arg); 28 | }); 29 | } else { 30 | console.log(msg + ': ', args); 31 | } 32 | } 33 | } 34 | } 35 | //====================================================================== 36 | // requests 37 | //====================================================================== 38 | function request(uri, method, data, sign) { 39 | var deferred = $q.defer(); 40 | // log arguments 41 | _log('uri: ', uri); 42 | _log('method: ', method); 43 | _log('data: ', data); 44 | _log('sign: ', sign); 45 | // log service endpoints 46 | var headers = { 47 | "Content-Type": "application/json" 48 | }; 49 | var opts = {}; 50 | if (method === 'get') { 51 | opts = { 52 | method: method, 53 | url: uri, 54 | headers: headers 55 | }; 56 | 57 | if (data) { 58 | for (var key in data) { 59 | opts.url = opts.url + "&" + key + "=" + encodeURIComponent(data[key]); 60 | } 61 | } 62 | 63 | } else if (method === 'post' || method === 'put' || method === 'delete') { 64 | opts = { 65 | method: method, 66 | url: uri, 67 | data: data, 68 | headers: headers 69 | }; 70 | } else { 71 | _log("REQUEST REJECTED ==> INVALID_METHOD"); 72 | deferred.reject(INVALID_METHOD); 73 | return; 74 | } 75 | _log("OPTS: ", opts); 76 | if (!opts || !opts.url) { 77 | deferred.reject(REQUEST_ABORTED); 78 | return; 79 | } else { 80 | _log("DISPATCHING REQUEST...") 81 | $http(opts).then(function(response) { 82 | if (response.statuscode !== REQUEST_OK_SC) { 83 | _log("REQUEST RESOLVED ==> REQUEST_OK_SC"); 84 | _log("REQUEST ==> RESPONSE.DATA:", response.data); 85 | deferred.resolve(response.data); 86 | } else { 87 | // Invalid response 88 | // Status codes less than -1 are normalized to zero. 89 | // -1 usually means the request was aborted, e.g. using a config.timeout 90 | if (response.statuscode === -1) { 91 | _log("REQUEST REJECTED ==> REQUEST_ABORTED"); 92 | deferred.reject(REQUEST_ABORTED); 93 | } else { 94 | _log("REQUEST REJECTED ==> !== REQUEST_OK_SC"); 95 | deferred.reject(response.data); 96 | } 97 | } 98 | }, function(error) { 99 | handleError(error); 100 | deferred.reject(error); 101 | }); 102 | } 103 | return deferred.promise; 104 | }; 105 | 106 | sdk.noop = function() { 107 | var deferred = $q.defer(); 108 | setTimeout(function() { 109 | deferred.resolve('noop'); 110 | },1000); 111 | return deferred.promise; 112 | } 113 | 114 | sdk.v2 = function(uri, type, parameters, usetoken) { 115 | return new Promise(function(resolve, reject) { 116 | request(uri, type, parameters, usetoken).then(function(response) { 117 | // console.log('GOT RESPONSE', response) 118 | 119 | var data = response.data; 120 | if (response.success) { 121 | resolve(response.data); 122 | } else { 123 | reject(response); 124 | } 125 | }).catch(function(err) { 126 | reject(err); 127 | }); 128 | }); 129 | } 130 | 131 | // get external url for import 132 | sdk.getExternalUrl = function(uri){ 133 | const method = "sdk.getExternalUrl"; 134 | var deferred = $q.defer(); 135 | $http({ 136 | method: 'GET', 137 | url: '/app/bots/:botid/import/external?uri=' + uri, 138 | headers: {"Content-Type": "application/json"} 139 | }).then(function successCallback(response) { 140 | // console.log(response); 141 | deferred.resolve(response); 142 | }, function errorCallback(response) { 143 | // console.log('err'); 144 | // console.log(response); 145 | deferred.reject(new Error(response)); 146 | }); 147 | return deferred.promise; 148 | }; 149 | 150 | 151 | //====================================================================== 152 | // commandService 153 | //====================================================================== 154 | sdk.getCommandById = function(botId, id) { 155 | return sdk.v2('/admin/api/script','post',{command: id}, true); 156 | }; 157 | 158 | sdk.getCommandByName = function(botId, id) { 159 | return sdk.v2('/admin/api/script','post',{command: id}, true); 160 | }; 161 | 162 | 163 | sdk.getCommandsByBot = function(id) { 164 | 165 | return sdk.v2('/admin/api/scripts','get',{}, true); 166 | 167 | }; 168 | 169 | sdk.getLUISIntents = function(id) { 170 | return sdk.v2('/admin/api/luisIntents','get',{}, true); 171 | }; 172 | 173 | sdk.removeCommand = function(bot_id, command) { 174 | command.deleted = true; 175 | var uri = '/admin/api/scripts/' + command.id; 176 | return sdk.v2(uri, 'delete',{deleted:true}, true); 177 | }; 178 | 179 | sdk.saveCommand = function(command) { 180 | 181 | var cloned = JSON.parse(JSON.stringify(command)); 182 | var clean_script = cloned.script; 183 | 184 | // remove all the weird ui fields that get jammed in here 185 | for (var t = 0; t < clean_script.length; t++) { 186 | delete clean_script[t].editable; 187 | for (var m = 0; m < clean_script[t].script.length; m++) { 188 | delete clean_script[t].script[m].first_in_group; 189 | delete clean_script[t].script[m].last_in_group; 190 | delete clean_script[t].script[m].middle_of_group; 191 | delete clean_script[t].script[m].focused; 192 | delete clean_script[t].script[m].focused_user; 193 | delete clean_script[t].script[m].invalid; 194 | delete clean_script[t].script[m].invalid_key; 195 | delete clean_script[t].script[m].placeholder; 196 | 197 | 198 | // remove flag fields on attachments 199 | if (clean_script[t].script[m].attachments) { 200 | for (var a = 0; a < clean_script[t].script[m].attachments.length; a++) { 201 | delete clean_script[t].script[m].attachments[a].hasAuthor; 202 | delete clean_script[t].script[m].attachments[a].hasFooter; 203 | delete clean_script[t].script[m].attachments[a].hasImage; 204 | } 205 | } 206 | 207 | // remove empty quick reply list, as this will be rejected by Facebook 208 | if (clean_script[t].script[m].quick_replies && !clean_script[t].script[m].quick_replies.length) { 209 | delete clean_script[t].script[m].quick_replies; 210 | } 211 | 212 | // remove selectable thread 213 | if(clean_script[t].script[m].collect && clean_script[t].script[m].collect.options){ 214 | for (var i = 0; i < clean_script[t].script[m].collect.options.length; i++) { 215 | if(clean_script[t].script[m].collect.options[i].selected_scripts_threads){ 216 | delete clean_script[t].script[m].collect.options[i].selected_scripts_threads 217 | } 218 | } 219 | } 220 | 221 | // remove selectable thread from a condition action 222 | // console.log('clean_script[t].script[m]: ', clean_script[t].script[m]); 223 | if(clean_script[t].script[m].conditional && clean_script[t].script[m].conditional.selected_scripts_threads){ 224 | delete clean_script[t].script[m].conditional.selected_scripts_threads 225 | } 226 | 227 | if(clean_script[t].script[m].conditional && clean_script[t].script[m].conditional.left == '_new'){ 228 | clean_script[t].script[m].conditional.left = clean_script[t].script[m].conditional.left_val; 229 | delete clean_script[t].script[m].conditional.left_val; 230 | } 231 | 232 | if(clean_script[t].script[m].conditional && clean_script[t].script[m].conditional.right == '_new'){ 233 | clean_script[t].script[m].conditional.right = clean_script[t].script[m].conditional.right_val; 234 | delete clean_script[t].script[m].conditional.right_val; 235 | } 236 | if(clean_script[t].script[m].conditional && clean_script[t].script[m].conditional.validators){ 237 | delete clean_script[t].script[m].conditional.validators; 238 | } 239 | 240 | // remove selectable threads from the complete actions 241 | var last_script = clean_script[t].script[clean_script[t].script.length-1] 242 | if(last_script.action === "execute_script"){ 243 | if(last_script.selected_scripts_threads){ 244 | delete last_script.selected_scripts_threads 245 | } 246 | } 247 | } 248 | } 249 | 250 | 251 | cloned.script = clean_script; 252 | 253 | return sdk.v2('/admin/save','post',cloned, true); 254 | 255 | }; 256 | 257 | return sdk; 258 | }]); 259 | -------------------------------------------------------------------------------- /npm-debug.log: -------------------------------------------------------------------------------- 1 | 0 info it worked if it ends with ok 2 | 1 verbose cli [ '/usr/local/bin/node', '/usr/local/bin/npm', 'start' ] 3 | 2 info using npm@4.1.2 4 | 3 info using node@v7.5.0 5 | 4 verbose stack Error: Failed to parse json 6 | 4 verbose stack Trailing comma in object at 27:3 7 | 4 verbose stack } 8 | 4 verbose stack ^ 9 | 4 verbose stack at parseError (/usr/local/lib/node_modules/npm/node_modules/read-package-json/read-json.js:390:11) 10 | 4 verbose stack at parseJson (/usr/local/lib/node_modules/npm/node_modules/read-package-json/read-json.js:79:23) 11 | 4 verbose stack at /usr/local/lib/node_modules/npm/node_modules/read-package-json/read-json.js:48:5 12 | 4 verbose stack at /usr/local/lib/node_modules/npm/node_modules/graceful-fs/graceful-fs.js:78:16 13 | 4 verbose stack at tryToString (fs.js:426:3) 14 | 4 verbose stack at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:413:12) 15 | 5 verbose cwd /Users/benbrown/Dropbox/bots/studio-editor-only 16 | 6 error Darwin 15.6.0 17 | 7 error argv "/usr/local/bin/node" "/usr/local/bin/npm" "start" 18 | 8 error node v7.5.0 19 | 9 error npm v4.1.2 20 | 10 error file /Users/benbrown/Dropbox/bots/studio-editor-only/package.json 21 | 11 error code EJSONPARSE 22 | 12 error Failed to parse json 23 | 12 error Trailing comma in object at 27:3 24 | 12 error } 25 | 12 error ^ 26 | 13 error File: /Users/benbrown/Dropbox/bots/studio-editor-only/package.json 27 | 14 error Failed to parse package.json data. 28 | 14 error package.json must be actual JSON, not just JavaScript. 29 | 14 error 30 | 14 error This is not a bug in npm. 31 | 14 error Tell the package author to fix their package.json file. JSON.parse 32 | 15 verbose exit [ 1, true ] 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botkit-cms", 3 | "version": "0.0.3", 4 | "description": "an open tool for designing, building and managing interactive dialog systems", 5 | "license": "MIT", 6 | "keywords": [ 7 | "bots", 8 | "dialog editor", 9 | "content management", 10 | "botkit", 11 | "chatbots", 12 | "bot" 13 | ], 14 | "homepage": "https://github.com/howdyai/botkit-cms", 15 | "main": "src/api.js", 16 | "author": "benbrown@gmail.com", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/howdyai/botkit-cms.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/howdyai/botkit-cms/issues" 23 | }, 24 | "scripts": { 25 | "start": "node index.js", 26 | "build": "./node_modules/gulp/bin/gulp.js", 27 | "gulp": "./node_modules/gulp/bin/gulp.js", 28 | "eslint": "./node_modules/eslint/bin/eslint.js js/ src/ components/", 29 | "test": "echo \"Error: no test specified\" && exit 1" 30 | }, 31 | "dependencies": { 32 | "async": "^2.6.1", 33 | "body-parser": "^1.17.2", 34 | "debug": "^2.6.8", 35 | "dotenv": "^6.0.0", 36 | "express": "^4.15.3", 37 | "express-basic-auth": "^1.1.5", 38 | "express-hbs": "^1.0.4", 39 | "querystring": "^0.2.0", 40 | "uuid": "^3.3.2" 41 | }, 42 | "devDependencies": { 43 | "eslint": "^5.7.0", 44 | "gulp": "^4.0.0", 45 | "gulp-concat": "^2.6.1", 46 | "gulp-sass": "^4.0.2", 47 | "gulp-util": "^3.0.7" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/bot_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/howdyai/botkit-cms/23128e26982d3818423911cb15d18b377da0389b/public/bot_avatar.png -------------------------------------------------------------------------------- /public/js/partials/ciscospark_attachments.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • 4 | 5 |
      6 |
    • 7 |
       
      8 |
      URL
      9 |
    • 10 |
    • 11 |
      12 | 13 |
      14 |
      15 | 16 |
      17 |
    • 18 |
    • 19 | 20 |
    • 21 |
    22 |
  • 23 |
24 |
25 | 26 | 27 |
28 | -------------------------------------------------------------------------------- /public/js/partials/conditional.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • 4 | 5 |
      6 |
    • 7 | 8 | 16 | 17 |
    • 18 |
    • 19 | 20 | 26 |
    • 27 |
    • 28 | 29 | 37 | 38 |
    • 39 |
    40 |
  • 41 | 42 |
  • 43 | 44 |
      45 |
    • 46 | 63 |
    • 64 |
    • 65 | 66 |
      67 | 69 |
      70 |
    • 71 |
    • 72 | 73 |
      74 | 76 |
      77 |
    • 78 |
    79 |
  • 80 | 81 |
82 | 83 | 84 | 85 |
86 |
87 |
88 | 89 | 90 |
91 |
92 |
93 | 94 | 95 | 96 |
97 | -------------------------------------------------------------------------------- /public/js/partials/facebook_attachments.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • 4 | 5 |
  • 6 |
7 |
    8 |
  • 9 |
     
    10 |
    Title
    11 |
    Payload
    12 |
    Image URL
    13 |
  • 14 |
  • 15 |
    16 | 17 |
    18 |
    19 | 20 | 21 | 22 | 23 | Location 24 | 25 |
    26 |
    27 | 28 | 29 | 30 |
    31 |
    32 | 33 | 34 | 35 |
    36 |
  • 37 |
  • 38 | 39 | 40 |
  • 41 |
42 | 43 |
44 | 45 |
46 |
47 |
    48 |
  • 49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | 64 |
65 | -------------------------------------------------------------------------------- /public/js/partials/facebook_button_attachment.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 | 4 | 5 |
  • 6 |
7 |
    8 |
  • 9 |
    10 |
    Type
    11 |
    Title
    12 |
    Payload Value
    13 |
    Size
    14 |
  • 15 |
16 |
    17 |
  • 18 |
    19 | 20 |
    21 |
    22 | {% button.type %} 23 |
    24 |
    25 | 26 |
    27 |
    28 | 29 |
    30 |
    31 | 32 |
    33 |
    34 | 35 |
    36 |
    37 | 42 |
    43 |
    44 |
    45 |
  • 46 |
  • 47 | 48 | 49 | 50 |
  • 51 |
52 | -------------------------------------------------------------------------------- /public/js/partials/facebook_generic_attachment.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 | 4 | Carousel Element 5 |
  • 6 |
  • 7 | 8 | 9 |
  • 10 |
  • 11 | 12 | 13 |
  • 14 |
  • 15 | 16 | 17 |
  • 18 |
  • 19 | 20 | 21 |
  • 22 |
  • 23 | 24 |
  • 25 |
  • 26 |
      27 |
    • 28 |
      29 |
      Type
      30 |
      Title
      31 |
      Payload Value
      32 |
      Size
      33 |
    • 34 | 35 |
    • 36 |
      37 | 38 |
      39 |
      40 | {% translateButtonType(button.type) %} 41 |
      42 |
      43 | 44 |
      45 |
      46 | 47 |
      48 |
      49 | 50 |
      51 |
      52 | 53 |
      54 |
      55 | 60 |
      61 |
      62 | Share 63 |
      64 |
      65 |
      66 | 67 |
      68 |
      69 | 70 |
    • 71 |
    • 72 | 73 | 74 | 75 | 76 |
    • 77 |
    78 | 79 |
  • 80 |
81 | 82 | 83 |
    84 |
  • 85 | 86 |
  • 87 |
88 | -------------------------------------------------------------------------------- /public/js/partials/facebook_media_attachment.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 | 4 | 5 |
  • 6 |
  • 7 | 8 | 14 |
  • 15 |
  • 16 | 17 | 18 |
  • 19 |
20 | -------------------------------------------------------------------------------- /public/js/partials/meta_attachments.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • Custom Fields 4 |
5 |
    6 |
  • 7 |
     
    8 |
    Key
    9 |
    Value
    10 |
  • 11 |
  • 12 |
    13 | 14 |
    15 |
    16 | 17 |
    18 |
    19 | 20 |
    21 |
  • 22 |
  • 23 | 24 |
  • 25 |
26 |
27 | -------------------------------------------------------------------------------- /public/js/partials/properties_incoming.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • 4 | 5 | {% ui.incoming_message.text[0] %} 6 |
  • 7 |
  • 8 | 9 | 12 |
  • 13 |
  • 14 |
    15 | 16 | 17 | 18 |
    19 | lowercase letters, numbers, _, - 20 | 21 |
  • 22 | 23 |
  • 24 | 25 |
  • 26 |
27 |
28 | 29 |
30 |
    31 |
  • 32 | 33 |
  • 34 |
35 |
    36 |
  • 37 | 38 | 39 | 40 | 41 | 42 | 47 |
  • 48 |
  • 49 | 50 | 55 |
  • 56 |
  • 57 | 58 | 75 | 76 | 77 | 78 |
  • 79 | 80 |
    81 | 83 |
    84 |
  • 85 | 86 | 87 |
  • 88 | 89 |
    90 | 92 |
    93 |
  • 94 | 95 | 96 |
    97 |
    98 |
    99 | 100 | 101 |
    102 |
    103 |
    104 | 105 |
106 |
    107 |
  • 108 | 109 |
  • 110 |
111 | 112 |
    113 |
  • 114 | 115 | 136 | 137 |
    138 | 139 |
    140 | 142 |
    143 |
    144 | 145 | 146 |
    147 | 148 |
    149 | 151 |
    152 |
    153 | 154 | 155 |
    156 |
    157 |
    158 | 159 | 160 |
    161 |
    162 |
    163 | 164 | 165 | 166 | 167 |
  • 168 |
169 | -------------------------------------------------------------------------------- /public/js/partials/properties_none.html: -------------------------------------------------------------------------------- 1 | Click a message to edit it 2 | -------------------------------------------------------------------------------- /public/js/partials/properties_outgoing.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • 4 | 5 | 10 | 19 |
  • 20 | 21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 | 29 | 30 |
31 |
    32 |
  • 33 | 34 |
  • 35 |
36 |
    37 |
  • 38 | 39 | 42 |
  • 43 |
  • 44 |
    45 | 46 | 47 | 48 |
    49 | lowercase letters, numbers, _, - 50 | 51 |
  • 52 | 53 |
  • 54 | 55 |
  • 56 |
57 |
58 | 59 |
60 |
    61 |
  • 62 | 63 |
  • 64 |
65 |
    66 |
  • 67 | 68 | 69 | 70 | 71 | 72 | 77 |
  • 78 |
  • 79 | 80 | 85 |
  • 86 |
  • 87 | 88 | 106 | 107 | 108 | 109 |
  • 110 | 111 |
    112 | 114 |
    115 |
  • 116 | 117 | 118 |
  • 119 | 120 |
    121 | 123 |
    124 |
  • 125 | 126 | 127 |
    128 |
    129 |
    130 | 131 | 132 |
    133 |
    134 |
    135 | 136 |
137 |
    138 |
  • 139 | 140 |
  • 141 |
142 | 143 |
    144 |
  • 145 | 146 | 168 | 169 |
    170 | 171 |
    172 | 174 |
    175 |
    176 | 177 | 178 |
    179 | 180 |
    181 | 183 |
    184 |
    185 | 186 | 187 |
    188 |
    189 |
    190 | 191 | 192 |
    193 |
    194 |
    195 | 196 |
  • 197 |
198 | -------------------------------------------------------------------------------- /public/js/partials/slack_attachments.html: -------------------------------------------------------------------------------- 1 | 4 |
5 |
    6 |
  • 7 | 14 | 15 |
  • 16 |
  • 17 | 18 | 19 |
  • 20 |
  • 21 | 22 | 23 |
  • 24 |
  • 25 | 26 | 27 |
  • 28 |
  • 29 | 30 | 31 | 32 |
  • 33 |
  • 34 | 35 | 36 |
  • 37 |
  • 38 | 39 |
  • 40 |
  • 41 | 42 | 43 |
  • 44 |
  • 45 |
      46 |
    • 47 | 48 | 49 |
    • 50 |
    • 51 | 52 | 53 |
    • 54 |
    55 |
  • 56 |
  • 57 | 58 |
  • 59 |
  • 60 | 61 | 62 |
  • 63 |
  • 64 |
      65 |
    • 66 | 67 | 68 |
    • 69 |
    70 |
  • 71 |
  • 72 | 73 |
  • 74 |
  • 75 | 76 | 77 | 78 |
  • 79 |
  • 80 |
      81 |
    • 82 | 83 |
    • 84 |
    85 |
  • 86 |
  • 87 | 88 |
    89 |
      90 |
    • 91 |
       
      92 |
      Title
      93 |
      Value
      94 |
      Short
      95 |
       
      96 |
    • 97 |
    98 |
      99 |
    • 105 |
      106 | 107 |
      108 |
      109 | 110 |
      111 |
      112 | 113 |
      114 |
      115 | 116 |
      117 |
      118 | 119 |
      120 | 121 |
    • 122 |
    123 |
      124 |
    • 125 | 126 |
    • 127 |
    128 |
    129 |
  • 130 |
  • 131 | 132 |
  • 133 |
  • 134 |
    135 |
      136 |
    • 137 |
       
      138 |
      Type
      139 |
      Title
      140 |
      Name
      141 |
      Value
      142 |
      143 |
       
      144 |
    • 145 |
    146 |
      147 |
    • 153 |
      154 | 155 |
      156 |
      157 | {% action.type %} 158 |
      159 |
      160 | 161 |
      162 |
      163 | 164 |
      165 |
      166 | 167 | 168 |
      169 |
      170 | 177 |
      178 |
      179 | 180 | 181 | 182 |
      183 |
      184 | 185 |
      186 |
      187 | 188 |
      189 |
    • 190 |
    191 |
      192 |
    • 193 | 194 | 195 | 196 |
    • 197 |
    198 |
  • 199 |
  • 200 | 201 | 202 |
  • 203 |
204 | 205 |
    206 |
  • 207 | 208 |
  • 209 |
210 |
211 | 212 |
213 | -------------------------------------------------------------------------------- /public/js/partials/teams_attachments.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
    4 |
  • Attachments
  • 5 |
6 |
    7 |
  • 8 | 9 | 10 |
  • 11 | 12 |
    13 | 14 |
15 |
    16 |
  • 17 | 18 | 19 | 20 |
  • 21 |
22 |
23 | 24 |
25 | 26 |
    27 |
  • 28 | 29 | 33 |
  • 34 |
35 | 36 | 37 |
38 | 39 | 40 |
41 | -------------------------------------------------------------------------------- /public/js/partials/teams_attachments_file.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 |
  • 5 |
  • 6 | 7 | 8 |
  • 9 |
  • 10 | 11 | 16 |
  • 17 | -------------------------------------------------------------------------------- /public/js/partials/teams_attachments_hero.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 |
  • 5 |
  • 6 | 7 | 8 |
  • 9 |
  • 10 | 11 | 12 |
  • 13 | 14 |
  • 15 | 16 | 17 |
  • 18 |
  • 19 | 20 | 21 |
  • 22 |
  • 23 | 24 |
  • 25 |
  • 26 |
      27 |
    • 28 |
       
      29 |
      Title
      30 |
      Action
      31 |
      Value
      32 |
    • 33 |
    • 34 |
      35 | 36 |
      37 |
      38 | 39 |
      40 |
      41 | 46 |
      47 |
      48 | 49 |
      50 |
    • 51 |
    • 52 | 53 |
    • 54 |
    55 |
  • 56 |
  • 57 | 58 |
  • 59 |
  • 60 | 61 | 62 |
  • 63 |
  • 64 | 65 | 70 |
  • 71 |
  • 72 | 73 | 74 |
  • 75 | -------------------------------------------------------------------------------- /public/js/partials/teams_attachments_thumbnail.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 |
  • 5 |
  • 6 | 7 | 8 |
  • 9 |
  • 10 | 11 | 12 |
  • 13 | 14 |
  • 15 | 16 | 17 |
  • 18 |
  • 19 | 20 | 21 |
  • 22 |
  • 23 | 24 |
  • 25 |
  • 26 |
      27 |
    • 28 |
       
      29 |
      Title
      30 |
      Action
      31 |
      Value
      32 |
    • 33 |
    • 34 |
      35 | 36 |
      37 |
      38 | 39 |
      40 |
      41 | 46 |
      47 |
      48 | 49 |
      50 |
    • 51 |
    • 52 | 53 |
    • 54 |
    55 |
  • 56 |
  • 57 | 58 |
  • 59 |
  • 60 | 61 | 62 |
  • 63 |
  • 64 | 65 | 70 |
  • 71 |
  • 72 | 73 | 74 |
  • 75 | -------------------------------------------------------------------------------- /public/js/partials/web_attachments.html: -------------------------------------------------------------------------------- 1 |
    2 |
      3 |
    • 4 | 5 |
    • 6 |
    7 |
      8 |
    • 9 |
       
      10 |
      Title
      11 |
      Payload
      12 |
       
      13 |
    • 14 |
    15 |
      16 |
    • 21 |
      22 | 23 |
      24 |
      25 | 26 | 27 | 28 |
      29 |
      30 | 31 | 32 | 33 |
      34 |
      35 | 36 |
      37 |
    • 38 |
    39 |
      40 |
    • 41 | 42 |
    • 43 |
    44 | 45 | 46 |
    47 | 48 |
    49 |
      50 |
    • 51 | 52 |
        53 |
      • 54 |
         
        55 |
        URL
        56 |
      • 57 |
      • 58 |
        59 | 60 |
        61 |
        62 | 63 |
        64 |
      • 65 |
      • 66 | 67 |
      • 68 |
      69 |
    • 70 |
    71 |
    72 | 73 | 74 |
    75 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Botkit CMS 2 | 3 | ## An open tool for designing, building and managing interactive dialog systems 4 | 5 | ![Dialog Editor](https://raw.githubusercontent.com/howdyai/botkit-cms/master/docs/screenshots/dialog.png) 6 | 7 | ## Install and run it locally 8 | 9 | Clone and install botkit-cms 10 | ``` 11 | git clone https://github.com/howdyai/botkit-cms.git 12 | cd botkit-cms 13 | npm install 14 | ``` 15 | Create an .env file from .env_sample and change the variables 16 | ``` 17 | cp .env_sample .env 18 | ``` 19 | ``` 20 | PLATFORM=web 21 | TOKENS=youwillneverguessmysecretbottoken 22 | USERS=admin:123secret 23 | ``` 24 | 25 | Create .data folder, create a scripts.json inside. Copy the content from sample-scripts.json 26 | ``` 27 | mkdir .data 28 | cp sample_scripts.json .data/scripts.json 29 | ``` 30 | 31 | Run cms and open localhost:3000/admin and enter the credentials from the USERS env variable. 32 | ``` 33 | npm run build 34 | npm start 35 | ``` 36 | 37 | ## Create Botkit Dialog Editor & API Service 38 | 39 | Clone this repo and set it up on a public host somewhere. Clicking the Glitch link below will do this for you. 40 | 41 | [Configure the .env file.](#configuration) 42 | 43 | Launch the app, then load it in your web browser. You should see a link to the editor screen. 44 | 45 | Make sure your new service is available at a public address on the web. Then, modify your Botkit app to include a pointer to this new service. 46 | 47 | ``` 48 | var controller = Botkit.platform({ 49 | studio_command_uri: 'https://my.new.service', 50 | studio_token: 'a shared secret' 51 | }) 52 | ``` 53 | 54 | [![Remix on Glitch](https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button.svg)](https://glitch.com/edit/#!/remix/botkit-cms) 55 | 56 | ### Configuration 57 | 58 | Options for this service are controlled via environment variables, which can be stored in a `.env` file at the project root. 59 | 60 | Here is an example `.env` file: 61 | 62 | ``` 63 | # Chat platform 64 | # PLATFORM= 65 | # valid options are slack, teams, ciscospark, web, facebook 66 | PLATFORM=slack 67 | 68 | # authentication tokens for Bot users 69 | # TOKENS="123 456" 70 | TOKENS=youwillneverguessmysecretbottoken 71 | 72 | # Admin users for UI 73 | # USERS="username:password username2:password2 username3:password3" 74 | USERS=admin:123secret 75 | 76 | # LUIS Endpoint 77 | # URL to published LUIS Endpoint in the form of https://.api.cognitive.microsoft.com/luis/v2.0/apps/?subscription-key=&verbose=true&timezoneOffset=-360&q= 78 | # Get this from LUIS.ai Keys and Endpoint settings 79 | # LUIS_ENDPOINT= 80 | 81 | # LUIS App Version 82 | # Defaults to 0.1, update if you manage multiple LUIS app versions 83 | # LUIS_VERSION=0.1 84 | ``` 85 | 86 | ### Using LUIS.ai 87 | 88 | This project includes support for using LUIS.ai to determine the intent represented by an incoming message. 89 | To enable LUIS, add the `LUIS_ENDPOINT` variable to your environment. 90 | 91 | After enabling LUIS, new options will appear in the Trigger dialog that will allow you to assign intents from LUIS as triggers. 92 | 93 | ### Editor Configuration 94 | 95 | The Botkit dialog editor can be used in one of several different flavors, controlled by the `PLATFORM` environment variable. 96 | 97 | | Value | Description 98 | |--- |--- 99 | | web | Botkit Anywhere mode 100 | | slack | Slack mode 101 | | teams | Microsoft Teams mode 102 | | ciscospark | Cisco Spark / Webex Teams mode 103 | | facebook | Facebook mode 104 | 105 | 106 | ### Securing Admin / Editor Access 107 | 108 | Access can be limited and users can be controlled using the `USERS` environment variable. 109 | 110 | Set the variable to a space separated list of user:password pairs. Users will be shown a login prompt when accessing any url within the `/admin/` url. 111 | 112 | ### Securing API Access 113 | 114 | You can lock down access to the API by specifying one or more access tokens in the TOKENS environment variable (or in the .env file). 115 | 116 | If any tokens are specified, access to the API requies a valid value in the `access_token` url query parameter. Botkit will automatically use the Botkit Studio `studio_token` value for this. 117 | 118 | ## Building Project 119 | 120 | Modifications to the front end application or css should be done to their original source files, then compiled by the build process. To build the Javascript and CSS files from their source locations, run the following command: 121 | 122 | ```bash 123 | npm run build 124 | ``` 125 | 126 | The front end editor application included in this project is built with Angular. The source code of the this application is broken up into several component files in the `js/` folder. These are compiled into a single source file and moved to a final home at `public/js/scripts.js` by the build process. 127 | 128 | The CSS is controlled by SASS files in the `sass/` folder. These are compiled into a single source file and moved to a final home at `public/css/new.css` by the build process. 129 | 130 | 131 | ## Alternate: Use this as a component in your Botkit App 132 | 133 | First, npm install this: 134 | 135 | ```bash 136 | npm install --save botkit-cms 137 | ``` 138 | 139 | Get your existing scripts from an instance of Botkit CMS, and put the resulting `scripts.json` into your bot project. 140 | 141 | Then, add to your bot's main file, just after defining your controller object: 142 | 143 | ```js 144 | var cms = require('botkit-cms')(); 145 | cms.useLocalStudio(controller); 146 | 147 | cms.loadScriptsFromFile(__dirname + '/scripts.json').catch(function(err) { 148 | console.error('Error loading scripts', err); 149 | }); 150 | ``` 151 | 152 | Note that you might need to modify the call to `cms.loadScriptsFromFile` depending on where you put the scripts.json file. 153 | -------------------------------------------------------------------------------- /sample_scripts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "0-0-0-0-0", 4 | "command": "hello", 5 | "description": "Respond when a human says hello!", 6 | "script": [ 7 | { 8 | "topic": "default", 9 | "script": [ 10 | { 11 | "text": [ 12 | "Hello Human!", 13 | "How do you do?", 14 | "Nice to meet you Human.", 15 | "Hi!", 16 | "How’s it going?", 17 | "Hey!", 18 | "Hey there!", 19 | "Howdy!", 20 | "G`day human!", 21 | "Salut!", 22 | "Ciao!", 23 | "Hola!", 24 | "Shalom!" 25 | ] 26 | }, 27 | { 28 | "action": "complete" 29 | } 30 | ] 31 | } 32 | ], 33 | "triggers": [ 34 | { 35 | "pattern": "hell.*", 36 | "type": "regexp", 37 | "id": "495" 38 | }, 39 | { 40 | "type": "string", 41 | "pattern": "hello" 42 | }, 43 | { 44 | "type": "string", 45 | "pattern": "hey" 46 | }, 47 | { 48 | "type": "string", 49 | "pattern": "hi" 50 | }, 51 | { 52 | "type": "string", 53 | "pattern": "howdy" 54 | } 55 | ], 56 | "variables": [], 57 | "modified": "2018-12-12T15:23:41.416Z" 58 | } 59 | ] -------------------------------------------------------------------------------- /sass/_vars.scss: -------------------------------------------------------------------------------- 1 | $spacing-unit: 30px; 2 | $half-spacing-unit: $spacing-unit / 2; 3 | $unit: 5px; 4 | 5 | // Colors 6 | $white: #fff; 7 | $light-gray: #f7f7f7; 8 | $gray: #e6e6e6; 9 | // $gray-2: #dde; 10 | $mid-gray: #888; 11 | $dark-gray: #444; 12 | $black: #111; 13 | $code-dark: #002240; 14 | $code-white: #e7e7e7; 15 | $orange: #ff8917; 16 | $main-blue: #1432e0; 17 | $light-blue: #eef1fd; 18 | $lavender: #bbc5f6; 19 | $green: #14e07f; 20 | $seafoam: #bdf4ed; 21 | $yellow: #ffe517; 22 | 23 | $cantelope: #f9bf8c; 24 | $red: #f81645; 25 | // $purple: #b195f4; 26 | $purple: #666; 27 | $gray-2: #ccc; 28 | 29 | 30 | // translucent colors 31 | $translucent-blue: rgba(187, 197, 246, .5); 32 | $translucent-red: rgba(248, 22, 69, .5); 33 | $translucent-white: rgba(255, 255, 255, .4); 34 | $translucent-black: rgba(0, 0, 0, .3); 35 | $translucent-cantelope: rgba(249, 191, 140 , .6); 36 | $shade: rgba(0,0,0,0.6); 37 | 38 | //tinted cd-colors 39 | $seafoam-light: #d3f8f3; 40 | $seafoam-dark: #a7f0e7; 41 | $green-light: #8af2a7; 42 | $green-hint: #dbf9d4; 43 | $yellow-hint: #fffdd9; 44 | $orange-hint: #ffeed9; 45 | $main-blue-hint: #dde3fa; 46 | $pink-hint: #f8d3f0; 47 | $lavender-light: #abdbef; 48 | $main-blue-dark: #1028b4; 49 | $purple-hint: lighten($purple, 10%); 50 | 51 | //topic color settings 52 | $danger-color: darken($red, 10%); 53 | $warning-color: darken($cantelope, 10%); 54 | $success-color: $green; 55 | $app-background-blue: #eef1fd; 56 | 57 | //talkabot colors 58 | $talkabot-background-color: #109acb; 59 | $talkabot-magenta: #D8117D; 60 | 61 | //botkit colors 62 | $botkit-brand-color: $main-blue; //$cantelope; 63 | 64 | //botkit studio colors 65 | $botkit-studio-brand-color: $purple; 66 | $botkit-light: lighten($botkit-studio-brand-color, 10%); 67 | $botkit-dark: darken($botkit-studio-brand-color, 10%); 68 | $transition-speed: .2s; 69 | 70 | 71 | $highlight: #FFFFF0; 72 | $body-font: "helvetica",sans-serif; 73 | $headline-font: "helvetica",sans-serif; 74 | $headline-oblique-font: 'helvetica',sans-serif; 75 | $purple: #a795ef; 76 | $green: #b0f0ab; 77 | $blue: #283ed8; 78 | $lavender: #bcc4f1; 79 | $seafoam: #cff4ed; 80 | $cantaloupe: #eabd91; // $red: #dd99be; 81 | $white: #FFF; 82 | $pink: #D8117D; 83 | 84 | @mixin truncate-ul($lineHeight: 30px, $lineCount: 6, $bgColor: white) { 85 | //display: -webkit-box; 86 | //-webkit-line-clamp: 6; 87 | //-webkit-box-orient: vertical; 88 | overflow: hidden; 89 | position: relative; 90 | line-height: $lineHeight; 91 | max-height: $lineHeight * $lineCount; 92 | text-align: justify; 93 | margin-right: -1em; 94 | padding-right: 1em; 95 | } 96 | @mixin inline-list { 97 | display: inline; 98 | list-style: none; 99 | float: left; 100 | } 101 | 102 | @mixin attention { 103 | // @include pulsate(); 104 | transition: all .3s; 105 | border: 2px solid $cantelope; 106 | box-shadow: 0px 5px 1px -5px rgba(0,0,0,0.20); 107 | z-index: 100; 108 | } 109 | 110 | @mixin clearfix { 111 | &:after { 112 | content: ""; 113 | display: table; 114 | clear: both; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /sass/features/_input.scss: -------------------------------------------------------------------------------- 1 | $input-border-color: $mid-gray; 2 | 3 | @mixin ng-invalid { 4 | &.ng-invalid.ng-dirty { 5 | border-color: $danger-color; 6 | } 7 | } 8 | 9 | @mixin script-text-input { 10 | @include ng-invalid; 11 | border: 2px solid $input-border-color; 12 | padding: $unit; 13 | font-weight: 100; 14 | &:focus, 15 | &:active { 16 | border: 2px solid $main-blue; 17 | outline: 0; 18 | } 19 | &:hover { 20 | border-color: $main-blue; 21 | } 22 | &.invalid { 23 | border-color: $danger-color; 24 | } 25 | } 26 | 27 | @mixin textarea-grow { 28 | resize: vertical; 29 | resize: none; 30 | } 31 | 32 | @mixin list-text-button-combo { 33 | display: flex; 34 | font-size: 16px; 35 | input { 36 | margin-bottom: 0; 37 | flex: 1; 38 | box-sizing: border-box; 39 | } 40 | form { 41 | margin: 0; 42 | display: flex; 43 | width: 100%; 44 | } 45 | button { 46 | float: right; 47 | } 48 | } 49 | 50 | @mixin inset-shadow { 51 | box-shadow: inset 0 2px 3px 0 rgba(0, 0, 0, .4); 52 | } 53 | 54 | .script-settings, .script-editor, .login { 55 | input[type="text"], 56 | input[type="url"], 57 | input[type="password"], 58 | textarea { 59 | @include script-text-input; 60 | border: 2px solid $input-border-color; 61 | font-size: 1em; 62 | } 63 | .script_type { 64 | margin-bottom: $unit; 65 | } 66 | } 67 | 68 | .invalid, 69 | .invalid .select-search, 70 | .invalid oi-select.focused .select-search { 71 | border-color: $danger-color; 72 | background-color: lighten($danger-color, 50%); 73 | @include attention; 74 | input { 75 | color: $danger-color; 76 | background-color: lighten($danger-color, 50%); 77 | } 78 | } 79 | 80 | .label-input { 81 | display: flex; 82 | label { 83 | display: inline-block; 84 | font-weight: 400; 85 | width: auto; 86 | line-height: 38px; 87 | margin-right: $unit*2; 88 | } 89 | input[type="text"], 90 | input[type="url"] { 91 | flex: 1; 92 | } 93 | padding: $unit; 94 | } 95 | 96 | .add-entry { 97 | @include list-text-button-combo; 98 | input[type="text"] { 99 | padding: 5px; 100 | font-weight: 100; 101 | border: 2px solid $mid-gray; 102 | border-right: 0; 103 | font-size: 1em; 104 | } 105 | button { 106 | border-bottom-left-radius: 0; 107 | border-top-left-radius: 0; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /sass/features/_inspector.scss: -------------------------------------------------------------------------------- 1 | $inspector-cell-width: 18em; 2 | $inspector-background-color: #fff; 3 | 4 | @mixin inspector-block { 5 | padding: $unit $spacing-unit/2 $unit $spacing-unit/2; 6 | 7 | &:empty { 8 | padding: 0; 9 | } 10 | } 11 | 12 | @mixin inspector-cell { 13 | // @include ng-fade; 14 | } 15 | 16 | .section-list li { 17 | margin-bottom: 2em; 18 | h6 { 19 | margin: 0.5em 0; 20 | } 21 | .detail-list { 22 | margin: 0.5em 0.2em 0 0; 23 | li { 24 | margin-bottom: 0.3em; 25 | } 26 | } 27 | } 28 | .inspector_container { 29 | overflow-y: auto; 30 | width: 50%; 31 | max-width: 500px; 32 | 33 | } 34 | 35 | .script-editor .inspector { 36 | @include sidebar-topics; 37 | 38 | background-color: $inspector-background-color; 39 | // flex: 0 0 $inspector-cell-width; 40 | 41 | transition: all ease-out 0.2s; 42 | //box-shadow: -2px 0px 2px 0px rgba(0,0,0,0.31); 43 | //z-index: 100; 44 | &.closed { 45 | flex: 0 0 0; 46 | .inspector-content { 47 | display: none; 48 | } 49 | } 50 | .toggle { 51 | position: absolute; 52 | margin-left: -40px; 53 | width: 40px; 54 | height: 45px; 55 | border-top-left-radius: 20px; 56 | border-bottom-left-radius: 20px; 57 | background-color: $app-background-blue; 58 | box-sizing: border-box; 59 | font-size: 24px; 60 | line-height: 40px; 61 | padding-left: 4px; 62 | z-index: 10; 63 | cursor: pointer; 64 | i.active { color: #5cb85c} 65 | i.inactive {color: #d9534f} 66 | pre { 67 | background-color: transparent; 68 | } 69 | .well { 70 | padding-top: 10px; 71 | padding-left: 5px; 72 | font-size: 24px; 73 | } 74 | } 75 | &.hidden { 76 | max-width: 0; 77 | * { 78 | opacity: 0; 79 | } 80 | } 81 | &.ng-hide { 82 | flex: 0 0 0; 83 | * { 84 | // @include fadeOut(.2s); 85 | } 86 | } 87 | 88 | @media all and (max-width: 800px) { 89 | display: none; 90 | } 91 | section { 92 | display: block; 93 | } 94 | .inspector-title { 95 | padding: $spacing-unit /2; 96 | background-color: $lavender-light; 97 | font-family: $headline-font; 98 | font-weight: bold; 99 | font-size: 1.4rem; 100 | vertical-align: bottom; 101 | } 102 | * { 103 | opacity: 1; 104 | } 105 | ul.branches, ul.triggers, ul.variables { 106 | @include sidebar-ul; 107 | } 108 | .duration { 109 | @include inspector-cell; 110 | input[type="text"] { 111 | width: 30px; 112 | margin-top: 2px; 113 | margin-bottom: 2px; 114 | } 115 | } 116 | .invalid-time { 117 | background-color: lighten($danger-color, 50%); 118 | color: $danger-color; 119 | } 120 | .tokens-cell { 121 | @include inspector-cell; 122 | padding: $unit * 2; 123 | } 124 | .branches, .triggers, .variables { 125 | @include inspector-cell; 126 | // @include ng-fade; 127 | } 128 | .script-info { 129 | input[type="text"], 130 | input[type="url"] { 131 | margin-bottom: $unit; 132 | width: 100%; 133 | } 134 | } 135 | h5 { 136 | background-color: $gray-2; 137 | &.expandable { 138 | &::after { 139 | float: right; 140 | margin-right: unit; 141 | color: $mid-gray; 142 | padding: 6px; 143 | font-size: 14px; 144 | content: "\25bc"; 145 | } 146 | &.closed::after { 147 | content: "\25b6"; 148 | } 149 | } 150 | } 151 | p { 152 | @include inspector-block; 153 | strong { 154 | // @include p1; 155 | font-weight: bold; 156 | } 157 | } 158 | input { 159 | margin-left: 0; 160 | } 161 | .sidebar-option-block { 162 | @include inspector-block; 163 | } 164 | .sidebar-suboptions { 165 | label { 166 | margin-left: 3px; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /sass/features/_modal.scss: -------------------------------------------------------------------------------- 1 | .modal-overlay { 2 | @include flexbox; 3 | display: none; 4 | opacity: 0; 5 | // @include fadeIn(.3s); 6 | z-index: 2000; 7 | position: fixed; 8 | background-color: $shade; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 100vh; 13 | align-items: center; 14 | justify-content: center; 15 | .modal { 16 | @include inset-shadow; 17 | // @include popIn(.2s); 18 | z-index: 200; 19 | background: $white; 20 | text-align: center; 21 | max-width: 800px; 22 | padding: $spacing-unit; 23 | button { 24 | @include text-button; 25 | &:first-of-type { 26 | margin-left: 2em; 27 | } 28 | } 29 | } 30 | 31 | &.visible { 32 | display: flex; 33 | opacity: 1; 34 | } 35 | } 36 | 37 | 38 | .export_modal { 39 | width: 600px; 40 | } 41 | -------------------------------------------------------------------------------- /sass/features/_sidebar.scss: -------------------------------------------------------------------------------- 1 | $editor-sidebar-width: 16em; 2 | $editor-secondary-sidebar-width: 16em; 3 | $sidebar-background-color: #fff; 4 | $secondary-sidebar-background-color: #f7f7f7; 5 | $selected-bot-background-color: #fff; 6 | 7 | 8 | 9 | @mixin sidebar-ul { 10 | margin: 0; 11 | box-sizing: border-box; 12 | line-height: 24px; 13 | p { 14 | font-size: 14px; 15 | } 16 | li { 17 | box-sizing: border-box; 18 | padding: 4px $spacing-unit/2; 19 | line-height: 30px; 20 | a { 21 | display: block; 22 | color: $main-blue; 23 | text-decoration: none; 24 | } 25 | &.active { 26 | font-weight: bold; 27 | background-color: $yellow-hint; 28 | } 29 | 30 | 31 | &:nth-child(even) { 32 | background-color: $light-gray; 33 | } 34 | button { 35 | float: right; 36 | } 37 | strong { 38 | font-weight: bold; 39 | color: $dark-gray; 40 | } 41 | } 42 | .delete { 43 | @include row-delete-button; 44 | position: relative; 45 | } 46 | .edit { 47 | @include circle-button($orange); 48 | display: inline-block; 49 | margin-left: $unit; 50 | margin-top: 4px; 51 | &::before { 52 | font-family: "Font Awesome 5 Free"; 53 | font-weight: 900; 54 | content: '\f040'; 55 | color: $white; 56 | } 57 | } 58 | } 59 | @mixin sidebar-topics { 60 | h5 { 61 | font-family: $headline-font; 62 | font-weight: 300; 63 | font-size: 1.4rem; 64 | padding: $unit $unit $unit $spacing-unit /2; 65 | cursor: pointer; 66 | transition: all 0.2s; 67 | a { 68 | display: block; 69 | text-decoration: none; 70 | color: $dark-gray; 71 | } 72 | &.expandable { 73 | &::after { 74 | float: right; 75 | margin-right: unit; 76 | padding: 6px; 77 | font-size: 14px; 78 | content: "\25bc"; 79 | color: $mid-gray; 80 | } 81 | &.closed::after { 82 | content: "\25b6"; 83 | } 84 | } 85 | // &.selected { 86 | // background-color: $main-blue; 87 | // color: $white; 88 | // &::after { 89 | // color: $white; 90 | // } 91 | // a { 92 | // color: $white; 93 | // } 94 | // } 95 | &:hover { 96 | background-color: $light-gray; 97 | } 98 | } 99 | } 100 | .sidebar-cell { 101 | background-color: $white; 102 | 103 | p { 104 | font-size: 14px; 105 | } 106 | textarea { 107 | @include textarea-grow; 108 | width: 100%; 109 | } 110 | @include clearfix; 111 | } 112 | .edit-script-info { 113 | @include clearfix; 114 | input[type="text"] { 115 | width: 100%; 116 | } 117 | p { 118 | margin-bottom: $unit; 119 | } 120 | } 121 | .sidebar { 122 | @include sidebar-topics; 123 | box-shadow: 1px 0px 1px 0px rgba(0,0,0,0.2); 124 | z-index: 100; 125 | background-color: $sidebar-background-color; 126 | flex: 0 0 $editor-sidebar-width; 127 | overflow-y: auto; 128 | overflow-x: hidden; 129 | ul.bot-options { 130 | margin-left: $spacing-unit/2; 131 | background-color: lighten($lavender-light, 10%); 132 | h5.selected { 133 | background-color: $lavender-light; 134 | } 135 | } 136 | .selected-bot { 137 | padding: $spacing-unit /2; 138 | background-color: $selected-bot-background-color; 139 | .breadcrumbsbar { 140 | font-size: 16px; 141 | margin-bottom: $unit; 142 | } 143 | img { 144 | max-height: 40px; 145 | margin-right: $unit * 2; 146 | } 147 | .bot-name { 148 | font-family: $headline-font; 149 | font-weight: bold; 150 | font-size: 1.4rem; 151 | line-height: 40px; 152 | vertical-align: bottom; 153 | text-decoration: none; 154 | text-overflow: ellipsis; 155 | word-break: break-all; 156 | } 157 | } 158 | .script-commands { 159 | .show-all { 160 | @include action-button($botkit-dark); 161 | } 162 | button { 163 | float: right; 164 | margin: $unit; 165 | } 166 | } 167 | .commands { 168 | @include sidebar-ul; 169 | @include clearfix; 170 | @include truncate-ul; 171 | button { 172 | float: right; 173 | } 174 | &.expanded { 175 | overflow: auto; 176 | position: relative; 177 | max-height: none; 178 | } 179 | } 180 | &.hidden { 181 | max-width: 0; 182 | * { 183 | opacity: 0; 184 | } 185 | } 186 | &.hidden-add.hidden-add-active { 187 | transition: max-width 0.5s; 188 | max-width: 0; 189 | * { 190 | transition: opacity 0.5s; 191 | } 192 | } 193 | h5 { 194 | background-color: $gray-2; 195 | &.active { 196 | font-weight: bold; 197 | background-color: $yellow-hint; 198 | } 199 | a { 200 | display: block; 201 | } 202 | } 203 | h6 { 204 | margin: 0.5em 1em; 205 | } 206 | &.closed { 207 | transition: flex 0.3s; 208 | flex: 0 0 0; 209 | * { 210 | // @include fadeOut(.3s); 211 | } 212 | } 213 | a:visited { 214 | color: $dark-gray; 215 | } 216 | @media all and (max-width: 800px) { 217 | display: none; 218 | } 219 | > li { 220 | padding: 0.25em 1em; 221 | color: $main-blue; 222 | cursor: pointer; 223 | &.active { 224 | background: #1432e0; 225 | color: #fff; 226 | } 227 | } 228 | } 229 | .secondary-sidebar { 230 | //display: none; //off for now 231 | // flex: 0 0 232 | width: $editor-secondary-sidebar-width; 233 | background-color: $secondary-sidebar-background-color; 234 | overflow-y: auto; 235 | padding-bottom: 0; 236 | transition: all ease-out 0.2s; 237 | * { 238 | z-index: 0; 239 | } 240 | ul { 241 | @include sidebar-ul; 242 | } 243 | ul.branches { 244 | padding-bottom: 0; 245 | li { 246 | color: $main-blue; 247 | cursor: pointer; 248 | position: relative; 249 | button { 250 | display: none; 251 | } 252 | &.active { 253 | button { 254 | display: block; 255 | } 256 | } 257 | } 258 | .add-entry { 259 | button { 260 | display: block; 261 | } 262 | } 263 | } 264 | &.hidden { 265 | max-width: 0; 266 | * { 267 | opacity: 0; 268 | } 269 | } 270 | h5 { 271 | background-color: $gray-2; 272 | font-size: 18px; 273 | padding: $unit; 274 | padding-left: 14px; 275 | font-family: $headline-font; 276 | } 277 | h6 { 278 | margin: 0.5em 1em; 279 | } 280 | &.ng-hide { 281 | flex: 0 0 0; 282 | * { 283 | // @include fadeOut(.2s); 284 | } 285 | } 286 | .script-info { 287 | p, span { 288 | font-size: 14px; 289 | } 290 | p { 291 | margin: $unit; 292 | padding: $unit; 293 | } 294 | 295 | p.editable { 296 | border: 2px solid transparent; 297 | &:hover { 298 | border: 2px solid $main-blue; 299 | } 300 | } 301 | padding: $unit; 302 | } 303 | .edit-script-info { 304 | 305 | > form { 306 | margin: $unit; 307 | padding: $unit; 308 | } 309 | } 310 | .add-entry { 311 | button { 312 | min-width: 47px; 313 | display: block; 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /sass/features/_text-tokens.scss: -------------------------------------------------------------------------------- 1 | $response-color: $seafoam-dark; 2 | $condition-color: $purple; 3 | $action-color: $orange-hint; 4 | $variable-color: $lavender-light; 5 | $list-color: lighten($green, 20%); 6 | 7 | //not yet implemented 8 | $date-color: $pink-hint; 9 | $user-color: $green; 10 | 11 | // token colors are above, the following classes are only used in the script editor's token section 12 | 13 | span.variable { 14 | background-color: lighten($variable-color, 10%); 15 | font-weight: 800; 16 | font-family: monospace; 17 | padding: 3px; 18 | border-radius: 3px; 19 | color: $dark-gray; 20 | } 21 | 22 | @mixin autobracket { 23 | &::before { 24 | color: $mid-gray; 25 | content: '{{'; 26 | } 27 | 28 | &::after { 29 | color: $mid-gray; 30 | content: '}}'; 31 | } 32 | } 33 | 34 | @mixin autoquote { 35 | &::before { 36 | color: $mid-gray; 37 | content: '"'; 38 | } 39 | 40 | &::after { 41 | color: $mid-gray; 42 | content: '"'; 43 | } 44 | } 45 | 46 | @mixin text-token { 47 | border-radius: 3px; 48 | background-color: $lavender-light; 49 | color: $dark-gray; 50 | font-family: monospace; 51 | font-weight: 800; 52 | line-height: normal; 53 | margin: 3px; 54 | padding: 3px; 55 | 56 | &.date { 57 | background-color: lighten($date-color, 10%); 58 | @include autobracket; 59 | } 60 | 61 | &.condition { 62 | background-color: lighten($condition-color, 10%); 63 | @include autoquote; 64 | } 65 | 66 | &.response { 67 | background-color: lighten($response-color, 10%); 68 | @include autobracket; 69 | } 70 | 71 | &.list { 72 | background-color: lighten($list-color, 10%); 73 | //@include autobracket; 74 | } 75 | } 76 | 77 | 78 | .sidebar ul.tokens { 79 | li { 80 | @include inline-list; 81 | @include text-token; 82 | @include autobracket; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /sass/foundation/_buttons.scss: -------------------------------------------------------------------------------- 1 | @mixin gray-underline { 2 | border-bottom: 2px solid $gray; 3 | } 4 | @mixin green-underline { 5 | border-bottom: 2px solid $green; 6 | } 7 | @mixin red-underline { 8 | border-bottom: 2px solid $red; 9 | } 10 | @mixin text-button { 11 | cursor: pointer; 12 | font-family: $body-font; 13 | border-bottom: 2px solid $main-blue; 14 | background-color: transparent; 15 | padding: 0.25em 0.5em; 16 | padding-top: .5em; 17 | border-radius: 0; 18 | font-size: 1em; 19 | line-height: 1.5em; 20 | margin: 0 0 0 0.5em; 21 | text-decoration: none; 22 | font-weight: 400; 23 | color: $black; 24 | outline: none; 25 | transition: background-color $transition-speed; 26 | &:hover { 27 | @include blue-background; 28 | color: $black; 29 | } 30 | &:active { 31 | @include green-underline; 32 | } 33 | &:focus { 34 | outline: none; 35 | } 36 | &.delete { 37 | @include text-button-red-hover; 38 | @include red-underline; 39 | } 40 | &.disabled { 41 | @include gray-underline; 42 | } 43 | } 44 | @mixin action-button ($action-button-color) { 45 | transition: background-color $transition-speed; 46 | font-size: 1em; 47 | line-height: 1.5em; 48 | text-decoration: none; 49 | box-sizing: border-box; 50 | //border-radius: 3px; 51 | color: $white; 52 | cursor: pointer; 53 | background-color: $action-button-color; 54 | outline: none; 55 | vertical-align: middle; 56 | border: 0; 57 | &:hover { 58 | background-color: lighten($action-button-color, 10%); 59 | color: $white; 60 | } 61 | &.delete, 62 | .danger { 63 | background-color: $red; 64 | &:hover { 65 | background-color: lighten($red, 10%); 66 | } 67 | } 68 | &.disabled { 69 | background-color: $gray; 70 | cursor: default; 71 | &:hover { 72 | background-color: $gray; 73 | } 74 | } 75 | &.secondary { 76 | background-color: $cantelope; 77 | cursor: default; 78 | &:hover { 79 | background-color: saturate($cantelope, 20%); 80 | } 81 | } 82 | } 83 | @mixin blue-background { 84 | background-color: $translucent-blue; 85 | } 86 | @mixin blue-hover { 87 | &:hover { 88 | @include blue-background; 89 | } 90 | } 91 | @mixin red-background { 92 | background-color: $translucent-red; 93 | } 94 | @mixin red-hover { 95 | &:hover { 96 | @include red-background; 97 | } 98 | } 99 | @mixin green-background { 100 | background-color: $green; 101 | } 102 | @mixin green-focus { 103 | &:focus { 104 | @include green-background; 105 | } 106 | } 107 | @mixin red-background { 108 | background-color: $translucent-red; 109 | } 110 | @mixin text-button-red-hover { 111 | &:hover { 112 | @include red-background; 113 | } 114 | } 115 | // link "button" utility classes 116 | .howdy-text-button, 117 | a.howdy-text-button { 118 | @include text-button; 119 | } 120 | // script ui buttons 121 | @mixin corner-button ($corner-button-color) { 122 | background-color: $corner-button-color; 123 | color: $white; 124 | font-size: 16px; 125 | line-height: 16px; 126 | font-weight: 800; 127 | width: 20px; 128 | height: 20px; 129 | padding-bottom: 2px; 130 | &::before { 131 | content: '\00D7'; 132 | } 133 | &:hover { 134 | background-color: lighten($corner-button-color, 10%); 135 | } 136 | &:active, 137 | &:focus { 138 | outline: none; 139 | } 140 | } 141 | @mixin square-button ($square-button-color) { 142 | background-color: $square-button-color; 143 | padding: 0; 144 | border: none; 145 | width: 21px; 146 | height: 21px; 147 | font-size: 16px; 148 | line-height: 16px; 149 | display: block; 150 | font-weight: 800; 151 | &:hover { 152 | background-color: saturate($square-button-color, 30%); 153 | } 154 | &:active, 155 | &:focus { 156 | outline: none; 157 | } 158 | } 159 | @mixin circle-button ($circle-button-color) { 160 | background-color: $circle-button-color; 161 | border-radius: 50%; 162 | padding: 1px; 163 | width: 21px; 164 | height: 21px; 165 | font-size: 14px; 166 | display: block; 167 | font-family: "Font Awesome 5 Free"; 168 | font-weight: 900; 169 | &:hover { 170 | background-color: saturate($circle-button-color, 30%); 171 | } 172 | &:active, 173 | &:focus { 174 | outline: none; 175 | } 176 | &::before { 177 | line-height: 21px; 178 | font-size: 14px; 179 | } 180 | } 181 | @mixin add-button($add-button-color) { 182 | @include circle-button($add-button-color); 183 | &::before { 184 | content: '\f067'; 185 | color: $white; 186 | } 187 | } 188 | @mixin check-button($check-button-color) { 189 | @include circle-button($check-button-color); 190 | &::before { 191 | content: '\2713 '; 192 | color: $white; 193 | } 194 | } 195 | @mixin add-botkit-button($add-botkit-color) { 196 | display: block; 197 | &::before { 198 | content: url('../../assets/icon/icon27.svg'); 199 | fill: $add-botkit-color; 200 | } 201 | &:hover { 202 | //fill: lighten($add-botkit-color, 20%); //only works with inline svg 203 | } 204 | &:active, 205 | &:focus { 206 | outline: none; 207 | } 208 | } 209 | 210 | @mixin save-button { 211 | @include action-button($green); 212 | transition: all 0.2s; 213 | color: $white; 214 | float: right; 215 | &:hover { 216 | 217 | } 218 | &.saving { 219 | text-align: left; 220 | min-width: 75px; 221 | background-color: lighten($green, 20%); 222 | &::after { 223 | overflow: hidden; 224 | display: inline-block; 225 | vertical-align: bottom; 226 | -webkit-animation: ellipsis steps(4,end) 900ms infinite; 227 | animation: ellipsis steps(4,end) 900ms infinite; 228 | content: "\2026"; 229 | /* ascii code for the ellipsis character */ 230 | width: 0; 231 | } 232 | } 233 | } 234 | 235 | @mixin slack-style { 236 | vertical-align: middle; 237 | background-color: transparent; 238 | border: 1px solid rgba(160,160,162,.5); 239 | color: #565759; 240 | max-width: 150px; 241 | font-size: .8125rem; 242 | font-weight: 700; 243 | height: 30px; 244 | padding: 0 10px; 245 | margin: 0 8px 0 0; 246 | text-shadow: none; 247 | border-radius: 3px; 248 | text-overflow: ellipsis; 249 | overflow: hidden; 250 | word-wrap: break-word; 251 | display: inline-block; 252 | float: left; 253 | } 254 | 255 | @mixin row-delete-button { 256 | @include circle-button($danger-color); 257 | display: inline-block; 258 | margin-left: $unit; 259 | margin-top: 4px; 260 | &::before { 261 | font-family: "Font Awesome 5 Free"; 262 | font-weight: 900; 263 | content: '\f00d'; 264 | color: $white; 265 | } 266 | } 267 | 268 | .action-button { 269 | @include action-button($purple); 270 | float: right; 271 | } 272 | .action-button-green { 273 | @include action-button($green); 274 | float: right; 275 | } 276 | 277 | button.save { 278 | @include save-button; 279 | } 280 | 281 | .tertiary { 282 | @include action-button($purple); 283 | color: $white; 284 | &:hover { 285 | background-color: lighten($purple, 10%); 286 | } 287 | } 288 | 289 | 290 | .primary_action_button { 291 | display: block; 292 | background-color: $main-blue; 293 | color: #FFFFFF; 294 | padding: 0.25em 0.5em; 295 | padding-top: .5em; 296 | margin: 20px; 297 | font-family: $headline-font; 298 | text-decoration: none; 299 | font-weight: bold; 300 | letter-spacing: 1px; 301 | border-radius: 3px; 302 | 303 | } 304 | -------------------------------------------------------------------------------- /sass/foundation/_flex-page.scss: -------------------------------------------------------------------------------- 1 | //flex wrapper wraps your whole page 2 | @mixin flexbox { 3 | display: -webkit-box; 4 | display: -webkit-flex; 5 | display: -moz-flex; 6 | display: -ms-flexbox; 7 | display: flex; 8 | } 9 | @mixin inline-flex { 10 | display: -webkit-inline-box; 11 | display: -webkit-inline-flex; 12 | display: -moz-inline-flex; 13 | display: -ms-inline-flexbox; 14 | display: inline-flex; 15 | } 16 | %flex-wrapper { 17 | @include flexbox; 18 | flex-direction: column; 19 | min-height: 100vh; 20 | } 21 | //flex content wraps everything but footer and header to make it grow to fill the page 22 | %flex-content { 23 | @include flexbox; 24 | flex: 1; 25 | overflow-y: auto; 26 | } 27 | 28 | .flex-column { 29 | flex-direction: column; 30 | width: 100%; 31 | } 32 | 33 | .flex-row { 34 | flex-direction: row; 35 | } 36 | 37 | .full-width { 38 | width: 100%; 39 | } 40 | 41 | .column { 42 | display: flex-column; 43 | } 44 | -------------------------------------------------------------------------------- /sass/foundation/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | // font-size: 100%; 23 | // font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | button { 51 | border: none; 52 | padding: 0; 53 | } 54 | -------------------------------------------------------------------------------- /sass/new.scss: -------------------------------------------------------------------------------- 1 | @import 2 | "vars", 3 | "foundation/reset", 4 | "foundation/buttons", 5 | "foundation/flex-page", 6 | "features/text-tokens", 7 | "features/input", 8 | "features/modal", 9 | "features/sidebar", 10 | "features/inspector", 11 | "skeleton", 12 | "ed" 13 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var fs = require('fs'); 3 | 4 | const INTENT_CONFIDENCE_THRESHOLD = process.env.INTENT_CONFIDENCE_THRESHOLD || 0.7; 5 | 6 | module.exports = function() { 7 | 8 | var api = {} 9 | var scripts = []; 10 | var triggers = []; 11 | var PATH_TO_SCRIPTS; 12 | 13 | api.parseAdminUsers = function(string) { 14 | if (!string) { 15 | string = ''; 16 | } 17 | 18 | var creds = string.split(/\s+/); 19 | 20 | var users = {}; 21 | creds.forEach(function(u) { 22 | var bits = u.split(/\:/); 23 | users[bits[0]] = bits[1]; 24 | }); 25 | 26 | return users; 27 | 28 | } 29 | 30 | api.loadScriptsFromFile = function(src, alt_path) { 31 | return new Promise(function(resolve, reject) { 32 | 33 | if (fs.existsSync(src)) { 34 | try { 35 | scripts = require(src); 36 | } catch(err) { 37 | return reject('Cannot load scripts from file: ' + err.message); 38 | } 39 | } else { 40 | console.warn('Loading sample scripts...'); 41 | try { 42 | scripts = require(alt_path); 43 | } catch(err) { 44 | return reject(err); 45 | } 46 | } 47 | 48 | PATH_TO_SCRIPTS = src; 49 | api.mapTriggers(); 50 | resolve(scripts); 51 | }); 52 | } 53 | 54 | api.writeScriptsToFile = function(new_scripts, alt_path) { 55 | 56 | return new Promise(function(resolve, reject) { 57 | try { 58 | require('fs').writeFileSync(alt_path || PATH_TO_SCRIPTS, JSON.stringify(new_scripts,null,2)); 59 | } catch(err) { 60 | return reject('Cannot write scripts to file: ' + err.message); 61 | } 62 | 63 | scripts = new_scripts; 64 | api.mapTriggers(); 65 | resolve(scripts); 66 | }); 67 | 68 | } 69 | 70 | api.mapTriggers = function() { 71 | for (var s = 0; s < scripts.length; s++) { 72 | 73 | // TODO: remove this when ID is part of datafile 74 | if (!scripts[s].id) { 75 | scripts[s].id = s; 76 | } 77 | 78 | for (var t = 0; t < scripts[s].triggers.length; t++) { 79 | triggers.push({trigger: scripts[s].triggers[t], script: s}); 80 | } 81 | } 82 | 83 | // sort in the order of _descending pattern length_ 84 | triggers.sort(function(a,b) { 85 | 86 | return b.trigger.pattern.length - a.trigger.pattern.length; 87 | 88 | }); 89 | } 90 | 91 | api.enrichMessage = function(message_text) { 92 | return new Promise(function(resolve, reject) { 93 | var query = { 94 | text: message_text 95 | }; 96 | 97 | // endpoint in the form of 98 | // https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/?subscription-key=&verbose=true&timezoneOffset=-360&q= 99 | if (process.env.LUIS_ENDPOINT) { 100 | 101 | var luis_uri = process.env.LUIS_ENDPOINT + query.text; 102 | request(luis_uri, function(error, response, body) { 103 | if (error) { 104 | console.error('Error communicating with LUIS', error); 105 | resolve(query); 106 | } else { 107 | var luis_results = {}; 108 | 109 | try { 110 | luis_results = JSON.parse(body); 111 | } catch(err) { 112 | console.error('Error parsing LUIS response', err); 113 | return resolve(query); 114 | } 115 | 116 | if (!luis_results.intents) { 117 | console.warn('No intents returned from LUIS.ai. Key may be invalid'); 118 | resolve(query); 119 | } else { 120 | if (String(luis_results.Message) === 'The request is invalid.') { 121 | console.warn('No intents returned from LUIS.ai. Key may be invalid'); 122 | resolve(query); 123 | } else { 124 | 125 | query.luis = luis_results; 126 | 127 | query.intents = []; 128 | query.entities = []; 129 | 130 | luis_results.intents.forEach(function(i) { 131 | query.intents.push(i); 132 | }); 133 | 134 | luis_results.entities.forEach(function(e) { 135 | query.entities.push(e); 136 | }); 137 | 138 | resolve(query); 139 | } 140 | } 141 | } 142 | }); 143 | } else { 144 | resolve(query); 145 | } 146 | }) 147 | } 148 | 149 | api.evaluateTriggers = function(message_text) { 150 | 151 | return new Promise(function(resolve, reject) { 152 | var res = []; 153 | 154 | api.enrichMessage(message_text).then(function(query) { 155 | 156 | // if any intents were detected, check if they match a trigger... 157 | if (query.intents && query.intents.length) { 158 | // check intents first 159 | for (var t = 0; t < triggers.length; t++) { 160 | var trigger = triggers[t].trigger; 161 | if (trigger.type == 'intent') { 162 | for (var i = 0; i < query.intents.length; i++) { 163 | var intent = query.intents[i]; 164 | if (Number(intent.score) >= INTENT_CONFIDENCE_THRESHOLD) { 165 | if (intent.intent === trigger.pattern) { 166 | res.push(triggers[t].script); 167 | } 168 | } 169 | } 170 | } 171 | } 172 | } 173 | 174 | // check regular expressions 175 | for (var t = 0; t < triggers.length; t++) { 176 | var trigger = triggers[t].trigger; 177 | if (trigger.type == 'regexp') { 178 | 179 | var found = false; 180 | try { 181 | var test = new RegExp(trigger.pattern,'i'); 182 | found = query.text.match(test); 183 | } catch(err) { 184 | console.error('ERROR IN TRIGGER REGEX', err); 185 | } 186 | 187 | if (found !== false && found !== null) { 188 | res.push(triggers[t].script); 189 | } 190 | } 191 | } 192 | 193 | // check keywords 194 | for (var t = 0; t < triggers.length; t++) { 195 | var trigger = triggers[t].trigger; 196 | 197 | if (trigger.type == 'string') { 198 | 199 | var found = false; 200 | try { 201 | var test = new RegExp('^' + trigger.pattern + '\\b','i'); 202 | found = query.text.match(test); 203 | } catch(err) { 204 | console.error('ERROR IN TRIGGER REGEX', err); 205 | } 206 | 207 | if (found !== false && found !== null) { 208 | res.push(triggers[t].script); 209 | } 210 | } 211 | } 212 | 213 | // check for no results... 214 | if (!res.length) { 215 | // find a script set with is_fallback true 216 | for (var s = 0; s < scripts.length; s++) { 217 | if (scripts[s].is_fallback === true) { 218 | res.push(s); 219 | } 220 | } 221 | } 222 | 223 | if (res.length) { 224 | 225 | // this is the script that will be triggered. 226 | var triggered = scripts[res[0]]; 227 | 228 | // copy entities from LUIS into the conversation script 229 | if (query.entities && query.entities.length) { 230 | query.entities.forEach(function(e) { 231 | var ne = { 232 | name: e.type, 233 | value: e.entity, 234 | type: 'entity' 235 | }; 236 | var cv = triggered.variables.filter(function(v) { 237 | return v.name === ne.name && v.value === ne.value && v.type === ne.type; 238 | }); 239 | if (cv.length === 0) { 240 | triggered.variables.push(ne); 241 | } 242 | }); 243 | } 244 | 245 | // if LUIS results exist, pass them down to the bot. 246 | if (query.luis) { 247 | triggered.luis = query.luis; 248 | } 249 | 250 | resolve(triggered); 251 | } else { 252 | reject(); 253 | } 254 | }).catch(reject); 255 | }); 256 | 257 | } 258 | 259 | api.getScript = function(name) { 260 | 261 | return new Promise(function(resolve, reject) { 262 | for (var s = 0; s < scripts.length; s++) { 263 | if (name.toLowerCase() == scripts[s].command.toLowerCase()) { 264 | return resolve(scripts[s]); 265 | } 266 | } 267 | reject(); 268 | }); 269 | } 270 | 271 | api.getScriptById = function(id) { 272 | 273 | return new Promise(function(resolve, reject) { 274 | for (var s = 0; s < scripts.length; s++) { 275 | if (id == scripts[s]._id || id == scripts[s].id) { 276 | return resolve(scripts[s]); 277 | } 278 | } 279 | reject(); 280 | }); 281 | } 282 | 283 | api.getScripts = function(tag) { 284 | 285 | return new Promise(function(resolve, reject) { 286 | 287 | var response = scripts; 288 | if (tag) { 289 | response = scripts.filter(function(s) { 290 | return s.tags ? (s.tags.indexOf(tag) >= 0) : false; 291 | }); 292 | } 293 | 294 | // for backwards compatibility with Botkit Studio, map the command field to name 295 | response = response.map(function(s) { 296 | s.name = s.command; 297 | return s; 298 | }); 299 | resolve(response); 300 | }); 301 | 302 | } 303 | 304 | 305 | api.useLocalStudio = function(botkit) { 306 | 307 | var mutagen = require(__dirname + '/botkit_mutagen.js'); 308 | return mutagen(api, botkit); 309 | } 310 | 311 | return api; 312 | 313 | } 314 | -------------------------------------------------------------------------------- /src/botkit_mutagen.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api, controller) { 2 | 3 | // evaluate incoming data 4 | controller.studio.evaluateTrigger = function(bot, text, user) { 5 | return api.evaluateTriggers(text); 6 | }; 7 | 8 | // get command list 9 | controller.studio.getScripts = function(bot, tag) { 10 | return api.getScripts(tag); 11 | }; 12 | 13 | // create a simple script 14 | // with a single trigger and single reply 15 | controller.studio.createScript = function(bot, trigger, text) { 16 | return new Promise(function(resolve, reject) { 17 | // TODO: Wtf does this do? 18 | // noop 19 | resolve(); 20 | }); 21 | }; 22 | 23 | // load a script from the pro service 24 | controller.studio.getScriptById = function(bot, id, user) { 25 | return api.getScript(id); 26 | }; 27 | 28 | // load a script from the pro service 29 | controller.studio.getScript = function(bot, text, user) { 30 | return api.getScript(text); 31 | }; 32 | 33 | // get Botkit Studio identity 34 | controller.studio.identify = function() { 35 | return new Promise(function(resolve, reject) { 36 | resolve({ 37 | name: process.env.BOT_NAME || 'Botkit Bot', 38 | platforms: [{type:(process.env.PLATFORM || 'web')}] 39 | }); 40 | }); 41 | } 42 | 43 | return controller; 44 | 45 | } 46 | -------------------------------------------------------------------------------- /views/config.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> nav}} 3 |
    4 | 5 |
    6 |

    Configure Existing Botkit application

    7 |

    To use Botkit CMS with your Botkit application, 8 | modify your bot application code (not the CMS code!) to include the `studio_command_uri` parameter as demonstrated below. 9 | Note that depending on the platform you are connecting to, your code may look slightly different! 10 |

    11 | 12 | 16 | 17 |

    18 | Then, set the values in your bot application's .env file (or other environment management tool) as shown below: 19 |

    20 | 21 | 23 | {{#if multiple_tokens}} 24 |

    Make sure your `studio_token` parameter matches one of these values:

    25 | 26 | 27 | {{/if}} 28 | 29 |
    30 |
    31 | Version {{version}}. Check for updates. 32 |
    33 |
    34 |
    35 | -------------------------------------------------------------------------------- /views/edit.hbs: -------------------------------------------------------------------------------- 1 | 6 | {{> modal}} 7 |
    8 | {{> nav}} 9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 | 16 |
    17 | 18 | 19 | 20 |
    21 | 22 | ❗ {% ui.validation_error %} 23 | 24 |
    25 |
    26 |
    27 | 28 |
    29 | {{> editor/modal_triggers}} 30 | {{> editor/modal_branch}} 31 | {{> editor/modal_duplicate}} 32 | {{> editor/modal_menu}} 33 | {{> editor/modal_info}} 34 | 35 |
    36 | 37 |
    38 | {{> editor/description}} 39 | {{> editor/branches}} 40 |
    41 | 42 |
    43 |
    44 |
    45 | 46 | 50 |
    51 |
    52 | 53 | {{> editor/message_list}} 54 | 55 |
    56 |
    57 | 58 | 59 |   60 |
    61 |
    62 | 63 |
    64 |
    65 |
    66 | 67 |
    68 |
    69 |
    70 |
    71 | -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 | {{> modal}} 2 | 3 |
    4 | {{> nav}} 5 |
    6 |
    7 |
    8 | 9 | 10 |
    11 |
    12 | 16 | 17 |
    18 | 19 |
    20 | 21 | 22 | 23 |
    24 | 25 |
    26 | 27 |
    28 |
    29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 49 | 50 | 51 | 53 | 63 | 64 | 65 | 66 | 72 | 73 |
    Script NameDescriptionModified ActionsExport
    40 | {% command.command %} 41 |
    (Fallback)
    42 |
    44 |
    {% command.description %}
    45 |
    46 | {% t %} 47 |
    48 |
    52 | 54 | 62 |
    67 | 71 |
    74 | 75 | 76 |
    77 | {{> importexport/modal_create}} 78 |
    79 | 80 | 81 |
    82 | {{> importexport/modal_export}} 83 |
    84 | 85 |
    86 | {{> importexport/modal_import}} 87 |
    88 |
    89 |
    90 |
    91 |
    92 | -------------------------------------------------------------------------------- /views/instructions.hbs: -------------------------------------------------------------------------------- 1 |

    Botkit CMS

    2 | 3 | Access admin tools 4 | -------------------------------------------------------------------------------- /views/layouts/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> head}} 3 | 4 | {{{body}}} 5 | {{> foot}} 6 | 7 | 8 | -------------------------------------------------------------------------------- /views/partials/editor/branches.hbs: -------------------------------------------------------------------------------- 1 | 41 | -------------------------------------------------------------------------------- /views/partials/editor/description.hbs: -------------------------------------------------------------------------------- 1 |
    2 | Description 3 | 4 |

    {% command.description %}

    5 |
    6 | -------------------------------------------------------------------------------- /views/partials/editor/modal_branch.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 |
    6 |

    Modify Thread

    7 |
    8 |
    9 |

    Add Thread

    10 |
    11 |
    12 |
    13 |

    14 | 15 | 16 |

    17 |
    18 |
    19 |
    20 | 21 | 22 | 23 |
    24 |
    25 |
    26 | -------------------------------------------------------------------------------- /views/partials/editor/modal_duplicate.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 |

    Duplicate Thread

    6 |
    7 |
    8 |
    9 |

    10 | 11 | 12 |

    13 |
    14 |
    15 |
    16 | 17 | 18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /views/partials/editor/modal_export.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Export Script

    4 |

    Download exported scripts, or copy the JSON below into your own file.

    5 | 6 |
    7 |
    8 | 9 |
    10 |
    11 | Download 12 | 13 | 14 |
    15 |
    16 | -------------------------------------------------------------------------------- /views/partials/editor/modal_info.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 |

    Script Info

    6 |
    7 |
    8 |
    9 |

    10 | 11 | 12 |

    13 |

    14 | 15 | 16 |

    17 | 18 | 19 |
    20 | 21 | 22 |
    23 |
    24 | 25 | 26 | {% t %} 27 | 28 | 29 |
    30 | 31 |
    32 | 33 |
    34 | 35 | 36 |
    37 |
    38 |
    39 | 40 | 41 |
    42 | 43 |
    44 |
    45 |
    46 | -------------------------------------------------------------------------------- /views/partials/editor/modal_menu.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    Edit Menu Options

    5 |
    6 |
    7 |
      8 |
    • 9 |
      10 |
      11 | Text 12 |
      13 |
      14 | Value 15 |
      16 |
      17 | Description 18 |
      19 |
      20 | 21 |
      22 |
    • 23 |
    • 24 |
      25 | 26 |
      27 |
      28 | 29 |
      30 |
      31 | 32 |
      33 |
      34 | 35 |
      36 |
      37 | 38 |
      39 |
    • 40 |
    • 41 | 42 |
    • 43 |
    44 |
    45 |
    46 | 47 |
    48 |
    49 |
    50 | -------------------------------------------------------------------------------- /views/partials/editor/modal_triggers.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Add Trigger

    4 | 5 |
    6 | 7 |

    8 | 9 | 14 |

    15 | 16 |

    17 | 18 | 20 |

    21 | 22 |

    23 | 24 | 25 |

    26 | 27 | 28 |
    29 |
    30 |
    31 | 32 | 33 | 34 | 37 | 40 | 41 | 42 | 45 | 48 | 51 | 52 |
    35 | Trigger Type 36 | 38 | Pattern 39 |
    43 | 44 | 46 | {% trigger.type %} 47 | 49 | {% trigger.pattern %} 50 |
    53 |
    54 |
    55 | 56 |
    57 |
    58 | -------------------------------------------------------------------------------- /views/partials/foot.hbs: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /views/partials/head.hbs: -------------------------------------------------------------------------------- 1 | 2 | Botkit CMS 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /views/partials/header.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 |
    7 |
    8 | {{>modal}} 9 | -------------------------------------------------------------------------------- /views/partials/importexport/modal_create.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    New Script

    5 |
    6 |
    7 |
    8 |

    9 | 10 | 11 |

    12 | 13 |

    14 | 15 | 16 |

    17 |
    18 |
    19 |
    20 | 21 | 22 | 23 |
    24 |
    25 |
    26 | -------------------------------------------------------------------------------- /views/partials/importexport/modal_export.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Script Export

    4 |

    Download exported scripts, or copy the JSON below into your own file.

    5 |
    6 |
    7 | 8 |
    9 |
    10 | Download 11 | 12 | 13 |
    14 |
    15 | -------------------------------------------------------------------------------- /views/partials/importexport/modal_import.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Import Scripts

    4 |
    5 |
    6 |
    7 |

    Import JSON

    8 | 9 | 10 |
    11 |
    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 |
    ImportScript NameDescription
    20 | {% c.command %}{% c.description %}
    24 |
    25 |
    26 |
    27 | 28 | 29 |
    30 |
    31 |
    32 | -------------------------------------------------------------------------------- /views/partials/modal.hbs: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /views/partials/nav.hbs: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------