diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..af0f0c3d --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} \ No newline at end of file diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index bdde09d0..00000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -dist/* -node_modules/* \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 30ac265b..3eb1dea9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,178 +1,7 @@ --- -ecmaFeatures: - jsx: false - modules: true - -env: - es6: true - -globals: - require: false - module: false - process: false +extends: + airbnb rules: - no-alert: 2 - no-array-constructor: 2 - no-bitwise: 2 - no-caller: 2 - no-catch-shadow: 2 - no-comma-dangle: 2 - no-cond-assign: 2 - no-console: 2 - no-constant-condition: 2 - no-continue: 0 - no-control-regex: 2 - no-debugger: 2 - no-delete-var: 2 - no-div-regex: 0 - no-dupe-keys: 2 - no-dupe-args: 2 - no-duplicate-case: 2 - no-else-return: 0 - no-empty: 2 - no-empty-class: 2 - no-empty-label: 2 - no-eq-null: 0 - no-eval: 2 - no-ex-assign: 2 - no-extend-native: 2 - no-extra-bind: 2 - no-extra-boolean-cast: 2 - no-extra-parens: 2 - no-extra-semi: 2 - no-extra-strict: 2 - no-fallthrough: 2 - no-floating-decimal: 0 - no-func-assign: 2 - no-implied-eval: 2 - no-inline-comments: 0 - no-inner-declarations: [2, "functions"] - no-invalid-regexp: 2 - no-irregular-whitespace: 2 - no-iterator: 2 - no-label-var: 2 - no-labels: 2 - no-lone-blocks: 2 - no-lonely-if: 2 - no-loop-func: 2 - no-mixed-requires: 2 - no-mixed-spaces-and-tabs: 2 - no-multi-spaces: 2 - no-multi-str: 2 - no-multiple-empty-lines: [2, { max: 2 }] - no-native-reassign: 2 - no-negated-in-lhs: 2 - no-nested-ternary: 2 - no-new: 2 - no-new-func: 2 - no-new-object: 2 - no-new-require: 2 - no-new-wrappers: 2 - no-obj-calls: 2 - no-octal: 2 - no-octal-escape: 2 - no-param-reassign: 0 - no-path-concat: 0 - no-plusplus: 0 - no-process-env: 0 - no-process-exit: 2 - no-proto: 2 - no-redeclare: 2 - no-regex-spaces: 2 - no-reserved-keys: 0 - no-restricted-modules: 0 - no-return-assign: 2 - no-script-url: 2 - no-self-compare: 2 - no-sequences: 2 - no-shadow: 2 - no-shadow-restricted-names: 2 - no-space-before-semi: 0 - no-spaced-func: 2 - no-sparse-arrays: 2 - no-sync: 0 - no-ternary: 0 - no-trailing-spaces: 2 - no-throw-literal: 0 - no-undef: 2 - no-undef-init: 2 - no-undefined: 0 - no-underscore-dangle: 0 - no-unreachable: 2 - no-unused-expressions: 2 - no-unused-vars: [2, { - vars: "all", - args: "after-used" }] - no-use-before-define: 2 - no-void: 0 - no-var: 0 - no-warning-comments: 0 - no-with: 2 - no-wrap-func: 2 - block-scoped-var: 0 - brace-style: [2, "1tbs"] - camelcase: 2 - comma-dangle: [2, "never"] - comma-spacing: 2 - comma-style: [2, "last"] - complexity: [2, 11] - consistent-return: 2 - consistent-this: [2, "self"] - curly: [2, "all"] - default-case: 0 - dot-notation: [2, { allowKeywords: true }] - eol-last: 2 - eqeqeq: 2 - func-names: 0 - func-style: [2, "expression"] - generator-star: 0 - generator-star-spacing: 0 - global-strict: [2, "never"] - guard-for-in: 0 - handle-callback-err: 0 - indent: [2, 2] - key-spacing: [2, { - beforeColon: false, - afterColon: true }] - max-depth: [2, 4] - max-len: [2, 100, 2] - max-nested-callbacks: [2, 3] - max-params: [1, 4] - max-statements: [2, 20] - new-cap: 2 - new-parens: 2 - newline-after-var: 0 - object-shorthand: 0 - one-var: [2, "never"] - operator-assignment: [2, "always"] - operator-linebreak: 0 - padded-blocks: 0 - quote-props: 0 - quotes: [2, "double"] - radix: 0 - semi: 2 - semi-spacing: [2, { before : false, after: true}] - sort-vars: 0 - space-after-keywords: [2, "always"] - space-before-blocks: [2, "always"] - space-before-function-paren: [2, { - anonymous: "always", - named: "never" }] - space-in-brackets: [0, "always"] - space-in-parens: [2, "never"] - space-infix-ops: 2 - space-return-throw-case: 2 - space-unary-ops: [2, { - words: true, - nonwords: false }] - spaced-line-comment: [2, "always"] - - strict: 0 - use-isnan: 2 - valid-jsdoc: 2 - valid-typeof: 2 - vars-on-top: 0 - wrap-iife: 0 - wrap-regex: 0 - yoda: [2, "never"] + no-plusplus: [2, { allowForLoopAfterthoughts: true }] + handle-callback-err: [2, "^(err|error)$" ] diff --git a/.gitignore b/.gitignore index 9c9f74ce..014973db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,9 @@ .DS_Store +lib/* node_modules/* logs/* npm-debug.log test/fixtures/cache/* -systemDB -factsystem -botfacts dump.rdb dump/* coverage diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..7e35cdfb --- /dev/null +++ b/.npmignore @@ -0,0 +1,24 @@ +src/* +test/* +example/* +.github/* +.babelrc +.eslintrc +.travis.yml +changes.md +contribute.md +LICENSE.md +readme.md +logs/* +test/fixtures/cache/* +dump.rdb +dump/* +coverage +*.sw* + +# Node profiler logs +isolate-*-v8.log + +# IDEA/WebStorm Project Files +.idea +*.iml diff --git a/.travis.yml b/.travis.yml index 741abd31..c760525c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,20 @@ language: node_js node_js: - "0.12" + - "4" + - "5" + - "6" +# env and addons required to make RE2 on Travis +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 services: - mongodb script: "npm run-script test-travis" # Send coverage data to Coveralls -after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" \ No newline at end of file +after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" diff --git a/bin/bot-init.js b/bin/bot-init.js deleted file mode 100755 index ffb7fee5..00000000 --- a/bin/bot-init.js +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -var program = require('commander'); -var fs = require("fs"); -var path = require("path"); - -program - .version('0.0.1') - .usage('botname [options]') - .option('-c, --client [telnet]', 'Bot client (telnet or slack)', 'telnet') - .parse(process.argv); - -// console.log(program); -if (program.output) console.log('Init new Bot'); - -if (!program.args[0]) { - program.help(); - process.exit(1) -} - -var botName = program.args[0]; -var botPath = path.join(process.cwd(), path.sep, botName); -var ssRoot = path.join(__dirname, "../"); -console.log("Creating %s bot with a %s client.", program.args[0], program.client); - -function write(path, str, mode) { - fs.writeFileSync(path, str, { mode: mode || 0666 }); - console.log(' \x1b[36mcreate\x1b[0m : ' + path); -} - -// Creating the path for your bot. -fs.mkdir(botPath, function(err, res){ - if (err && err.code === "EEXIST") { - console.log("\n\nThere is already a bot named %s at %s.\nPlease remove it or pick a new name for your bot before continuing.\n", botName, botPath); - process.exit(1) - } else if (err) { - console.log("We could not create the bot.", err); - process.exit(1) - } - - fs.mkdirSync(path.join(botPath, path.sep, "topics")); - fs.mkdirSync(path.join(botPath, path.sep, "plugins")); - fs.mkdirSync(path.join(botPath, path.sep, "logs")); - - // TODO: Pull out plugins that have dialogue and move them to the new bot. - - fs.createReadStream(ssRoot + path.sep + "clients" + path.sep + program.client + '.js').pipe(fs.createWriteStream(botPath + path.sep + 'server.js')); - - // package.json - var pkg = { - name: botName - , version: '0.0.0' - , private: true - , dependencies: { - 'superscript': 'latest' - , 'sfacts':'latest' - , 'mongoose':'3.8.24' - ,'debug': '~2.0.0' - } - } - - if (program.client == "slack") { - pkg.dependencies['slack-client'] = '~1.2.2'; - } - - if (program.client == "hangout") { - pkg.dependencies['simple-xmpp'] = '~1.3.0'; - } - - var firstRule = "+ ~emohello [*~2]\n- Hi!\n- Hi, how are you?\n- How are you?\n- Hello\n- Howdy\n- Ola" - - write(path.join(botPath, path.sep, 'package.json'), JSON.stringify(pkg, null, 2)); - write(path.join(botPath, path.sep, 'topics', path.sep, 'main.ss'), firstRule); -}); diff --git a/bin/cleanup.js b/bin/cleanup.js deleted file mode 100755 index 13ffdea6..00000000 --- a/bin/cleanup.js +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env node - -/////////////////////////// -// Superscript Refresher // -/////////////////////////// - -var Promise = require('bluebird'), - facts = require("sfacts"), - rmdir = Promise.promisify( require('rimraf') ), - program = require('commander'), - superscript = require('../index'), - fs = require("fs"), - mongoose = require('mongoose'), - util = require('util'); - -var collectionsToRemove = ['users', 'topics', 'replies', 'gambits']; - -program - .version('0.0.2') - .option('--facts [type]', 'Fact Directory', './systemDB') - .option('--host [type]', 'Mongo Host', 'localhost') - .option('--port [type]', 'Mongo Port', '27017') - .option('--mongo [type]', 'Mongo Database Name', 'systemDB') - .option('--mongoURI [type]', 'Mongo URI') - .option('--topic [type]', 'Topic Directory', './topics') - .option('--skip-remove-all', 'Skip removal of: ' + collectionsToRemove.join(', ')) - .option('--flush-topics', 'Flush imported topics, implies --skip-remove-all') - .option('--preserve-random', 'When used with --flush-topics, it will not empty the random topic') - .parse(process.argv); - -function removeAll (db) { - /** - * @param {Object} MongoDB instance - * @return {Promise} Resolved after listed collections are removed and the fact system directory has been recursively cleared - */ - - if (program.skipRemoveAll || program.flushTopics) return; - - return Promise.map(collectionsToRemove, function(collName) { - var coll = db.collection(collName); - return coll - .removeAsync({}) - .then(isClear.bind(collName)); - }) - .then(function() { - // Delete the old fact system directory - return rmdir(program.facts) - }); -} - - -function isClear () { - console.log(this + ' is clear'); - return true; -} - - -function createFresh () { - /** - * @return {Promise} Resolves after all data from the Topics folder has been loaded into Mongodb - */ - - // Generate Shared Fact System - // If not pre-generated, superscript will throw error on initialization - var factSystem = facts.create(program.facts), - parser = require('ss-parser')(factSystem), - loadDirectory = Promise.promisify( parser.loadDirectory, parser ); - - function importAll (data) { - /** - * @param {Object} Parsed data from a .ss file - * @return {Promise} Resolved when import is complete - */ - - // console.log('Importing', data); - return new Promise(function(resolve, reject) { - mongoose.connect(mongoURL) - - new superscript({factSystem: factSystem, mongoose: mongoose }, function(err, bot) { - if (!err) bot.topicSystem.importerData(data, resolve, program.flushTopics, program.preserveRandom); - else reject(err); - }); - }); - } - - return loadDirectory(program.topic) - .then(importAll); -} - - -// Setup Mongo Client: accepts MONGO_URI from environment, and URI or components or defaults as provided in options. -var MongoClient = Promise.promisifyAll( require('mongodb') ).MongoClient, - mongoURL = process.env.MONGO_URI || program.mongoURI || util.format('mongodb://%s:%s/%s', program.host, program.port, program.mongo); - - -// DO ALL THE THINGS -MongoClient.connectAsync(mongoURL) - .then(removeAll) - .then(createFresh) - .catch(function(e) { - console.log(e); - }) - .then(function(data) { - console.log('Everything imported'); - return 0; - }, function(e) { - console.log('Import error', e); - }) - .then(process.exit); diff --git a/bin/parse.js b/bin/parse.js deleted file mode 100755 index 32bb4b00..00000000 --- a/bin/parse.js +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -var program = require('commander'); - -var fs = require("fs"); - -program - .version('0.0.1') - .option('-p, --path [type]', 'Input Path', './topics') - .option('-o, --output [type]', 'Output options', 'data.json') - .option('-f, --force [type]', 'Force Save if output file already exists', false) - .parse(process.argv); - -if (program.output) console.log('parse topics'); - -var parse = require("ss-parser")(); -fs.exists(program.output, function (exists) { - if (!exists || program.force === true) { - parse.loadDirectory(program.path, function(err, result){ - fs.writeFile(program.output, JSON.stringify(result, null, 4), function (err) { - if (err) throw err; - console.log('Saved output to ' + program.output); - }); - }); - } else { - console.log("File", program.output, "already exists, remove file first or use -f to force save."); - } -}); \ No newline at end of file diff --git a/clients/hangout.js b/clients/hangout.js index 8c4dfa39..f4df9c8d 100644 --- a/clients/hangout.js +++ b/clients/hangout.js @@ -1,59 +1,50 @@ -var xmpp = require('simple-xmpp'); -var net = require("net"); -var superscript = require("superscript"); -var mongoose = require("mongoose"); -var facts = require("sfacts"); -var factSystem = facts.create('hangoutFacts'); - -mongoose.connect('mongodb://localhost/superscriptDB'); - -var options = {}; -var sockets = []; - -var TopicSystem = require("superscript/lib/topics/index")(mongoose, factSystem); - -options['factSystem'] = factSystem; -options['mongoose'] = mongoose; - - -//You need authorize this authentication method in Google account. -var botHandle = function(err, bot) { - xmpp.connect({ - jid : 'EMAIL ADRESS', - password : 'PASSWORD', - host : 'talk.google.com', - port : 5222, - reconnect : true - }); - - xmpp.on('online', function(data) { - console.log('Connected with JID: ' + data.jid.user); - console.log('Yes, I\'m connected!'); - }); - - xmpp.on('chat', function(from, message) { - receiveData(from, bot, message); - }); - - xmpp.on('error', function(err) { - console.error(err); - }); +import SuperScript from 'superscript'; +import xmpp from 'simple-xmpp'; + +const receiveData = function receiveData(from, bot, data) { + // Handle incoming messages. + let message = `${data}`; + + message = message.replace(/[\x0D\x0A]/g, ''); + + bot.reply(from, message.trim(), (err, reply) => { + xmpp.send(from, reply.string); + }); }; -var receiveData = function(from, bot, data) { - // Handle incoming messages. - var message = "" + data; +// You need authorize this authentication method in Google account. +const botHandle = function botHandle(err, bot) { + xmpp.connect({ + jid: 'EMAIL ADRESS', + password: 'PASSWORD', + host: 'talk.google.com', + port: 5222, + reconnect: true, + }); + + xmpp.on('online', (data) => { + console.log(`Connected with JID: ${data.jid.user}`); + console.log('Yes, I\'m connected!'); + }); - message = message.replace(/[\x0D\x0A]/g, ""); + xmpp.on('chat', (from, message) => { + receiveData(from, bot, message); + }); - bot.reply(from, message.trim(), function(err, reply){ - xmpp.send(from, reply.string); - }); + xmpp.on('error', (err) => { + console.error(err); + }); }; // Main entry point -TopicSystem.importerFile('./data.json', function(){ - new superscript(options, function(err, botInstance){ - botHandle(null, botInstance); - }); -}); \ No newline at end of file +const options = { + factSystem: { + name: 'hangoutFacts', + clean: true, + }, + importFile: './data.json', +}; + +SuperScript(options, (err, bot) => { + botHandle(null, bot); +}); diff --git a/clients/slack.js b/clients/slack.js index 9f6e50ea..7e470112 100644 --- a/clients/slack.js +++ b/clients/slack.js @@ -1,90 +1,52 @@ - -// Auth Token - You can generate your token from -// https://.slack.com/services/new/bot -var token = "..."; - -// This is the main Bot interface -var superscript = require("superscript"); -var mongoose = require("mongoose"); -mongoose.connect('mongodb://localhost/superscriptDB'); - +import SuperScript from 'superscript'; // slack-client provides auth and sugar around dealing with the RealTime API. -var Slack = require("slack-client"); +import Slack from 'slack-client'; -var debug = require('debug')("Slack Client"); -var facts = require("sfacts"); -var factSystem = facts.explore("botfacts"); -var TopicSystem = require("superscript/lib/topics/index")(mongoose, factSystem); +// Auth Token - You can generate your token from +// https://.slack.com/services/new/bot +const token = '...'; -// How should we reply to the user? +// How should we reply to the user? // direct - sents a DM // atReply - sents a channel message with @username // public sends a channel reply with no username -var replyType = "atReply"; +const replyType = 'atReply'; -var atReplyRE = /<@(.*?)>/; -var options = {}; -options['factSystem'] = factSystem; -options['mongoose'] = mongoose; +const atReplyRE = /<@(.*?)>/; -var slack = new Slack(token, true, true); - -var botHandle = function(err, bot) { - slack.login(); - - slack.on('error', function(error) { - console.error("Error:"); - console.log(error); - }); - - slack.on('open', function(){ - console.log("Welcome to Slack. You are %s of %s", slack.self.name, slack.team.name); - }); - - slack.on('close', function() { - console.warn("Disconnected"); - }); - - slack.on('message', function(data) { - receiveData(slack, bot, data); - }); -}; - -var receiveData = function(slack, bot, data) { +const slack = new Slack(token, true, true); +const receiveData = function receiveData(slack, bot, data) { // Fetch the user who sent the message; - var user = data._client.users[data.user]; - var channel; - var messageData = data.toJSON(); - var message = ""; + const user = data._client.users[data.user]; + let channel; + const messageData = data.toJSON(); + let message = ''; if (messageData && messageData.text) { - message = "" + messageData.text.trim(); + message = `${messageData.text.trim()}`; } - - - var match = message.match(atReplyRE); - + + const match = message.match(atReplyRE); + // Are they talking to us? if (match && match[1] === slack.self.id) { - message = message.replace(atReplyRE, '').trim(); - if (message[0] == ':') { - message = message.substring(1).trim(); + if (message[0] === ':') { + message = message.substring(1).trim(); } - bot.reply(user.name, message, function(err, reply){ + bot.reply(user.name, message, (err, reply) => { // We reply back direcly to the user - switch (replyType) { - case "direct": + case 'direct': channel = slack.getChannelGroupOrDMByName(user.name); break; - case "atReply": - reply.string = "@" + user.name + " " + reply.string; + case 'atReply': + reply.string = `@${user.name} ${reply.string}`; channel = slack.getChannelGroupOrDMByID(messageData.channel); break; - case "public": + case 'public': channel = slack.getChannelGroupOrDMByID(messageData.channel); break; } @@ -92,24 +54,48 @@ var receiveData = function(slack, bot, data) { if (reply.string) { channel.send(reply.string); } - }); - - } else if (messageData.channel[0] == "D") { - bot.reply(user.name, message, function(err, reply){ + } else if (messageData.channel[0] === 'D') { + bot.reply(user.name, message, (err, reply) => { channel = slack.getChannelGroupOrDMByName(user.name); if (reply.string) { channel.send(reply.string); } }); } else { - console.log("Ignoring...", messageData); + console.log('Ignoring...', messageData); } }; -// Main entry point -TopicSystem.importerFile('./data.json', function(){ - new superscript(options, function(err, botInstance){ - botHandle(null, botInstance); +const botHandle = function botHandle(err, bot) { + slack.login(); + + slack.on('error', (error) => { + console.error(`Error: ${error}`); }); + + slack.on('open', () => { + console.log('Welcome to Slack. You are %s of %s', slack.self.name, slack.team.name); + }); + + slack.on('close', () => { + console.warn('Disconnected'); + }); + + slack.on('message', (data) => { + receiveData(slack, bot, data); + }); +}; + +// Main entry point +const options = { + factSystem: { + name: 'slackFacts', + clean: true, + }, + importFile: './data.json', +}; + +SuperScript(options, (err, bot) => { + botHandle(null, bot); }); diff --git a/clients/telegram.js b/clients/telegram.js index 82bc0227..04368087 100644 --- a/clients/telegram.js +++ b/clients/telegram.js @@ -1,57 +1,54 @@ -var TelegramBot = require('node-telegram-bot-api'); -var superscript = require("superscript"); -var mongoose = require("mongoose"); -var facts = require("sfacts"); -var factSystem = facts.create('telegramFacts'); - -mongoose.connect('mongodb://localhost/superscriptDB'); - -var TopicSystem = require("superscript/lib/topics/index")(mongoose, factSystem); -// TopicSystem.importerFile('./data.json', function(){ }) - -var options = {}; -options['factSystem'] = factSystem; -options['mongoose'] = mongoose; - -new superscript(options, function(err, bot) { - // Auth Token - You can generate your token from @BotFather - // @BotFather is the one bot to rule them all. - var token = '...'; - - //=== Polling === - var telegram = new TelegramBot(token, { - polling: true - } - - - //=== Webhook === - //Choose a port - //var port = 8080; - - //var telegram = new TelegramBot(token, { - // webHook: { - // port: port, - // host: 'localhost' - // } - //}); - - //Use `ngrok http 8080` to tunnels localhost to a https endpoint. Get it at https://ngrok.com/ - //telegram.setWebHook('https://_____.ngrok.io/' + token); - - telegram.on('message', function(msg) { - var fromId = msg.from.id; - var text = msg.text.trim(); - - bot.reply(fromId, text, function(err, reply) { - if (reply.string) { - telegram.sendMessage(fromId, reply.string); - - // From file - //var photo = __dirname+'/../test/bot.gif'; - //telegram.sendPhoto(fromId, photo, {caption: "I'm a bot!"}); - - //For more examples, check out https://github.com/yagop/node-telegram-bot-api - } - }); +import TelegramBot from 'node-telegram-bot-api'; +import SuperScript from 'superscript'; + +const options = { + factSystem: { + name: 'telegramFacts', + clean: true, + }, + importFile: './data.json', +}; + +SuperScript(options, (err, bot) => { + if (err) { + console.error(err); + } + // Auth Token - You can generate your token from @BotFather + // @BotFather is the one bot to rule them all. + const token = '...'; + + //= == Polling === + const telegram = new TelegramBot(token, { + polling: true, + }); + + //= == Webhook === + // Choose a port + // var port = 8080; + + // var telegram = new TelegramBot(token, { + // webHook: { + // port: port, + // host: 'localhost' + // } + // }); + + // Use `ngrok http 8080` to tunnels localhost to a https endpoint. Get it at https://ngrok.com/ + // telegram.setWebHook('https://_____.ngrok.io/' + token); + + telegram.on('message', (msg) => { + const fromId = msg.from.id; + const text = msg.text.trim(); + + bot.reply(fromId, text, (err, reply) => { + if (reply.string) { + telegram.sendMessage(fromId, reply.string); + // From file + // var photo = __dirname+'/../test/bot.gif'; + // telegram.sendPhoto(fromId, photo, {caption: "I'm a bot!"}); + + // For more examples, check out https://github.com/yagop/node-telegram-bot-api + } }); + }); }); diff --git a/clients/telnet.js b/clients/telnet.js index 0be4569d..af7fb148 100644 --- a/clients/telnet.js +++ b/clients/telnet.js @@ -1,95 +1,86 @@ // Run this and then telnet to localhost:2000 and chat with the bot -var net = require("net"); -var superscript = require("superscript"); -var mongoose = require("mongoose"); -var facts = require("sfacts"); -var factSystem = facts.create('telnetFacts'); -mongoose.connect('mongodb://localhost/superscriptDB'); +import net from 'net'; +import SuperScript from 'superscript'; -var options = {}; -var sockets = []; +const sockets = []; -var TopicSystem = require("superscript/lib/topics/index")(mongoose, factSystem); - -options['factSystem'] = factSystem; -options['mongoose'] = mongoose; - -var botHandle = function(err, bot) { - - var receiveData = function(socket, bot, data) { +const botHandle = function botHandle(err, bot) { + const receiveData = function receiveData(socket, bot, data) { // Handle incoming messages. - var message = "" + data; + let message = `${data}`; - message = message.replace(/[\x0D\x0A]/g, ""); + message = message.replace(/[\x0D\x0A]/g, ''); - if (message.indexOf("/quit") === 0 || data.toString('hex',0,data.length) === "fff4fffd06") { - socket.end("Good-bye!\n"); + if (message.indexOf('/quit') === 0 || data.toString('hex', 0, data.length) === 'fff4fffd06') { + socket.end('Good-bye!\n'); return; } // Use the remoteIP as the name since the PORT changes on ever new connection. - bot.reply(socket.remoteAddress, message.trim(), function(err, reply){ - + bot.reply(socket.remoteAddress, message.trim(), (err, reply) => { // Find the right socket - var i = sockets.indexOf(socket); - var soc = sockets[i]; - - soc.write("\nBot> " + reply.string + "\n"); - soc.write("You> "); + const i = sockets.indexOf(socket); + const soc = sockets[i]; + soc.write(`\nBot> ${reply.string}\n`); + soc.write('You> '); }); }; - var closeSocket = function(socket, bot) { - var i = sockets.indexOf(socket); - var soc = sockets[i]; + const closeSocket = function closeSocket(socket, bot) { + const i = sockets.indexOf(socket); + const soc = sockets[i]; - console.log("User '" + soc.name + "' has disconnected.\n"); + console.log(`User '${soc.name}' has disconnected.\n`); - if (i != -1) { + if (i !== -1) { sockets.splice(i, 1); } }; - var newSocket = function (socket) { - socket.name = socket.remoteAddress + ":" + socket.remotePort; - console.log("User '" + socket.name + "' has connected.\n"); + const newSocket = function newSocket(socket) { + socket.name = `${socket.remoteAddress}:${socket.remotePort}`; + console.log(`User '${socket.name}' has connected.\n`); sockets.push(socket); - + // Send a welcome message. socket.write('Welcome to the Telnet server!\n'); - socket.write("Hello " + socket.name + "! " + "Type /quit to disconnect.\n\n"); - + socket.write(`Hello ${socket.name}! ` + `Type /quit to disconnect.\n\n`); // Send their prompt. - socket.write("You> "); + socket.write('You> '); - socket.on('data', function(data) { + socket.on('data', (data) => { receiveData(socket, bot, data); }); // Handle disconnects. - socket.on('end', function() { + socket.on('end', () => { closeSocket(socket, bot); }); - }; // Start the TCP server. - var server = net.createServer(newSocket); + const server = net.createServer(newSocket); server.listen(2000); - console.log("TCP server running on port 2000.\n"); + console.log('TCP server running on port 2000.\n'); }; // This assumes the topics have been compiled to data.json first -// See superscript/bin/parse for information on how to do that. +// See superscript/src/bin/parse for information on how to do that. // Main entry point -TopicSystem.importerFile('./data.json', function(){ - new superscript(options, function(err, botInstance){ - botHandle(null, botInstance); - }); +const options = { + factSystem: { + name: 'telnetFacts', + clean: true, + }, + importFile: './data.json', +}; + +SuperScript(options, (err, bot) => { + botHandle(null, bot); }); diff --git a/clients/twilio.js b/clients/twilio.js index 24f6803e..49009848 100644 --- a/clients/twilio.js +++ b/clients/twilio.js @@ -1,112 +1,101 @@ -var express = require('express'); -var app = express(); -var session = require('express-session'); -var MongoStore = require('connect-mongo')(session); -var bodyParser = require('body-parser'); -var twilio = require('twilio'); -var superscript = require("superscript"); -var mongoose = require("mongoose"); -var facts = require("sfacts"); -var factSystem = facts.create('twilioFacts'); - -// Database -mongoURI = process.env.MONGO_URI || 'mongodb://localhost/superscriptDB'; -mongoose.connect(mongoURI); -var db = mongoose.connection; - -db.on('error', console.error.bind(console, 'connection error:')); -db.once('open', function () { - console.log('Mongodb connection open'); -}); +import express from 'express'; +import session from 'express-session'; +import connectMongo from 'connect-mongo'; +import bodyParser from 'body-parser'; +import twilio from 'twilio'; +import SuperScript from 'superscript'; +const app = express(); +const MongoStore = connectMongo(session); // Twilio Configuration // Number format should be "+19876543210", with "+1" the country code -var config_twilio = { - account: "[YOUR_TWILIO_SID]", - token: "[YOUR_TWILIO_TOKEN]", - number: "[YOUR_TWILIO_NUMBER]" - } +const twilioConfig = { + account: '[YOUR_TWILIO_SID]', + token: '[YOUR_TWILIO_TOKEN]', + number: '[YOUR_TWILIO_NUMBER]', +}; -var accountSid = process.env.TWILIO_SID || config_twilio.account; -var authToken = process.env.TWILIO_AUTH || config_twilio.token; -var twilioNum = process.env.NUM || config_twilio.number; +const accountSid = process.env.TWILIO_SID || twilioConfig.account; +const authToken = process.env.TWILIO_AUTH || twilioConfig.token; +const twilioNum = process.env.NUM || twilioConfig.number; twilio.client = twilio(accountSid, authToken); twilio.handler = twilio; twilio.authToken = authToken; twilio.num = twilioNum; -// Middleware -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); -app.use(session({ - secret: 'cellar door', - resave: true, - saveUninitialized: false, - store: new MongoStore({mongooseConnection: db }) -})); - -// PORT -var port = process.env.PORT || 3000; - -// START SERVER -var server = app.listen(port, function() { - console.log('Listening on:' + port); -}); +// Send Twilio text message +const sendSMS = function sendSMS(recipient, sender, message) { + twilio.client.messages.create({ + to: recipient, + from: sender, + body: message, + }, (err, result) => { + if (!err) { + console.log('Reply sent! The SID for this message is: '); + console.log(result.sid); + console.log('Message sent on'); + console.log(result.dateCreated); + } else { + console.log('Error sending message'); + console.log(err); + } + }); +}; -var options = {}; +const dataHandle = function dataHandle(data, phoneNumber, twilioNumber, bot) { + // Format message + let message = `${data}`; -options['factSystem'] = factSystem; -options['mongoose'] = mongoose; + message = message.replace(/[\x0D\x0A]/g, ''); + bot.reply(message.trim(), (err, reply) => { + sendSMS(phoneNumber, twilioNumber, reply.string); + }); +}; // TWILIO // In your Twilio account, set up Twilio Messaging "Request URL" as HTTP POST // If running locally and using ngrok, should look something like: http://b2053b5e.ngrok.io/api/messages -var botHandle = function(err, bot) { - app.post('/api/messages', function(req, res) { +const botHandle = function (err, bot) { + app.post('/api/messages', (req, res) => { if (twilio.handler.validateExpressRequest(req, twilio.authToken)) { - console.log("Twilio Message Received: " + req.body.Body) + console.log(`Twilio Message Received: ${req.body.Body}`); dataHandle(req.body.Body, req.body.From, twilio.num, bot); } else { - res.set('Content-Type', 'text/xml').status(403).send("Error handling text messsage. Check your request params"); + res.set('Content-Type', 'text/xml').status(403).send('Error handling text messsage. Check your request params'); } - }); }; -var dataHandle = function(data, phoneNumber, twilioNumber, bot) { - // Format message - var message = "" + data; +// Main entry point +const options = { + factSystem: { + name: 'twilioFacts', + clean: true, + }, + importFile: './data.json', +}; - message = message.replace(/[\x0D\x0A]/g, ""); +SuperScript(options, (err, bot) => { + // Middleware + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({ extended: true })); + app.use(session({ + secret: 'cellar door', + resave: true, + saveUninitialized: false, + store: new MongoStore({ mongooseConnection: bot.db }), + })); - bot.reply(message.trim(), function(err, reply){ - sendSMS(phoneNumber, twilioNumber, reply.string); - }); -}; + // PORT + const port = process.env.PORT || 3000; -// Send Twilio text message -var sendSMS = function (recipient, sender, message) { - twilio.client.messages.create({ - to: recipient, - from: sender, - body: message, - }, function(err, result) { - if (!err) { - console.log('Reply sent! The SID for this message is: ') - console.log(result.sid); - console.log('Message sent on') - console.log(result.dateCreated) - } else { - console.log("Error sending message"); - console.log(err); - } + // START SERVER + app.listen(port, () => { + console.log(`Listening on port: ${port}`); }); -}; -// Main entry point -new superscript(options, function(err, botInstance){ - botHandle(null, botInstance); + botHandle(null, bot); }); diff --git a/index.js b/index.js deleted file mode 100644 index 2e2bc928..00000000 --- a/index.js +++ /dev/null @@ -1,318 +0,0 @@ -var util = require("util"); -var events = require("events"); -var EventEmitter = events.EventEmitter; -var async = require("async"); -var qtypes = require("qtypes"); -var _ = require("lodash"); -var norm = require("node-normalizer"); -var requireDir = require("require-dir"); -var debug = require("debug-levels")("SS:Script"); -var facts = require("sfacts"); -var gTopicsSystem = require("./lib/topics/index"); -var Message = require("./lib/message"); -var Users = require("./lib/users"); -var getreply = require("./lib/getreply"); -var Utils = require("./lib/utils"); -var processHelpers = require("./lib/reply/common"); -var mergex = require("deepmerge"); - -function SuperScript(options, callback) { - EventEmitter.call(this); - var mongoose; - var self = this; - options = options || {}; - - // Create a new connection if non is provided. - if (options.mongoose) { - mongoose = options.mongoose; - } else { - mongoose = require("mongoose"); - mongoose.connect("mongodb://localhost/superscriptDB"); - } - - this._plugins = []; - this.normalize = null; - this.question = null; - - Utils.mkdirSync("./plugins"); - this.loadPlugins("./plugins"); - this.loadPlugins(process.cwd() + "/plugins"); - // this.intervalId = setInterval(this.check.bind(this), 500); - - this.factSystem = options.factSystem ? options.factSystem : facts.create("systemDB"); - this.topicSystem = gTopicsSystem(mongoose, this.factSystem); - - // This is a kill switch for filterBySeen which is useless in the editor. - this.editMode = options.editMode || false; - - // We want a place to store bot related data - this.memory = options.botfacts ? options.botfacts : this.factSystem.createUserDB("botfacts"); - - this.scope = {}; - this.scope = _.extend(options.scope || {}); - this.scope.bot = this; - this.scope.facts = this.factSystem; - this.scope.topicSystem = this.topicSystem; - this.scope.botfacts = this.memory; - this.users = new Users(mongoose, this.factSystem); - - norm.loadData(function () { - self.normalize = norm; - new qtypes(function (question) { - self.question = question; - debug.verbose("System Loaded, waiting for replies"); - callback(null, self); - }); - }); -} - -var messageItorHandle = function (user, system) { - var messageItor = function (msg, next) { - - var options = { - user: user, - system: system, - message: msg - }; - - processHelpers.getTopic(options.system.topicsSystem, system.topicName, function (err, topicData) { - if (topicData) { - options.aTopics = []; - options.aTopics.push(topicData); - } - - getreply(options, function (err, replyObj) { - // Convert the reply into a message object too. - - var msgString = ""; - var messageOptions = { - qtypes: system.question, - norm: system.normalize, - facts: system.facts - }; - - if (replyObj) { - messageOptions.replyId = replyObj.replyId; - msgString = replyObj.string; - - if (replyObj.clearConvo) { - messageOptions.clearConvo = replyObj.clearConvo; - } - - } else { - replyObj = {}; - } - - new Message(msgString, messageOptions, function (replyMessageObject) { - user.updateHistory(msg, replyMessageObject, replyObj, function(err, log) { - - // We send back a smaller message object to the clients. - var clientObject = { - replyId: replyObj.replyId, - createdAt: replyMessageObject.createdAt || new Date(), - string: msgString || "", // replyMessageObject.raw || "", - topicName: replyObj.topicName, - subReplies: replyObj.subReplies, - debug: log - }; - - var newClientObject = mergex(clientObject, replyObj.props || {}); - - user.save(function (err, res) { - debug.verbose(err, res); - // TODO - Seeing RangeError here. (investigate Mongoose 4.0) - return next(null, newClientObject); - }); - }); - }); - }); - }); - }; - - return messageItor; -}; - -// This takes a message and breaks it into chucks to be passed though -// the sytem. We put them back together on the other end. -// FIXME: with chunking removed this is not needed. -var messageFactory = function (options, cb) { - - var rawMsg = options.msg; - var normalize = options.normalize; - var messageParts = []; - - var cleanMsg = normalize.clean(rawMsg).trim(); - debug.verbose("IN MessageFactory", cleanMsg); - - var hasExtraScope = options.extraScope ? true : false; - - var messageOptions = { - qtypes: options.question, - norm: normalize, - facts: options.factSystem, - original: rawMsg, - extraScope: hasExtraScope - }; - - return new Message(cleanMsg, messageOptions, function (tmsg) { - var mset = _.isEmpty(tmsg) ? [] : [tmsg] - return cb(null, mset); - }); -}; - -util.inherits(SuperScript, EventEmitter); - -SuperScript.prototype.message = function (msgString, callback) { - - var messageOptions = { - qtypes: this.question, - norm: this.normalize, - facts: this.factSystem - }; - - var message = new Message(msgString, messageOptions, function (msgObj) { - callback(null, msgObj); - }); -}; - -// This is like doing a topicRedirect -SuperScript.prototype.directReply = function (userId, topic, msg, callback) { - debug.log("[ New DirectReply - '%s']- %s", userId, msg) - var options = { - userId: userId, - topicName: topic, - msgString: msg, - extraScope: {} - }; - - this._reply(options, callback); - -}; - -// Convert msg into message object, then check for a match -SuperScript.prototype.reply = function (userId, msg, callback, extraScope) { - if (arguments.length === 2 && typeof msg === "function") { - callback = msg; - msg = userId; - userId = Math.random().toString(36).substr(2, 5); - extraScope = {}; - } - - debug.log("[ New Message - '%s']- %s", userId, msg) - var options = { - userId: userId, - msgString: msg, - extraScope: extraScope - }; - - this._reply(options, callback); -}; - -SuperScript.prototype._reply = function(options, callback) { - var self = this; - - // Ideally these will come from a cache, but self is a exercise for a rainy day - var system = { - - // getReply - topicsSystem: self.topicSystem, - plugins: self._plugins, - scope: self.scope, - messageScope: options.extraScope, - - // Pass in the topic if it - topicName: options.topicName || null, - - // Message - question: self.question, - normalize: self.normalize, - facts: self.factSystem, - editMode: self.editMode - }; - - var prop = { - currentTopic: "random", - status: 0, - conversation: 0, volley: 0, rally: 0 - }; - - this.users.findOrCreate({ id: options.userId }, prop, function (err1, user) { - if (err1) { - debug.error(err1); - } - - var opt = { - msg: options.msgString, - question: self.question, - normalize: self.normalize, - factSystem: self.factSystem, - extraScope: options.extraScope - }; - - messageFactory(opt, function (err, messages) { - // FIXME: `messages` will always be one now that we no longer chunk - async.mapSeries(messages, messageItorHandle(user, system), function (err2, messageArray) { - if (err2) { - debug.error(err2); - } - - var reply = {}; - var messageArray = Utils.cleanArray(messageArray); - - if (_.isEmpty(messageArray)) { - reply.string = ""; - } else if (messageArray.length === 1) { - reply = messageArray[0]; - } - - debug.verbose("Update and Reply to user '%s'", user.id, reply) - debug.info("[ Final Reply - '%s']- '%s'", user.id, reply.string) - - return callback(err2, reply); - }); - }); - }); -} - - -SuperScript.prototype.loadPlugins = function (path) { - var plugins = requireDir(path); - - for (var file in plugins) { - for (var func in plugins[file]) { - debug.verbose("Loading Plugin", path, func); - this._plugins[func] = plugins[file][func]; - } - } -}; - -SuperScript.prototype.getPlugins = function () { - return this._plugins; -}; - -SuperScript.prototype.getTopics = function () { - return this.topics; -}; - -SuperScript.prototype.getUsers = function (cb) { - this.users.find({}, "id", cb); -}; - -SuperScript.prototype.getUser = function (userId, cb) { - this.users.findOne({id: userId}, function (err, usr) { - cb(err, usr); - }); -}; - -SuperScript.prototype.findOrCreateUser = function (userId, callback) { - var properties = { id: userId }; - var prop = { - currentTopic: "random", - status: 0, - conversation: 0, volley: 0, rally: 0 - }; - - this.users.findOrCreate(properties, prop, callback); -}; - -module.exports = SuperScript; diff --git a/lib/dict.js b/lib/dict.js deleted file mode 100644 index b94c6be9..00000000 --- a/lib/dict.js +++ /dev/null @@ -1,99 +0,0 @@ -var _ = require("lodash"); -var debug = require("debug")("Dict"); - -var Dict = function (wordArray) { - this.words = []; - - for (var i = 0; i < wordArray.length; i++) { - this.words.push({word: wordArray[i], position: i }); - } -}; - -Dict.prototype.add = function (name, array) { - for (var i = 0; i < array.length; i++) { - this.words[i][name] = array[i]; - } -}; - -Dict.prototype.get = function (word) { - debug("Getting", word); - for (var i = 0; i < this.words.length; i++) { - if (this.words[i].word === word) { - return this.words[i]; - } - if (this.words[i].lemma === word) { - return this.words[i]; - } - } -}; - -Dict.prototype.contains = function (word) { - var rv = false; - for (var i = 0; i < this.words.length; i++) { - if (this.words[i].word === word || this.words[i].lemma === word) { - rv = true; - } - } - return rv; -}; - -Dict.prototype.containsHLC = function (concept) { - var rv = false; - for (var i = 0; i < this.words.length; i++) { - if (_.includes(this.words[i].hlc, concept)) { - rv = true; - } - } - return rv; -}; - -Dict.prototype.fetchHLC = function (thing) { - for (var i = 0; i < this.words.length; i++) { - if (_.includes(this.words[i].hlc, thing)) { - return this.words[i]; - } - } -}; - -Dict.prototype.fetch = function (list, thing) { - var rl = []; - for (var i = 0; i < this.words.length; i++) { - if (_.isArray(thing)) { - if (_.includes(thing, this.words[i][list])) { - rl.push(this.words[i].lemma); - } - } else if (_.isArray(this.words[i][list])) { - if (_.includes(this.words[i][list], thing)) { - rl.push(this.words[i].lemma); - } - } - } - return rl; -}; - -Dict.prototype.addHLC = function (array) { - debug("HLC", array); - var extra = []; - for (var i = 0; i < array.length; i++) { - var word = array[i].word; - var concepts = array[i].hlc; - var item = this.get(word); - if (item) { - item.hlc = concepts; - } else { - debug("HLC Extra (or Missing) word/phrase", word); - extra.push(word); - } - } - return extra; -}; - -Dict.prototype.findByLem = function (word) { - for (var i = 0; i < this.words.length; i++) { - if (this.words[i].lemma === word) { - return this.words[i]; - } - } -}; - -module.exports = Dict; diff --git a/lib/getreply.js b/lib/getreply.js deleted file mode 100644 index 3c31b2b9..00000000 --- a/lib/getreply.js +++ /dev/null @@ -1,468 +0,0 @@ -var async = require("async"); -var _ = require("lodash"); -var debug = require("debug-levels")("SS:GetReply"); -var Utils = require("./utils"); -var processTags = require("./processtags"); -const RE2 = require('re2') -const regexes = require('./regexes') - -var topicSystem; -var gPlugins; -var gScope; -var gDepth; -var gQtypes; -var gNormalize; -var gFacts; -var gEditMode; - -// Topic iterator, we call this on each topic or conversation reply looking for a match. -// All the matches are stored and returned in the callback. -var topicItorHandle = function (topSystem, message, user, localOptions) { - - return function (topicData, callback) { - if (topicData.type === "TOPIC") { - topSystem.topic.findOne({_id: topicData.id}) - .populate("gambits") - .populate("conditions") - .exec(function (err, topic) { - topic.checkConditions(message, user, gPlugins, gScope, localOptions, function(err, matches) { - debug.verbose("Checking for conditions in %s", topic.name) - - if (!_.isEmpty(matches)) { - callback(err, matches); - } else { - if (topic) { - user.clearConversationState(function() { - debug.verbose("Topic findMatch %s", topic.name) - // We do realtime post processing on the input aginst the user object - topic.findMatch(message, user, gPlugins, gScope, localOptions, callback); - }); - } else { - // We call back if there is no topic Object - // Non-existant topics return false - callback(null, false); - } - } - }); - } - ); - } else if (topicData.type === "REPLY") { - topSystem.reply.findOne({_id: topicData.id}) - .populate("gambits") - .exec(function (err, reply) { - if (err) { - console.log(err); - } - debug.verbose("Conversation Reply Thread", reply); - if (reply) { - reply.findMatch(message, user, gPlugins, gScope, localOptions, callback); - } else { - callback(null, false); - } - } - ); - } else { - debug.verbose("We shouldn't hit this"); - callback(null, false); - } - }; -}; - -var afterHandle = function (user, callback) { - // Note, the first arg is the ReplyBit (normally the error); - // We are breaking the matchItorHandle flow on data stream. - return function (replyBit, matchSet) { - - debug.verbose("MatchSet", replyBit, matchSet); - - // remove empties - matchSet = _.compact(matchSet); - - var minMatchSet = []; - var props = {}; - var clearConvo = false; - var lastTopicToMatch = null; - var lastStarSet = null; - var lastReplyId = null; - var replyString = ""; - var lastSubReplies = null; - var lastBreakBit = null; - - for (var i = 0; i < matchSet.length; i++) { - var item = matchSet[i]; - var mmm = { - topic: item.matched_topic_string || item.topic, - input: item.trigger, - reply: item.matched_reply_string, - }; - - if (!_.isEmpty(item.minMatchSet)) { - mmm.subset = item.minMatchSet; - } else { - mmm.output = item.reply.reply; - } - - minMatchSet.push(mmm); - - - if (item && item.reply) { - replyString += item.reply.reply + " "; - } - - props = _.assign(props, item.props); - lastTopicToMatch = item.topic; - lastStarSet = item.stars; - lastReplyId = item.reply._id; - lastSubReplies = item.subReplies; - lastBreakBit = item.breakBit; - - if (item.clearConvo) { - clearConvo = item.clearConvo; - } - } - - var threadsArr = []; - if (_.isEmpty(lastSubReplies)) { - threadsArr = processTags.threads(replyString); - } else { - threadsArr[0] = replyString; - threadsArr[1] = lastSubReplies; - } - - // only remove one trailing space (because spaces may have been added deliberately) - let replyStr = new RE2('(?:^[ \\t]+)|(?:[ \\t]$)').replace(threadsArr[0], '') - - var cbdata = { - replyId: lastReplyId, - props: props, - clearConvo: clearConvo, - topicName: lastTopicToMatch, - minMatchSet: minMatchSet, - string: replyStr, - subReplies: threadsArr[1], - stars: lastStarSet, - breakBit: lastBreakBit - }; - - debug.verbose("afterHandle", cbdata); - - callback(null, cbdata); - }; -}; - -// This may be called several times, once for each topic. -var filterRepliesBySeen = function (filteredResults, user, callback) { - debug.verbose("filterRepliesBySeen", filteredResults); - var bucket = []; - var eachResultItor = function (filteredResult, next) { - - var topicName = filteredResult.topic; - topicSystem.topic.findOne({name: topicName }).exec(function (err, currentTopic) { - if (err) { - console.log(err); - } - - // var repIndex = filteredResult.id; - var repIndex = filteredResult.reply._id; - var reply = filteredResult.reply; - var inputId = filteredResult.trigger_id2; - var seenReply = false; - - // Filter out SPOKEN replies. - // If something is said on a different trigger we don't remove it. - // If the trigger is very open ie "*", you should consider putting a {keep} flag on it. - - for (var i = 0; i <= 10; i++) { - var topicItem = user.__history__.topic[i]; - if (topicItem !== undefined) { - - var pastGambit = user.__history__.reply[i]; - var pastInput = user.__history__.input[i]; - - // Sometimes the history has null messages because we spoke first. - if (!_.isNull(pastGambit) && !_.isNull(pastInput)) { - // Do they match and not have a keep flag - - debug.verbose("--------------- FILTER SEEN ----------------"); - debug.verbose("repIndex", repIndex); - debug.verbose("pastGambit.replyId", pastGambit.replyId); - debug.verbose("pastInput id", String(pastInput.gambitId)); - debug.verbose("current inputId", String(inputId)); - debug.verbose("reply.keep", reply.keep); - debug.verbose("currentTopic.keep", currentTopic.keep); - - if (String(repIndex) === String(pastGambit.replyId) && - // TODO, For conversation threads this should be disbled becasue we are looking - // the wrong way. - // But for forward theads it should be enabled. - // String(pastInput.gambitId) === String(inputId) && - reply.keep === false && - currentTopic.keep === false - ) { - debug.verbose("Already Seen", reply); - seenReply = true; - } - } - } - } - - if (!seenReply || gEditMode) { - bucket.push(filteredResult); - } - next(); - }); - }; - - async.each(filteredResults, eachResultItor, function eachResultCompleteHandle() { - debug.verbose("Bucket", bucket); - if (!_.isEmpty(bucket)) { - callback(null, Utils.pickItem(bucket)); - } else { - callback(true); - } - }); -}; // end filterBySeen - -var filterRepliesByFunction = function (replies, user, opt, callback) { - - var filterHandle = function (reply, cb) { - - // We support a single filter function in the reply - // It returns true/false to aid in the selection. - - if (reply.reply.filter !== "") { - debug.verbose("Filter Function Found"); - - var filterFunction = regexes.filter.match(reply.reply.filter) - var pluginName = Utils.trim(filterFunction[1]); - var partsStr = Utils.trim(filterFunction[2]); - var args = Utils.replaceCapturedText(partsStr.split(","), [''].concat(reply.stars)); - - if (gPlugins[pluginName]) { - - var filterScope = gScope; - filterScope.message = opt.message; - filterScope.user = opt.user; - filterScope.message_props = opt.messageScope; - args.push(function customFilterFunctionHandle(err, filterReply) { - if (err) { - console.log(err); - } - - if (filterReply === "true" || filterReply === true) { - cb(true); - } else { - cb(false); - } - }); - - debug.verbose("Calling Plugin Function", pluginName); - gPlugins[pluginName].apply(filterScope, args); - - } else { - // If a function is missing, we kill the line and return empty handed - // Lets remove it and try to carry on. - console.log("\nWARNING:\nYou have a missing filter function (" + pluginName + ") - your script will not behave as expected!"); - // Wow, worst variable name ever - sorry. - reply = Utils.trim(reply.reply.reply.replace(filterFunction[0], "")); - cb(true); - } - } else { - cb(true); - } - }; - - async.filter(replies, filterHandle, function (filteredReplies) { - debug.verbose("filterByFunction Results", filteredReplies); - filterRepliesBySeen(filteredReplies, user, function afterFilterRepliesBySeen(err, reply) { - if (err) { - debug.error(err); - // Keep looking for results - callback(); - } else { - - var options = { - plugins: gPlugins, - scope: gScope, - topicSystem: topicSystem, - depth: gDepth, - - // Some options are global, these are local! - localOptions: opt, - - // For creating new Messages - qtypes: gQtypes, - normalize: gNormalize, - facts: gFacts - }; - - var replyString = reply.reply.reply; - var topicString = reply.topic; - - processTags.replies(reply, user, options, function (err2, replyObj) { - if (!_.isEmpty(replyObj)) { - - replyObj.matched_reply_string = replyString; - replyObj.matched_topic_string = topicString; - - debug.verbose("ProcessTags Return", replyObj); - - if (replyObj.breakBit === false) { - debug.info("Forcing CHECK MORE Mbit"); - callback(null, replyObj); - } else if (replyObj.breakBit === true || replyObj.reply.reply !== "" ) { - callback(true, replyObj); - } else { - debug.info("Forcing CHECK MORE Empty Reply"); - callback(null, replyObj); - } - } else { - debug.verbose("ProcessTags Empty"); - if (err2) { - debug.verbose("There was an err in processTags", err2); - } - callback(null, null); - } - }); - } - }); - }); -}; - -// match is an array -var matchItorHandle = function (user, message, localOptions) { - - return function (match, callback) { - debug.verbose("match itor", match.trigger); - var replies = []; - var rootTopic; - var stars = match.stars; - - if (!_.isEmpty(message.stars)) { - stars = message.stars; - } - - // In some edge cases, replies were not being populated... - // lets do it here - topicSystem.gambit.findById(match.trigger._id) - .populate("replies") - .exec(function (err, triggerExpanded) { - if (err) { - console.log(err); - } - - match.trigger = triggerExpanded; - - match.trigger.getRootTopic(function (err, topic) { - if (err) { - console.log(err); - } - - if (match.topic) { - rootTopic = match.topic; - } else { - rootTopic = topic; - } - - for (var i = 0; i < match.trigger.replies.length; i++) { - - var rep = match.trigger.replies[i]; - var mdata = { - id: rep.id, - stars: stars, - topic: rootTopic, - reply: rep, - - // For the logs - trigger: match.trigger.input, - - trigger_id: match.trigger.id, - trigger_id2: match.trigger._id - }; - replies.push(mdata); - } - - // Find a reply for the match. - filterRepliesByFunction(replies, user, localOptions, callback); - - }); - } - ); - }; -}; - - -var getreply = function (options, callback) { - var user = options.user; - var message = options.message; - - message.messageScope = options.system.messageScope || {}; - - gPlugins = options.system.plugins; - gScope = options.system.scope; - gDepth = options.depth || 0; - - gQtypes = options.system.question; - gNormalize = options.system.normalize; - gFacts = options.system.facts; - gEditMode = options.system.editMode || false; - - var localOptions = { - message: message, - user: user, - messageScope: options.system.messageScope || {} - }; - - // New TopicSystem - topicSystem = options.system.topicsSystem; - - // This method can be called recursively. - if (options.depth) { - debug.verbose("Called Recursively", gDepth); - if (gDepth >= 50) { - return callback(null, null); - } - } - // Find Topics for User or override with passed in option - topicSystem.topic.findPendingTopicsForUser(user, message, function (err, aTopics) { - if (err) { - console.log(err); - } - - // Flash topics with pre-set list - if (!_.isEmpty(options.aTopics)) { - debug.verbose("Flashed topics (directReply, respond or topicRedirect)"); - } - - aTopics = !_.isEmpty(options.aTopics) ? options.aTopics : aTopics; - debug.info("Topics to check", aTopics.map(function(x) { return x.name })) - - // We use map here because it will bail on error. - // The error is our escape hatch when we have a reply WITH data. - async.mapSeries( - aTopics, - topicItorHandle(topicSystem, message, user, localOptions), - function topicCompleteHandle(err2, results) { - if (err2) { - console.log(err2); - } - - // Remove the empty topics, and flatten the array down. - var matches = _.flatten(_.filter(results, function (e) { - return e; - })); - - // TODO - This sort should happen in the process sort logic. - // Lets sort the matches by qType.length - matches = matches.sort(function(a, b) { - return a.trigger.qType.length < b.trigger.qType.length; - }); - - // Was `eachSeries` - async.mapSeries(matches, matchItorHandle(user, message, localOptions), afterHandle(user, callback)); - } - ); - }); -}; - -module.exports = getreply; diff --git a/lib/math.js b/lib/math.js deleted file mode 100644 index c1e12225..00000000 --- a/lib/math.js +++ /dev/null @@ -1,298 +0,0 @@ -/*eslint no-eval:0 */ -// TODO - Make this into its own project - -var _ = require("lodash"); -var debug = require("debug")("math"); - -var cardinalNumberPlural = { - "first": 1, - "second": 2, - "third": 3, - "fourth": 4, - "fifth": 5, - "sixth": 6, - "seventh": 7, - "eigth": 8, - "ninth": 9, - "tenth": 10, - "eleventh": 11, - "twelfth ": 12, - "thirteenth": 13, - "fourteenth": 14, - "fifteenth": 15, - "sixteenth": 16, - "seventeenth": 17, - "eighteenth": 18, - "nineteenth": 19, - "twentieth": 20, - "twenty-first": 21, - "twenty-second": 22, - "twenty-third": 23, - "twenty-fourth": 24, - "twenty-fifth": 25, - "twenty-sixth": 26 -}; - -var cardinalNumbers = { - "one": 1, - "two": 2, - "three": 3, - "four": 4, - "five": 5, - "six": 6, - "seven": 7, - "eight": 8, - "nine": 9, - "ten": 10, - "eleven": 11, - "twelve": 12, - "thirteen": 13, - "fourteen": 14, - "fifteen": 15, - "sixteen": 16, - "seventeen": 17, - "eighteen": 18, - "nineteen": 19, - "twenty": 20, - "thirty": 30, - "forty": 40, - "fifty": 50, - "sixty": 60, - "seventy": 70, - "eighty": 80, - "ninety": 90 -}; - -var multiplesOfTen = { - "twenty": 20, - "thirty": 30, - "forty": 40, - "fifty": 50, - "sixty": 60, - "seventy": 70, - "eighty": 80, - "ninety": 90 -}; - -var mathExpressionSubs = { - "plus": "+", - "minus": "-", - "multiply": "*", - "multiplied": "*", - "x": "*", - "times": "*", - "divide": "/", - "divided": "/" -}; - -var mathTerms = ["add", "plus", "and", "+", "-", "minus", "subtract", "x", - "times", "multiply", "multiplied", "of", "divide", "divided", "/", "half", - "percent", "%"]; -exports.mathTerms = mathTerms; - -// Given an array for words it returns the evauated sum. -// TODO - fractions -// TODO, words should be the dict object with lem words to fix muliply / multipled etc -exports.parse = function (words, prev) { - debug("In parse with", words); - prev = prev || 0; - var expression = []; - var newexpression = []; - var i; - var word; - - for (i = 0; i < words.length; i++) { - var digit = convertWordToNumber(words[i]); - if (digit !== undefined) { - words[i] = digit; - } - - word = words[i]; - if (mathExpressionSubs[word] !== undefined) { - words[i] = mathExpressionSubs[word]; - } - } - - for (i = 0; i < words.length; i++) { - word = words[i]; - if (/[\d\*\+\-\/=%]|of|half|percent/.test(word)) { - if (word === "half") { - newexpression.push(0.5); - } else if (word === "of") { - expression.push("*"); - } else if ((word === "%" || word === "percent") && isNumeric(words[i - 1])) { - expression.pop(); - expression.push(parseInt(words[i - 1]) / 100); - } else { - expression.push(word); - } - } - } - - for (i = 0; i < expression.length; i++) { - var curr = expression[i]; - var next = expression[i + 1]; - newexpression.push(curr); - if (/\d/.test(curr) && /\d/.test(next)) { - newexpression.push("+"); - } - } - - try { - // reintruduce - if (newexpression.length === 2 || newexpression[0] === "+") { - newexpression.unshift(prev); - } - debug("Eval", newexpression.join(" ")); - var value = eval(newexpression.join(" ")); - return +value.toFixed(2); - } catch (e) { - debug("Error", e); - return null; - } -}; - -// Given an array of words, lets convert them to numbers -// We want to subsitute one - one thousand to numberic form -// TODO handle "two-hundred" hypenated hundred/thousand -exports.convertWordsToNumbers = function (wordArray) { - var mult = {hundred: 100, thousand: 1000}; - var results = []; - var i; - - for (i = 0; i < wordArray.length; i++) { - // Some words need lookahead / lookbehind like hundred, thousand - if (["hundred", "thousand"].indexOf(wordArray[i]) >= 0) { - results.push(String(parseInt(results.pop()) * mult[wordArray[i]])); - } else { - results.push(convertWordToNumber(wordArray[i])); - } - } - - // Second Pass add 'and's together - for (i = 0; i < results.length; i++) { - if (isNumeric(results[i]) && results[i + 1] === "and" && isNumeric(results[i + 2])) { - var val = parseInt(results[i]) + parseInt(results[i + 2]); - results.splice(i, 3, String(val)); - i--; - } - } - return results; -}; - -var convertWordToNumber = function (word) { - var number; - var multipleOfTen; - var cardinalNumber; - - if (word !== undefined) { - if (word.indexOf("-") === -1) { - if (_.includes(Object.keys(cardinalNumbers), word)) { - number = String(cardinalNumbers[word]); - } else { - number = word; - } - } else { - multipleOfTen = word.split("-")[0]; // e.g. "seventy" - cardinalNumber = word.split("-")[1]; // e.g. "six" - if (multipleOfTen !== "" && cardinalNumber !== "") { - var n = multiplesOfTen[multipleOfTen] + cardinalNumbers[cardinalNumber]; - if (isNaN(n)) { - number = word; - } else { - number = String(n); - } - } else { - number = word; - } - } - return number; - } else { - return word; - } -}; - -exports.convertWordToNumber = convertWordToNumber; - -var numberLookup = function (number) { - var multipleOfTen; - var word = ""; - - if (number < 20) { - for (var cardinalNumber in cardinalNumbers) { - if (number === cardinalNumbers[cardinalNumber]) { - word = cardinalNumber; - break; - } - } - } else if (number < 100) { - if (number % 10 === 0) { // If the number is a multiple of ten - for (multipleOfTen in multiplesOfTen) { - if (number === multiplesOfTen[multipleOfTen]) { - word = multipleOfTen; - break; - } - } - } else { // not a multiple of ten - for (multipleOfTen in multiplesOfTen) { - for (var i = 9; i > 0; i--) { - if (number === multiplesOfTen[multipleOfTen] + i) { - word = multipleOfTen + "-" + convertNumberToWord(i); - break; - } - } - } - } - } else { - // TODO - - console.log("We don't handle numbers greater than 99 yet."); - } - - return word; -}; - -var convertNumberToWord = function (number) { - if (number === 0) { - return "zero"; - } - - if (number < 0) { - return "negative " + numberLookup(Math.abs(number)); - } else { - return numberLookup(number); - } -}; - -exports.cardPlural = function (wordNumber) { - return cardinalNumberPlural[wordNumber]; -}; - -exports.arithGeo = function (arr) { - var ap; - var gp; - - for (var i = 0; i < arr.length - 2; i++) { - if (!(ap = arr[i + 1] - arr[i] === arr[i + 2] - arr[i + 1])) { - break; - } - } - - if (ap) { - return "Arithmetic"; - } - - for (i = 0; i < arr.length - 2; i++) { - if (!(gp = arr[i + 1] / arr[i] === arr[i + 2] / arr[i + 1])) { - break; - } - } - - if (gp) { - return "Geometric"; - } - return -1; -}; - -var isNumeric = function (num) { - return !isNaN(num); -}; diff --git a/lib/message.js b/lib/message.js deleted file mode 100644 index 5016f689..00000000 --- a/lib/message.js +++ /dev/null @@ -1,440 +0,0 @@ -var pos = require("parts-of-speech"); -var _ = require("lodash"); -var natural = require("natural"); -var math = require("./math"); -var ngrams = natural.NGrams; -var moment = require("moment"); -var Lemmer = require("lemmer"); -var Dict = require("./dict"); -var Utils = require("./utils"); -var async = require("async"); -var string = require("string"); -var debug = require("debug-levels")("SS:Message"); - -var patchList = function (fullEntities, things) { - var stopList = ["I"]; - - things = things.filter(function (item) { - return !(stopList.indexOf(item) !== -1); - }); - - for (var i = 0; i < fullEntities.length; i++) { - for (var j = 0; j < things.length; j++) { - var thing = things[j]; - if (fullEntities[i].indexOf(thing) > 0) { - things[j] = fullEntities[i]; - } - } - } - return things; -}; - -var cleanIncomming = function (message) { - message = message.replace(/\./g, " "); - message = message.replace(/\s,\s/g, " "); - // these used to be bursted but are not anymore. - message = message.replace(/([a-zA-Z]),\s/g, "$1 "); - message = message.replace(/"(.*)"/g, "$1"); - message = message.replace(/\s"\s?/g, " "); - message = message.replace(/\s'\s?/g, " "); - message = message.replace(/\s?!\s?/g, " "); - message = message.replace(/\s?!\s?/g, " "); - return message; -}; - - -// The message could be generated by a repy or raw input -// If it is a reply, we want to save the ID so we can filter them out if said again -var Message = function (msg, options, cb) { - debug.verbose("Creating message from:", msg); - var self = this; - var extraScope = options.extraScope; - - // if there is an empty reply, let it through if the bot was also provided with extraScope - if (!msg && !extraScope) { - debug.verbose("Callback Early, empty msg, and no extraScope"); - return cb({}); - } - - self.id = Utils.genId(); - - // If this message is based on a Reply. - if (options.replyId) { - self.replyId = options.replyId; - } - - if (options.clearConvo) { - self.clearConvo = options.clearConvo; - } - - self.facts = options.facts; - self.createdAt = new Date(); - self.raw = cleanIncomming(msg); - - // This version of the message is `EXACTLY AS WRITTEN` by the user - self.original = options.original; - self.props = {}; - - - // self.clean = options.norm.clean(self.raw).trim(); - self.clean = self.raw.trim(); - var wordArray = new pos.Lexer().lex(self.clean); - - // This is where we keep the words - self.dict = new Dict(wordArray); - - // TODO Phase out words, cwords - self.words = wordArray; - self.cwords = math.convertWordsToNumbers(wordArray); - self.taggedWords = new pos.Tagger().tag(self.cwords); - - var posArray = self.taggedWords.map(function (hash) { - return hash[1]; - }); - - self.lemma(function (err, lemWords) { - if (err) { - console.log(err); - } - - self.lemWords = lemWords; - var lemString = self.lemWords.join(" "); - - self.lemString = lemString; - self.posString = posArray.join(" "); - - self.dict.add("num", self.cwords); - self.dict.add("lemma", self.lemWords); - self.dict.add("pos", posArray); - - // Classify Question - self.qtype = options.qtypes.classify(lemString); - self.qSubType = options.qtypes.questionType(self.raw); - self.isQuestion = options.qtypes.isQuestion(self.raw); - - self._extendBase(); - self._checkMath(); - - // Things are nouns + complex nouns so - // turkey and french fries would return ['turkey','french fries'] - // this should probably run the list though concepts or something else to validate them more - // than NN NN etc. - self.fetchNE(function (entities) { - - var things = self.fetchComplexNouns("nouns"); - var fullEntities = entities.map(function (item) { - return item.join(" "); - }); - - self.entities = patchList(fullEntities, things); - self.list = patchList(fullEntities, self.list); - - debug.verbose("Message", self); - cb(self); - }); - }); -}; - -Message.prototype._extendBase = function () { - // Sentence Sentiment - this.sentiment = 0; - - // Get Nouns and Noun Phrases. - this.nouns = this.fetchNouns(); - this.names = this.fetchComplexNouns("names"); - - // A list of terms - // this would return an array of thems this are a, b and c; - // Helpful for choosing something when the qSubType is CH - this.list = this.fetchList(); - this.adjectives = this.fetchAdjectives(); - this.adverbs = this.fetchAdverbs(); - this.verbs = this.fetchVerbs(); - this.pronouns = this.pnouns = this.fetchProNouns(); - this.compareWords = this.fetchCompareWords(); - this.numbers = this.fetchNumbers(); - this.compare = this.compareWords.length !== 0; - this.date = this.fetchDate(); - - this.names = _.uniq(this.names, function (name) { - return name.toLowerCase(); - }); - - // Nouns with Names removed. - var t = this.names.map(function (name) { - return name.toLowerCase(); - }); - - this.cNouns = _.filter(this.nouns, function (item) { - return !_.includes(t, item.toLowerCase()); - }); -}; - -Message.prototype._checkMath = function () { - var numCount = 0; - var oppCount = 0; - var i; - - for (i = 0; i < this.taggedWords.length; i++) { - if (this.taggedWords[i][1] === "CD") { - numCount++; - } - if (this.taggedWords[i][1] === "SYM" || - math.mathTerms.indexOf(this.taggedWords[i][0]) !== -1) { - // Half is a number and not an opp - if (this.taggedWords[i][0] === "half") { - numCount++; - } else { - oppCount++; - } - } - } - - // Augment the Qtype for Math Expressions - this.numericExp = numCount >= 2 && oppCount >= 1 ? true : false; - this.halfNumericExp = numCount === 1 && oppCount === 1 ? true : false; - - if (this.numericExp || this.halfNumericExp) { - this.qtype = "NUM:expression"; - this.isQuestion = true; - } - -}; - -Message.prototype.fetchCompareWords = function () { - return this.dict.fetch("pos", ["JJR", "RBR"]); -}; - -Message.prototype.fetchAdjectives = function () { - return this.dict.fetch("pos", ["JJ", "JJR", "JJS"]); -}; - -Message.prototype.fetchAdverbs = function () { - return this.dict.fetch("pos", ["RB", "RBR", "RBS"]); -}; - -Message.prototype.fetchNumbers = function () { - return this.dict.fetch("pos", ["CD"]); -}; - -Message.prototype.fetchVerbs = function () { - return this.dict.fetch("pos", ["VB", "VBN", "VBD", "VBZ", "VBP", "VBG"]); -}; - -Message.prototype.fetchProNouns = function () { - return this.dict.fetch("pos", ["PRP", "PRP$"]); -}; - - -// TODO - Move this to utils -var pennToWordnet = function (pennTag) { - if (string(pennTag).startsWith("J")) { - return "a"; - } else if (string(pennTag).startsWith("V")) { - return "v"; - } else if (string(pennTag).startsWith("N")) { - return "n"; - } else if (string(pennTag).startsWith("R")) { - return "r"; - } else { - return null; - } -}; - -// We only want to lemmatize the nouns, verbs, adverbs and adjectives. -Message.prototype.lemma = function (callback) { - - var itor = function (hash, next) { - var word = hash[0].toLowerCase(); - var tag = pennToWordnet(hash[1]); - - // console.log(word, tag); - // next(null, [word]); - - if (tag) { - try { - Lemmer.lemmatize(word + "#" + tag, next); - } catch (e) { - console.log("Caught in Excption", e); - // This is probably because it isn't an english word. - next(null, [word]); - } - } else { - // Some words don't have a tag ie: like, to. - next(null, [word]); - } - }; - - async.map(this.taggedWords, itor, function (err, transformed) { - var res = _.map(_.flatten(transformed), function (a) { - return a.split("#")[0]; - }); - callback(err, res); - }); -}; - -Message.prototype.fetchNouns = function () { - return this.dict.fetch("pos", ["NN", "NNS", "NNP", "NNPS"]); -}; - -// Fetch list looks for a list of items -// a or b -// a, b or c -Message.prototype.fetchList = function () { - debug.verbose("Fetch List"); - var self = this; - var l = []; - if (/NNP? CC(?:\s*DT\s|\s)NNP?/.test(self.posString) || - /NNP? , NNP?/.test(self.posString) || - /NNP? CC(?:\s*DT\s|\s)JJ NNP?/.test(self.posString)) { - var sn = false; - for (var i = 0; i < self.taggedWords.length; i++) { - if (self.taggedWords[i + 1] && - (self.taggedWords[i + 1][1] === "," || - self.taggedWords[i + 1][1] === "CC" || - self.taggedWords[i + 1][1] === "JJ")) { - sn = true; - } - if (self.taggedWords[i + 1] === undefined) { - sn = true; - } - if (sn === true && Utils._isTag(self.taggedWords[i][1], "nouns")) { - l.push(self.taggedWords[i][0]); - sn = false; - } - } - } - return l; -}; - -Message.prototype.fetchDate = function () { - var self = this; - var date = null; - var months = ["january", "february", "march", - "april", "may", "june", "july", "august", "september", - "october", "november", "december"]; - - // http://rubular.com/r/SAw0nUqHJh - var re = /([a-z]{3,10}\s+[\d]{1,2}\s?,?\s+[\d]{2,4}|[\d]{2}\/[\d]{2}\/[\d]{2,4})/i; - - if (self.clean.match(re)) { - var m = self.clean.match(re); - debug.verbose("Date", m); - date = moment(Date.parse(m[0])); - } - - if (self.qtype === "NUM:date" && date === null) { - debug.verbose("Try to resolve Date"); - // TODO, in x months, x months ago, x months from now - if (_.includes(self.nouns, "month")) { - if (self.dict.includes("next")) { - date = moment().add("M", 1); - } - if (self.dict.includes("last")) { - date = moment().subtract("M", 1); - } - } else if (Utils.inArray(self.nouns, months)) { - // IN month vs ON month - var p = Utils.inArray(self.nouns, months); - date = moment(self.nouns[p] + " 1", "MMM D"); - } - } - - return date; -}; - -// Fetch Named Entities. -// Pulls concepts from the bigram DB. -Message.prototype.fetchNE = function (callback) { - var self = this; - var bigrams = ngrams.bigrams(this.taggedWords); - - var sentencebigrams = _.map(bigrams, function (bigram) { - return _.map(bigram, function (item) { - return item[0]; - }); - }); - - var itor = function (item, cb) { - var bigramLookup = {subject: item.join(" "), predicate: "isa", object: "bigram" }; - self.facts.db.get(bigramLookup, function (err, res) { - if (err) { - debug.error(err); - } - - if (!_.isEmpty(res)) { - cb(true); - } else { - cb(); - } - }); - }; - - async.filter(sentencebigrams, itor, function (res) { - callback(res); - }); -}; - -// This function will return proper nouns and group them together if they need be. -// This function will also return regular nonus or common nouns grouped as well. -// Rob Ellis and Brock returns ['Rob Ellis', 'Brock'] -// @tags - Array, Words with POS [[word, pos], [word, pos]] -// @lookupType String, "nouns" or "names" -Message.prototype.fetchComplexNouns = function (lookupType) { - var tags = this.taggedWords; - var bigrams = ngrams.bigrams(tags); - var nouns; - var tester; - - if (lookupType === "names") { - tester = function (item) { - return item[1] === "NNP" || item[1] === "NNPS"; - }; - } else { - tester = function (item) { - return item[1] === "NN" || item[1] === "NNS" || item[1] === "NNP" || item[1] === "NNPS"; - }; - } - nouns = _.filter(_.map(tags, function (item) { - return tester(item) ? item[0] : null; - }), Boolean); - - var nounBigrams = ngrams.bigrams(nouns); - - // Get a list of term - var neTest = _.map(bigrams, function (bigram) { - return _.map(bigram, function (item) { - return tester(item); - }); - }); - - // Return full names from the list - var fullnames = _.map(_.filter(_.map(neTest, function (item, key) { - return _.every(item, _.identity) ? bigrams[key] : null; }), Boolean), - function (item) { - return (_.map(item, function (item2) { - return item2[0]; - })).join(" "); - } - ); - - debug.verbose("fullnames", lookupType, fullnames); - - var x = _.map(nounBigrams, function (item) { - return _.includes(fullnames, item.join(" ")); - }); - - // Filter X out of the bigrams or names? - _.filter(nounBigrams, function (item, key) { - if (x[key]) { - // Remove these from the names - nouns.splice(nouns.indexOf(item[0]), 1); - nouns.splice(nouns.indexOf(item[1]), 1); - return nouns; - } - }); - - return nouns.concat(fullnames); -}; - -module.exports = Message; diff --git a/lib/processtags.js b/lib/processtags.js deleted file mode 100644 index 1587ef81..00000000 --- a/lib/processtags.js +++ /dev/null @@ -1,349 +0,0 @@ -/** -* -* Parse the reply for additional tags, this is called once we have a reply candidate filtered out. -* -* @param {Object} replyObj - The Reply Object -* @param {string} replyObj.id - This is the 8 digit id mapping back to the ss parsed json -* @param {array} replyObj.stars - All of the matched values -* @param {string} replyObj.topic - The Topic name we matched on -* @param {Object} replyObj.reply - This is the Mongo Reply Gambit -* @param {string} replyObj.trigger_id - The trigger id (8 digit) -* @param {string} replyObj.trigger_id2 - The trigger id (mongo id) -* -* @param {Object} user - The User Object -* -* @param {Object} options - Extra cached items that are loaded async during load-time -* @param {array} options.plugins - An array of plugins loaded from the plugin folder -* @param {Object} options.scope - All of the data available to `this` inside of the plugin during execution -* @param {Object} options.topicSystem - Reference to the topicSystem (Mongo) -* @param {number} options.depth - (Global counter) How many times this function is called recursivly. - -* @param {Object} options.qtypes - For Message Object: Cached qtypes -* @param {Object} options.normalize - For Message Object: Chached Normaizer -* @param {Object} options.facts - For Message Object: Cached Fact system - -* Replies can have the following: -* Basic (captured text) subsitution ie: `I like ` -* Input (parts of speech) subsitution ie: `I like ` -* Expanding terms using wordnet ie: `I like ~sport` -* Alternate terms to choose at random ie: `I like (baseball|hockey)` -* Custom functions that can be called ie: `I like ^chooseSport()` -* Redirects to another reply ie: `I like {@sport}` -*/ - -var Utils = require("./utils"); -var processRedirects = require("./reply/inlineRedirect"); -var topicRedirect = require("./reply/topicRedirect"); -var respond = require("./reply/respond"); -var customFunction = require("./reply/customFunction"); -var processHelpers = require("./reply/common"); -var async = require("async"); -var _ = require("lodash"); -var merge = require("deepmerge"); -var replace = require("async-replace"); -var debug = require("debug-levels")("SS:ProcessTags"); -const RE2 = require('re2') -const regexes = require('./regexes') - -// xxx: RegExp instead of RE2 because it is passed to async-replace -var WORDNET_REGEX = /(~)(\w[\w]+)/g; - - -var replies = function (replyObj, user, options, callback) { - debug.verbose("Depth", options.depth); - var reply = replyObj.reply.reply; - var globalScope = options.scope; - - var msg = options.localOptions.message; - - // This is the options for the (get)reply function, used for recursive traversal. - var replyOptions = { - user: user, - topic: replyObj.topic, - depth: options.depth + 1, - localOptions: options.localOptions, - system: { - plugins: options.plugins, - scope: globalScope, - topicsSystem: options.topicSystem, - - // Message - question: options.qtypes, - normalize: options.normalize, - facts: options.facts - } - }; - - debug.info("Reply '%s'", reply) - - // Lets set the currentTopic to whatever we matched on, providing it isn't already set - // The reply text might override that later. - if (_.isEmpty(user.pendingTopic)) { - user.setTopic(replyObj.topic); - } - - // The topicSetter returns an array with the reply and topic - var parsedTopicArr = processHelpers.topicSetter(reply); - var newtopic = (parsedTopicArr[1] !== "") ? parsedTopicArr[1] : null; - - if (newtopic && !_.isEmpty(newtopic)) { - debug.verbose("New topic found", newtopic); - user.setTopic(newtopic); - } - - reply = parsedTopicArr[0]; - - var stars = [""]; - stars.push.apply(stars, replyObj.stars); - - // expand captures - reply = new RE2('', 'ig').replace(reply, (c, p1) => { - const index = p1 ? Number.parseInt(p1) : 1 - return index < stars.length ? stars[index] : c - }) - - // So this is to address GH-207, pulling the stars out of the history and - // feeding them forward into new replies. It allows us to save a tiny bit of - // context though a conversation cycle. - let matches = regexes.pcaptures.match(reply) - if (matches) { - // xxx: handle captures within captures, but only 1 level deep - for (var i = 0; i < matches.length; i++) { - var m = regexes.pcapture.match(matches[i]) - var historyPtr = +m[1] - 1; - var starPtr = +m[2] - 1; - if (user.__history__.stars[historyPtr] && user.__history__.stars[historyPtr][starPtr]) { - var term = user.__history__.stars[historyPtr][starPtr]; - reply = reply.replace(matches[i], term); - } - } - } - - - // clean up the reply by unescaping newlines and hashes - reply = new RE2('\\\\(n|#)', 'ig').replace(Utils.trim(reply), (c, p1) => p1 === '#' ? '#' : '\n') - - var clearConvoBit = false; - // Threre SHOULD only be 0 or 1. - var clearMatch = regexes.clear.match(reply) - if (clearMatch) { - debug.verbose("Adding Clear Conversation Bit"); - reply = reply.replace(clearMatch[0], ""); - reply = reply.trim(); - clearConvoBit = true; - } - - var mbit = null; - var mbitMatch = regexes.continue.match(reply) - if (mbitMatch) { - debug.verbose("Adding CONTINUE Conversation Bit"); - reply = reply.replace(mbitMatch[0], ""); - reply = reply.trim(); - mbit = false; - } - - var mbit2Match = regexes.end.match(reply) - if (mbit2Match) { - debug.verbose("Adding END Conversation Bit"); - reply = reply.replace(mbit2Match[0], ""); - reply = reply.trim(); - mbit = true; - } - - - // and - // Special case, we have no items in the history yet, - // This could only happen if we are trying to match the first input. - // Kinda edgy. - - if (!_.isNull(msg)) { - reply = new RE2('', 'ig').replace(reply, msg.clean) - } - - reply = new RE2('<(input|reply)([1-9]?)>', 'ig').replace(reply, (c, p1, p2) => { - const data = p1 === 'input' ? user.__history__.input : user.__history__.reply - return data[p2 ? Number.parseInt(p2) - 1 : 0] - }) - - replace(reply, WORDNET_REGEX, processHelpers.wordnetReplace, function(err, wordnetReply) { - var orig_reply = reply; - reply = wordnetReply; - - // Inline redirector. - var redirectMatch = regexes.redirect.match(reply) - var topicRedirectMatch = regexes.topic.match(reply) - var respondMatch = regexes.respond.match(reply) - var customFunctionMatch = regexes.customFn.match(reply) - - var match = false; - if (redirectMatch || topicRedirectMatch || respondMatch || customFunctionMatch) { - var obj = []; - obj.push({name: 'redirectMatch', index: (redirectMatch) ? redirectMatch.index : -1}); - obj.push({name: 'topicRedirectMatch', index: (topicRedirectMatch) ? topicRedirectMatch.index : -1}); - obj.push({name: 'respondMatch', index: (respondMatch) ? respondMatch.index : -1}); - obj.push({name: 'customFunctionMatch', index: (customFunctionMatch) ? customFunctionMatch.index : -1}); - - match = _.result(_.find(_.sortBy(obj, 'index'), function(chr) { - return chr.index >= 0; - }), 'name'); - } - - var augmentCallbackHandle = function(err, replyString, messageProps, getReplyObject, mbit1) { - if (err) { - // If we get an error, we back out completly and reject the reply. - debug.verbose("We got an error back from one of the Handlers", err); - return callback(err, {}); - } else { - - var newReplyObject; - if (_.isEmpty(getReplyObject)) { - newReplyObject = replyObj; - newReplyObject.reply.reply = replyString; - - // This is a new bit to stop us from matching more. - if (mbit !== null) { - newReplyObject.breakBit = mbit; - } - // If the function has the bit set, override the existing one - if (mbit1 !== null) { - newReplyObject.breakBit = mbit1; - } - - // Clear the conversation thread (this is on the next cycle) - newReplyObject.clearConvo = clearConvoBit; - } else { - - // TODO we flush everything except stars.. - - debug.verbose("getReplyObject", getReplyObject); - newReplyObject = replyObj; - newReplyObject.reply = getReplyObject.reply; - newReplyObject.topic = getReplyObject.topicName; - // update the root id with the reply id (it may have changed in respond) - newReplyObject.id = getReplyObject.reply.id; - - // This is a new bit to stop us from matching more. - if (mbit !== null) { - newReplyObject.breakBit = mbit; - } - // If the function has the bit set, override the existing one - if (mbit1 !== null) { - newReplyObject.breakBit = mbit1; - } - - if (getReplyObject.clearConvo === true) { - newReplyObject.clearConvo = getReplyObject.clearConvo; - } else { - newReplyObject.clearConvo = clearConvoBit; - } - - if (getReplyObject.subReplies) { - if (newReplyObject.subReplies && _.isArray(newReplyObject.subReplies)) { - newReplyObject.subReplies.concat(getReplyObject.subReplies); - } else { - newReplyObject.subReplies = getReplyObject.subReplies; - } - } - - // We also want to transfer forward any message props too - if (getReplyObject.props) { - newReplyObject.props = getReplyObject.props; - } - - newReplyObject.minMatchSet = getReplyObject.minMatchSet; - } - - debug.verbose("Return back to replies to re-process for more tags", newReplyObject); - // Okay Lets call this function again - return replies(newReplyObject, user, options, callback); - } - }; - - if (redirectMatch && match === "redirectMatch") { - return processRedirects(reply, redirectMatch, replyOptions, augmentCallbackHandle); - } - - if (topicRedirectMatch && match === "topicRedirectMatch") { - return topicRedirect(reply, stars, topicRedirectMatch, replyOptions, augmentCallbackHandle); - } - - if (respondMatch && match === "respondMatch") { - // In some edge cases you could name a topic with a ~ and wordnet will remove it. - // respond needs a topic so we re-try again with the origional reply. - if (respondMatch[1] === "") { - reply = orig_reply; - respondMatch = regexes.respond.match(reply) - } - - return respond(reply, respondMatch, replyOptions, augmentCallbackHandle); - } - - if (customFunctionMatch && match === "customFunctionMatch") { - return customFunction(reply, customFunctionMatch, replyOptions, augmentCallbackHandle); - } - - // Using global callback and user. - var afterHandle = function(topic, cb) { - return function(err, finalReply) { - if (err) { - console.log(err); - } - - // This will update the reply with wordnet replaced changes and alternates - finalReply = processHelpers.processAlternates(finalReply); - - var msgStateMatch = regexes.state.match(finalReply) - if (msgStateMatch && finalReply.indexOf("delay") === -1) { - for(var i = 0; i < msgStateMatch.length; i++) { - var stateObj = processHelpers.addStateData(msgStateMatch[i]); - debug.verbose("Found Conversation State", stateObj); - user.conversationState = merge(user.conversationState, stateObj); - finalReply = finalReply.replace(msgStateMatch[i], ""); - } - finalReply = finalReply.trim(); - } - - - replyObj.reply.reply = Utils.decodeCommas(new RE2('\\\\s', 'g').replace(finalReply, ' ')); - - if (clearConvoBit && clearConvoBit === true) { - replyObj.clearConvo = clearConvoBit; - } - - // This is a new bit to stop us from matching more. - if (!replyObj.breakBit && mbit !== null) { - replyObj.breakBit = mbit; - } - - debug.verbose("Calling back with", replyObj); - - if (!replyObj.props && msg.props) { - replyObj.props = msg.props; - } else { - replyObj.props = merge(replyObj.props, msg.props); - } - - cb(err, replyObj); - }; - }; - - replace(reply, WORDNET_REGEX, processHelpers.wordnetReplace, afterHandle(newtopic, callback)); - }); - -}; - -exports.replies = replies; - -exports.threads = function(string) { - const threads = [] - const strings = [] - string.split('\n').forEach(line => { - const match = regexes.delay.match(line) - if (match) { - threads.push({delay: match[1], string: Utils.trim(line.replace(match[0], ''))}) - } else { - strings.push(line) - } - }) - return [strings.join('\n'), threads] -}; diff --git a/lib/reply/common.js b/lib/reply/common.js deleted file mode 100644 index 0aea0029..00000000 --- a/lib/reply/common.js +++ /dev/null @@ -1,116 +0,0 @@ -var Utils = require("../utils"); -var wordnet = require("./wordnet"); -var debug = require("debug-levels")("SS:ProcessHelpers"); - -exports.getTopic = function (topicsSystem, name, cb) { - if (name) { - topicsSystem.topic.findOne({name: name}, function(err, topicData) { - if (!topicData) { - cb(new Error("No Topic Found")); - } else { - debug.verbose("Getting Topic Data for", topicData); - cb(err, {id: topicData._id, name: name, type: "TOPIC"}); - } - }); - } else { - cb(null, null); - } -}; - -// TODO - Topic Setter should have its own property -exports.topicSetter = function (reply) { - - var TOPIC_REGEX = /\{topic=(.+?)\}/i; - - var match = reply.match(TOPIC_REGEX); - var giveup = 0; - var newtopic; - - while (match) { - giveup++; - if (giveup >= 50) { - debug.verbose("Infinite loop looking for topic tag!"); - break; - } - var name = match[1]; - newtopic = name; - reply = reply.replace(new RegExp("{topic=" + Utils.quotemeta(name) + "}", "ig"), ""); - match = reply.match(TOPIC_REGEX); // Look for more - } - debug.verbose("New Topic", newtopic); - return [reply, newtopic]; -}; - -exports.processAlternates = function (reply) { - // Reply Alternates. - var match = reply.match(/\(\((.+?)\)\)/); - var giveup = 0; - while (match) { - debug.verbose("Reply has Alternates"); - - giveup++; - if (giveup >= 50) { - debug.verbose("Infinite loop when trying to process optionals in trigger!"); - return ""; - } - - var parts = match[1].split("|"); - var opts = []; - for (var i = 0; i < parts.length; i++) { - opts.push(parts[i].trim()); - } - - var resp = Utils.getRandomInt(0, opts.length - 1); - reply = reply.replace(new RegExp("\\(\\(\\s*" + Utils.quotemeta(match[1]) + "\\s*\\)\\)"), opts[resp]); - match = reply.match(/\(\((.+?)\)\)/); - } - - return reply; -}; - -// Handle WordNet in Replies -exports.wordnetReplace = function (match, sym, word, p3, offset, done) { - wordnet.lookup(word, sym, function (err, words) { - if (err) { - console.log(err); - } - - words = words.map(function (item) { - return item.replace(/_/g, " "); - }); - debug.verbose("Wordnet Replies", words); - var resp = Utils.pickItem(words); - done(null, resp); - }); -}; - -var KEYVALG_REGEX = /\s*([a-z0-9]{2,20})\s*=\s*([a-z0-9\'\"]{2,20})\s*/ig; -var KEYVALI_REGEX = /([a-z0-9]{2,20})\s*=\s*([a-z0-9\'\"]{2,20})/i; -exports.addStateData = function(data) { - // Do something with the state - var items = data.match(KEYVALG_REGEX); - var stateData = {}; - - for (var i = 0; i < items.length; i++) { - var x = items[i].match(KEYVALI_REGEX); - var key = x[1]; - var val = x[2]; - - // for strings - val = val.replace(/[\"\']/g, ""); - - if (/^[\d]+$/.test(val)) { - val = +val; - } - - if (val === "true" || val === "false") { - switch(val.toLowerCase().trim()){ - case "true": val = true; break; - case "false": val = false; break; - } - } - stateData[key] = val; - } - - return stateData; -} diff --git a/lib/reply/customFunction.js b/lib/reply/customFunction.js deleted file mode 100644 index faa34db8..00000000 --- a/lib/reply/customFunction.js +++ /dev/null @@ -1,94 +0,0 @@ -var Utils = require("../utils"); -var async = require("async"); -var debug = require("debug-levels")("SS:Reply:customFunction"); -var _ = require("lodash"); - -module.exports = function (reply, match, options, callback) { - - var plugins = options.system.plugins; - var scope = options.system.scope; - var localOptions = options.localOptions; - - scope.message_props = localOptions.messageScope; - scope.message = localOptions.message; - scope.user = localOptions.user; - - var mbit = null; - - // We use async to capture multiple matches in the same reply - return async.whilst( - function () { - return match; - }, - function (cb) { - // Call Function here - - var main = match[0]; - var pluginName = Utils.trim(match[1]); - var partsStr = Utils.trim(match[2]); - var parts = partsStr.split(","); - - debug.verbose("-- Function Arguments --", parts); - var args = []; - for (var i = 0; i < parts.length; i++) { - if (parts[i] !== "") { - args.push(Utils.decodeCommas(parts[i].trim())); - } - } - - if (plugins[pluginName]) { - - // SubReply is the results of the object coming back - // TODO. Subreply should be optional and could be undefined, or null - args.push(function customFunctionHandle(err, subreply, matchBit) { - - var replyStr; - - if (_.isPlainObject(subreply)) { - if (subreply.hasOwnProperty('text')) { - replyStr = subreply.text; - delete subreply.text; - } - - if (subreply.hasOwnProperty('reply')) { - replyStr = subreply.reply; - delete subreply.reply; - } - scope.message.props = _.assign(scope.message.props, subreply); - - } else { - replyStr = subreply; - } - - match = false; - reply = reply.replace(main, replyStr); - match = reply.match(/\^(\w+)\(([~\w<>,\s]*)\)/); - mbit = matchBit; - if (err) { - cb(err); - } else { - cb(); - } - }); - - debug.verbose("Calling Plugin Function", pluginName); - plugins[pluginName].apply(scope, args); - - } else if (pluginName === "topicRedirect" || pluginName === "respond") { - debug.verbose("Existing, we have a systemFunction", pluginName); - match = false; - cb(null, ""); - } else { - // If a function is missing, we kill the line and return empty handed - console.log("WARNING:\nCustom Function (" + pluginName + ") was not found. Your script may not behave as expected"); - debug.verbose("Custom Function not-found", pluginName); - match = false; - cb(true, ""); - } - }, - function (err) { - debug.verbose("Callback from custom function", err); - return callback(err, reply, scope.message.props, {}, mbit); - } - ); -}; diff --git a/lib/reply/inlineRedirect.js b/lib/reply/inlineRedirect.js deleted file mode 100644 index f48a4172..00000000 --- a/lib/reply/inlineRedirect.js +++ /dev/null @@ -1,69 +0,0 @@ -var Message = require("../message"); -var Utils = require("../utils"); -var processHelpers = require("./common"); -var async = require("async"); -var debug = require("debug-levels")("SS:Reply:inline"); - -module.exports = function(reply, redirectMatch, options, callback) { - - var messageOptions = { - qtypes: options.system.question, - norm: options.system.normalize, - facts: options.system.facts - }; - - return async.whilst( - function () { - return redirectMatch; - }, - function (cb) { - - var target = redirectMatch[1]; - debug.verbose("Inline redirection to: '%s'", target) - - // if we have a special topic, reset it to the previous one - // in order to preserve the context for inline redirection - if (options.topic === "__pre__" || options.topic === "__post__") { - if (options.user.__history__.topic.length) { - options.topic = options.user.__history__.topic[0]; - } - } - - processHelpers.getTopic(options.system.topicsSystem, options.topic, function (err, topicData) { - options.aTopics = []; - options.aTopics.push(topicData); - options.system.messageScope = options.localOptions.messageScope; - - new Message(target, messageOptions, function (replyMessageObject) { - options.message = replyMessageObject; - debug.verbose("replyMessageObject", replyMessageObject); - - var getreply = require("../getreply"); - getreply(options, function (err, subreply) { - if (err) { - console.log(err); - } - - debug.verbose("subreply", subreply); - - if (subreply) { - var rd1 = new RegExp("\\{@" + Utils.quotemeta(target) + "\\}", "i"); - reply = reply.replace(rd1, subreply.string); - redirectMatch = reply.match(/\{@(.+?)\}/); - } else { - redirectMatch = false; - reply = reply.replace(new RegExp("\\{@" + Utils.quotemeta(target) + "\\}", "i"), ""); - } - - cb((options.depth === 50) ? "Depth Error" : null); - - }); // getReply - }); // Message - }); - }, - function (err) { - debug.verbose("CallBack from inline redirect", Utils.trim(reply)); - return callback(err, Utils.trim(reply), options.localOptions.message.props, {}); - } - ); -}; diff --git a/lib/reply/respond.js b/lib/reply/respond.js deleted file mode 100644 index 0b379159..00000000 --- a/lib/reply/respond.js +++ /dev/null @@ -1,73 +0,0 @@ -var processHelpers = require("./common"); -var Message = require("../message"); -var Utils = require("../utils"); -var async = require("async"); -var debug = require("debug-levels")("SS:Reply:Respond"); - -var RESPOND_REGEX = /\^respond\(\s*([\w~]*)\s*\)/; -module.exports = function(reply, respondMatch, options, callback) { - - return async.whilst( - function () { - return respondMatch; - }, - function (cb) { - var getreply = require("../getreply"); - var newTopic = Utils.trim(respondMatch[1]); - debug.verbose("Topic Check with new Topic: %s", newTopic) - - processHelpers.getTopic(options.system.topicsSystem, newTopic, function (err, topicData) { - - options.aTopics = []; - options.message = options.localOptions.message; - options.system.messageScope = options.localOptions.messageScope - options.aTopics.push(topicData); - - getreply(options, function (err, subreply) { - if (err) { - console.log(err); - } - - // The topic is not set correctly in getReply! - debug.verbose("CallBack from respond topic (getReplyObj)", subreply); - - - if (subreply && subreply.replyId) { - debug.verbose("subreply", subreply); - // We need to do a lookup on subreply.replyId and flash the entire reply. - options.system.topicsSystem.reply.findById(subreply.replyId) - .exec(function (err, fullReply) { - if (err) { - debug.error(err); - } - - debug.verbose("fullReply", fullReply); - - debug.verbose("Setting the topic to the matched one"); - options.user.setTopic(newTopic); - - reply = fullReply.reply || ""; - replyObj = subreply; - replyObj.reply = fullReply; - replyObj.topicName = newTopic; - respondMatch = reply.match(RESPOND_REGEX); - - cb((options.depth === 50) ? "Depth Error" : null); - - }); - } else { - respondMatch = false; - reply = ""; - replyObj = {}; - - cb((options.depth === 50) ? "Depth Error" : null); - } - }); - }); - }, - function (err) { - debug.verbose("CallBack from Respond Function", replyObj ); - return callback(err, Utils.trim(reply), options.localOptions.message.props, replyObj); - } - ); -}; diff --git a/lib/reply/topicRedirect.js b/lib/reply/topicRedirect.js deleted file mode 100644 index eb96d25a..00000000 --- a/lib/reply/topicRedirect.js +++ /dev/null @@ -1,121 +0,0 @@ -var processHelpers = require("./common"); -var Message = require("../message"); -var Utils = require("../utils"); -var async = require("async"); -var debug = require("debug-levels")("SS:Reply:topicRedirect"); - -var TOPIC_REGEX = /\^topicRedirect\(\s*([~\w<>\s]*),([~\w<>\s]*)\s*\)/; - -module.exports = function(reply, stars, redirectMatch, options, callback) { - - var messageOptions = { - qtypes: options.system.question, - norm: options.system.normalize, - facts: options.system.facts - }; - - options.system.messageScope = options.localOptions.messageScope; - - - var replyObj = {}; - - // Undefined, unless it is being passed back - var mbit; - - return async.whilst( - function () { - return redirectMatch; - }, - function (cb) { - var main = Utils.trim(redirectMatch[0]); - var topic = Utils.trim(redirectMatch[1]); - var target = Utils.trim(redirectMatch[2]); - var getreply = require("../getreply"); - - debug.verbose("Topic Redirection to: %s topic: %s", target, topic) - options.user.setTopic(topic); - - // Here we are looking for gambits in the NEW topic. - processHelpers.getTopic(options.system.topicsSystem, topic, function (err, topicData) { - if (err) { - /* - In this case the topic does not exist, we want to just pretend it wasn't - provided and reply with whatever else is there. - */ - redirectMatch = reply.match(TOPIC_REGEX); - reply = Utils.trim(reply.replace(main, "")); - debug.verbose("Invalid Topic", reply); - return cb(null); - } - - options.aTopics = []; - options.aTopics.push(topicData); - - new Message(target, messageOptions, function (replyMessageObject) { - options.message = replyMessageObject; - - // Pass the stars (captured wildcards) forward - options.message.stars = stars.slice(1); - - getreply(options, function (err, subreply) { - if (err) { - cb(null); - } - - if (subreply) { - - // We need to do a lookup on subreply.replyId and flash the entire reply. - debug.verbose("CallBack from topicRedirect", subreply); - options.system.topicsSystem.reply.findById(subreply.replyId) - .exec(function (err, fullReply) { - if (err) { - console.log("No SubReply ID found", err); - } - - // This was changed as a result of gh-236 - // reply = reply.replace(main, fullReply.reply); - reply = reply.replace(main, subreply.string); - replyObj = subreply; - - debug.verbose("SubReply", subreply); - debug.verbose("fullReply", fullReply); - - if ((fullReply === null && !replyObj.reply) || err) { - debug.verbose("Something bad happened upstream"); - cb("upstream error"); - } else { - // Override the subreply string with the new complex one - replyObj.string = reply; - - replyObj.reply = fullReply; - replyObj.reply.reply = reply; - - // Lets capture this data too for better logs - replyObj.minMatchSet = subreply.minMatchSet; - - // This may be set before the redirect. - mbit = replyObj.breakBit; - - redirectMatch = reply.match(TOPIC_REGEX); - cb((options.depth === 50) ? "Depth Error" : null); - - } - }); - } else { - redirectMatch = false; - reply = reply.replace(main, ""); - replyObj = {}; - cb((options.depth === 50) ? "Depth Error" : null); - } - - }); // getReply - }); // Message - - }); - }, - function (err) { - debug.verbose("CallBack from topic redirect", reply, replyObj); - return callback(err, Utils.trim(reply), options.localOptions.message.props, replyObj, mbit); - } - ); -}; diff --git a/lib/reply/wordnet.js b/lib/reply/wordnet.js deleted file mode 100644 index d51aa51a..00000000 --- a/lib/reply/wordnet.js +++ /dev/null @@ -1,106 +0,0 @@ -// This is a shim for wordnet lookup. -// http://wordnet.princeton.edu/wordnet/man/wninput.5WN.html - -var natural = require("natural"); -var wordnet = new natural.WordNet(); -var async = require("async"); -var _ = require("lodash"); - -exports.define = function (word, cb) { - wordnet.lookup(word, function (results) { - if (!_.isEmpty(results)) { - cb(null, results[0].def); - } else { - cb("no results"); - } - }); -}; - -// Does a word lookup -// @word can be a word or a word/pos to filter out unwanted types -var wdlookup = exports.lookup = function (word, pointerSymbol, cb) { - - var match; - var pos = null; - - pointerSymbol = pointerSymbol || "~"; - match = word.match(/~(\w)$/); - if (match) { - pos = match[1]; - word = word.replace(match[0], ""); - } - - var itor = function (word1, next) { - wordnet.get(word1.synsetOffset, word1.pos, function (sub) { - next(null, sub.lemma); - }); - }; - - var synets = []; - - wordnet.lookup(word, function (results) { - results.forEach(function (result) { - result.ptrs.forEach(function (part) { - if (pos !== null && part.pos === pos && part.pointerSymbol === pointerSymbol) { - synets.push(part); - } else if (pos === null && part.pointerSymbol === pointerSymbol) { - synets.push(part); - } - }); - }); - - async.map(synets, itor, - function (err, items) { - items = _.uniq(items); - items = items.map(function (x) { - return x.replace(/_/g, " "); - }); - cb(err, items); - } - ); - }); -}; - - -// Used to explore a word or concept -// Spits out lots of info on the word -exports.explore = function (word, cb) { - var ptrs = []; - - wordnet.lookup(word, function (results) { - - for (var i = 0; i < results.length; i++) { - ptrs.push(results[i].ptrs); - } - - ptrs = _.uniq(_.flatten(ptrs)); - ptrs = _.map(ptrs, function (item) { - return { pos: item.pos, sym: item.pointerSymbol }; - }); - - ptrs = _.chain(ptrs) - .groupBy("pos") - .map(function (value, key) { - return { - pos: key, - ptr: _.uniq(_.pluck(value, "sym")) - }; - }) - .value(); - - var itor = function (item, next) { - var itor2 = function (ptr, next2) { - wdlookup(word + "~" + item.pos, ptr, function (err, res) { - console.log(err); - console.log(word, item.pos, ":", ptr, res.join(", ")); - // console.log(res); - next2(); - }); - }; - async.map(item.ptr, itor2, next); - }; - async.each(ptrs, itor, function () { - cb(); - }); - }); -}; diff --git a/lib/topics/common.js b/lib/topics/common.js deleted file mode 100644 index 63d77681..00000000 --- a/lib/topics/common.js +++ /dev/null @@ -1,293 +0,0 @@ -// These are shared helpers for the models. - -var async = require("async"); -var debug = require("debug-levels")("SS:Common"); -var postParse = require("../postParse"); -var Utils = require("../utils"); -var _ = require("lodash"); - -module.exports = function(mongoose) { - var getReply = function() { return mongoose.model('Reply'); }; - var getGambit = function() { return mongoose.model('Gambit'); }; - var getCondition = function() { return mongoose.model('Condition'); }; - var getTopic = function() { return mongoose.model('Topic'); }; - - var _walkReplyParent = function (repId, replyIds, cb) { - getReply().findById(repId) - .populate("parent") - .exec(function (err, reply) { - if (err) { - debug.error(err); - } - - - debug.info("Walk", reply); - - if (reply) { - replyIds.push(reply._id); - - if (reply.parent && reply.parent.parent) { - _walkReplyParent(reply.parent.parent, replyIds, cb); - } else { - cb(null, replyIds); - } - } else { - cb(null, replyIds); - } - }); - }; - - var _walkGambitParent = function (gambitId, gambitIds, cb) { - getGambit().findOne({_id: gambitId}) - .populate("parent") - .exec(function (err, gambit) { - if (err) { - console.log(err); - } - - if (gambit) { - gambitIds.push(gambit._id); - if (gambit.parent && gambit.parent.parent) { - _walkGambitParent(gambit.parent.parent, gambitIds, cb); - } else { - cb(null, gambitIds); - } - } else { - cb(null, gambitIds); - } - }); - }; - - // This will find all the gambits to process by parent (topic or conversation) - var eachGambit = function (type, id, options, callback) { - // Lets Query for Gambits - - var execHandle = function (err, mgambits) { - - if (err) { - console.log(err); - } - - var populateGambits = function (gambit, cb) { - getReply().populate(gambit, {path: "replies"}, cb); - }; - - async.each(mgambits.gambits, populateGambits, function populateGambitsComplete(err2) { - if (err2) { - console.log(err2); - } - async.map(mgambits.gambits, _eachGambitHandle(options), - function eachGambitHandleComplete(err3, matches) { - callback(null, _.flatten(matches)); - } - ); - }); - }; - - if (type === "topic") { - debug.verbose("Looking back Topic", id); - getTopic().findOne({_id: id}, "gambits") - .populate({path: "gambits", match: {isCondition: false }}) - .exec(execHandle); - } else if (type === "reply") { - debug.verbose("Looking back at Conversation", id); - getReply().findOne({_id: id}, "gambits") - .populate({path: "gambits", match: {isCondition: false }}) - .exec(execHandle); - } else if (type === "condition") { - debug.verbose("Looking back at Conditions", id); - getCondition().findOne({_id: id}, "gambits") - .populate("gambits") - .exec(execHandle); - } else { - debug.verbose("We should never get here"); - callback(true); - } - }; - - var _afterHandle = function (match, matches, trigger, topic, cb) { - debug.verbose("Match found", trigger, match); - debug.info("Match found '%s' in topic %s'", trigger.input, topic) - var stars = []; - if (match.length > 1) { - for (var j = 1; j < match.length; j++) { - if (match[j]) { - var starData = Utils.trim(match[j]); - // Concepts are not allowed to be stars or captured input. - starData = (starData[0] == "~") ? starData.substr(1) : starData; - stars.push(starData); - } - } - } - - var data = {stars: stars, trigger: trigger }; - if (topic !== "reply") { - data.topic = topic; - } - - matches.push(data); - cb(null, matches); - }; - - var _doesMatch = function(trigger, message, user, callback) { - - var match = false; - - postParse.postParse(trigger.trigger, message, user, function complexFunction(regexp) { - var pattern = new RegExp("^" + regexp + "$", "i"); - - debug.verbose("Try to match (clean)'%s' against %s (%s)", message.clean, trigger.trigger, regexp) - debug.verbose("Try to match (lemma)'%s' against %s (%s)", message.lemString, trigger.trigger, regexp) - - // Match on the question type (qtype / qsubtype) - if (trigger.isQuestion && message.isQuestion) { - - if (_.isEmpty(trigger.qSubType) && _.isEmpty(trigger.qType) && message.isQuestion === true) { - match = message.clean.match(pattern); - if (!match) { - match = message.lemString.match(pattern); - } - } else { - if ((!_.isEmpty(trigger.qType) && message.qtype.indexOf(trigger.qType) !== -1) || - message.qSubType === trigger.qSubType) { - match = message.clean.match(pattern); - if (!match) { - match = message.lemString.match(pattern); - } - } - } - } else { - // This is a normal match - if (trigger.isQuestion === false) { - match = message.clean.match(pattern); - if (!match) { - match = message.lemString.match(pattern); - } - } - } - - callback(null, match); - }); - }; - - // This is the main function that looks for a matching entry - var _eachGambitHandle = function (options) { - var filterRegex = /\s*\^(\w+)\(([\w<>,\|\s]*)\)\s*/i; - - return function (trigger, callback) { - - var match = false; - var matches = []; - - var message = options.message; - var user = options.user; - var plugins = options.plugins; - var scope = options.scope; - var topic = options.topic || "reply"; - - _doesMatch(trigger, message, user, function (err, match) { - - if (match) { - if (trigger.filter !== "") { - // We need scope and functions - debug.verbose("We have a filter function", trigger.filter); - - var filterFunction = trigger.filter.match(filterRegex); - debug.verbose("Filter Function Found", filterFunction); - - var pluginName = Utils.trim(filterFunction[1]); - var partsStr = Utils.trim(filterFunction[2]); - var parts = partsStr.split(","); - - var args = []; - for (var i = 0; i < parts.length; i++) { - if (parts[i] !== "") { - args.push(parts[i].trim()); - } - } - - if (plugins[pluginName]) { - - var filterScope = scope; - filterScope.message = options.localOptions.message; - filterScope.message_props = options.localOptions.messageScope; - filterScope.user = options.localOptions.user; - - args.push(function customFilterFunctionHandle(err, filterReply) { - if (err) { - console.log(err); - } - - if (filterReply === "true" || filterReply === true) { - debug.verbose("filterReply", filterReply); - - if (trigger.redirect !== "") { - debug.verbose("Found Redirect Match with topic %s", topic) - getTopic().findTriggerByTrigger(trigger.redirect, function (err2, gambit) { - if (err2) { - console.log(err2); - } - - trigger = gambit; - callback(null, matches); - }); - - } else { - // Tag the message with the found Trigger we matched on - message.gambitId = trigger._id; - _afterHandle(match, matches, trigger, topic, callback); - } - } else { - debug.verbose("filterReply", filterReply); - callback(null, matches); - } - }); - - debug.verbose("Calling Plugin Function", pluginName); - plugins[pluginName].apply(filterScope, args); - - } else { - debug.verbose("Custom Filter Function not-found", pluginName); - callback(null, matches); - } - } else { - - if (trigger.redirect !== "") { - debug.verbose("Found Redirect Match with topic"); - getTopic().findTriggerByTrigger(trigger.redirect, function (err, gambit) { - if (err) { - console.log(err); - } - - debug.verbose("Redirecting to New Gambit", gambit); - trigger = gambit; - // Tag the message with the found Trigger we matched on - message.gambitId = trigger._id; - _afterHandle(match, matches, trigger, topic, callback); - }); - } else { - // Tag the message with the found Trigger we matched on - message.gambitId = trigger._id; - _afterHandle(match, matches, trigger, topic, callback); - } - } - } else { - callback(null, matches); - } - - }); // end regexReply - }; // end EachGambit - }; - - return { - walkReplyParent: function (repId, cb) { - _walkReplyParent(repId, [], cb); - }, - walkGambitParent: function (gambitId, cb) { - _walkGambitParent(gambitId, [], cb); - }, - eachGambit: eachGambit, - doesMatch: _doesMatch - }; - -}; //end export diff --git a/lib/topics/condition.js b/lib/topics/condition.js deleted file mode 100644 index fdedcb79..00000000 --- a/lib/topics/condition.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - - A Condition is a type of Gambit that contains a set of gambits, but instead - of having a static regex trigger it has some conditional logic - -**/ - -module.exports = function (mongoose) { - var Common = require("./common")(mongoose); - var Utils = require("../utils"); - var findOrCreate = require("mongoose-findorcreate"); - - var conditionSchema = new mongoose.Schema({ - id: {type: String, index: true, default: Utils.genId()}, - - condition: {type: String}, - - // An array of gambits that belong to this condition. - gambits: [{ type: String, ref: "Gambit"}] - - }); - - - // At this point we just want to see if the condition matches, then pass the gambits to Common eachGambit - conditionSchema.methods.doesMatch = function (options, callback) { - var self = this; - - Common.eachGambit("condition", self._id, options, callback); - }; - - conditionSchema.plugin(findOrCreate); - - try { - return mongoose.model("Condition", conditionSchema); - } catch(e) { - return mongoose.model("Condition"); - } -}; diff --git a/lib/topics/gambit.js b/lib/topics/gambit.js deleted file mode 100644 index c3ed2bc6..00000000 --- a/lib/topics/gambit.js +++ /dev/null @@ -1,169 +0,0 @@ -/** - - A Gambit is a Trigger + Reply or Reply Set - - We define a Reply as a subDocument in Mongo. - -**/ - -// var regexreply = require("../parse/regexreply"); - -module.exports = function (mongoose, facts) { - var Utils = require("../utils"); - var regexreply = require("ss-parser/lib/regexreply"); - var debug = require("debug-levels")("SS:Gambit"); - var async = require("async"); - var norm = require("node-normalizer"); - var findOrCreate = require("mongoose-findorcreate"); - var Common = require("./common")(mongoose); - var gNormalizer; - - norm.loadData(function () { - gNormalizer = norm; - debug.verbose("Normaizer Loaded."); - }); - - /** - - I trigger is the matching rule behind a piece of input. It lives in a topic or several topics. - A trigger also contains one or more replies. - - **/ - - var gambitSchema = new mongoose.Schema({ - id: {type: String, index: true, default: Utils.genId()}, - - // This is the input string that generates a rule, - // In the event we want to export this, we will use this value. - // Make this filed conditionally required if trigger is supplied - input: {type: String}, - - // The Trigger is a partly baked regex. - trigger: {type: String, index: true}, - - // If the trigger is a Question Match - isQuestion: {type: Boolean, default: false}, - - // If this gambit is nested inside a conditional block - isCondition: {type: Boolean, default: false}, - - // If the trigger is a Answer Type Match - qType: {type: String, default: ""}, - qSubType: {type: String, default: ""}, - - // The filter function for the the expression - filter: {type: String, default: ""}, - - // An array of replies. - replies: [{ type: String, ref: "Reply"}], - - // Save a reference to the parent Reply, so we can walk back up the tree - parent: { type: String, ref: 'Reply' }, - - // This will redirect anything that matches elsewhere. - // If you want to have a conditional rediect use reply redirects - // TODO, change the type to a ID and reference another gambit directly - // this will save us a lookup down the road (and improve performace.) - redirect: {type: String, default: ""} - }); - - gambitSchema.pre("save", function (next) { - var self = this; - - // FIXME: This only works when the replies are populated which is not always the case. - // self.replies = _.uniq(self.replies, function(item, key, id) { - // return item.id; - // }); - - // If input was supplied, we want to use it to generate the trigger - if (self.input) { - var input = gNormalizer.clean(self.input); - // We want to convert the input into a trigger. - regexreply.parse(Utils.quotemeta(input, true), facts, function (trigger) { - self.trigger = trigger; - next(); - }); - } else { - // Otherwise we populate the trigger normally - next(); - } - }); - - gambitSchema.methods.addReply = function (replyData, callback) { - var self = this; - var Reply = mongoose.model('Reply'); - - if (!replyData) { - return callback("No data"); - } - - var reply = new Reply(replyData); - reply.save(function (err) { - if (err) { - return callback(err); - } - self.replies.addToSet(reply._id); - self.save(function (err) { - callback(err, reply); - }); - }); - }; - - gambitSchema.methods.doesMatch = function (message, callback) { - Common.doesMatch(this, message, null, callback); - }; - - gambitSchema.methods.clearReplies = function (callback) { - var self = this; - var Reply = mongoose.model('Reply'); - - var clearReply = function (replyId, cb) { - self.replies.pull({ _id: replyId }); - Reply.remove({ _id: replyId }, function (err) { - if (err) { - console.log(err); - } - - debug.verbose('removed reply %s', replyId) - - cb(null, replyId); - }); - }; - - async.map(self.replies, clearReply, function (err, clearedReplies) { - self.save(function (err2) { - callback(err2, clearedReplies); - }); - }); - }; - - - gambitSchema.methods.getRootTopic = function (cb) { - var self = this; - var Topic = mongoose.model('Topic'); - - if (!self.parent) { - Topic.findOne({ gambits: { $in: [ self._id ] } }).exec(function (err, doc) { - cb(err, doc.name); - }); - } else { - Common.walkGambitParent(self._id, function (err, gambits) { - if (gambits.length !== 0) { - Topic.findOne({ gambits: { $in: [ gambits.pop() ] } }).exec(function (err, topic) { - cb(null, topic.name); - }); - } else { - cb(null, "random"); - } - }); - } - }; - - - gambitSchema.plugin(findOrCreate); - - try { - return mongoose.model("Gambit", gambitSchema); - } catch(e) { - return mongoose.model("Gambit"); - } -}; diff --git a/lib/topics/import.js b/lib/topics/import.js deleted file mode 100755 index 6cdcb8fd..00000000 --- a/lib/topics/import.js +++ /dev/null @@ -1,231 +0,0 @@ -/** - - Import a data file into MongoDB - -**/ - -var fs = require("fs"); -var async = require("async"); -var _ = require("lodash"); -var Utils = require("../utils"); -var debug = require("debug-levels")("SS:Importer"); - -var KEEP_REGEX = new RegExp("\{keep\}", "i"); -var FILTER_REGEX = /\{\s*\^(\w+)\(([\w<>,\s]*)\)\s*\}/i; - -module.exports = function (factSystem, Topic, Gambit, Reply) { - - var _rawToGambitData = function (gambitId, itemData) { - - var gambitData = { - id: gambitId, - isQuestion: itemData.options.isQuestion, - isCondition: itemData.options.isConditional, - qType: itemData.options.qType === false ? "" : itemData.options.qType, - qSubType: itemData.options.qSubType === false ? "" : itemData.options.qSubType, - filter: itemData.options.filter === false ? "" : itemData.options.filter, - trigger: itemData.trigger - }; - - // This is to capture anything pre 5.1 - if (itemData.raw) { - gambitData.input = itemData.raw; - } else { - gambitData.input = itemData.trigger; - } - - if (itemData.redirect !== null) { - gambitData.redirect = itemData.redirect; - } - - return gambitData; - }; - - var importData = function (data, callback, flushTopics, preserveRandom) { - - var gambitsWithConversation = []; - - var eachReplyItor = function (gambit) { - return function (replyId, nextReply) { - debug.verbose("Reply process: %s", replyId) - var replyString = data.replys[replyId]; - var properties = { id: replyId, reply: replyString, parent: gambit._id }; - var match = properties.reply.match(KEEP_REGEX); - if (match) { - properties.keep = true; - properties.reply = Utils.trim(properties.reply.replace(match[0], "")); - } - match = properties.reply.match(FILTER_REGEX); - if (match) { - properties.filter = "^" + match[1] + "(" + match[2] + ")"; - properties.reply = Utils.trim(properties.reply.replace(match[0], "")); - } - - gambit.addReply(properties, function (err) { - if (err) { - console.log(err); - } - nextReply(); - }); - }; - }; - - var findOrCreateTopic = function (topicName, properties, callback) { - Topic.findOrCreate({name: topicName}, properties, function (err, topic) { - if (flushTopics && !(topicName === 'random' && preserveRandom)) { - topic.clearGambits(function() { - Topic.remove({ _id: topic.id }, function (err2) { - if (err2) { - console.log(err); - } - - debug.verbose('removed topic %s (%s)', topicName, topic.id) - - Topic.findOrCreate({name: topicName}, properties, function (err3, topic2) { - callback(err3, topic2); - }); - }); - }); - } else { - callback(err, topic); - } - }); - }; - - var eachTopicItor = function (topicName, nextTopic) { - debug.verbose("Find or create", topicName); - var properties = { - name: topicName, - keep: data.topics[topicName].flags.indexOf("keep") !== -1, - nostay: data.topics[topicName].flags.indexOf("nostay") !== -1, - system: data.topics[topicName].flags.indexOf("system") !== -1, - keywords: data.topics[topicName].keywords ? data.topics[topicName].keywords : [], - filter: (data.topics[topicName].filter) ? data.topics[topicName].filter : "" - }; - - findOrCreateTopic(topicName, properties, function (err, topic) { - if (err) { - console.log(err); - } - - var eachGambitItor = function (gambitId, nextGambit) { - if (!_.isUndefined(data.gambits[gambitId].options.conversations)) { - gambitsWithConversation.push(gambitId); - nextGambit(); - - } else if (data.gambits[gambitId].topic === topicName) { - debug.verbose("Gambit process: %s", gambitId) - var gambitRawData = data.gambits[gambitId]; - var gambitData = _rawToGambitData(gambitId, gambitRawData); - - topic.createGambit(gambitData, function (err3, gambit) { - if (err3) { - return new Error(err3); - } - async.eachSeries(gambitRawData.replys, eachReplyItor(gambit), function (err4) { - if (err4) { - return new Error(err4); - } - nextGambit(); - }); - }); - } else { - nextGambit(); - } - }; - - async.eachSeries(Object.keys(data.gambits), eachGambitItor, function (err5) { - if (err5) { - console.log(err5); - } - debug.verbose("All gambits for %s processed.", topicName) - nextTopic(); - }); - }); - }; - - var eachConvItor = function (gambitId) { - return function (replyId, nextConv) { - debug.verbose("conversation/reply: %s", replyId) - Reply.findOne({id: replyId}, function (err, reply) { - if (err) { - console.log(err); - } - if (reply) { - reply.gambits.addToSet(gambitId); - reply.save(function () { - reply.sortGambits(function () { - debug.verbose("All conversations for %s processed.", gambitId) - nextConv(); - }); - }); - } else { - debug.warn("No reply found!"); - nextConv(); - } - }); - }; - }; - - async.eachSeries(Object.keys(data.topics), eachTopicItor, function () { - - - async.eachSeries(_.uniq(gambitsWithConversation), function (gambitId, finish) { - var gambitData = data.gambits[gambitId]; - - var conversations = gambitData.options.conversations || []; - if (conversations.length === 0) { - return finish(); - } - - var convoGambit = _rawToGambitData(gambitId, gambitData); - var replyId = conversations[0]; - - // TODO??: Add reply.addGambit(...) - Reply.findOne({id:replyId}, function(err, replyObj) { - var cGambit = new Gambit(convoGambit); - async.eachSeries(gambitData.replys, eachReplyItor(cGambit), function (err, res) { - debug.verbose('All replys processed.'); - cGambit.parent = replyObj._id; - cGambit.save(function(err, gam){ - debug.verbose("Saving New Gambit", err, gam); - async.map(conversations, eachConvItor(gam._id), function (err, results) { - debug.verbose('All conversations for %s processed.', gambitId) - finish(); - }); - }); - }); - }); - }, function () { - - // Move on to conditions (convos) - var convoItor = function(convoId, next) { - var condition = data.convos[convoId]; - Topic.findOne({name: condition.topic}, function(err, topic) { - topic.createCondition(condition, function(err, condition) { - next(); - }); - }); - }; - - async.eachSeries(Object.keys(data.convos), convoItor, function() { - debug.verbose("Add Conditions Added"); - callback(null, "done"); - }); // end - - }); - - }); - }; - - var importFile = function (path, callback) { - fs.readFile(path, function(err, jsonFile){ - return importData(JSON.parse(jsonFile), callback); - }); - }; - - return { - file: importFile, - data: importData - }; -}; diff --git a/lib/topics/index.js b/lib/topics/index.js deleted file mode 100644 index aca78adc..00000000 --- a/lib/topics/index.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - I want to create a more organic approch to authoring new gambits, topics and replies - Right now, the system parses flat files to a intermediate JSON object that SS reads and - creates an in-memory topic representation. - - I belive by introducing a Topic DB with a clean API we can have a faster more robust authoring - expierence parseing input will become more intergrated into the topics, and Im propising - changing the existing parse inerface with a import/export to make sharing SuperScript - data (and advanced authoring?) easier. - - We also want to put more focus on the Gambit, and less on topics. A Gambit should be - able to live in several topics. - -**/ - -module.exports = function (mongoose, factSystem) { - var Gambit = require("./gambit")(mongoose, factSystem); - var Topic = require("./topic")(mongoose); - var Condition = require("./condition")(mongoose); - var Reply = require("./reply")(mongoose); - var Importer = require("./import")(factSystem, Topic, Gambit, Reply); - - Gambit.count({}, function(err, gambits) { - console.log("Number of Gambits:", gambits); - }); - - Reply.count({}, function(err, replycount) { - console.log("Number of Replies:", replycount); - }); - - return { - gambit: Gambit, - topic: Topic, - reply: Reply, - condition: Condition, - importerFile: Importer.file, - importerData: Importer.data - }; -}; diff --git a/lib/topics/reply.js b/lib/topics/reply.js deleted file mode 100644 index d8e2a92d..00000000 --- a/lib/topics/reply.js +++ /dev/null @@ -1,64 +0,0 @@ -var Utils = require("../utils"); -var debug = require("debug")("Reply"); -var dwarn = require("debug")("Reply:Error"); -var Sort = require("./sort"); -var async = require("async"); - -module.exports = function (mongoose) { - var Common = require("./common")(mongoose); - - var replySchema = new mongoose.Schema({ - id: {type: String, index: true, default: Utils.genId()}, - reply: {type: String, required: "{reply} is required."}, - keep: {type: Boolean, default: false }, - filter: {type: String, default: ""}, - parent: { type: String, ref: 'Gambit' }, - - // Replies could referece other gambits - // This forms the basis for the 'previous' - These are Children - gambits: [{ type: String, ref: 'Gambit' }] - }); - - // This method is simular to the topic.findMatch - replySchema.methods.findMatch = function (message, user, plugins, scope, localOptions, callback) { - var self = this; - - var options = { - message: message, - user: user, - plugins: plugins, - scope: scope, - localOptions: localOptions - }; - - Common.eachGambit("reply", self._id, options, callback); - }; - - replySchema.methods.sortGambits = function (callback) { - var self = this - , Gambit = mongoose.model('Gambit') - ; - var expandReorder = function (gambitId, cb) { - Gambit.findById(gambitId, function (err, gambit) { - cb(err, gambit); - }); - }; - - async.map(this.gambits, expandReorder, function (err, newGambitList) { - if (err) { - console.log(err); - } - - var newList = Sort.sortTriggerSet(newGambitList); - self.gambits = newList.map(function (g) { - return g._id; - }); - self.save(callback); - }); - }; - try { - return mongoose.model("Reply", replySchema); - } catch(e) { - return mongoose.model("Reply"); - } -}; diff --git a/lib/topics/sort.js b/lib/topics/sort.js deleted file mode 100644 index e4952479..00000000 --- a/lib/topics/sort.js +++ /dev/null @@ -1,156 +0,0 @@ -var Utils = require("../utils"); -var debug = require("debug")("Sort"); - -exports.sortTriggerSet = function (triggers) { - var trig; - var cnt; - var i; - var j; - var k; - var l; - var inherits; - - var lengthSort = function (a, b) { - return b.length - a.length; - }; - - // Create a priority map. - var prior = { - 0: [] // Default priority = 0 - }; - - // Sort triggers by their weights. - for (i = 0; i < triggers.length; i++) { - trig = triggers[i]; - var match = trig.input.match(/\{weight=(\d+)\}/i); - var weight = 0; - if (match && match[1]) { - weight = match[1]; - } - - if (!prior[weight]) { - prior[weight] = []; - } - prior[weight].push(trig); - } - - var sortFwd = function (a, b) { - return b - a; - }; - - var sortRev = function (a, b) { - return a - b; - }; - - // Keep a running list of sorted triggers for this topic. - var running = []; - - // Sort them by priority. - var priorSort = Object.keys(prior).sort(sortFwd); - - for (i = 0; i < priorSort.length; i++) { - var p = priorSort[i]; - debug("Sorting triggers with priority " + p); - - // Loop through and categorize these triggers. - var track = {}; - - for (j = 0; j < prior[p].length; j++) { - trig = prior[p][j]; - - inherits = -1; - if (!track[inherits]) { - track[inherits] = initSortTrack(); - } - - if (trig.qType !== "") { - // Qtype included - cnt = trig.qType.length; - debug("Has a qType with " + trig.qType.length + " length."); - - if (!track[inherits].qtype[cnt]) { - track[inherits].qtype[cnt] = []; - } - track[inherits].qtype[cnt].push(trig); - - } else if (trig.input.indexOf("*") > -1) { - // Wildcard included. - cnt = Utils.wordCount(trig.input); - debug("Has a * wildcard with " + cnt + " words."); - if (cnt > 1) { - if (!track[inherits].wild[cnt]) { - track[inherits].wild[cnt] = []; - } - track[inherits].wild[cnt].push(trig); - } else { - track[inherits].star.push(trig); - } - } - else if (trig.input.indexOf("[") > -1) { - // Optionals included. - cnt = Utils.wordCount(trig.input); - debug("Has optionals with " + cnt + " words."); - if (!track[inherits].option[cnt]) { - track[inherits].option[cnt] = []; - } - track[inherits].option[cnt].push(trig); - } else { - // Totally atomic. - cnt = Utils.wordCount(trig.input); - debug("Totally atomic trigger and " + cnt + " words."); - if (!track[inherits].atomic[cnt]) { - track[inherits].atomic[cnt] = []; - } - track[inherits].atomic[cnt].push(trig); - } - } - - // Move the no-{inherits} triggers to the bottom of the stack. - track[0] = track["-1"]; - delete track["-1"]; - - // Add this group to the sort list. - var trackSorted = Object.keys(track).sort(sortRev); - - for (j = 0; j < trackSorted.length; j++) { - var ip = trackSorted[j]; - debug("ip=" + ip); - - var kinds = ["qtype", "atomic", "option", "alpha", "number", "wild"]; - for (k = 0; k < kinds.length; k++) { - var kind = kinds[k]; - - var kindSorted = Object.keys(track[ip][kind]).sort(sortFwd); - - for (l = 0; l < kindSorted.length; l++) { - var item = kindSorted[l]; - running.push.apply(running, track[ip][kind][item]); - } - } - - // We can sort these using Array.sort - var underSorted = track[ip].under.sort(lengthSort); - var poundSorted = track[ip].pound.sort(lengthSort); - var starSorted = track[ip].star.sort(lengthSort); - - running.push.apply(running, underSorted); - running.push.apply(running, poundSorted); - running.push.apply(running, starSorted); - } - } - return running; -}; - -var initSortTrack = function () { - return { - "qtype": {}, // Sort by Question Types Length - "atomic": {}, // Sort by number of whole words - "option": {}, // Sort optionals by number of words - "alpha": {}, // Sort alpha wildcards by no. of words - "number": {}, // Sort number wildcards by no. of words - "wild": {}, // Sort wildcards by no. of words - "pound": [], // Triggers of just # - "under": [], // Triggers of just _ - "star": [] // Triggers of just * - }; -}; diff --git a/lib/topics/topic.js b/lib/topics/topic.js deleted file mode 100644 index 6faad7a4..00000000 --- a/lib/topics/topic.js +++ /dev/null @@ -1,415 +0,0 @@ -/*global _walkParent */ -/** - TODO Where is walkParent defined? - Topics are a grouping of gambits. - The order of the Gambits are important, and a gambit can live in more than one topic. - -**/ - -var natural = require("natural"); -var _ = require("lodash"); -var async = require("async"); -var findOrCreate = require("mongoose-findorcreate"); -var debug = require("debug-levels")("SS:Topics"); -var Sort = require("./sort"); -var safeEval = require("safe-eval"); - -var TfIdf = natural.TfIdf; -var tfidf = new TfIdf(); - -module.exports = function (mongoose) { - var Common = require("./common")(mongoose); - - natural.PorterStemmer.attach(); - - var topicSchema = new mongoose.Schema({ - name: {type: String, index: true, unique: true}, - keep: {type: Boolean, default: false }, - system: {type: Boolean, default: false}, - nostay: {type: Boolean, default: false}, - filter: {type: String, default: ""}, - keywords: {type: Array}, - gambits: [{ type: String, ref: "Gambit"}], - conditions: [{ type: String, ref: "Condition"}] - }); - - topicSchema.pre("save", function (next) { - var self = this; - var kw; - - if (!_.isEmpty(this.keywords)) { - kw = self.keywords.join(" "); - if (kw) { - tfidf.addDocument(kw.tokenizeAndStem(), self.name); - } - } - next(); - }); - - topicSchema.methods.createCondition = function(conditionData, callback) { - var self = this; - var Gambit = mongoose.model('Gambit'); - var Condition = mongoose.model('Condition'); - - if (!conditionData) { - return callback("No data"); - } - - if (_.isEmpty(conditionData.gambits)) { - return callback("No gambits"); - } else { - var gambits = []; - var gambitLookup = function(gambit_id, next) { - Gambit.findOne({id: gambit_id}, function(error, gambit) { - gambits.push(gambit._id); - next(); - }); - }; - - async.each(conditionData.gambits, gambitLookup, function() { - - conditionData.gambits = gambits; - var condition = new Condition(conditionData); - - condition.save(function (err) { - if (err) { - return callback(err); - } - self.conditions.addToSet(condition._id); - self.save(function (err2) { - callback(err2, condition); - }); - }); - }); - } - }; - - // This will create the Gambit and add it to the model - topicSchema.methods.createGambit = function (gambitData, callback) { - var Gambit = mongoose.model('Gambit'); - - if (!gambitData) { - return callback("No data"); - } - - var gambit = new Gambit(gambitData); - var self = this; - gambit.save(function (err) { - if (err) { - return callback(err); - } - self.gambits.addToSet(gambit._id); - self.save(function (err2) { - callback(err2, gambit); - }); - }); - }; - - topicSchema.methods.sortGambits = function (callback) { - var self = this; - var Gambit = mongoose.model('Gambit'); - - var expandReorder = function (gambitId, cb) { - Gambit.findById(gambitId, function (err, gambit) { - if (err) { - console.log(err); - } - cb(null, gambit); - }); - }; - - async.map(self.gambits, expandReorder, function (err, newGambitList) { - if (err) { - console.log(err); - } - - var newList = Sort.sortTriggerSet(newGambitList); - self.gambits = newList.map(function (g) { - return g._id; - }); - self.save(callback); - }); - }; - - topicSchema.methods.checkConditions = function(message, user, plugins, scope, localOptions, callback) { - var self = this; - // Okay, were is the user state? - var conditionItor = function(cond, next) { - - var context = user.conversationState || {}; - - debug.verbose("CheckItor - Context", context); - debug.verbose("CheckItor - Condition", cond.condition); - - try { - if (safeEval(cond.condition, context)) { - debug.verbose("--- Condition TRUE YAY ---"); - - var options = { - message: message, - user: user, - plugins: plugins, - scope: scope, - topic: self.name, - localOptions: localOptions - }; - - cond.doesMatch(options, next); - } else { - next(true); - } - } catch(e) { - next(true); - } - }; - - async.mapSeries(this.conditions, conditionItor, function(err, res) { - if (err) { - return callback(true, []); - } else { - callback(err, _.flatten(res)); - } - }); - - - }; - - topicSchema.methods.findMatch = function (message, user, plugins, scope, localOptions, callback) { - var self = this; - - var options = { - message: message, - user: user, - plugins: plugins, - scope: scope, - topic: this.name, - localOptions: localOptions - }; - - Common.eachGambit("topic", self._id, options, callback); - }; - - // Lightweight match for one topic - // TODO offload this to common - topicSchema.methods.doesMatch = function (message, cb) { - var self = this; - var Topic = mongoose.model('Topic'); - - var itor = function (gambit, next) { - gambit.doesMatch(message, function (err, match2) { - if (err) { - debug.error(err); - } - next(match2 ? gambit._id : null); - }); - }; - - Topic.findOne({name: self.name}, "gambits") - .populate("gambits") - .exec(function (err, mgambits) { - if (err) { - debug.error(err); - } - async.filter(mgambits.gambits, itor, function (res) { - cb(null, res); - }); - } - ); - }; - - topicSchema.methods.clearGambits = function (callback) { - var self = this; - var Gambit = mongoose.model('Gambit'); - - var clearGambit = function (gambitId, cb) { - self.gambits.pull({ _id: gambitId }); - Gambit.findById(gambitId, function (err, gambit) { - if (err) { - debug.error(err); - } - - gambit.clearReplies(function() { - Gambit.remove({ _id: gambitId }, function (err) { - if (err) { - debug.error(err); - } - - debug.verbose('removed gambit %s', gambitId) - - cb(null, gambitId); - }); - }); - }); - }; - - async.map(self.gambits, clearGambit, function (err, clearedGambits) { - self.save(function (err2) { - callback(err2, clearedGambits); - }); - }); - }; - - // This will find a gambit in any topic - topicSchema.statics.findTriggerByTrigger = function (input, callback) { - var Gambit = mongoose.model('Gambit'); - Gambit.findOne({input: input}).exec(callback); - }; - - topicSchema.statics.findByName = function (name, callback) { - this.findOne({name: name}, {}, callback); - }; - - // Private function to score the topics by TF-IDF - var _score = function (msg) { - var docs = []; - var tas = msg.lemString.tokenizeAndStem(); - debug.verbose("Token And Stem Words", tas); - // Here we score the input aginst the topic kewords to come up with a topic order. - tfidf.tfidfs(tas, function (index, m, k) { - - // Filter out system topic pre/post - if (k !== "__pre__" && k !== "__post__") { - docs.push({topic: k, score: m}); - } - }); - - // Removes duplicate entries. - docs = _.uniq(docs, function (item, key, a) { - return item.topic; - }); - - debug.verbose("Score Inner", docs); - var topicOrder = _.sortBy(docs, function (item) { - return item.score; - }).reverse(); - - return _.map(topicOrder, function (item) { - return {name: item.topic, score: item.score, type:'TOPIC'}; - }); - }; - - exports.rootTopic = function (repId, cb) { - _walkParent(repId, [], cb); - }; - - - topicSchema.statics.findPendingTopicsForUser = function (user, msg, callback) { - var self = this; - var currentTopic = user.getTopic(); - var aTopics = []; - var i; - var Reply = mongoose.model('Reply'); - - var scoredTopics = _score(msg); - debug.verbose("Topic Score", scoredTopics); - - var removeMissingTopics = function(top) { - return _.filter(top, function(item) { - return item.id; - }); - }; - - self.find({}, function (err, allTopics) { - if (err) { - debug.error(err); - } - - // Add the current topic to the top of the stack. - scoredTopics.unshift({name: currentTopic, type:'TOPIC'}); - - var otherTopics = allTopics; - otherTopics = _.map(otherTopics, function (item) { - return {id: item._id, name: item.name, system: item.system}; - }); - - // This gets a list if all the remaining topics. - otherTopics = _.filter(otherTopics, function (obj) { - return !_.find(scoredTopics, {name: obj.name}); - }); - - // We remove the system topics - otherTopics = _.filter(otherTopics, function (obj) { - return obj.system === false; - }); - - aTopics.push({name: "__pre__", type:"TOPIC"}); - - for (i = 0; i < scoredTopics.length; i++) { - if (scoredTopics[i].name !== "__post__" && scoredTopics[i].name !== "__pre__") { - aTopics.push(scoredTopics[i]); - } - } - - for (i = 0; i < otherTopics.length; i++) { - if (otherTopics[i].name !== "__post__" && otherTopics[i].name !== "__pre__") { - otherTopics[i].type = "TOPIC"; - aTopics.push(otherTopics[i]); - } - } - - aTopics.push({name: "__post__", type:"TOPIC"}); - - // Lets assign the ids to the topics - for (i = 0; i < aTopics.length; i++) { - var tName = aTopics[i].name; - for (var n = 0; n < allTopics.length; n++) { - if (allTopics[n].name === tName) { - aTopics[i].id = allTopics[n]._id; - } - } - } - - // If we are currently in a conversation, we want the entire chain added - // to the topics to search - var lastReply = user.__history__.reply[0]; - if (!_.isEmpty(lastReply)) { - - // If the message is 5 Minutes old we continue - var delta = new Date() - lastReply.createdAt; - if (delta <= 1000 * 300) { - var replyId = lastReply.replyId; - var clearBit = lastReply.clearConvo; - - debug("Last reply: ", lastReply.raw, replyId, clearBit); - - if (clearBit === true) { - debug("Conversation RESET by clearBit"); - callback(null, removeMissingTopics(aTopics)); - } else { - Reply.findOne({_id: replyId}).exec(function (err, reply) { - if (!reply) { - debug("We couldn't match the last reply. Continuing."); - callback(null, removeMissingTopics(aTopics)); - } else { - Common.walkReplyParent(reply._id, function (err, replyThreads) { - - replyThreads = replyThreads.map(function (item) { - return {id: item, type: "REPLY"}; - }); - - replyThreads.unshift(1, 0); - Array.prototype.splice.apply(aTopics, replyThreads); - - callback(null, removeMissingTopics(aTopics)); - }); - } - }); - } - } else { - debug.info("The conversation thread was to old to continue it."); - callback(null, removeMissingTopics(aTopics)); - } - } else { - callback(null, removeMissingTopics(aTopics)); - } - }); - }; - - topicSchema.plugin(findOrCreate); - - try { - return mongoose.model("Topic", topicSchema); - } catch(e) { - return mongoose.model("Topic"); - } -}; diff --git a/lib/users.js b/lib/users.js deleted file mode 100644 index d0e8b1cb..00000000 --- a/lib/users.js +++ /dev/null @@ -1,183 +0,0 @@ -var fs = require("fs"); -var _ = require("lodash"); -var debug = require("debug-levels")("SS:User"); -var findOrCreate = require("mongoose-findorcreate"); -var mkdirp = require("mkdirp"); - -var UX = function (mongoose, sfacts) { - - mkdirp.sync(process.cwd() + "/logs/"); - - var userSchema = mongoose.Schema({ - id: String, - status: Number, - currentTopic: String, - pendingTopic: String, - conversationStartedAt: Date, - lastMessageSentAt: Date, - volley: Number, - rally: Number, - conversation: Number, - prevAns: Number, - slot1: Object, - slot2: Object, - conversationState: Object, - __history__: { - input: [], - reply: [], - topic: [], - stars: [] - } - }); - - userSchema.pre("save", function (next) { - debug.verbose("Pre-Save Hook"); - this.__history__.input = this.__history__.input.slice(0, 15); - this.__history__.reply = this.__history__.reply.slice(0, 15); - this.__history__.topic = this.__history__.topic.slice(0, 15); - this.__history__.stars = this.__history__.stars.slice(0, 15); - next(); - }); - - userSchema.methods.clearConversationState = function(callback) { - this.conversationState = {}; - this.save(callback); - }; - - userSchema.methods.setTopic = function (topic) { - if (topic !== "" || topic !== "undefined") { - debug.verbose("setTopic", topic); - this.pendingTopic = topic; - this.save(function() { - // We should probably have a callback here. - debug.verbose("setTopic Complete"); - }); - - } else { - debug.warn("Trying to set topic to someting invalid"); - } - }; - - userSchema.methods.getTopic = function () { - debug.verbose("getTopic", this.currentTopic); - return this.currentTopic; - }; - - userSchema.methods.updateHistory = function (msg, reply, replyObj, cb) { - var stars = replyObj.stars; - var self = this; - - if (!_.isNull(msg)) { - this.lastMessageSentAt = new Date(); - } - - // New Log format. - var log = { - user_id: this.id, - raw_input: msg.original, - normalized_input: msg.clean, - matched_gambit: replyObj.minMatchSet, - final_output: reply.clean, - timestamp: msg.createdAt - }; - - var cleanId = this.id.replace(/\W/g, ''); - fs.appendFileSync(process.cwd() + "/logs/" + cleanId + "_trans.txt", JSON.stringify(log) + "\r\n"); - - // Did we successfully volley? - // In order to keep the conversation flowing we need to have rythum and this means we always - // need to continue to engage. - if (reply.isQuestion) { - this.volley = 1; - this.rally = this.rally + 1; - } else { - // We killed the rally - this.volley = 0; - this.rally = 0; - } - - this.conversation = this.conversation + 1; - - debug.verbose("Updating History"); - msg.messageScope = null; - - this.__history__.stars.unshift(stars); - this.__history__.input.unshift(msg); - this.__history__.reply.unshift(reply); - this.__history__.topic.unshift(this.currentTopic); - - if (self.pendingTopic !== undefined && self.pendingTopic !== "") { - var pt = self.pendingTopic; - self.pendingTopic = null; - - mongoose.model('Topic').findOne({name:pt}, function(err, topicData) { - if (topicData && topicData.nostay === true) { - self.currentTopic = self.__history__.topic[0]; - } else { - self.currentTopic = pt; - } - self.save(function() { - cb(err, log); - }); - }); - } else { - cb(null, log); - } - }; - - - userSchema.methods.getVar = function (key, cb) { - debug.verbose("getVar", key); - - this.memory.db.get({subject: key, predicate: this.id}, function resultHandle(err, res) { - if (res && res.length !== 0) { - cb(err, res[0].object); - } else { - cb(err, null); - } - }); - }; - - userSchema.methods.setVar = function (key, value, cb) { - debug.verbose("setVar", key, value); - var self = this; - - self.memory.db.get({subject: key, predicate: self.id}, function (err, results) { - if (err) { - console.log(err); - } - - if (!_.isEmpty(results)) { - self.memory.db.del(results[0], function () { - var opt = {subject: key, predicate: self.id, object: value}; - self.memory.db.put(opt, function () { - cb(); - }); - }); - } else { - var opt = {subject: key, predicate: self.id, object: value}; - self.memory.db.put(opt, function (err2) { - if (err2) { - console.log(err2); - } - - cb(); - }); - } - }); - }; - - userSchema.plugin(findOrCreate); - userSchema.virtual("memory").get(function () { - return sfacts.createUserDB(this.id); - }); - - try { - return mongoose.model("User", userSchema); - } catch(e) { - return mongoose.model("User"); - } -}; - - -module.exports = UX; diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index e70e178b..00000000 --- a/lib/utils.js +++ /dev/null @@ -1,251 +0,0 @@ -var debug = require("debug-levels")("SS:Utils"); -var _ = require("lodash"); -var Lex = require("parts-of-speech").Lexer; -var fs = require("fs"); -const RE2 = require('re2') -const regexes = require('./regexes') - - -const encodeCommas = s => s ? regexes.commas.replace(s, '') : s -exports.encodeCommas = encodeCommas - -const encodedCommasRE = new RE2('', 'g') -exports.decodeCommas = s => s ? encodedCommasRE.replace(s, '') : s - - -// todo: rename to normlize to avoid confusion with string.trim() semantics -/** - * Remove extra whitespace from a string, while preserving new lines. - * @param {string} text - the string to tidy up - */ -exports.trim = (text = '') => regexes.space.inner.replace(regexes.whitespace.both.replace(text, ''), ' ') - - -const wordSepRE = new RE2('[\\s*#_|]+') -/** - * Count the number of real words in a string - * @param {string} text - the text to count - * @returns {number} the number of words in `text` - */ -exports.wordCount = text => wordSepRE.split(text).filter(w => w.length > 0).length - -// If needed, switch to _ or lodash -// Array.prototype.chunk = function (chunkSize) { -// var R = []; -// for (var i = 0; i < this.length; i += chunkSize) { -// R.push(this.slice(i, i + chunkSize)); -// } -// return R; -// }; - - -// Contains with value being list -exports.inArray = function (list, value) { - if (_.isArray(value)) { - var match = false; - for (var i = 0; i < value.length; i++) { - if (_.includes(list, value[i]) > 0) { - match = _.indexOf(list, value[i]); - } - } - return match; - } else { - return _.indexOf(list, value); - } -}; - -exports.sentenceSplit = function (message) { - var lexer = new Lex(); - var bits = lexer.lex(message); - var R = []; - var L = []; - for (var i = 0; i < bits.length; i++) { - if (bits[i] === ".") { - // Push the punct - R.push(bits[i]); - L.push(R.join(" ")); - R = []; - } else if (bits[i] === "," && - R.length >= 3 && - _.includes(["who", "what", "where", "when", "why"], bits[i + 1]) - ) { - R.push(bits[i]); - L.push(R.join(" ")); - R = []; - } else { - R.push(bits[i]); - } - } - - // if we havd left over R, push it into L (no punct was found) - if (R.length !== 0) { - L.push(R.join(" ")); - } - - return L; -}; - - -const commandsRE = new RE2('[\\\\.+?${}=!:]', 'g') -const nonCommandsRE = new RE2('[\\\\.+*?\\[^\\]$(){}=!<>|:]', 'g') -/** - * Escape a string sp that it can be used in a regular expression. - * @param {string} string - the string to escape - * @param {boolean} commands - - */ -exports.quotemeta = (string, commands = false) => (commands ? commandsRE : nonCommandsRE).replace(string, c => `\\${c}`) - - -exports.cleanArray = function (actual) { - var newArray = []; - for (var i = 0; i < actual.length; i++) { - if (actual[i]) { - newArray.push(actual[i]); - } - } - return newArray; -}; - - -const aRE = new RE2('^(([bcdgjkpqtuvwyz]|onc?e|onetime)$|e[uw]|uk|ur[aeiou]|use|ut([^t])|uni(l[^l]|[a-ko-z]))', 'i') -const anRE = new RE2('^([aefhilmnorsx]$|hono|honest|hour|heir|[aeiou])', 'i') -const upcaseARE = new RE2('^(UN$)') -const upcaseANRE = new RE2('^$') -const dashSpaceRE = new RE2('[- ]') -const indefiniteArticlerize = word => { - const first = dashSpaceRE.split(word, 2)[0] - const prefix = (anRE.test(first) || upcaseARE.test(first)) && !(aRE.test(first) || upcaseANRE.test(first)) ? 'an' : 'a' - return `${prefix} ${word}` -} -exports.indefiniteArticlerize = indefiniteArticlerize - -exports.indefiniteList = list => { - const n = list.map(indefiniteArticlerize) - if (n.length > 1) { - const last = n.pop(); - return `${n.join(', ')} and ${last}` - } else { - return n.join(", ") - } -} - -var getRandomInt = function (min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; -}; - -exports.getRandomInt = getRandomInt; - -const underscoresRE = new RE2('_', 'g') -exports.pickItem = function (arr) { - // TODO - Item may have a wornet suffix meal~2 or meal~n - var ind = getRandomInt(0, arr.length - 1); - return _.isString(arr[ind]) ? underscoresRE.replace(arr[ind], ' ') : arr[ind] -}; - - -// todo: remove this, use _.capitalize instead -exports.ucwords = function (str) { - return str.toLowerCase().replace(/\b[a-z]/g, function (letter) { - return letter.toUpperCase(); - }); -}; - -// Capital first letter, and add period. -exports.makeSentense = function (string) { - return string.charAt(0).toUpperCase() + string.slice(1) + "."; -}; - - -var tags = { - wword: ["WDT", "WP", "WP$", "WRB"], - nouns: ["NN", "NNP", "NNPS", "NNS"], - verbs: ["VB", "VBD", "VBG", "VBN", "VBP", "VBZ"], - adjectives: ["JJ", "JJR", "JJS"] -}; - -exports._isTag = function (pos, _class) { - return !!(tags[_class].indexOf(pos) > -1); -}; - -exports.mkdirSync = function (path) { - try { - fs.mkdirSync(path); - } catch(e) { - if (e.code !== "EEXIST") { - throw e; - } - } -}; - -exports.genId = function () { - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (var i = 0; i < 8; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -}; - - -/** - * Search each string in `strings` for `` tags and replace them with values from `caps`. - * - * Replacement is positional so `` replaces with `caps[1]` and so on, with `` also replacing from `caps[1]`. - * Empty `strings` are removed from the result. - * - * @param {Array} strings - text to search for `` tags - * @param {Array} caps - replacement text - */ -exports.replaceCapturedText = (strings, caps) => { - const encoded = caps.map(s => encodeCommas(s)) - return strings - .filter(s => !_.isEmpty(s)) - .map(s => regexes.captures.replace(s, (m, p1) => encoded[Number.parseInt(p1 || 1)])) -} - - -var walk = function (dir, done) { - - if (fs.statSync(dir).isFile()) { - debug.verbose("Expected directory, found file, simulating directory with only one file: %s", dir) - return done(null, [dir]); - } - - var results = []; - fs.readdir(dir, function (err1, list) { - if (err1) { - return done(err1); - } - var pending = list.length; - if (!pending) { - return done(null, results); - } - list.forEach(function (file) { - file = dir + "/" + file; - fs.stat(file, function (err2, stat) { - if (err2) { - console.log(err2); - } - - if (stat && stat.isDirectory()) { - var cbf = function (err3, res) { - results = results.concat(res); - if (!--pending) { - done(err3, results); - } - }; - - walk(file, cbf); - } else { - results.push(file); - if (!--pending) { - done(null, results); - } - } - }); - }); - }); -}; - -exports.walk = walk; diff --git a/package.json b/package.json index a522f5a6..3ff6fa65 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,29 @@ { "name": "superscript", - "version": "0.12.2", + "version": "1.0.0-alpha2", "description": "A dialog system and bot engine for creating human-like chat bots.", - "main": "index.js", + "main": "lib/bot/index.js", "scripts": { - "lint": "eslint --env node lib *.js", - "test": "mocha test -R spec -s 1700 -t 300000", - "profile": "mocha test -R spec -s 1700 -t 300000 --prof --log-timer-events", - "dtest": "DEBUG=*,-mquery,-mocha*, mocha test -R spec -s 1700 -t 300000", - "itest": "DEBUG=SS* DEBUG_LEVEL=info mocha test -R spec -s 1700 -t 300000", - "test-travis": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- -R spec test -s 1700 -t 300000" + "build": "babel src --presets babel-preset-es2015 --out-dir lib", + "lint": "eslint --env node src *.js", + "test": "mocha --compilers js:babel-register test -R spec -s 1700 -t 300000 --recursive", + "prepublish": "npm run build", + "profile": "mocha --compilers js:babel-register test -R spec -s 1700 -t 300000 --prof --log-timer-events --recursive", + "dtest": "DEBUG=*,-mquery,-mocha*, mocha --compilers js:babel-register test -R spec -s 1700 -t 300000 --recursive", + "itest": "DEBUG=SS* DEBUG_LEVEL=info mocha --compilers js:babel-register test -R spec -s 1700 -t 300000 --recursive", + "test-travis": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- --compilers js:babel-register -R spec test -s 1700 -t 300000 --recursive" }, "repository": { "type": "git", - "url": "https://github.com/silentrob/superscript" + "url": "https://github.com/superscriptjs/superscript" }, "homepage": "http://superscriptjs.com", "bugs": { - "url": "https://github.com/silentrob/superscript/issues" + "url": "https://github.com/superscriptjs/superscript/issues" }, "bin": { - "parse": "bin/parse.js", - "bot-init": "bin/bot-init.js", - "bot-cleanup": "bin/cleanup.js" + "parse": "lib/bin/parse.js", + "bot-init": "lib/bin/bot-init.js" }, "author": "Rob Ellis", "contributors": [ @@ -30,66 +31,50 @@ "Issam Hakimi ", "Marius Ursache ", "Michael Lewkowitz ", - "John Wehr " - ], - "licenses": [ - { - "type": "MIT", - "url": "https://raw.github.com/silentrob/superscript/master/LICENSE" - } + "John Wehr ", + "Ben James " ], + "license": "MIT", "dependencies": { - "async": "^1.5.2", + "async": "^2.1.2", "async-replace": "^1.0.1", - "bluebird": "^3.0.5", - "checksum": "^0.1.1", "commander": "^2.4.0", - "crc": "^3.0.0", "debug": "^2.2.0", "debug-levels": "^0.2.0", - "deepmerge": "^0.2.7", "lemmer": "0.1.6", - "lodash": "^4.12.0", - "mkdirp": "^0.5.0", + "lodash": "^4.16.5", + "mkdirp": "^0.5.1", "moment": "^2.13.0", - "mongodb": "^2.2.9", "mongoose": "^4.5.10", "mongoose-findorcreate": "^0.1.2", - "mongoose-path-tree": "^1.3.5", "natural": "^0.4.0", - "node-normalizer": "^0.1.4", - "node-tense": "0.0.4", + "node-normalizer": "^1.0.0-alpha3", "parts-of-speech": "^0.3.0", - "pluralize": "2.0.0", - "qtypes": "^0.1.6", + "pluralize": "^3.0.0", + "qtypes": "^1.0.0-alpha1", "re2": "^1.3.3", - "require-dir": "0.3.0", - "rhyme": "0.0.3", - "rimraf": "^2.4.4", - "roman-numerals": "~0.3.2", - "safe-eval": "^0.2.0", - "sfacts": "^0.2.0", - "ss-parser": "^0.5.0", + "require-dir": "^0.3.1", + "rhymes": "^1.0.1", + "roman-numerals": "^0.3.2", + "safe-eval": "^0.3.0", + "sfacts": "^1.0.0-alpha2", + "ss-parser": "^1.0.0-alpha1", "string": "^3.3.1", - "syllablistic": "~0.1.0" + "syllablistic": "^0.1.0", + "wordnet-db": "^3.1.2" }, "devDependencies": { - "blanket": "~1.1.6", - "bluebird": "^2.9.34", + "babel-cli": "^6.16.0", + "babel-preset-es2015": "^6.16.0", + "babel-register": "^6.18.0", "coveralls": "^2.11.9", - "eslint": "^0.23.0", - "istanbul": "^0.4.3", - "mkdirp": "^0.5.0", + "eslint": "^3.7.1", + "eslint-config-airbnb": "^12.0.0", + "eslint-plugin-import": "^1.16.0", + "eslint-plugin-jsx-a11y": "^2.2.3", + "eslint-plugin-react": "^6.4.1", + "istanbul": "^1.1.0-alpha.1", "mocha": "^3.0.2", - "mongodb": "^2.0.39", - "rimraf": "^2.4.2", - "rmdir": "^1.0.4", "should": "^11.1.0" - }, - "config": { - "blanket": { - "pattern": "/lib", - "data-cover-never": "node_modules" - } } } diff --git a/plugins/alpha.js b/plugins/alpha.js deleted file mode 100644 index 2ac95548..00000000 --- a/plugins/alpha.js +++ /dev/null @@ -1,144 +0,0 @@ -var rhyme = require('rhyme'); -var syllabistic = require('syllablistic'); -var debug = require("debug")("AlphaPlugins"); -var _ = require("lodash"); - -var getRandomInt = function (min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; -}; - -exports.oppisite = function(word, cb) { - - debug("oppisite", word); - - this.facts.db.get({subject:word, predicate: "opposite"}, function(err, opp) { - - if (!_.isEmpty(opp)) { - var oppisiteWord = opp[0].object; - oppisiteWord = oppisiteWord.replace(/_/g, " "); - cb(null, oppisiteWord); - } else { - cb(null, ""); - } - - }); -}; - -// This uses rhyme and it is painfully slow -exports.rhymes = function(word, cb) { - - debug("rhyming", word); - - rhyme(function (r) { - var rhymedWords = r.rhyme(word); - var i = getRandomInt(0, rhymedWords.length - 1); - - if (rhymedWords.length !== 0) { - cb(null, rhymedWords[i].toLowerCase()); - } else { - cb(null, null); - } - }); -}; - -exports.syllable = function(word, cb) { - cb(null, syllabistic.text(word)); -}; - -exports.letterLookup = function(cb) { - - var math = require("../lib/math"); - var reply = ""; - - var lastWord = this.message.lemWords.slice(-1)[0]; - debug("--LastWord", lastWord); - debug("LemWords", this.message.lemWords); - var alpha = "abcdefghijklmonpqrstuvwxyz".split(""); - var pos = alpha.indexOf(lastWord); - debug("POS", pos); - if (this.message.lemWords.indexOf("before") != -1) { - if (alpha[pos - 1]) { - reply = alpha[pos - 1].toUpperCase(); - } else { - reply = "Don't be silly, there is nothing before A"; - } - } else if (this.message.lemWords.indexOf("after") != -1) { - if (alpha[pos + 1]) { - reply = alpha[pos + 1].toUpperCase(); - } else { - reply = "haha, funny."; - } - } else { - var i = this.message.lemWords.indexOf("letter"); - var loc = this.message.lemWords[i - 1]; - - if (loc == "first") { - reply = "It is A."; - } else if (loc == "last") { - reply = "It is Z."; - } else { - - // Number or word number - // 1st, 2nd, 3rd, 4th or less then 99 - if ((loc === "st" || loc === "nd" || loc === "rd" || loc === "th") && this.message.numbers.length !== 0 ) { - var num = parseInt(this.message.numbers[0]); - if (num > 0 && num <= 26) { - reply = "It is " + alpha[num - 1].toUpperCase(); - } else { - reply = "seriously..."; - } - } - } - } - cb(null, reply); -}; - -exports.wordLength = function(cap, cb) { - if (typeof cap == "string") { - var parts = cap.split(" "); - if (parts.length == 1) { - cb(null, cap.length); - } else { - if (parts[0].toLowerCase() == "the" && parts.length == 3) { - // name bill, word bill - cb(null, parts.pop().length); - } else if (parts[0] == "the" && parts[1].toLowerCase() == "alphabet") { - cb(null, "26"); - } else if (parts[0] == "my" && parts.length == 2) { - // Varible lookup - var lookup = parts[1]; - this.user.getVar(lookup, function(e,v){ - if (v !== null && v.length) { - cb(null, "There are "+ v.length +" letters in your " + lookup + "."); - } else { - cb(null, "I don't know"); - } - }); - } else if (parts[0] == "this" && parts.length == 2) { - // this phrase, this sentence - cb(null, "That phrase has " + this.message.raw.length + " characters. I think."); - } else { - cb(null, "I think there is about 10 characters. :)"); - } - } - - } else { - cap(null,""); - } -}; - -exports.nextNumber = function(cb) { - var reply = ""; - var num = this.message.numbers.slice(-1)[0]; - - if (num) { - if (this.message.lemWords.indexOf("before") !== -1) { - reply = parseInt(num) - 1; - } - if (this.message.lemWords.indexOf("after") !== -1) { - reply = parseInt(num) + 1; - } - } - - cb(null, reply); -}; \ No newline at end of file diff --git a/plugins/compare.js b/plugins/compare.js deleted file mode 100644 index b57cfbdc..00000000 --- a/plugins/compare.js +++ /dev/null @@ -1,269 +0,0 @@ -var debug = require("debug")("Compare Plugin"); -var history = require("../lib/history"); -var Utils = require("../lib/utils"); -var _ = require("lodash"); -var async = require("async"); - -exports.createFact = function(s, v, o, cb) { - var that = this; - this.user.memory.create(s, v, o, false, function() { - that.facts.db.get({subject:v, predicate: 'opposite'}, function(e,r){ - if (r.length != 0) { - that.user.memory.create(o, r[0].object, s, false, function() { - cb(null,""); - }); - } else { - cb(null,""); - } - }); - }); -} - -exports.resolveAdjective = function(cb) { - - var candidates = history(this.user, { names: true }); - var message = this.message; - var factsDB = this.user.memory.db; - var gDB = this.facts.db; - var that = this; - - - - var negatedTerm = function(msg, names, cb) { - // Are we confused about what we are looking for??! - // Could be "least tall" negated terms - if (_.contains(msg.adjectives, "least") && msg.adjectives.length == 2) { - - // We need to flip the adjective to the oppisite and do a lookup. - var cmpWord = _.without(msg.adjectives,"least"); - getOpp(cmpWord[0], function(err, oppWord) { - - // TODO - What if there is no oppWord? - // TODO - What if we have more than 2 names? - - debug("Lookup", oppWord, names); - if (names.length === 2) { - - var pn1 = names[0].toLowerCase(); - var pn2 = names[1].toLowerCase(); - - factsDB.get({ subject: pn1, predicate: oppWord, object: pn2 }, function(e, r) { - // r is treated as 'truthy' - if (!_.isEmpty(r)){ - cb(null, Utils.ucwords(pn1) + " is " + oppWord+"er."); - } else { - cb(null, Utils.ucwords(pn2) + " is " + oppWord+"er."); - } - }); - - } else { - cb(null, Utils.ucwords(names) + " is " + oppWord+"er."); - } - }); - - } else { - // We have no idea what they are searching for - cb(null, "???"); - } - } - - var getOpp = function(term, callback) { - - gDB.search({ subject: term, predicate: 'opposite', object: - gDB.v('opp')}, function(e, oppResult) { - if (!_.isEmpty(oppResult)) { - callback(null, oppResult[0].opp); - } else { - callback(null, null); - } - }); - } - - // This will return the adjective from the message, or the oppisite term in some cases - // "least short" => tall - // "less tall" => short - var baseWord = null; - var getAdjective = function(m, cb) { - var cmpWord; - - if (findOne(m.adjectives, ["least", "less"])) { - cmpWord = _.first(_.difference(m.adjectives, ["least", "less"])); - baseWord = cmpWord; - getOpp(cmpWord, cb); - } else { - cb(null, (m.compareWords[0]) ? m.compareWords[0] : m.adjectives[0]); - } - } - - // We may want to roll though all the candidates?!? - // These examples are somewhat forced. (over fitted) - if (candidates) { - var prevMessage = candidates[0]; - - if (prevMessage && prevMessage.names.length === 1) { - - cb(null, "It is " + prevMessage.names[0] + "."); - - } else if (prevMessage && prevMessage.names.length > 1) { - - // This could be: - // Jane is older than Janet. Who is the youngest? - // Jane is older than Janet. Who is the younger A or B? - // My parents are John and Susan. What is my mother called? - - if ((message.compareWords.length === 1 || message.adjectives.length === 1)) { - - var handle = function(e, cmpTerms) { - var compareWord = cmpTerms[0]; - var compareWord2 = cmpTerms[1]; - - debug("CMP ", compareWord, compareWord2); - - - gDB.get({ subject: compareWord, predicate: 'opposite', object: compareWord2}, function(e, oppResult) { - - debug("Looking for Opp of", compareWord, oppResult); - - // Jane is older than Janet. Who is the older Jane or Janet? - if (!_.isEmpty(message.names)) { - debug("We have names", message.names); - // Make sure we say a name they are looking for. - var nameOne = message.names[0].toLowerCase() - - factsDB.get({ subject: nameOne, predicate: compareWord }, function(e, result) { - - if (_.isEmpty(result)) { - // So the fact is wrong, lets try the other way round - - factsDB.get({ object: nameOne, predicate: compareWord}, function(e, result) { - debug("RES", result) - - if (!_.isEmpty(result)) { - if (message.names.length === 2 && result[0].subject == message.names[1]) { - cb(null, Utils.ucwords(result[0].subject) + " is " + compareWord+"er than " + Utils.ucwords(result[0].object) + "."); - } else if (message.names.length === 2 && result[0].subject != message.names[1]) { - // We can guess or do something more clever? - cb(null, Utils.ucwords(message.names[1]) + " is " + compareWord+"er than " + Utils.ucwords(result[0].object) + "."); - } else { - cb(null, Utils.pickItem(message.names) + " is " + compareWord+"er?"); - } - - } else { - // Lets do it again if we have another name - cb(null, Utils.pickItem(message.names) + " is " + compareWord+"er?"); - } - }); - } else { - // This could be a <-> b <-> c (is a << c ?) - - factsDB.search([ - {subject: nameOne, predicate: compareWord, object: factsDB.v("f") }, - {subject: factsDB.v("f"), predicate: compareWord, object: factsDB.v("v")} - ], function(err, results) { - if (!_.isEmpty(results)) { - if (results[0]['v'] == message.names[1].toLowerCase()) { - cb(null, Utils.ucwords(message.names[0]) + " is " + compareWord + "er than " + Utils.ucwords(message.names[1]) + "."); - } else { - // Test this - cb(null, Utils.ucwords(message.names[1]) + " is " + compareWord + "er than " + Utils.ucwords(message.names[0]) + "."); - } - } else { - // Test this block - cb(null, Utils.pickItem(message.names) + " is " + compareWord+"er?"); - } - }); - } - }); - - } else { - - debug("NO NAMES"); - // Which of them is the ? - // This message has NO names - // Jane is older than Janet. **Who is the older?** - // Jane is older than Janet. **Who is the youngest?** - - // We pre-lemma the adjactives, so we need to fetch the raw word from the dict. - // We could have "Who is the oldest" - // If the word has been flipped, it WONT be in the dictionary, but we have a cache of it - var fullCompareWord = (baseWord) ? - message.dict.findByLem(baseWord).word : - message.dict.findByLem(compareWord).word; - - - // Looking for an end term - if (fullCompareWord.indexOf("est") > 0) { - - factsDB.search([ - {subject: - factsDB.v("oldest"), predicate: compareWord, object: - factsDB.v("rand1") }, - {subject: - factsDB.v("oldest"), predicate: compareWord, object: - factsDB.v("rand2") } - ], function(err, results) { - if (!_.isEmpty(results)) { - cb(null, Utils.ucwords(results[0]['oldest']) + " is the " + compareWord + "est."); - } else { - // Pick one. - cb(null, Utils.ucwords(Utils.pickItem(prevMessage.names)) + " is the " + compareWord + "est."); - } - }); - } else { - - if (!_.isEmpty(oppResult)) { - // They are oppisite, but lets check to see if we have a true fact - - factsDB.get({ subject: prevMessage.names[0].toLowerCase(), predicate: compareWord }, function(e, result) { - if (!_.isEmpty(result)) { - if (message.qSubType == "YN") { - cb(null, "Yes, " + Utils.ucwords(result[0].object) + " is " + compareWord+"er.") - } else { - cb(null, Utils.ucwords(result[0].object) + " is " + compareWord+"er than " + prevMessage.names[0] + ".") - } - } else { - if (message.qSubType == "YN") { - cb(null, "Yes, " + Utils.ucwords(prevMessage.names[1]) + " is " + compareWord+"er."); - } else { - cb(null, Utils.ucwords(prevMessage.names[1]) + " is " + compareWord+"er than " + prevMessage.names[0] + "."); - } - } - }); - } else if (compareWord == compareWord2) { - // They are the same adjectives - // No names. - if (message.qSubType == "YN") { - cb(null, "Yes, " + Utils.ucwords(prevMessage.names[0]) + " is " + compareWord+"er."); - } else { - cb(null, Utils.ucwords(prevMessage.names[0]) + " is " + compareWord+"er than " + prevMessage.names[1] + "."); - } - - } else { - // not opposite terms. - cb(null, "Those things don't make sense to compare."); - } - } - } - - }); - - } - - async.map([message, prevMessage], getAdjective, handle); - - } else { - negatedTerm(message, prevMessage.names, cb); - } - - } - } else { - cb(null, "??"); - } - -} - -var findOne = function (haystack, arr) { - return arr.some(function (v) { - return haystack.indexOf(v) >= 0; - }); -}; diff --git a/plugins/math.js b/plugins/math.js deleted file mode 100644 index de7a11ee..00000000 --- a/plugins/math.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - - Math functions for - - evaluating expressions - - converting functions - - sequence functions -*/ - -var math = require("../lib/math"); -var roman = require('roman-numerals'); -var debug = require("debug")("mathPlugin"); - -exports.evaluateExpression = function(cb) { - if (this.message.numericExp || (this.message.halfNumericExp && this.user.prevAns)) { - var answer = math.parse(this.message.cwords, this.user.prevAns); - if (answer) { - this.user.prevAns = answer; - console.log("Prev", this.user); - var suggestedReply = "I think it is " + answer; - } else { - var suggestedReply = "What do I look like, a computer?"; - } - cb(null, suggestedReply); - } else { - cb(true, ""); - } -} - -exports.numToRoman = function(cb) { - suggest = "I think it is " + roman.toRoman(this.message.numbers[0]); - cb(null, suggest); -} - -exports.numToHex = function(cb) { - suggest = "I think it is " + parseInt(this.message.numbers[0], 10).toString(16); - cb(null, suggest); -} - -exports.numToBinary = function(cb) { - var suggest = "I think it is " + parseInt(this.message.numbers[0], 10).toString(2); - cb(null, suggest); -} - - -exports.numMissing = function(cb) { - // What number are missing 1, 3, 5, 7 - if (this.message.lemWords.indexOf("missing") != -1 && this.message.numbers.length != 0) { - var numArray = this.message.numbers.sort(); - var mia = []; - for(var i = 1; i < numArray.length; i++) { - if(numArray[i] - numArray[i-1] != 1) { - var x = numArray[i] - numArray[i-1]; - var j = 1; - while (j < x) { - mia.push(parseFloat(numArray[i-1])+j); - j++; - } - } - } - var s = mia.sort(function(a, b){return a-b}); - cb(null, "I think it is " + s.join(" ")); - } else { - cb(true, ""); - } -} - -// Sequence -exports.numSequence = function(cb) { - if (this.message.lemWords.indexOf("sequence") != -1 && this.message.numbers.length != 0) { - debug("Finding the next number in the series") - var numArray = this.message.numbers.map(function(item){return parseInt(item)}) - numArray = numArray.sort(function(a, b){return a-b}); - - if (math.arithGeo(numArray) == "Arithmetic") { - for(var i = 1; i < numArray.length; i++) { - var x = numArray[i] - numArray[i-1]; - } - suggest = "I think it is " + (parseInt(numArray.pop()) + x); - } else if (math.arithGeo(numArray) == "Geometric") { - var a = numArray[1]; - var r = a / numArray[0]; - suggest = "I think it is " + numArray.pop() * r; - } - - cb(null, suggest); - } else { - cb(true, ""); - } -} \ No newline at end of file diff --git a/plugins/reason.js b/plugins/reason.js deleted file mode 100644 index 2e45888b..00000000 --- a/plugins/reason.js +++ /dev/null @@ -1,361 +0,0 @@ - -var debug = require("debug")("Reason Plugin"); -var history = require("../lib/history"); -var Utils = require("../lib/utils"); -var _ = require("lodash"); -var moment = require("moment"); -var wd = require("../lib/reply/wordnet"); - -exports.hasName = function(bool, cb) { - this.user.getVar('name', function(e,name){ - if (name !== null) { - cb(null, (bool == "true") ? true : false) - } else { - // We have no name - cb(null, (bool == "false") ? true : false) - } - }); -} - -exports.has = function(value, cb) { - this.user.getVar(value, function(e, uvar){ - cb(null, (uvar === undefined) ? false : true); - }); -} - -exports.findLoc = function(cb) { - var candidates = history(this.user, { names: true }); - if (!_.isEmpty(candidates)) { - debug("history candidates", candidates); - var c = candidates[0]; - - if (c.names.length == 1) { - suggest = "In " + c.names[0]; - } else if (c.names.length == 2) { - suggest = "In " + c.names[0] + ", " + c.names[1] + "."; - } else { - suggest = "In " + Utils.pickItem(c.names); - } - - cb(null, suggest); - } else { - cb(null, "I'm not sure where you lived."); - } -} - -exports.tooAdjective = function(cb) { - // what is/was too small? - var message = this.message; - var candidates = history(this.user, { adjectives: message.adjectives }); - debug("adj candidates", candidates); - - if (candidates.length != 0 && candidates[0].cNouns.length != 0) { - var choice = candidates[0].cNouns.filter(function(item){ return item.length >= 3 }); - var too = (message.adverbs.indexOf("too") != -1) ? "too " : ""; - suggest = "The " + choice.pop() + " was " + too + message.adjectives[0] + "."; - // suggest = "The " + choice.pop() + " was too " + message.adjectives[0] + "."; - } else { - suggest = ""; - } - - cb(null, suggest); -} - -exports.usedFor = function(cb) { - var that = this; - this.cnet.usedForForward(that.message.nouns[0], function(e,r){ - if (!_.isEmpty(r)) { - var res = (r) ? Utils.makeSentense(r[0].sentense) : ""; - cb(null, res); - } else { - cb(null,""); - } - }); -} - -exports.resolveFact = function(cb) { - // Resolve this - var message = this.message; - var t1 = message.nouns[0]; - var t2 = message.adjectives[0]; - - this.cnet.resolveFact(t1, t2, function(err, res){ - if (res) { - cb(null, "It sure is."); - } else { - cb(null, "I'm not sure."); - } - }); -} - - -exports.putA = function(cb) { - var that = this; - var thing = (that.message.entities[0]) ? that.message.entities[0] : that.message.nouns[0]; - var userfacts = that.user.memory.db; - - if (thing) { - this.cnet.putConcept(thing, function(e, putThing){ - if (putThing) { - cb(null, Utils.makeSentense(Utils.indefiniteArticlerize(putThing))); - } else { - cb(null, ""); - } - }); - } -} - -exports.isA = function(cb) { - var that = this; - var thing = (that.message.entities[0]) ? that.message.entities[0] : that.message.nouns[0]; - var userfacts = that.user.memory.db; - var userID = that.user.name; - - if (thing) { - this.cnet.isAForward(thing, function(e,r){ - if (!_.isEmpty(r)) { - var res = (r) ? Utils.makeSentense(r[0].sentense) : ""; - cb(null, res); - } else { - // Lets try wordnet - wd.define(thing, function(err, result){ - if (err) { - cb(null, ""); - } else { - cb(null, result); - } - }); - } - }); - } else { - var thing = ""; - // my x is adj => what is adj - if (that.message.adverbs[0]) { - thing = that.message.adverbs[0]; - } else { - thing = that.message.adjectives[0]; - } - userfacts.get({object:thing, predicate: userID}, function(err, list) { - if (!_.isEmpty(list)){ - // Because it came from userID it must be his - cb(null, "You said your " + list[0].subject + " is " + thing + "."); - } else { - // find example of thing? - cb(null, ""); - } - }); - } -} - -exports.colorLookup = function(cb) { - var that = this; - var message = this.message; - var things = message.entities.filter(function(item) { if (item != "color") return item; }); - var suggest = ""; - var facts = that.facts.db; - var userfacts = that.user.memory.db; - var botfacts = that.botfacts.db; - var userID = that.user.name; - - // TODO: This could be improved adjectives may be empty - var thing = (things.length == 1) ? things[0] : message.adjectives[0]; - - if(thing != "" && message.pnouns.length == 0) { - - // What else is green (AKA Example of green) OR - // What color is a tree? - - var fthing = thing.toLowerCase().replace(" ", "_"); - - // ISA on thing - facts.get({ object: fthing, predicate:'color'}, function(err, list) { - if (!_.isEmpty(list)) { - var thingOfColor = Utils.pickItem(list); - var toc = thingOfColor.subject.replace(/_/g, " "); - - cb(null, Utils.makeSentense(Utils.indefiniteArticlerize(toc) + " is " + fthing)); - } else { - facts.get({ subject: fthing, predicate:'color'}, function(err, list) { - if (!_.isEmpty(list)) { - suggest = "It is " + list[0].object + "."; - cb(null, suggest); - } else { - - that.cnet.resolveFact("color", thing, function(err, res){ - if (res) { - suggest = "It is " + res + "."; - } else { - suggest = "It depends, maybe brown?"; - } - cb(null, suggest); - }); - } - }); - } - }); - - } else if (message.pronouns.length != 0){ - // Your or My color? - // TODO: Lookup a saved or cached value. - - // what color is my car - // what is my favoirute color - if (message.pronouns.indexOf("my") != -1) { - - // my car is x - userfacts.get({subject:message.nouns[1], predicate: userID}, function(err, list) { - - if (!_.isEmpty(list)) { - var color = list[0].object; - var lookup = message.nouns[1]; - var toSay = ["Your " + lookup + " is " + color + "."] - - facts.get({object:color, predicate: 'color'}, function(err, list) { - if (!_.isEmpty(list)) { - var thingOfColor = Utils.pickItem(list); - var toc = thingOfColor.subject.replace(/_/g, " "); - toSay.push("Your " + lookup + " is the same color as a " + toc + "."); - } - cb(null, Utils.pickItem(toSay)); - }); - } else { - // my fav color - we need - var pred = message.entities[0]; - userfacts.get({subject: thing, predicate: pred }, function(err, list) { - debug("!!!!", list) - if (!_.isEmpty(list)) { - var color = list[0].object; - cb(null,"Your " + thing + " " + pred + " is " + color + "."); - } else { - cb(null,"You never told me what color your " + thing + " is."); - } - }); - - } - }); - } else if (message.pronouns.indexOf("your") != -1) { - // Do I have a /thing/ and if so, what color could or would it be? - - botfacts.get({subject:thing, predicate: 'color'}, function(err, list) { - if (!_.isEmpty(list)) { - var thingOfColor = Utils.pickItem(list); - var toc = thingOfColor.object.replace(/_/g, " "); - cb(null, "My " + thing + " color is " + toc + "."); - } else { - debug("---", {subject:thing, predicate: 'color'}) - // Do I make something up or just continue? - cb(null, ""); - } - }); - } - } else { - suggest = "It is blue-green in color."; - cb(null, suggest); - } -} - -exports.makeChoice = function(cb) { - var that = this; - if (!_.isEmpty(that.message.list)) { - // Save the choice so we can refer to our decision later - var sect = _.difference(that.message.entities, that.message.list); - // So I believe sect[0] is the HEAD noun - - if(sect.length === 0){ - // What do you like? - var choice = Utils.pickItem(that.message.list); - cb(null, "I like " + choice + "."); - } else { - // Which do you like? - that.cnet.filterConcepts(that.message.list, sect[0], function(err, results) { - var choice = Utils.pickItem(results); - cb(null, "I like " + choice + "."); - }); - } - - } else { - cb(null,"") - } -} - -exports.findMoney = function(cb) { - - var candidates = history(this.user, { nouns: this.message.nouns, money: true }); - if (candidates.length != 0) { - cb(null, "It would cost $" + candidates[0].numbers[0] + "."); - } else { - cb(null, "Not sure."); - } -} - -exports.findDate = function(cb){ - var candidates = history(this.user, { date: true }); - if (candidates.length != 0) { - debug("DATE", candidates[0]) - cb(null, "It is in " + moment(candidates[0].date).format("MMMM") + "."); - } else { - cb(null, "Not sure."); - } -} - -exports.locatedAt = function(cb) { - debug("LocatedAt"); - var args = Array.prototype.slice.call(arguments); - var place; - - if (args.length === 2) { - place = args[0]; - cb = args[1]; - } else { - cb = args[0]; - // Pull the place from the history - var reply = this.user.getLastReply(); - if (reply && reply.nouns.length != 0); - place = reply.nouns.pop(); - } - - // var thing = entities.filter(function(item){if (item != "name") return item }) - this.cnet.atLocationReverse(place, function(err, results){ - if (!_.isEmpty(results)) { - var itemFound = Utils.pickItem(results); - cb(null,Utils.makeSentense("you might find " + Utils.indefiniteArticlerize(itemFound.c1_text) + " at " + Utils.indefiniteArticlerize(place))); - } else { - cb(null,""); - } - - }); -} - -exports.aquireGoods = function(cb) { - // Do you own a - var that = this; - var message = that.message; - var thing = (message.entities[0]) ? message.entities[0] : message.nouns[0]; - var botfacts = that.botfacts.db; - var cnet = that.cnet; - var reason = ""; - - botfacts.get({subject:thing, predicate: 'ownedby', object: 'bot'}, function(err, list) { - debug("!!!", list) - if (!_.isEmpty(list)){ - // Lets find out more about it. - - cb(null, "Yes"); - } else { - // find example of thing? - // what is it? - cnet.usedForForward(thing, function(err, res){ - - if (res) { - reason = Utils.pickItem(res); - reason = reason.frame2; - botfacts.put({subject:thing, predicate: 'ownedby', object: 'bot'}, function(err, list) { - cb(null, "Yes, I used it for " + reason + "."); - }); - } else { - cb(null, "NO"); - } - }) - } - }); -} diff --git a/plugins/test.js b/plugins/test.js deleted file mode 100644 index 1825aab1..00000000 --- a/plugins/test.js +++ /dev/null @@ -1,84 +0,0 @@ -var _ = require("lodash"); - -// This is used in a test to verify fall though works -// TODO: Move this into a fixture. -exports.bail = function(cb) { - cb(true, null); -} - -exports.one = function(cb) { - cb(null, "one"); -} - -exports.num = function(n, cb) { - cb(null, n); -} - -exports.changetopic = function(n,cb) { - this.user.setTopic(n); - cb(null, ""); -} - -exports.changefunctionreply = function(newtopic,cb) { - cb(null, "{topic="+ newtopic + "}"); -} - -exports.doSomething = function(cb) { - console.log('this.message.raw', this.message.raw); - cb(null, "function"); -} - -exports.break = function(cb) { - cb(null, "", true); -} - -exports.nobreak = function(cb) { - cb(null, "", false); -} - -exports.objparam1 = function(cb) { - - var data = { - "text": "world", - "attachments": [ - { - "text": "Optional text that appears *within* the attachment", - } - ] - } - cb(null, data); -} - -exports.objparam2 = function(cb) { - cb(null, {test: "hello", text: "world"}); -} - - -exports.showScope = function(cb) { - cb(null, this.message_props.key + " " + this.user.id + " " + this.message.raw); -} - -exports.word = function(word1, word2, cb) { - cb(null, word1 === word2); -} - -exports.hasFirstName = function(bool, cb) { - this.user.getVar('firstName', function(e,name){ - if (name != null) { - cb(null, (bool == "true") ? true : false) - } else { - cb(null, (bool == "false") ? true : false) - } - }); -} - -exports.getUserId = function(cb) { - var userID = this.user.id; - var that = this; - // console.log("CMP1", _.isEqual(userID, that.user.id)); - return that.bot.getUser("userB", function(err, user) { - console.log("CMP2", _.isEqual(userID, that.user.id)); - cb(null, that.user.id); - }); -} - diff --git a/plugins/time.js b/plugins/time.js deleted file mode 100644 index 82debe39..00000000 --- a/plugins/time.js +++ /dev/null @@ -1,96 +0,0 @@ -var moment = require("moment"); -var COEFF = 1000 * 60 * 5; - -var getSeason = function() { - - var now = moment(); - now.dayOfYear() - var doy = now.dayOfYear(); - - if (doy > 80 && doy < 172) { - return "spring"; - } else if (doy > 172 && doy < 266) { - return "summer" - } else if (doy > 266 && doy < 357) { - return "fall" - } else if ( doy < 80 || doy > 357) { - return "winter"; - } -} - -exports.getDOW = function(cb) { - cb(null, moment().format("dddd")); -} - -exports.getDate = function(cb) { - cb(null, moment().format("ddd, MMMM Do")); -} - -exports.getDateTomorrow = function(cb) { - var date = moment().add('d', 1).format("ddd, MMMM Do"); - cb(null, date); -} - -exports.getSeason = function(cb) { - var date = moment().add('d', 1).format("ddd, MMMM Do"); - cb(null, getSeason()); -} - -exports.getTime = function(cb) { - var date = new Date(); - var rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); - var time = moment(rounded).format("h:mm"); - cb(null, "The time is " + time); -} - -exports.getGreetingTimeOfDay = function(cb) { - var date = new Date(); - var rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); - var time = moment(rounded).format("H") - var tod - if (time < 12) { - tod = "morning" - } else if (time < 17) { - tod = "afternoon" - } else { - tod = "evening" - } - - cb(null, tod); -} - -exports.getTimeOfDay = function(cb) { - var date = new Date(); - var rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); - var time = moment(rounded).format("H") - var tod - if (time < 12) { - tod = "morning" - } else if (time < 17) { - tod = "afternoon" - } else { - tod = "night" - } - - cb(null, tod); -} - -exports.getDayOfWeek = function(cb) { - cb(null, moment().format("dddd")); -} - -exports.getMonth = function(cb) { - var reply = ""; - if (this.message.words.indexOf("next") != -1) { - reply = moment().add('M', 1).format("MMMM"); - } else if (this.message.words.indexOf("previous") != -1) { - reply = moment().subtract('M', 1).format("MMMM"); - } else if (this.message.words.indexOf("first") != -1) { - reply = "January"; - } else if (this.message.words.indexOf("last") != -1) { - reply = "December"; - } else { - var reply = moment().format("MMMM"); - } - cb(null, reply); -} diff --git a/plugins/user.js b/plugins/user.js deleted file mode 100644 index 89193230..00000000 --- a/plugins/user.js +++ /dev/null @@ -1,92 +0,0 @@ -var debug = require("debug")("SS:UserFacts"); -var _ = require("lodash"); -exports.save = function(key, value, cb) { - var memory = this.user.memory; - var userId = this.user.id; - - if (arguments.length !== 3) { - console.log("WARNING\nValue not found in save function."); - if (_.isFunction(value)) { - cb = value; - value = ""; - } - } - - memory.db.get({subject:key, predicate: userId }, function(err, results) { - if (!_.isEmpty(results)) { - memory.db.del(results[0], function(){ - memory.db.put({subject:key, predicate: userId, object: value}, function(){ - cb(null,""); - }); - }); - } else { - memory.db.put({subject:key, predicate: userId, object: value}, function(err){ - cb(null, ""); - }); - } - }); - - -} - -exports.hasItem = function(key, bool, cb) { - - var memory = this.user.memory; - var userId = this.user.id; - - debug("getVar", key, bool, userId); - memory.db.get({subject:key, predicate: userId}, function resultHandle(err, res){ - if (!_.isEmpty(res)) { - cb(null, (bool == "true") ? true : false) - } else { - cb(null, (bool == "false") ? true : false) - } - }); -} - -exports.get = function(key, cb) { - - var memory = this.user.memory; - var userId = this.user.id; - - debug("getVar", key, userId); - - memory.db.get({subject:key, predicate: userId}, function resultHandle(err, res){ - if (res && res.length != 0) { - cb(err, res[0].object); - } else { - cb(err, ""); - } - }); -} - -exports.createUserFact = function(s,v,o,cb) { - this.user.memory.create(s,v,o,false, function(){ - cb(null,""); - }); -} - - -exports.known = function(bool, cb) { - var memory = this.user.memory; - var name = (this.message.names && !_.isEmpty(this.message.names)) ? this.message.names[0] : ""; - memory.db.get({subject:name.toLowerCase()}, function resultHandle(err, res1){ - memory.db.get({object:name.toLowerCase()}, function resultHandle(err, res2){ - - if (_.isEmpty(res1) && _.isEmpty(res2)) { - cb(null, (bool == "false") ? true : false) - } else { - cb(null, (bool == "true") ? true : false) - } - }); - }); -} - - -exports.inTopic = function(topic, cb) { - if (topic == this.user.currentTopic) { - cb(null, "true"); - } else { - cb(null, "false"); - } -} \ No newline at end of file diff --git a/plugins/wordnet.js b/plugins/wordnet.js deleted file mode 100644 index 8e538f7b..00000000 --- a/plugins/wordnet.js +++ /dev/null @@ -1,16 +0,0 @@ -var wd = require("../lib/reply/wordnet"); - -exports.wordnetDefine = function(cb) { - var args = Array.prototype.slice.call(arguments); - var word; - - if (args.length == 2) { - word = args[0]; - } else { - word = this.message.words.pop(); - } - - wd.define(word, function(err, result){ - cb(null, "The Definition of " + word + " is " + result); - }) -} \ No newline at end of file diff --git a/plugins/words.js b/plugins/words.js deleted file mode 100644 index bca0e37b..00000000 --- a/plugins/words.js +++ /dev/null @@ -1,33 +0,0 @@ -var pluralize = require("pluralize"); -var debug = require("debug")("Word Plugin"); -var utils = require("../lib/utils"); - -exports.plural = function(word, cb) { - // Sometimes WordNet will give us more then one word - var parts, reply; - parts = word.split(" "); - - if (parts.length == 2) { - reply = pluralize.plural(parts[0]) + " " + parts[1]; - } else { - reply = pluralize.plural(word); - } - - cb(null, reply); -} - -exports.not = function(word, cb) { - var words = word.split("|"); - var results = utils.inArray(this.message.words, words); - debug("RES", results); - cb(null, (results === false)); -} - -exports.lowercase = function(word, cb) { - if (word) { - cb(null, word.toLowerCase()); - } else { - cb(null, ""); - } - -} \ No newline at end of file diff --git a/src/bin/bot-init.js b/src/bin/bot-init.js new file mode 100755 index 00000000..32b18192 --- /dev/null +++ b/src/bin/bot-init.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node + +import program from 'commander'; +import fs from 'fs'; +import path from 'path'; + +program + .version('1.0.0') + .usage('botname [options]') + .option('-c, --client [telnet]', 'Bot client (telnet or slack)', 'telnet') + .parse(process.argv); + +if (!program.args[0]) { + program.help(); + process.exit(1); +} + +const botName = program.args[0]; +const botPath = path.join(process.cwd(), path.sep, botName); +const ssRoot = path.join(__dirname, '../../'); +console.log('Creating %s bot with a %s client.', program.args[0], program.client); + +const write = function write(path, str, mode = 0o666) { + fs.writeFileSync(path, str, { mode }); + console.log(` \x1b[36mcreate\x1b[0m : ${path}`); +}; + +// Creating the path for your bot. +fs.mkdir(botPath, (err, res) => { + if (err && err.code === 'EEXIST') { + console.log('\n\nThere is already a bot named %s at %s.\nPlease remove it or pick a new name for your bot before continuing.\n', botName, botPath); + process.exit(1); + } else if (err) { + console.log('We could not create the bot.', err); + process.exit(1); + } + + fs.mkdirSync(path.join(botPath, path.sep, 'topics')); + fs.mkdirSync(path.join(botPath, path.sep, 'plugins')); + fs.mkdirSync(path.join(botPath, path.sep, 'logs')); + fs.mkdirSync(path.join(botPath, path.sep, 'src')); + + // TODO: Pull out plugins that have dialogue and move them to the new bot. + fs.createReadStream(`${ssRoot}clients${path.sep}${program.client}.js`) + .pipe(fs.createWriteStream(`${botPath + path.sep}src${path.sep}server.js`)); + + // package.json + const pkg = { + name: botName, + version: '0.0.0', + private: true, + dependencies: { + superscript: 'latest', + debug: '~2.0.0', + }, + devDependencies: { + 'babel-cli': '^6.16.0', + 'babel-preset-es2015': '^6.16.0', + }, + scripts: { + build: 'babel src --presets babel-preset-es2015 --out-dir lib', + start: 'npm run build && node lib/server.js', + }, + }; + + if (program.client === 'slack') { + pkg.dependencies['slack-client'] = '~1.2.2'; + } + + if (program.client === 'hangout') { + pkg.dependencies['simple-xmpp'] = '~1.3.0'; + } + + const firstRule = '+ ~emohello [*~2]\n- Hi!\n- Hi, how are you?\n- How are you?\n- Hello\n- Howdy\n- Ola'; + + write(path.join(botPath, path.sep, 'package.json'), JSON.stringify(pkg, null, 2)); + write(path.join(botPath, path.sep, 'topics', path.sep, 'main.ss'), firstRule); +}); diff --git a/src/bin/parse.js b/src/bin/parse.js new file mode 100755 index 00000000..9683296b --- /dev/null +++ b/src/bin/parse.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +import program from 'commander'; +import fs from 'fs'; +import parser from 'ss-parser'; + +program + .version('1.0.0') + .option('-p, --path [type]', 'Input path', './topics') + .option('-o, --output [type]', 'Output options', 'data.json') + .option('-f, --force [type]', 'Force save if output file already exists', false) + .parse(process.argv); + +fs.exists(program.output, (exists) => { + if (!exists || program.force) { + // TODO: Allow use of own fact system in this script + parser.loadDirectory(program.path, (err, result) => { + if (err) { + console.error(`Error parsing bot script: ${err}`); + } + fs.writeFile(program.output, JSON.stringify(result, null, 4), (err) => { + if (err) throw err; + console.log(`Saved output to ${program.output}`); + }); + }); + } else { + console.log('File', program.output, 'already exists, remove file first or use -f to force save.'); + } +}); diff --git a/src/bot/chatSystem.js b/src/bot/chatSystem.js new file mode 100644 index 00000000..a1203ba0 --- /dev/null +++ b/src/bot/chatSystem.js @@ -0,0 +1,37 @@ +/** + I want to create a more organic approach to authoring new gambits, topics and replies. + Right now, the system parses flat files to a intermediate JSON object that SS reads and + creates an in-memory topic representation. + + I believe by introducing a Topic DB with a clean API we can have a faster more robust authoring + expierence parseing input will become more intergrated into the topics, and Im propising + changing the existing parse inerface with a import/export to make sharing SuperScript + data (and advanced authoring?) easier. + + We also want to put more focus on the Gambit, and less on topics. A Gambit should be + able to live in several topics. + */ + +import createConditionModel from './db/models/condition'; +import createGambitModel from './db/models/gambit'; +import createReplyModel from './db/models/reply'; +import createTopicModel from './db/models/topic'; +import createUserModel from './db/models/user'; + +const createChatSystem = function createChatSystem(db, factSystem) { + const Condition = createConditionModel(db); + const Gambit = createGambitModel(db, factSystem); + const Reply = createReplyModel(db); + const Topic = createTopicModel(db); + const User = createUserModel(db, factSystem); + + return { + Condition, + Gambit, + Reply, + Topic, + User, + }; +}; + +export default createChatSystem; diff --git a/src/bot/db/connect.js b/src/bot/db/connect.js new file mode 100644 index 00000000..0787fdba --- /dev/null +++ b/src/bot/db/connect.js @@ -0,0 +1,12 @@ +import mongoose from 'mongoose'; + +export default (mongoURI, dbName) => { + const db = mongoose.createConnection(`${mongoURI}${dbName}`); + + db.on('error', console.error); + + // If you want to debug mongoose + // mongoose.set('debug', true); + + return db; +}; diff --git a/src/bot/db/helpers.js b/src/bot/db/helpers.js new file mode 100644 index 00000000..c72597a9 --- /dev/null +++ b/src/bot/db/helpers.js @@ -0,0 +1,297 @@ +// These are shared helpers for the models. + +import async from 'async'; +import _ from 'lodash'; +import debuglog from 'debug-levels'; + +import Utils from '../utils'; +import postParse from '../postParse'; + +const debug = debuglog('SS:Common'); + +const _walkReplyParent = function _walkReplyParent(db, replyId, replyIds, cb) { + db.model('Reply').findById(replyId) + .populate('parent') + .exec((err, reply) => { + if (err) { + debug.error(err); + } + + debug.info('Walk', reply); + + if (reply) { + replyIds.push(reply._id); + if (reply.parent && reply.parent.parent) { + _walkReplyParent(db, reply.parent.parent, replyIds, cb); + } else { + cb(null, replyIds); + } + } else { + cb(null, replyIds); + } + }); +}; + +const _walkGambitParent = function _walkGambitParent(db, gambitId, gambitIds, cb) { + db.model('Gambit').findOne({ _id: gambitId }) + .populate('parent') + .exec((err, gambit) => { + if (err) { + console.log(err); + } + + if (gambit) { + gambitIds.push(gambit._id); + if (gambit.parent && gambit.parent.parent) { + _walkGambitParent(db, gambit.parent.parent, gambitIds, cb); + } else { + cb(null, gambitIds); + } + } else { + cb(null, gambitIds); + } + }); +}; + +// This will find all the gambits to process by parent (topic or conversation) +// and return ones that match the message +const findMatchingGambitsForMessage = function findMatchingGambitsForMessage(db, type, id, message, options, callback) { + // Let's query for Gambits + const execHandle = function execHandle(err, gambitsParent) { + if (err) { + console.error(err); + } + + const populateGambits = function populateGambits(gambit, next) { + debug.verbose('Populating gambit'); + db.model('Reply').populate(gambit, { path: 'replies' }, next); + }; + + async.each(gambitsParent.gambits, populateGambits, (err) => { + debug.verbose('Completed populating gambits'); + if (err) { + console.error(err); + } + async.map(gambitsParent.gambits, + _eachGambitHandle(message, options), + (err3, matches) => { + callback(null, _.flatten(matches)); + } + ); + }); + }; + + if (type === 'topic') { + debug.verbose('Looking back Topic', id); + db.model('Topic').findOne({ _id: id }, 'gambits') + .populate({ path: 'gambits', match: { isCondition: false } }) + .exec(execHandle); + } else if (type === 'reply') { + options.topic = 'reply'; + debug.verbose('Looking back at Conversation', id); + db.model('Reply').findOne({ _id: id }, 'gambits') + .populate({ path: 'gambits', match: { isCondition: false } }) + .exec(execHandle); + } else if (type === 'condition') { + debug.verbose('Looking back at Conditions', id); + db.model('Condition').findOne({ _id: id }, 'gambits') + .populate('gambits') + .exec(execHandle); + } else { + debug.verbose('We should never get here'); + callback(true); + } +}; + +const _afterHandle = function _afterHandle(match, gambit, topic, cb) { + debug.verbose(`Match found: ${gambit.input} in topic: ${topic}`); + const stars = []; + if (match.length > 1) { + for (let j = 1; j < match.length; j++) { + if (match[j]) { + let starData = Utils.trim(match[j]); + // Concepts are not allowed to be stars or captured input. + starData = (starData[0] === '~') ? starData.substr(1) : starData; + stars.push(starData); + } + } + } + + const data = { stars, gambit }; + if (topic !== 'reply') { + data.topic = topic; + } + + const matches = [data]; + cb(null, matches); +}; + +/** + * Takes a gambit (trigger) and a message, and returns non-null if they match. + */ +const doesMatch = function (gambit, message, options, callback) { + let match = false; + + // Replace , etc. with the actual words in user message + postParse(gambit.trigger, message, options.user, (regexp) => { + const pattern = new RegExp(`^${regexp}$`, 'i'); + + debug.verbose(`Try to match (clean)'${message.clean}' against ${gambit.trigger} (${regexp})`); + debug.verbose(`Try to match (lemma)'${message.lemString}' against ${gambit.trigger} (${regexp})`); + + // Match on the question type (qtype / qsubtype) + if (gambit.isQuestion && message.isQuestion) { + if (_.isEmpty(gambit.qSubType) && _.isEmpty(gambit.qType) && message.isQuestion === true) { + match = message.clean.match(pattern); + if (!match) { + match = message.lemString.match(pattern); + } + } else { + if ((!_.isEmpty(gambit.qType) && message.questionType.indexOf(gambit.qType) !== -1) || + message.questionSubType === gambit.qSubType) { + match = message.clean.match(pattern); + if (!match) { + match = message.lemString.match(pattern); + } + } + } + } else { + // This is a normal match + if (gambit.isQuestion === false) { + match = message.clean.match(pattern); + if (!match) { + match = message.lemString.match(pattern); + } + } + } + + debug.verbose(`Match at the end of doesMatch was: ${match}`); + + callback(null, match); + }); +}; + +// This is the main function that looks for a matching entry +const _eachGambitHandle = function (message, options) { + const filterRegex = /\s*\^(\w+)\(([\w<>,\|\s]*)\)\s*/i; + + // This takes a gambit that is a child of a topic, reply or condition and checks if + // it matches the user's message or not. + return function (gambit, callback) { + const plugins = options.system.plugins; + const scope = options.system.scope; + const topic = options.topic || 'reply'; + const chatSystem = options.system.chatSystem; + + doesMatch(gambit, message, options, (err, match) => { + if (!match) { + debug.verbose('Gambit trigger does not match input.'); + return callback(null, []); + } + + // A filter is syntax that calls a plugin function such as: + // - {^functionX(true)} Yes, you are. + if (gambit.filter !== '') { + debug.verbose(`We have a filter function: ${gambit.filter}`); + + const filterFunction = gambit.filter.match(filterRegex); + debug.verbose(`Filter function matched against regex gave: ${filterFunction}`); + + const pluginName = Utils.trim(filterFunction[1]); + const parts = Utils.trim(filterFunction[2]).split(','); + + if (!plugins[pluginName]) { + debug.verbose('Custom Filter Function not-found', pluginName); + callback(null, []); + } + + // These are the arguments to the function (cleaned version of parts) + const args = []; + for (let i = 0; i < parts.length; i++) { + if (parts[i] !== '') { + args.push(parts[i].trim()); + } + } + + if (plugins[pluginName]) { + // The filterScope is what 'this' is during the execution of the plugin. + // This is so you can write plugins that can access, e.g. this.user or this.chatSystem + // Here we augment the global scope (system.scope) with any additional local scope for + // the current reply. + const filterScope = _.merge({}, scope); + filterScope.message = message; +// filterScope.message_props = options.localOptions.messageScope; + filterScope.user = options.user; + + args.push((err, filterReply) => { + if (err) { + console.error(err); + } + + debug.verbose(`Reply from filter function was: ${filterReply}`); + + // TODO: This seems weird... Investigate + if (filterReply === 'true' || filterReply === true) { + if (gambit.redirect !== '') { + debug.verbose('Found Redirect Match with topic %s', topic); + chatSystem.Topic.findTriggerByTrigger(gambit.redirect, (err2, trigger) => { + if (err2) { + console.error(err2); + } + + gambit = trigger; + callback(null, []); + }); + } else { + // Tag the message with the found Trigger we matched on + message.gambitId = gambit._id; + _afterHandle(match, gambit, topic, callback); + } + } else { + callback(null, []); + } + }); + + debug.verbose('Calling Plugin Function', pluginName); + plugins[pluginName].apply(filterScope, args); + } + } else { + if (gambit.redirect !== '') { + // If there's no filter, check if there's a redirect + // TODO: Check this works/is sane + debug.verbose('Found Redirect Match with topic'); + chatSystem.Topic.findTriggerByTrigger(gambit.redirect, (err, trigger) => { + if (err) { + console.log(err); + } + + debug.verbose('Redirecting to New Gambit', trigger); + gambit = trigger; + // Tag the message with the found Trigger we matched on + message.gambitId = gambit._id; + _afterHandle(match, gambit, topic, callback); + }); + } else { + // Tag the message with the found Trigger we matched on + message.gambitId = gambit._id; + _afterHandle(match, gambit, topic, callback); + } + } + }); // end regexReply + }; +}; // end EachGambit + +const walkReplyParent = (db, replyId, cb) => { + _walkReplyParent(db, replyId, [], cb); +}; + +const walkGambitParent = (db, gambitId, cb) => { + _walkGambitParent(db, gambitId, [], cb); +}; + +export default { + walkReplyParent, + walkGambitParent, + doesMatch, + findMatchingGambitsForMessage, +}; diff --git a/src/bot/db/import.js b/src/bot/db/import.js new file mode 100755 index 00000000..d222367c --- /dev/null +++ b/src/bot/db/import.js @@ -0,0 +1,225 @@ +/** + * Import a data file into MongoDB + */ + +import fs from 'fs'; +import async from 'async'; +import _ from 'lodash'; +import debuglog from 'debug-levels'; + +import Utils from '../utils'; + +const debug = debuglog('SS:Importer'); + +const KEEP_REGEX = new RegExp('\{keep\}', 'i'); +const FILTER_REGEX = /\{\s*\^(\w+)\(([\w<>,\s]*)\)\s*\}/i; + +const rawToGambitData = function rawToGambitData(gambitId, itemData) { + const gambitData = { + id: gambitId, + isQuestion: itemData.options.isQuestion, + isCondition: itemData.options.isConditional, + qType: itemData.options.qType === false ? '' : itemData.options.qType, + qSubType: itemData.options.qSubType === false ? '' : itemData.options.qSubType, + filter: itemData.options.filter === false ? '' : itemData.options.filter, + trigger: itemData.trigger, + }; + + // This is to capture anything pre 5.1 + if (itemData.raw) { + gambitData.input = itemData.raw; + } else { + gambitData.input = itemData.trigger; + } + + if (itemData.redirect !== null) { + gambitData.redirect = itemData.redirect; + } + + return gambitData; +}; + +const importData = function importData(chatSystem, data, callback) { + const Condition = chatSystem.Condition; + const Topic = chatSystem.Topic; + const Gambit = chatSystem.Gambit; + const Reply = chatSystem.Reply; + const User = chatSystem.User; + + const gambitsWithConversation = []; + + const eachReplyItor = function eachReplyItor(gambit) { + return (replyId, nextReply) => { + debug.verbose('Reply process: %s', replyId); + const replyString = data.replies[replyId]; + const properties = { id: replyId, reply: replyString, parent: gambit._id }; + let match = properties.reply.match(KEEP_REGEX); + if (match) { + properties.keep = true; + properties.reply = Utils.trim(properties.reply.replace(match[0], '')); + } + match = properties.reply.match(FILTER_REGEX); + if (match) { + properties.filter = `^${match[1]}(${match[2]})`; + properties.reply = Utils.trim(properties.reply.replace(match[0], '')); + } + + gambit.addReply(properties, (err) => { + if (err) { + console.error(err); + } + nextReply(); + }); + }; + }; + + const eachGambitItor = function eachGambitItor(topic) { + return (gambitId, nextGambit) => { + if (!_.isUndefined(data.gambits[gambitId].options.conversations)) { + gambitsWithConversation.push(gambitId); + nextGambit(); + } else if (data.gambits[gambitId].topic === topic.name) { + debug.verbose('Gambit process: %s', gambitId); + const gambitRawData = data.gambits[gambitId]; + const gambitData = rawToGambitData(gambitId, gambitRawData); + + topic.createGambit(gambitData, (err, gambit) => { + if (err) { + console.error(err); + } + async.eachSeries(gambitRawData.replies, eachReplyItor(gambit), (err) => { + if (err) { + console.error(err); + } + nextGambit(); + }); + }); + } else { + nextGambit(); + } + }; + }; + + const eachTopicItor = function eachTopicItor(topicName, nextTopic) { + debug.verbose(`Find or create topic with name '${topicName}'`); + const topicProperties = { + name: topicName, + keep: data.topics[topicName].flags.indexOf('keep') !== -1, + nostay: data.topics[topicName].flags.indexOf('nostay') !== -1, + system: data.topics[topicName].flags.indexOf('system') !== -1, + keywords: data.topics[topicName].keywords ? data.topics[topicName].keywords : [], + filter: (data.topics[topicName].filter) ? data.topics[topicName].filter : '', + }; + + Topic.findOrCreate({ name: topicName }, topicProperties, (err, topic) => { + if (err) { + console.error(err); + } + + async.eachSeries(Object.keys(data.gambits), eachGambitItor(topic), (err) => { + if (err) { + console.error(err); + } + debug.verbose(`All gambits for ${topicName} processed.`); + nextTopic(); + }); + }); + }; + + const eachConvItor = function eachConvItor(gambitId) { + return (replyId, nextConv) => { + debug.verbose('conversation/reply: %s', replyId); + Reply.findOne({ id: replyId }, (err, reply) => { + if (err) { + console.error(err); + } + if (reply) { + reply.gambits.addToSet(gambitId); + reply.save((err) => { + if (err) { + console.error(err); + } + reply.sortGambits(() => { + debug.verbose('All conversations for %s processed.', gambitId); + nextConv(); + }); + }); + } else { + debug.warn('No reply found!'); + nextConv(); + } + }); + }; + }; + + debug.info('Cleaning database: removing all data.'); + + async.each([Condition, Gambit, Reply, Topic, User], + (model, nextModel) => { + model.remove({}, err => nextModel()); + }, + (err) => { + async.eachSeries(Object.keys(data.topics), eachTopicItor, () => { + async.eachSeries(_.uniq(gambitsWithConversation), (gambitId, nextGambit) => { + const gambitRawData = data.gambits[gambitId]; + + const conversations = gambitRawData.options.conversations || []; + if (conversations.length === 0) { + return nextGambit(); + } + + const gambitData = rawToGambitData(gambitId, gambitRawData); + const replyId = conversations[0]; + + // TODO??: Add reply.addGambit(...) + Reply.findOne({ id: replyId }, (err, reply) => { + const gambit = new Gambit(gambitData); + async.eachSeries(gambitRawData.replies, eachReplyItor(gambit), (err) => { + debug.verbose('All replies processed.'); + gambit.parent = reply._id; + debug.verbose('Saving new gambit: ', err, gambit); + gambit.save((err, gam) => { + if (err) { + console.log(err); + } + async.mapSeries(conversations, eachConvItor(gam._id), (err, results) => { + debug.verbose('All conversations for %s processed.', gambitId); + nextGambit(); + }); + }); + }); + }); + }, () => { + // Move on to conditions + const conditionItor = function conditionItor(conditionId, next) { + const condition = data.conditions[conditionId]; + Topic.findOne({ name: condition.topic }, (err, topic) => { + topic.createCondition(condition, (err, condition) => { + if (err) { + console.log(err); + } + next(); + }); + }); + }; + + async.eachSeries(Object.keys(data.conditions), conditionItor, () => { + debug.verbose('All conditions processed'); + callback(null, 'done'); + }); + }); + }); + } + ); +}; + +const importFile = function importFile(chatSystem, path, callback) { + fs.readFile(path, (err, jsonFile) => { + if (err) { + console.log(err); + } + return importData(chatSystem, JSON.parse(jsonFile), callback); + }); +}; + +export default { importFile, importData }; diff --git a/src/bot/db/models/condition.js b/src/bot/db/models/condition.js new file mode 100644 index 00000000..061ad91f --- /dev/null +++ b/src/bot/db/models/condition.js @@ -0,0 +1,31 @@ +/** + * A Condition is a type of Gambit that contains a set of gambits, but instead + * of having a static regex trigger it has some conditional logic + */ + +import mongoose from 'mongoose'; +import findOrCreate from 'mongoose-findorcreate'; + +import helpers from '../helpers'; +import Utils from '../../utils'; + +const createConditionModel = function createConditionModel(db) { + const conditionSchema = new mongoose.Schema({ + id: { type: String, index: true, default: Utils.genId() }, + condition: { type: String }, + + // An array of gambits that belong to this condition. + gambits: [{ type: String, ref: 'Gambit' }], + }); + + // At this point we just want to see if the condition matches, then pass the gambits to Common findMatchingGambitsForMessage + conditionSchema.methods.doesMatch = function (message, options, callback) { + helpers.findMatchingGambitsForMessage(db, 'condition', this._id, message, options, callback); + }; + + conditionSchema.plugin(findOrCreate); + + return db.model('Condition', conditionSchema); +}; + +export default createConditionModel; diff --git a/src/bot/db/models/gambit.js b/src/bot/db/models/gambit.js new file mode 100644 index 00000000..4027484c --- /dev/null +++ b/src/bot/db/models/gambit.js @@ -0,0 +1,159 @@ +/** + + A Gambit is a Trigger + Reply or Reply Set + - We define a Reply as a subDocument in Mongo. + +**/ + +import mongoose from 'mongoose'; +import findOrCreate from 'mongoose-findorcreate'; +import norm from 'node-normalizer'; +import debuglog from 'debug-levels'; +import async from 'async'; +import regexReply from 'ss-parser/lib/regexReply'; + +import helpers from '../helpers'; +import Utils from '../../utils'; + +const debug = debuglog('SS:Gambit'); + +/** + + A trigger is the matching rule behind a piece of input. It lives in a topic or several topics. + A trigger also contains one or more replies. + +**/ + +const createGambitModel = function createGambitModel(db, factSystem) { + const gambitSchema = new mongoose.Schema({ + id: { type: String, index: true, default: Utils.genId() }, + + // This is the input string that generates a rule, + // In the event we want to export this, we will use this value. + // Make this filed conditionally required if trigger is supplied + input: { type: String }, + + // The Trigger is a partly baked regex. + trigger: { type: String, index: true }, + + // If the trigger is a Question Match + isQuestion: { type: Boolean, default: false }, + + // If this gambit is nested inside a conditional block + isCondition: { type: Boolean, default: false }, + + // If the trigger is a Answer Type Match + qType: { type: String, default: '' }, + qSubType: { type: String, default: '' }, + + // The filter function for the the expression + filter: { type: String, default: '' }, + + // An array of replies. + replies: [{ type: String, ref: 'Reply' }], + + // Save a reference to the parent Reply, so we can walk back up the tree + parent: { type: String, ref: 'Reply' }, + + // This will redirect anything that matches elsewhere. + // If you want to have a conditional rediect use reply redirects + // TODO, change the type to a ID and reference another gambit directly + // this will save us a lookup down the road (and improve performace.) + redirect: { type: String, default: '' }, + }); + + gambitSchema.pre('save', function (next) { + const self = this; + + // FIXME: This only works when the replies are populated which is not always the case. + // self.replies = _.uniq(self.replies, function(item, key, id) { + // return item.id; + // }); + + // If input was supplied, we want to use it to generate the trigger + if (self.input) { + const input = norm.clean(self.input); + // We want to convert the input into a trigger. + regexReply.parse(Utils.quotemeta(input, true), factSystem, (trigger) => { + self.trigger = trigger; + next(); + }); + } else { + // Otherwise we populate the trigger normally + next(); + } + }); + + gambitSchema.methods.addReply = function (replyData, callback) { + if (!replyData) { + return callback('No data'); + } + + const Reply = db.model('Reply'); + const reply = new Reply(replyData); + reply.save((err) => { + if (err) { + return callback(err); + } + this.replies.addToSet(reply._id); + this.save((err) => { + callback(err, reply); + }); + }); + }; + + gambitSchema.methods.doesMatch = function (message, options, callback) { + helpers.doesMatch(this, message, options, callback); + }; + + gambitSchema.methods.clearReplies = function (callback) { + const self = this; + + const clearReply = function (replyId, cb) { + self.replies.pull({ _id: replyId }); + db.model('Reply').remove({ _id: replyId }, (err) => { + if (err) { + console.log(err); + } + + debug.verbose('removed reply %s', replyId); + + cb(null, replyId); + }); + }; + + async.map(self.replies, clearReply, (err, clearedReplies) => { + self.save((err2) => { + callback(err2, clearedReplies); + }); + }); + }; + + gambitSchema.methods.getRootTopic = function (cb) { + if (!this.parent) { + db.model('Topic') + .findOne({ gambits: { $in: [this._id] } }) + .exec((err, doc) => { + cb(err, doc.name); + }); + } else { + helpers.walkGambitParent(db, this._id, (err, gambits) => { + if (gambits.length !== 0) { + db.model('Topic') + .findOne({ gambits: { $in: [gambits.pop()] } }) + .exec((err, topic) => { + cb(null, topic.name); + }); + } else { + cb(null, 'random'); + } + }); + } + }; + + gambitSchema.plugin(findOrCreate); + + return db.model('Gambit', gambitSchema); +}; + +export default createGambitModel; diff --git a/src/bot/db/models/reply.js b/src/bot/db/models/reply.js new file mode 100644 index 00000000..f0992804 --- /dev/null +++ b/src/bot/db/models/reply.js @@ -0,0 +1,50 @@ +import mongoose from 'mongoose'; +import async from 'async'; + +import Utils from '../../utils'; +import Sort from '../sort'; +import helpers from '../helpers'; + +const createReplyModel = function createReplyModel(db) { + const replySchema = new mongoose.Schema({ + id: { type: String, index: true, default: Utils.genId() }, + reply: { type: String, required: '{reply} is required.' }, + keep: { type: Boolean, default: false }, + filter: { type: String, default: '' }, + parent: { type: String, ref: 'Gambit' }, + + // Replies could referece other gambits + // This forms the basis for the 'previous' - These are Children + gambits: [{ type: String, ref: 'Gambit' }], + }); + + // This method is similar to the topic.findMatch + replySchema.methods.findMatch = function findMatch(message, options, callback) { + helpers.findMatchingGambitsForMessage(db, 'reply', this._id, message, options, callback); + }; + + replySchema.methods.sortGambits = function (callback) { + const self = this; + const expandReorder = function (gambitId, cb) { + db.model('Gambit').findById(gambitId, (err, gambit) => { + cb(err, gambit); + }); + }; + + async.map(this.gambits, expandReorder, (err, newGambitList) => { + if (err) { + console.log(err); + } + + const newList = Sort.sortTriggerSet(newGambitList); + self.gambits = newList.map((g) => { + return g._id; + }); + self.save(callback); + }); + }; + + return db.model('Reply', replySchema); +}; + +export default createReplyModel; diff --git a/src/bot/db/models/topic.js b/src/bot/db/models/topic.js new file mode 100644 index 00000000..ecc828ad --- /dev/null +++ b/src/bot/db/models/topic.js @@ -0,0 +1,380 @@ +/** + Topics are a grouping of gambits. + The order of the Gambits are important, and a gambit can live in more than one topic. +**/ + +import mongoose from 'mongoose'; +import natural from 'natural'; +import _ from 'lodash'; +import async from 'async'; +import findOrCreate from 'mongoose-findorcreate'; +import debuglog from 'debug-levels'; +import safeEval from 'safe-eval'; + +import Sort from '../sort'; +import helpers from '../helpers'; + +const debug = debuglog('SS:Topics'); + +const TfIdf = natural.TfIdf; +const tfidf = new TfIdf(); + +natural.PorterStemmer.attach(); + +// Function to score the topics by TF-IDF +const scoreTopics = function scoreTopics(message) { + let topics = []; + const tasMessage = message.lemString.tokenizeAndStem(); + debug.verbose('Tokenised and stemmed words: ', tasMessage); + + // Score the input against the topic keywords to come up with a topic order. + tfidf.tfidfs(tasMessage, (index, score, name) => { + // Filter out system topic pre/post + if (name !== '__pre__' && name !== '__post__') { + topics.push({ name, score, type: 'TOPIC' }); + } + }); + + // Removes duplicate entries. + topics = _.uniqBy(topics, 'name'); + + const topicOrder = _.sortBy(topics, 'score').reverse(); + debug.verbose('Scored topics: ', topicOrder); + + return topicOrder; +}; + +const createTopicModel = function createTopicModel(db) { + const topicSchema = new mongoose.Schema({ + name: { type: String, index: true, unique: true }, + keep: { type: Boolean, default: false }, + system: { type: Boolean, default: false }, + nostay: { type: Boolean, default: false }, + filter: { type: String, default: '' }, + keywords: { type: Array }, + gambits: [{ type: String, ref: 'Gambit' }], + conditions: [{ type: String, ref: 'Condition' }], + }); + + topicSchema.pre('save', function (next) { + if (!_.isEmpty(this.keywords)) { + const keywords = this.keywords.join(' '); + if (keywords) { + tfidf.addDocument(keywords.tokenizeAndStem(), this.name); + } + } + next(); + }); + + topicSchema.methods.createCondition = function (conditionData, callback) { + if (!conditionData) { + return callback('No data'); + } + + if (_.isEmpty(conditionData.gambits)) { + return callback('No gambits'); + } else { + const gambits = []; + const gambitLookup = (gambit_id, next) => { + db.model('Gambit').findOne({ id: gambit_id }, (error, gambit) => { + gambits.push(gambit._id); + next(); + }); + }; + + async.each(conditionData.gambits, gambitLookup, () => { + conditionData.gambits = gambits; + const Condition = db.model('Condition'); + const condition = new Condition(conditionData); + + condition.save((err) => { + if (err) { + return callback(err); + } + this.conditions.addToSet(condition._id); + this.save((err) => { + callback(err, condition); + }); + }); + }); + } + }; + + // This will create the Gambit and add it to the model + topicSchema.methods.createGambit = function (gambitData, callback) { + if (!gambitData) { + return callback('No data'); + } + + const Gambit = db.model('Gambit'); + const gambit = new Gambit(gambitData); + gambit.save((err) => { + if (err) { + return callback(err); + } + this.gambits.addToSet(gambit._id); + this.save((err) => { + callback(err, gambit); + }); + }); + }; + + topicSchema.methods.sortGambits = function (callback) { + const expandReorder = (gambitId, cb) => { + db.model('Gambit').findById(gambitId, (err, gambit) => { + if (err) { + console.log(err); + } + cb(null, gambit); + }); + }; + + async.map(this.gambits, expandReorder, (err, newGambitList) => { + if (err) { + console.log(err); + } + + const newList = Sort.sortTriggerSet(newGambitList); + this.gambits = newList.map(gambit => + gambit._id + ); + this.save(callback); + }); + }; + + /* This is a function to determine whether a certain key has been set to a certain value. + * The double percentage sign (%%) syntax is used in the script to denote that a gambit + * must meet a condition before being executed, e.g. + * + * %% (userKilledAlice === true) + * + I love you. + * - I still haven't forgiven you, you know. + * + * The context is whatever a user has previously set in any replies. So in this example, + * if a user has set {userKilledAlice = true}, then the gambit is matched. + */ + topicSchema.methods.checkConditions = function checkConditions(message, options, callback) { + const user = options.user; + const topic = this.name; + + const conditionItor = function conditionItor(condition, next) { + const context = user.conversationState || {}; + + debug.verbose('CheckItor - Context: ', context); + debug.verbose('CheckItor - Condition: ', condition.condition); + + try { + // TODO: Investigate why conditions are tied to topics and not gambits, which seems more intuitive + if (safeEval(condition.condition, context)) { + debug.verbose('--- Condition TRUE ---'); + + options.topic = topic; + + condition.doesMatch(message, options, next); + } else { + next(false); + } + } catch (e) { + debug.verbose(`Error in condition checking: ${e.stack}`); + next(false); + } + }; + + async.mapSeries(this.conditions, conditionItor, (err, res) => { + debug.verbose(`Results from conditions: ${res}`); + res = _.filter(res, x => x); + debug.verbose(`Results from conditions (filtered): ${res}`); + if (err || _.isEmpty(res)) { + return callback(true, []); + } else { + callback(err, _.flatten(res)); + } + }); + }; + + topicSchema.methods.findMatch = function findMatch(message, options, callback) { + options.topic = this.name; + + helpers.findMatchingGambitsForMessage(db, 'topic', this._id, message, options, callback); + }; + + // Lightweight match for one topic + // TODO: offload this to common + topicSchema.methods.doesMatch = function (message, options, cb) { + const itor = (gambit, next) => { + gambit.doesMatch(message, options, (err, match2) => { + if (err) { + debug.error(err); + } + next(err, match2 ? gambit._id : null); + }); + }; + + db.model('Topic').findOne({ name: this.name }, 'gambits') + .populate('gambits') + .exec((err, mgambits) => { + if (err) { + debug.error(err); + } + async.filter(mgambits.gambits, itor, (err, res) => { + cb(null, res); + }); + }); + }; + + topicSchema.methods.clearGambits = function (callback) { + const clearGambit = (gambitId, cb) => { + this.gambits.pull({ _id: gambitId }); + db.model('Gambit').findById(gambitId, (err, gambit) => { + if (err) { + debug.error(err); + } + + gambit.clearReplies(() => { + db.model('Gambit').remove({ _id: gambitId }, (err) => { + if (err) { + debug.error(err); + } + + debug.verbose('removed gambit %s', gambitId); + + cb(null, gambitId); + }); + }); + }); + }; + + async.map(this.gambits, clearGambit, (err, clearedGambits) => { + this.save((err) => { + callback(err, clearedGambits); + }); + }); + }; + + // This will find a gambit in any topic + topicSchema.statics.findTriggerByTrigger = function (input, callback) { + db.model('Gambit').findOne({ input }).exec(callback); + }; + + topicSchema.statics.findByName = function (name, callback) { + this.findOne({ name }, {}, callback); + }; + + topicSchema.statics.findPendingTopicsForUser = function (user, message, callback) { + const currentTopic = user.getTopic(); + const pendingTopics = []; + + const scoredTopics = scoreTopics(message); + + const removeMissingTopics = function removeMissingTopics(topics) { + return _.filter(topics, topic => + topic.id + ); + }; + + this.find({}, (err, allTopics) => { + if (err) { + debug.error(err); + } + + // Add the current topic to the front of the array. + scoredTopics.unshift({ name: currentTopic, type: 'TOPIC' }); + + let otherTopics = _.map(allTopics, topic => + ({ id: topic._id, name: topic.name, system: topic.system }) + ); + + // This gets a list if all the remaining topics. + otherTopics = _.filter(otherTopics, topic => + !_.find(scoredTopics, { name: topic.name }) + ); + + // We remove the system topics + otherTopics = _.filter(otherTopics, topic => + topic.system === false + ); + + pendingTopics.push({ name: '__pre__', type: 'TOPIC' }); + + for (i = 0; i < scoredTopics.length; i++) { + if (scoredTopics[i].name !== '__post__' && scoredTopics[i].name !== '__pre__') { + pendingTopics.push(scoredTopics[i]); + } + } + + for (i = 0; i < otherTopics.length; i++) { + if (otherTopics[i].name !== '__post__' && otherTopics[i].name !== '__pre__') { + otherTopics[i].type = 'TOPIC'; + pendingTopics.push(otherTopics[i]); + } + } + + pendingTopics.push({ name: '__post__', type: 'TOPIC' }); + + debug.verbose(`Pending topics before conversations: ${JSON.stringify(pendingTopics)}`); + + // Lets assign the ids to the topics + for (let i = 0; i < pendingTopics.length; i++) { + const topicName = pendingTopics[i].name; + for (let n = 0; n < allTopics.length; n++) { + if (allTopics[n].name === topicName) { + pendingTopics[i].id = allTopics[n]._id; + } + } + } + + // If we are currently in a conversation, we want the entire chain added + // to the topics to search + const lastReply = user.__history__.reply[0]; + if (!_.isEmpty(lastReply)) { + // If the message is less than 5 minutes old we continue + // TODO: Make this time configurable + const delta = new Date() - lastReply.createdAt; + if (delta <= 1000 * 300) { + const replyId = lastReply.replyId; + const clearBit = lastReply.clearConvo; + + debug('Last reply: ', lastReply.original, replyId, clearBit); + + if (clearBit === true) { + debug('Conversation RESET by clearBit'); + callback(null, removeMissingTopics(pendingTopics)); + } else { + db.model('Reply') + .findOne({ _id: replyId }) + .exec((err, reply) => { + if (!reply) { + debug("We couldn't match the last reply. Continuing."); + callback(null, removeMissingTopics(pendingTopics)); + } else { + helpers.walkReplyParent(db, reply._id, (err, replyThreads) => { + debug.verbose(`Threads found by walkReplyParent: ${replyThreads}`); + replyThreads = replyThreads.map(item => + ({ id: item, type: 'REPLY' }) + ); + + // This inserts the array replyThreads into pendingTopics after the first topic + replyThreads.unshift(1, 0); + Array.prototype.splice.apply(pendingTopics, replyThreads); + + callback(null, removeMissingTopics(pendingTopics)); + }); + } + }); + } + } else { + debug.info('The conversation thread was to old to continue it.'); + callback(null, removeMissingTopics(pendingTopics)); + } + } else { + callback(null, removeMissingTopics(pendingTopics)); + } + }); + }; + + topicSchema.plugin(findOrCreate); + + return db.model('Topic', topicSchema); +}; + +export default createTopicModel; diff --git a/src/bot/db/models/user.js b/src/bot/db/models/user.js new file mode 100644 index 00000000..54e6a0a5 --- /dev/null +++ b/src/bot/db/models/user.js @@ -0,0 +1,186 @@ +import fs from 'fs'; +import _ from 'lodash'; +import debuglog from 'debug-levels'; +import findOrCreate from 'mongoose-findorcreate'; +import mkdirp from 'mkdirp'; +import mongoose from 'mongoose'; + +const debug = debuglog('SS:User'); + +mkdirp.sync(`${process.cwd()}/logs/`); + +const createUserModel = function createUserModel(db, factSystem) { + const userSchema = mongoose.Schema({ + id: String, + status: Number, + currentTopic: String, + pendingTopic: String, + conversationStartedAt: Date, + lastMessageSentAt: Date, + volley: Number, + rally: Number, + conversation: Number, + prevAns: Number, + slot1: Object, + slot2: Object, + conversationState: Object, + __history__: { + input: [], + reply: [], + topic: [], + stars: [], + }, + }); + + userSchema.pre('save', function (next) { + debug.verbose('Pre-Save Hook'); + this.__history__.input = this.__history__.input.slice(0, 15); + this.__history__.reply = this.__history__.reply.slice(0, 15); + this.__history__.topic = this.__history__.topic.slice(0, 15); + this.__history__.stars = this.__history__.stars.slice(0, 15); + next(); + }); + + userSchema.methods.clearConversationState = function (callback) { + this.conversationState = {}; + this.save(callback); + }; + + userSchema.methods.setTopic = function (topic) { + if (topic !== '' || topic !== 'undefined') { + debug.verbose('setTopic', topic); + this.pendingTopic = topic; + /* this.save(function() { + // We should probably have a callback here. + debug.verbose("setTopic Complete"); + });*/ + } else { + debug.warn('Trying to set topic to someting invalid'); + } + }; + + userSchema.methods.getTopic = function () { + debug.verbose('getTopic', this.currentTopic); + return this.currentTopic; + }; + + userSchema.methods.updateHistory = function (msg, reply, replyObj, cb) { + if (!_.isNull(msg)) { + this.lastMessageSentAt = new Date(); + } + + // New Log format. + const log = { + user_id: this.id, + raw_input: msg.original, + normalized_input: msg.clean, + matched_gambit: replyObj.minMatchSet, + final_output: reply.clean, + timestamp: msg.createdAt, + }; + + const cleanId = this.id.replace(/\W/g, ''); + fs.appendFileSync(`${process.cwd()}/logs/${cleanId}_trans.txt`, `${JSON.stringify(log)}\r\n`); + + // Did we successfully volley? + // In order to keep the conversation flowing we need to have rythum and this means we always + // need to continue to engage. + if (reply.isQuestion) { + this.volley = 1; + this.rally = this.rally + 1; + } else { + // We killed the rally + this.volley = 0; + this.rally = 0; + } + + this.conversation = this.conversation + 1; + + debug.verbose('Updating History'); + msg.messageScope = null; + + const stars = replyObj.stars; + + // Don't serialize MongoDOWN to Mongo + msg.factSystem = null; + reply.factSystem = null; + + this.__history__.stars.unshift(stars); + this.__history__.input.unshift(msg); + this.__history__.reply.unshift(reply); + this.__history__.topic.unshift(this.currentTopic); + + if (this.pendingTopic !== undefined && this.pendingTopic !== '') { + const pendingTopic = this.pendingTopic; + this.pendingTopic = null; + + db.model('Topic').findOne({ name: pendingTopic }, (err, topicData) => { + if (topicData && topicData.nostay === true) { + this.currentTopic = this.__history__.topic[0]; + } else { + this.currentTopic = pendingTopic; + } + this.save((err) => { + debug.verbose('Saved user'); + if (err) { + console.error(err); + } + cb(err, log); + }); + }); + } else { + cb(null, log); + } + }; + + userSchema.methods.getVar = function (key, cb) { + debug.verbose('getVar', key); + + this.memory.db.get({ subject: key, predicate: this.id }, (err, res) => { + if (res && res.length !== 0) { + cb(err, res[0].object); + } else { + cb(err, null); + } + }); + }; + + userSchema.methods.setVar = function (key, value, cb) { + debug.verbose('setVar', key, value); + const self = this; + + self.memory.db.get({ subject: key, predicate: self.id }, (err, results) => { + if (err) { + console.log(err); + } + + if (!_.isEmpty(results)) { + self.memory.db.del(results[0], () => { + const opt = { subject: key, predicate: self.id, object: value }; + self.memory.db.put(opt, () => { + cb(); + }); + }); + } else { + const opt = { subject: key, predicate: self.id, object: value }; + self.memory.db.put(opt, (err2) => { + if (err2) { + console.log(err2); + } + + cb(); + }); + } + }); + }; + + userSchema.plugin(findOrCreate); + + userSchema.virtual('memory').get(function () { + return factSystem.createUserDB(this.id); + }); + + return db.model('User', userSchema); +}; + +export default createUserModel; diff --git a/src/bot/db/sort.js b/src/bot/db/sort.js new file mode 100644 index 00000000..40cb861d --- /dev/null +++ b/src/bot/db/sort.js @@ -0,0 +1,149 @@ +import debuglog from 'debug'; +import Utils from '../utils'; + +const debug = debuglog('Sort'); + +const initSortTrack = function initSortTrack() { + return { + qtype: {}, // Sort by Question Types Length + atomic: {}, // Sort by number of whole words + option: {}, // Sort optionals by number of words + alpha: {}, // Sort alpha wildcards by no. of words + number: {}, // Sort number wildcards by no. of words + wild: {}, // Sort wildcards by no. of words + pound: [], // Triggers of just # + under: [], // Triggers of just _ + star: [], // Triggers of just * + }; +}; + +const sortTriggerSet = function sortTriggerSet(triggers) { + let trig; + let cnt; + let inherits; + + const lengthSort = (a, b) => (b.length - a.length); + + // Create a priority map. + const prior = { + 0: [], // Default priority = 0 + }; + + // Sort triggers by their weights. + for (let i = 0; i < triggers.length; i++) { + trig = triggers[i]; + const match = trig.input.match(/\{weight=(\d+)\}/i); + let weight = 0; + if (match && match[1]) { + weight = match[1]; + } + + if (!prior[weight]) { + prior[weight] = []; + } + prior[weight].push(trig); + } + + const sortFwd = (a, b) => (b - a); + const sortRev = (a, b) => (a - b); + + // Keep a running list of sorted triggers for this topic. + const running = []; + + // Sort them by priority. + const priorSort = Object.keys(prior).sort(sortFwd); + + for (let i = 0; i < priorSort.length; i++) { + const p = priorSort[i]; + debug(`Sorting triggers with priority ${p}`); + + // Loop through and categorize these triggers. + const track = {}; + + for (let j = 0; j < prior[p].length; j++) { + trig = prior[p][j]; + + inherits = -1; + if (!track[inherits]) { + track[inherits] = initSortTrack(); + } + + if (trig.qType !== '') { + // Qtype included + cnt = trig.qType.length; + debug(`Has a qType with ${trig.qType.length} length.`); + + if (!track[inherits].qtype[cnt]) { + track[inherits].qtype[cnt] = []; + } + track[inherits].qtype[cnt].push(trig); + } else if (trig.input.indexOf('*') > -1) { + // Wildcard included. + cnt = Utils.wordCount(trig.input); + debug(`Has a * wildcard with ${cnt} words.`); + if (cnt > 1) { + if (!track[inherits].wild[cnt]) { + track[inherits].wild[cnt] = []; + } + track[inherits].wild[cnt].push(trig); + } else { + track[inherits].star.push(trig); + } + } else if (trig.input.indexOf('[') > -1) { + // Optionals included. + cnt = Utils.wordCount(trig.input); + debug(`Has optionals with ${cnt} words.`); + if (!track[inherits].option[cnt]) { + track[inherits].option[cnt] = []; + } + track[inherits].option[cnt].push(trig); + } else { + // Totally atomic. + cnt = Utils.wordCount(trig.input); + debug(`Totally atomic trigger and ${cnt} words.`); + if (!track[inherits].atomic[cnt]) { + track[inherits].atomic[cnt] = []; + } + track[inherits].atomic[cnt].push(trig); + } + } + + // Move the no-{inherits} triggers to the bottom of the stack. + track[0] = track['-1']; + delete track['-1']; + + // Add this group to the sort list. + const trackSorted = Object.keys(track).sort(sortRev); + + for (let j = 0; j < trackSorted.length; j++) { + const ip = trackSorted[j]; + debug(`ip=${ip}`); + + const kinds = ['qtype', 'atomic', 'option', 'alpha', 'number', 'wild']; + for (let k = 0; k < kinds.length; k++) { + const kind = kinds[k]; + + const kindSorted = Object.keys(track[ip][kind]).sort(sortFwd); + + for (let l = 0; l < kindSorted.length; l++) { + const item = kindSorted[l]; + running.push(...track[ip][kind][item]); + } + } + + // We can sort these using Array.sort + const underSorted = track[ip].under.sort(lengthSort); + const poundSorted = track[ip].pound.sort(lengthSort); + const starSorted = track[ip].star.sort(lengthSort); + + running.push(...underSorted); + running.push(...poundSorted); + running.push(...starSorted); + } + } + return running; +}; + +export default { + sortTriggerSet, +}; diff --git a/src/bot/dict.js b/src/bot/dict.js new file mode 100644 index 00000000..946376f2 --- /dev/null +++ b/src/bot/dict.js @@ -0,0 +1,101 @@ +import _ from 'lodash'; +import debuglog from 'debug-levels'; + +const debug = debuglog('SS:Dict'); + +class Dict { + constructor(wordArray) { + this.words = []; + + for (let i = 0; i < wordArray.length; i++) { + this.words.push({ word: wordArray[i], position: i }); + } + } + + add(key, array) { + for (let i = 0; i < array.length; i++) { + this.words[i][key] = array[i]; + } + } + + get(word) { + debug.verbose(`Getting word from dictionary: ${word}`); + for (let i = 0; i < this.words.length; i++) { + if (this.words[i].word === word || this.words[i].lemma === word) { + return this.words[i]; + } + } + return null; + } + + contains(word) { + for (let i = 0; i < this.words.length; i++) { + if (this.words[i].word === word || this.words[i].lemma === word) { + return true; + } + } + return false; + } + + addHLC(array) { + debug.verbose(`Adding HLCs to dictionary: ${array}`); + const extra = []; + for (let i = 0; i < array.length; i++) { + const word = array[i].word; + const concepts = array[i].hlc; + const item = this.get(word); + if (item) { + item.hlc = concepts; + } else { + debug.verbose(`HLC extra or missing for word/phrase: ${word}`); + extra.push(word); + } + } + return extra; + } + + getHLC(concept) { + for (let i = 0; i < this.words.length; i++) { + if (_.includes(this.words[i].hlc, concept)) { + return this.words[i]; + } + } + return null; + } + + containsHLC(concept) { + for (let i = 0; i < this.words.length; i++) { + if (_.includes(this.words[i].hlc, concept)) { + return true; + } + } + return false; + } + + fetch(list, thing) { + const results = []; + for (let i = 0; i < this.words.length; i++) { + if (_.isArray(thing)) { + if (_.includes(thing, this.words[i][list])) { + results.push(this.words[i].lemma); + } + } else if (_.isArray(this.words[i][list])) { + if (_.includes(this.words[i][list], thing)) { + results.push(this.words[i].lemma); + } + } + } + return results; + } + + findByLem(word) { + for (let i = 0; i < this.words.length; i++) { + if (this.words[i].lemma === word) { + return this.words[i]; + } + } + return null; + } +} + +export default Dict; diff --git a/src/bot/factSystem.js b/src/bot/factSystem.js new file mode 100644 index 00000000..a51da076 --- /dev/null +++ b/src/bot/factSystem.js @@ -0,0 +1,10 @@ +import facts from 'sfacts'; + +const createFactSystem = function createFactSystem({ name, clean, importData }, callback) { + if (importData) { + return facts.load(name, importData, clean, callback); + } + return facts.create(name, clean, callback); +}; + +export default createFactSystem; diff --git a/src/bot/getReply.js b/src/bot/getReply.js new file mode 100644 index 00000000..5d92ddc1 --- /dev/null +++ b/src/bot/getReply.js @@ -0,0 +1,447 @@ +import _ from 'lodash'; +import debuglog from 'debug-levels'; +import async from 'async'; +import RE2 from 're2'; + +import regexes from './regexes'; +import Utils from './utils'; +import processTags from './processTags'; + +const debug = debuglog('SS:GetReply'); + +// Topic iterator, we call this on each topic or conversation reply looking for a match. +// All the matches are stored and returned in the callback. +const topicItorHandle = function topicItorHandle(messageObject, options) { + const system = options.system; + + return (topicData, callback) => { + if (topicData.type === 'TOPIC') { + system.chatSystem.Topic.findOne({ _id: topicData.id }) + .populate('gambits') + .populate('conditions') + .exec((err, topic) => { + if (err) { + console.error(err); + } + if (topic) { + topic.checkConditions(messageObject, options, (err, matches) => { + debug.verbose('Checking for conditions in %s', topic.name); + + if (!_.isEmpty(matches)) { + callback(err, matches); + } else { + // TODO: Seems overkill to clear entire conversation state - should just kill state that + // exists within the topic + options.user.clearConversationState(() => { + debug.verbose("Topic either has no conditions or couldn't find any matches using the topic conditions"); + debug.verbose(`Defaulting to finding match for topic ${topic.name}`); + // We do realtime post processing on the input against the user object + topic.findMatch(messageObject, options, callback); + }); + } + }); + } else { + // We call back if there is no topic Object + // Non-existant topics return false + callback(null, false); + } + } + ); + } else if (topicData.type === 'REPLY') { + system.chatSystem.Reply.findOne({ _id: topicData.id }) + .populate('gambits') + .exec((err, reply) => { + if (err) { + console.error(err); + } + debug.verbose('Conversation reply thread: ', reply); + if (reply) { + reply.findMatch(messageObject, options, callback); + } else { + callback(null, false); + } + } + ); + } else { + debug.verbose("We shouldn't hit this! 'topicData.type' should be 'TOPIC' or 'REPLY'"); + callback(null, false); + } + }; +}; + +const afterHandle = function afterHandle(user, callback) { + // Note, the first arg is the ReplyBit (normally the error); + // We are breaking the matchItorHandle flow on data stream. + return (replyBit, matchSet) => { + debug.verbose('MatchSet', replyBit, matchSet); + + // remove empties + matchSet = _.compact(matchSet); + + const minMatchSet = []; + let props = {}; + let clearConvo = false; + let lastTopicToMatch = null; + let lastStarSet = null; + let lastReplyId = null; + let replyString = ''; + let lastSubReplies = null; + let lastBreakBit = null; + + for (let i = 0; i < matchSet.length; i++) { + const item = matchSet[i]; + const mmm = { + topic: item.matched_topic_string || item.topic, + input: item.trigger, + reply: item.matched_reply_string, + }; + + if (!_.isEmpty(item.minMatchSet)) { + mmm.subset = item.minMatchSet; + } else { + mmm.output = item.reply.reply; + } + + minMatchSet.push(mmm); + + if (item && item.reply && item.reply.reply) { + replyString += `${item.reply.reply} `; + } + + props = _.assign(props, item.props); + lastTopicToMatch = item.topic; + lastStarSet = item.stars; + lastReplyId = item.reply._id; + lastSubReplies = item.subReplies; + lastBreakBit = item.breakBit; + + if (item.clearConvo) { + clearConvo = item.clearConvo; + } + } + + let threadsArr = []; + if (_.isEmpty(lastSubReplies)) { + threadsArr = processTags.processThreadTags(replyString); + } else { + threadsArr[0] = replyString; + threadsArr[1] = lastSubReplies; + } + + // only remove one trailing space (because spaces may have been added deliberately) + const replyStr = new RE2('(?:^[ \\t]+)|(?:[ \\t]$)').replace(threadsArr[0], ''); + + const cbdata = { + replyId: lastReplyId, + props, + clearConvo, + topicName: lastTopicToMatch, + minMatchSet, + string: replyStr, + subReplies: threadsArr[1], + stars: lastStarSet, + breakBit: lastBreakBit, + }; + + debug.verbose('afterHandle', cbdata); + + callback(null, cbdata); + }; +}; + +// This may be called several times, once for each topic. +const filterRepliesBySeen = function filterRepliesBySeen(filteredResults, options, callback) { + const system = options.system; + debug.verbose('filterRepliesBySeen', filteredResults); + const bucket = []; + + const eachResultItor = function eachResultItor(filteredResult, next) { + const topicName = filteredResult.topic; + system.chatSystem.Topic + .findOne({ name: topicName }) + .exec((err, currentTopic) => { + if (err) { + console.log(err); + } + + // var repIndex = filteredResult.id; + const replyId = filteredResult.reply._id; + const reply = filteredResult.reply; + const gambitId = filteredResult.trigger_id2; + let seenReply = false; + + // Filter out SPOKEN replies. + // If something is said on a different trigger we don't remove it. + // If the trigger is very open ie "*", you should consider putting a {keep} flag on it. + + for (let i = 0; i <= 10; i++) { + const topicItem = options.user.__history__.topic[i]; + + if (topicItem !== undefined) { + // TODO: Come back to this and check names make sense + const pastGambit = options.user.__history__.reply[i]; + const pastInput = options.user.__history__.input[i]; + + // Sometimes the history has null messages because we spoke first. + if (pastGambit && pastInput) { + // Do they match and not have a keep flag + + debug.verbose('--------------- FILTER SEEN ----------------'); + debug.verbose('Past replyId', pastGambit.replyId); + debug.verbose('Current replyId', replyId); + debug.verbose('Past gambitId', String(pastInput.gambitId)); + debug.verbose('Current gambitId', String(gambitId)); + debug.verbose('reply.keep', reply.keep); + debug.verbose('currentTopic.keep', currentTopic.keep); + + if (String(replyId) === String(pastGambit.replyId) && + // TODO: For conversation threads this should be disabled because we are looking + // the wrong way. + // But for forward threads it should be enabled. + // String(pastInput.gambitId) === String(inputId) && + reply.keep === false && + currentTopic.keep === false) { + debug.verbose('Already Seen', reply); + seenReply = true; + } + } + } + } + + if (!seenReply || system.editMode) { + bucket.push(filteredResult); + } + next(); + }); + }; + + async.each(filteredResults, eachResultItor, () => { + debug.verbose('Bucket of selected replies: ', bucket); + if (!_.isEmpty(bucket)) { + callback(null, Utils.pickItem(bucket)); + } else { + callback(true); + } + }); +}; // end filterBySeen + +const filterRepliesByFunction = function filterRepliesByFunction(potentialReplies, options, callback) { + const filterHandle = function filterHandle(potentialReply, cb) { + const system = options.system; + + // We support a single filter function in the reply + // It returns true/false to aid in the selection. + + if (potentialReply.reply.filter !== '') { + const filterFunction = regexes.filter.match(potentialReply.reply.filter); + const pluginName = Utils.trim(filterFunction[1]); + const partsStr = Utils.trim(filterFunction[2]); + const args = Utils.replaceCapturedText(partsStr.split(','), [''].concat(potentialReply.stars)); + + debug.verbose(`Filter function found with plugin name: ${pluginName}`); + + if (system.plugins[pluginName]) { + args.push((err, filterReply) => { + if (err) { + console.log(err); + } + + if (filterReply === 'true' || filterReply === true) { + cb(err, true); + } else { + cb(err, false); + } + }); + + const filterScope = _.merge({}, system.scope); + filterScope.user = options.user; + filterScope.message = options.message; + filterScope.message_props = options.system.extraScope; + + debug.verbose(`Calling plugin function: ${pluginName} with args: ${args}`); + system.plugins[pluginName].apply(filterScope, args); + } else { + // If a function is missing, we kill the line and return empty handed + // Let's remove it and try to carry on. + console.log(`\nWARNING:\nYou have a missing filter function (${pluginName}) - your script will not behave as expected!"`); + // Wow, worst variable name ever - sorry. + potentialReply = Utils.trim(potentialReply.reply.reply.replace(filterFunction[0], '')); + cb(null, true); + } + } else { + cb(null, true); + } + }; + + async.filter(potentialReplies, filterHandle, (err, filteredReplies) => { + debug.verbose('filterByFunction results: ', filteredReplies); + + filterRepliesBySeen(filteredReplies, options, (err, reply) => { + if (err) { + debug.error(err); + // Keep looking for results + // Invoking callback with no arguments ensure mapSeries carries on looking at matches from other gambits + callback(); + } else { + processTags.processReplyTags(reply, options, (err, replyObj) => { + if (!_.isEmpty(replyObj)) { + // reply is the selected reply object that we created earlier (wrapped mongoDB reply) + // reply.reply is the actual mongoDB reply object + // reply.reply.reply is the reply string + replyObj.matched_reply_string = reply.reply.reply; + replyObj.matched_topic_string = reply.topic; + + debug.verbose('ProcessTags Return', replyObj); + + if (replyObj.breakBit === false) { + debug.info('Forcing CHECK MORE Mbit'); + callback(null, replyObj); + } else if (replyObj.breakBit === true || replyObj.reply.reply !== '') { + callback(true, replyObj); + } else { + debug.info('Forcing CHECK MORE Empty Reply'); + callback(null, replyObj); + } + } else { + debug.verbose('ProcessTags Empty'); + if (err) { + debug.verbose('There was an err in processTags', err); + } + callback(null, null); + } + }); + } + }); + }); +}; + +// Iterates through matched gambits +const matchItorHandle = function matchItorHandle(message, options) { + const system = options.system; + options.message = message; + + return (match, callback) => { + debug.verbose('Match itor: ', match.gambit); + + // In some edge cases, replies were not being populated... + // Let's do it here + system.chatSystem.Gambit.findById(match.gambit._id) + .populate('replies') + .exec((err, gambitExpanded) => { + if (err) { + console.log(err); + } + + match.gambit = gambitExpanded; + + match.gambit.getRootTopic((err, topic) => { + if (err) { + console.log(err); + } + + let rootTopic; + if (match.topic) { + rootTopic = match.topic; + } else { + rootTopic = topic; + } + + let stars = match.stars; + if (!_.isEmpty(message.stars)) { + stars = message.stars; + } + + const potentialReplies = []; + + for (let i = 0; i < match.gambit.replies.length; i++) { + const reply = match.gambit.replies[i]; + const replyData = { + id: reply.id, + topic: rootTopic, + stars, + reply, + + // For the logs + trigger: match.gambit.input, + trigger_id: match.gambit.id, + trigger_id2: match.gambit._id, + }; + potentialReplies.push(replyData); + } + + // Find a reply for the match. + filterRepliesByFunction(potentialReplies, options, callback); + }); + } + ); + }; +}; + +/** + * The real craziness to retreive a reply. + * @param {Object} messageObject - The instance of the Message class for the user input. + * @param {Object} options.system - The system. + * @param {Object} options.user - The user. + * @param {Number} options.depth - The depth of how many times this function has been recursively called. + * @param {Array} options.pendingTopics - A list of topics that have been specified to specifically search (usually via topicRedirect etc). + * @param {Function} callback - Callback function once the reply has been found. + */ +const getReply = function getReply(messageObject, options, callback) { + // This method can be called recursively. + if (options.depth) { + debug.verbose('Called Recursively', options.depth); + if (options.depth >= 50) { + console.error('getReply was called recursively 50 times - returning null reply.'); + return callback(null, null); + } + } + + // We already have a pre-set list of potential topics from directReply, respond or topicRedirect + if (!_.isEmpty(options.pendingTopics)) { + debug.verbose('Using pre-set topic list via directReply, respond or topicRedirect'); + debug.info('Topics to check: ', options.pendingTopics.map(topic => topic.name)); + afterFindPendingTopics(options.pendingTopics, messageObject, options, callback); + } else { + const chatSystem = options.system.chatSystem; + + // Find potential topics for the response based on the message (tfidfs) + chatSystem.Topic.findPendingTopicsForUser(options.user, messageObject, (err, pendingTopics) => { + if (err) { + console.log(err); + } + afterFindPendingTopics(pendingTopics, messageObject, options, callback); + }); + } +}; + +const afterFindPendingTopics = function afterFindPendingTopics(pendingTopics, messageObject, options, callback) { + debug.verbose(`Found pending topics/conversations: ${JSON.stringify(pendingTopics)}`); + + // We use map here because it will bail on error. + // The error is our escape hatch when we have a reply WITH data. + async.mapSeries( + pendingTopics, + topicItorHandle(messageObject, options), + (err, results) => { + if (err) { + console.error(err); + } + + // Remove the empty topics, and flatten the array down. + let matches = _.flatten(_.filter(results, n => n)); + + // TODO - This sort should happen in the process sort logic. + // Let's sort the matches by qType.length + matches = matches.sort((a, b) => + a.gambit.qType.length < b.gambit.qType.length + ); + + debug.verbose(`Matching gambits are: ${matches}`); + + // Was `eachSeries` + async.mapSeries(matches, matchItorHandle(messageObject, options), afterHandle(options.user, callback)); + } + ); +}; + +export default getReply; diff --git a/lib/history.js b/src/bot/history.js similarity index 56% rename from lib/history.js rename to src/bot/history.js index 9b01510d..db299ab9 100644 --- a/lib/history.js +++ b/src/bot/history.js @@ -1,29 +1,29 @@ -var _ = require("lodash"); -var debug = require("debug")("History"); +import _ from 'lodash'; +import debuglog from 'debug-levels'; + +const debug = debuglog('SS:History'); // This function walks the history input and looks for utterances previously spoken // to help answer the or solidify the statement - -var historyLookup = function (user, options) { - debug("History Lookup with", options); - var i; - var canidates = []; - var nn; - - var moneyWords = function (item) { - return item[1] === "$" || - item[0] === "quid" || - item[0] === "pounds" || - item[0] === "dollars" || - item[0] === "bucks" || - item[0] === "cost"; +const historyLookup = function historyLookup(user, options) { + debug.verbose('History Lookup with', options); + + const candidates = []; + let nn; + + const moneyWords = (item) => { + return item[1] === '$' || + item[0] === 'quid' || + item[0] === 'pounds' || + item[0] === 'dollars' || + item[0] === 'bucks' || + item[0] === 'cost'; }; - for (i = 0; i < user.__history__.input.length; i++) { - var pobj = user.__history__.input[i]; + for (let i = 0; i < user.__history__.input.length; i++) { + let pobj = user.__history__.input[i]; if (pobj !== undefined) { - // TODO - See why we are getting a nested array. if (Array.isArray(pobj)) { pobj = pobj[0]; @@ -31,46 +31,45 @@ var historyLookup = function (user, options) { if (options.numbers || options.number) { if (pobj.numbers.length !== 0) { - canidates.push(pobj); + candidates.push(pobj); } } // Special case of number if (options.money === true && options.nouns) { if (pobj.numbers.length !== 0) { - - var t = []; + const t = []; if (_.any(pobj.taggedWords, moneyWords)) { t.push(pobj); // Now filter out the nouns - for (var n = 0; n < t.length; n++) { - nn = _.any(t[n].nouns, function (item) { - for (i = 0; i < options.nouns.length; i++) { + for (let n = 0; n < t.length; n++) { + nn = _.any(t[n].nouns, (item) => { + for (let j = 0; j < options.nouns.length; j++) { return options.nouns[i] === item ? true : false; } }); } if (nn) { - canidates.push(pobj); + candidates.push(pobj); } } } } else if (options.money && pobj) { if (pobj.numbers.length !== 0) { if (_.any(pobj.taggedWords, moneyWords)) { - canidates.push(pobj); + candidates.push(pobj); } } } else if (options.nouns && pobj) { - debug("Noun Lookup"); + debug.verbose('Noun Lookup'); if (_.isArray(options.nouns)) { var s = 0; var c = 0; - nn = _.any(pobj.nouns, function (item) { - var x = _.includes(options.nouns, item); + nn = _.any(pobj.nouns, (item) => { + const x = _.includes(options.nouns, item); c++; s = x ? s + 1 : s; return x; @@ -78,32 +77,32 @@ var historyLookup = function (user, options) { if (nn) { pobj.score = s / c; - canidates.push(pobj); + candidates.push(pobj); } } else if (pobj.nouns.length !== 0) { - canidates.push(pobj); + candidates.push(pobj); } } else if (options.names && pobj) { - debug("Name Lookup"); + debug.verbose('Name Lookup'); if (_.isArray(options.names)) { - nn = _.any(pobj.names, function (item) { + nn = _.any(pobj.names, (item) => { return _.includes(options.names, item); }); if (nn) { - canidates.push(pobj); + candidates.push(pobj); } } else if (pobj.names.length !== 0) { - canidates.push(pobj); + candidates.push(pobj); } } else if (options.adjectives && pobj) { - debug("adjectives Lookup"); + debug.verbose('adjectives Lookup'); if (_.isArray(options.adjectives)) { s = 0; c = 0; - nn = _.any(pobj.adjectives, function (item) { - var x = _.includes(options.adjectives, item); + nn = _.any(pobj.adjectives, (item) => { + const x = _.includes(options.adjectives, item); c++; s = x ? s + 1 : s; return x; @@ -111,22 +110,22 @@ var historyLookup = function (user, options) { if (nn) { pobj.score = s / c; - canidates.push(pobj); + candidates.push(pobj); } } else if (pobj.adjectives.length !== 0) { - canidates.push(pobj); + candidates.push(pobj); } } if (options.date && pobj) { if (pobj.date !== null) { - canidates.push(pobj); + candidates.push(pobj); } } } } - return canidates; + return candidates; }; -module.exports = historyLookup; +export default historyLookup; diff --git a/src/bot/index.js b/src/bot/index.js new file mode 100644 index 00000000..003a138b --- /dev/null +++ b/src/bot/index.js @@ -0,0 +1,248 @@ +import _ from 'lodash'; +import requireDir from 'require-dir'; +import debuglog from 'debug-levels'; + +import Utils from './utils'; +import processHelpers from './reply/common'; +import connect from './db/connect'; +import createFactSystem from './factSystem'; +import createChatSystem from './chatSystem'; +import getReply from './getReply'; +import Importer from './db/import'; +import Message from './message'; + +const debug = debuglog('SS:SuperScript'); + +class SuperScript { + /** + * Creates a new SuperScript instance. Since SuperScript doesn't use global state, + * you may have multiple instances at once. + * @param {Object} options - Any configuration settings you want to use. + * @param {String} options.URL - The database URL you want to connect to. + * @param {String} options.factSystemName - The name you want to give to the fact system created. + */ + constructor(options) { + // Create a new database connection + this.db = connect(options.mongoURI, options.mongoDB); + + this.plugins = []; + + // For user plugins + Utils.mkdirSync('../plugins'); + this.loadPlugins('../plugins'); + // Built-in plugins + this.loadPlugins(`${__dirname}/../plugins`); + + // This is a kill switch for filterBySeen which is useless in the editor. + this.editMode = options.editMode || false; + } + + importFile(filePath, callback) { + Importer.importFile(this.chatSystem, filePath, (err) => { + console.log('Bot is ready for input!'); + debug.verbose('System loaded, waiting for replies'); + callback(err); + }); + } + + loadPlugins(path) { + const plugins = requireDir(path); + + for (const file in plugins) { + // For transpiled ES6 plugins + if (plugins[file].default) { + plugins[file] = plugins[file].default; + } + + for (const func in plugins[file]) { + debug.verbose('Loading plugin: ', path, func); + this.plugins[func] = plugins[file][func]; + } + } + } + + getUsers(callback) { + this.chatSystem.User.find({}, 'id', callback); + } + + getUser(userId, callback) { + this.chatSystem.User.findOne({ id: userId }, callback); + } + + findOrCreateUser(userId, callback) { + const findProps = { id: userId }; + const createProps = { + currentTopic: 'random', + status: 0, + conversation: 0, + volley: 0, + rally: 0, + }; + + this.chatSystem.User.findOrCreate(findProps, createProps, callback); + } + + // Converts msg into a message object, then checks for a match + reply(userId, messageString, callback, extraScope) { + // TODO: Check if random assignment of existing user ID causes problems + if (arguments.length === 2 && typeof messageString === 'function') { + callback = messageString; + messageString = userId; + userId = Math.random().toString(36).substr(2, 5); + extraScope = {}; + } + + debug.log("[ New Message - '%s']- %s", userId, messageString); + const options = { + userId, + extraScope, + }; + + this._reply(messageString, options, callback); + } + + // This is like doing a topicRedirect + directReply(userId, topicName, messageString, callback) { + debug.log("[ New DirectReply - '%s']- %s", userId, messageString); + const options = { + userId, + topicName, + extraScope: {}, + }; + + this._reply(messageString, options, callback); + } + + message(messageString, callback) { + const options = { + factSystem: this.factSystem, + }; + + Message.createMessage(messageString, options, (msgObj) => { + callback(null, msgObj); + }); + } + + _reply(messageString, options, callback) { + const system = { + // Pass in the topic if it has been set + topicName: options.topicName || null, + plugins: this.plugins, + scope: this.scope, + extraScope: options.extraScope, + chatSystem: this.chatSystem, + factSystem: this.factSystem, + editMode: this.editMode, + }; + + this.findOrCreateUser(options.userId, (err, user) => { + if (err) { + debug.error(err); + } + + const messageOptions = { + factSystem: this.factSystem, + }; + + Message.createMessage(messageString, messageOptions, (messageObject) => { + processHelpers.getTopic(system.chatSystem, system.topicName, (err, topicData) => { + const options = { + user, + system, + depth: 0, + }; + + if (topicData) { + options.pendingTopics = [topicData]; + } + + getReply(messageObject, options, (err, replyObj) => { + // Convert the reply into a message object too. + let replyMessage = ''; + const messageOptions = { + factSystem: system.factSystem, + }; + + if (replyObj) { + messageOptions.replyId = replyObj.replyId; + replyMessage = replyObj.string; + + if (replyObj.clearConvo) { + messageOptions.clearConvo = replyObj.clearConvo; + } + } else { + replyObj = {}; + console.log('There was no response matched.'); + } + + Message.createMessage(replyMessage, messageOptions, (replyMessageObject) => { + user.updateHistory(messageObject, replyMessageObject, replyObj, (err, log) => { + // We send back a smaller message object to the clients. + const clientObject = { + replyId: replyObj.replyId, + createdAt: replyMessageObject.createdAt || new Date(), + string: replyMessage || '', // replyMessageObject.raw || "", + topicName: replyObj.topicName, + subReplies: replyObj.subReplies, + debug: log, + }; + + const newClientObject = _.merge(clientObject, replyObj.props || {}); + + debug.verbose("Update and Reply to user '%s'", user.id, replyObj.string); + debug.info("[ Final Reply - '%s']- '%s'", user.id, replyObj.string); + + return callback(err, newClientObject); + }); + }); + }); + }); + }); + }); + } +} + +const defaultOptions = { + mongoURI: 'mongodb://localhost/', + mongoDB: 'superscriptDB', + importFile: null, + factSystem: { + name: 'botFacts', + clean: false, + importData: null, + }, + scope: {}, + editMode: false, +}; + +const create = function create(options = {}, callback) { + options = _.merge(defaultOptions, options); + const bot = new SuperScript(options); + + // Uses schemas to create models for the db connection to use + createFactSystem(options.factSystem, (err, factSystem) => { + if (err) { + return callback(err); + } + + bot.factSystem = factSystem; + bot.chatSystem = createChatSystem(bot.db, bot.factSystem); + + // We want a place to store bot related data + bot.memory = bot.factSystem.createUserDB('botfacts'); + + bot.scope = {}; + bot.scope = _.extend(options.scope || {}); + bot.scope.bot = bot; + bot.scope.facts = bot.factSystem; + bot.scope.chatSystem = bot.chatSystem; + bot.scope.botfacts = bot.memory; + + if (options.importFile) { + return bot.importFile(options.importFile, err => callback(err, bot)); + } + return callback(null, bot); + }); +}; + +export default create; diff --git a/src/bot/math.js b/src/bot/math.js new file mode 100644 index 00000000..24d537f7 --- /dev/null +++ b/src/bot/math.js @@ -0,0 +1,305 @@ +/* eslint no-eval:0 */ +// TODO - Make this into its own project + +import _ from 'lodash'; +import debuglog from 'debug'; + +const debug = debuglog('math'); + +const cardinalNumberPlural = { + first: 1, + second: 2, + third: 3, + fourth: 4, + fifth: 5, + sixth: 6, + seventh: 7, + eigth: 8, + ninth: 9, + tenth: 10, + eleventh: 11, + twelfth: 12, + thirteenth: 13, + fourteenth: 14, + fifteenth: 15, + sixteenth: 16, + seventeenth: 17, + eighteenth: 18, + nineteenth: 19, + twentieth: 20, + 'twenty-first': 21, + 'twenty-second': 22, + 'twenty-third': 23, + 'twenty-fourth': 24, + 'twenty-fifth': 25, + 'twenty-sixth': 26, +}; + +const cardinalNumbers = { + one: 1, + two: 2, + three: 3, + four: 4, + five: 5, + six: 6, + seven: 7, + eight: 8, + nine: 9, + ten: 10, + eleven: 11, + twelve: 12, + thirteen: 13, + fourteen: 14, + fifteen: 15, + sixteen: 16, + seventeen: 17, + eighteen: 18, + nineteen: 19, + twenty: 20, + thirty: 30, + forty: 40, + fifty: 50, + sixty: 60, + seventy: 70, + eighty: 80, + ninety: 90, +}; + +const multiplesOfTen = { + twenty: 20, + thirty: 30, + forty: 40, + fifty: 50, + sixty: 60, + seventy: 70, + eighty: 80, + ninety: 90, +}; + +const mathExpressionSubs = { + plus: '+', + minus: '-', + multiply: '*', + multiplied: '*', + x: '*', + times: '*', + divide: '/', + divided: '/', +}; + +const mathTerms = ['add', 'plus', 'and', '+', '-', 'minus', 'subtract', 'x', + 'times', 'multiply', 'multiplied', 'of', 'divide', 'divided', '/', 'half', + 'percent', '%']; + +const isNumeric = function isNumeric(num) { + return !isNaN(num); +}; + +// Given an array for words it returns the evauated sum. +// TODO - fractions +// TODO, words should be the dict object with lem words to fix muliply / multipled etc +const parse = function parse(words, prev = 0) { + debug('In parse with ', words); + const expression = []; + const newexpression = []; + let i; + let word; + + for (i = 0; i < words.length; i++) { + const digit = convertWordToNumber(words[i]); + if (digit !== undefined) { + words[i] = digit; + } + + word = words[i]; + if (mathExpressionSubs[word] !== undefined) { + words[i] = mathExpressionSubs[word]; + } + } + + for (i = 0; i < words.length; i++) { + word = words[i]; + if (/[\d\*\+\-\/=%]|of|half|percent/.test(word)) { + if (word === 'half') { + newexpression.push(0.5); + } else if (word === 'of') { + expression.push('*'); + } else if ((word === '%' || word === 'percent') && isNumeric(words[i - 1])) { + expression.pop(); + expression.push(parseInt(words[i - 1]) / 100); + } else { + expression.push(word); + } + } + } + + for (i = 0; i < expression.length; i++) { + const curr = expression[i]; + const next = expression[i + 1]; + newexpression.push(curr); + if (/\d/.test(curr) && /\d/.test(next)) { + newexpression.push('+'); + } + } + + try { + // reintruduce + if (newexpression.length === 2 || newexpression[0] === '+') { + newexpression.unshift(prev); + } + debug('Eval', newexpression.join(' ')); + const value = eval(newexpression.join(' ')); + return +value.toFixed(2); + } catch (e) { + debug('Error', e); + return null; + } +}; + +// Given an array of words, lets convert them to numbers +// We want to subsitute one - one thousand to numberic form +// TODO handle "two-hundred" hypenated hundred/thousand +const convertWordsToNumbers = function convertWordsToNumbers(wordArray) { + const mult = { hundred: 100, thousand: 1000 }; + const results = []; + let i; + + for (i = 0; i < wordArray.length; i++) { + // Some words need lookahead / lookbehind like hundred, thousand + if (['hundred', 'thousand'].indexOf(wordArray[i]) >= 0) { + results.push(String(parseInt(results.pop()) * mult[wordArray[i]])); + } else { + results.push(convertWordToNumber(wordArray[i])); + } + } + + // Second Pass add 'and's together + for (i = 0; i < results.length; i++) { + if (isNumeric(results[i]) && results[i + 1] === 'and' && isNumeric(results[i + 2])) { + const val = parseInt(results[i]) + parseInt(results[i + 2]); + results.splice(i, 3, String(val)); + i--; + } + } + return results; +}; + +const convertWordToNumber = function convertWordToNumber(word) { + let number; + let multipleOfTen; + let cardinalNumber; + + if (word !== undefined) { + if (word.indexOf('-') === -1) { + if (_.includes(Object.keys(cardinalNumbers), word)) { + number = String(cardinalNumbers[word]); + } else { + number = word; + } + } else { + multipleOfTen = word.split('-')[0]; // e.g. "seventy" + cardinalNumber = word.split('-')[1]; // e.g. "six" + if (multipleOfTen !== '' && cardinalNumber !== '') { + const n = multiplesOfTen[multipleOfTen] + cardinalNumbers[cardinalNumber]; + if (isNaN(n)) { + number = word; + } else { + number = String(n); + } + } else { + number = word; + } + } + return number; + } else { + return word; + } +}; + +const numberLookup = function numberLookup(number) { + let multipleOfTen; + let word = ''; + + if (number < 20) { + for (const cardinalNumber in cardinalNumbers) { + if (number === cardinalNumbers[cardinalNumber]) { + word = cardinalNumber; + break; + } + } + } else if (number < 100) { + if (number % 10 === 0) { // If the number is a multiple of ten + for (multipleOfTen in multiplesOfTen) { + if (number === multiplesOfTen[multipleOfTen]) { + word = multipleOfTen; + break; + } + } + } else { // not a multiple of ten + for (multipleOfTen in multiplesOfTen) { + for (let i = 9; i > 0; i--) { + if (number === multiplesOfTen[multipleOfTen] + i) { + word = `${multipleOfTen}-${convertNumberToWord(i)}`; + break; + } + } + } + } + } else { + // TODO - + console.log("We don't handle numbers greater than 99 yet."); + } + + return word; +}; + +const convertNumberToWord = function convertNumberToWord(number) { + if (number === 0) { + return 'zero'; + } + + if (number < 0) { + return `negative ${numberLookup(Math.abs(number))}`; + } + + return numberLookup(number); +}; + +const cardPlural = function cardPlural(wordNumber) { + return cardinalNumberPlural[wordNumber]; +}; + +const arithGeo = function arithGeo(arr) { + let ap; + let gp; + + for (let i = 0; i < arr.length - 2; i++) { + if (!(ap = arr[i + 1] - arr[i] === arr[i + 2] - arr[i + 1])) { + break; + } + } + + if (ap) { + return 'Arithmetic'; + } + + for (let i = 0; i < arr.length - 2; i++) { + if (!(gp = arr[i + 1] / arr[i] === arr[i + 2] / arr[i + 1])) { + break; + } + } + + if (gp) { + return 'Geometric'; + } + return -1; +}; + +export default { + arithGeo, + cardPlural, + convertWordToNumber, + convertWordsToNumbers, + mathTerms, + parse, +}; diff --git a/src/bot/message.js b/src/bot/message.js new file mode 100644 index 00000000..a0aa9171 --- /dev/null +++ b/src/bot/message.js @@ -0,0 +1,430 @@ +import _ from 'lodash'; +import qtypes from 'qtypes'; +import pos from 'parts-of-speech'; +import natural from 'natural'; +import moment from 'moment'; +import Lemmer from 'lemmer'; +import async from 'async'; +import debuglog from 'debug-levels'; +import normalize from 'node-normalizer'; + +import math from './math'; +import Dict from './dict'; +import Utils from './utils'; + +const debug = debuglog('SS:Message'); +const ngrams = natural.NGrams; + +const patchList = function (fullEntities, things) { + const stopList = ['I']; + + things = things.filter(item => + !(stopList.indexOf(item) !== -1) + ); + + for (let i = 0; i < fullEntities.length; i++) { + for (let j = 0; j < things.length; j++) { + const thing = things[j]; + if (fullEntities[i].indexOf(thing) > 0) { + things[j] = fullEntities[i]; + } + } + } + return things; +}; + +const cleanMessage = function cleanMessage(message) { + message = message.replace(/\./g, ' '); + message = message.replace(/\s,\s/g, ' '); + // these used to be bursted but are not anymore. + message = message.replace(/([a-zA-Z]),\s/g, '$1 '); + message = message.replace(/"(.*)"/g, '$1'); + message = message.replace(/\s"\s?/g, ' '); + message = message.replace(/\s'\s?/g, ' '); + message = message.replace(/\s?!\s?/g, ' '); + message = message.replace(/\s?!\s?/g, ' '); + return message; +}; + +// The message could be generated by a reply or raw input +// If it is a reply, we want to save the ID so we can filter them out if said again +class Message { + /** + * Creates a new Message object. + * @param {String} message - The cleaned message. + * @param {Object} options - The parameters. + * @param {String} options.original - The original message text. + * @param {Object} options.factSystem - The fact system to use. + * @param {String} [options.replyId] - If the message is based on a reply. + * @param {String} [options.clearConvo] - If you want to clear the conversation. + */ + constructor(message, options) { + debug.verbose(`Creating message from string: ${message}`); + + this.id = Utils.genId(); + + // If this message is based on a Reply. + if (options.replyId) { + this.replyId = options.replyId; + } + + if (options.clearConvo) { + this.clearConvo = options.clearConvo; + } + + this.factSystem = options.factSystem; + this.createdAt = new Date(); + + // This version of the message is `EXACTLY AS WRITTEN` by the user + this.original = message; + this.raw = normalize.clean(message).trim(); + this.clean = cleanMessage(this.raw).trim(); + debug.verbose('Message before cleaning: ', message); + debug.verbose('Message after cleaning: ', this.clean); + + this.props = {}; + + let words = new pos.Lexer().lex(this.clean); + // This is used in the wordnet plugin (removing it will break it!) + this.words = words; + + // This is where we keep the words + this.dict = new Dict(words); + + words = math.convertWordsToNumbers(words); + this.taggedWords = new pos.Tagger().tag(words); + } + + static createMessage(message, options, callback) { + if (!message) { + debug.verbose('Message received was empty, callback immediately'); + return callback({}); + } + + const messageObj = new Message(message, options); + messageObj.finishCreating(callback); + } + + finishCreating(callback) { + this.lemma((err, lemWords) => { + if (err) { + console.log(err); + } + + this.lemWords = lemWords; + this.lemString = this.lemWords.join(' '); + + this.posWords = this.taggedWords.map(hash => + hash[1] + ); + this.posString = this.posWords.join(' '); + + this.dict.add('lemma', this.lemWords); + this.dict.add('pos', this.posWords); + + // Classify Question + this.questionType = qtypes.classify(this.lemString); + this.questionSubType = qtypes.questionType(this.clean); + this.isQuestion = qtypes.isQuestion(this.raw); + + // TODO: This is currently unused - why? + // Sentence Sentiment + this.sentiment = 0; + + // Get Nouns and Noun Phrases. + this.nouns = this.fetchNouns(); + this.names = this.fetchComplexNouns('names'); + + // A list of terms + // this would return an array of thems this are a, b and c; + // Helpful for choosing something when the qSubType is CH + this.list = this.fetchList(); + this.adjectives = this.fetchAdjectives(); + this.adverbs = this.fetchAdverbs(); + this.verbs = this.fetchVerbs(); + this.pronouns = this.pnouns = this.fetchPronouns(); + this.compareWords = this.fetchCompareWords(); + this.numbers = this.fetchNumbers(); + this.compare = this.compareWords.length !== 0; + this.date = this.fetchDate(); + + this.names = _.uniq(this.names, name => + name.toLowerCase() + ); + + // Nouns with Names removed. + const lowerCaseNames = this.names.map(name => + name.toLowerCase() + ); + + this.cNouns = _.filter(this.nouns, item => + !_.includes(lowerCaseNames, item.toLowerCase()) + ); + + this.checkMath(); + + // Things are nouns + complex nouns so + // turkey and french fries would return ['turkey','french fries'] + // this should probably run the list though concepts or something else to validate them more + // than NN NN etc. + this.fetchNamedEntities((entities) => { + const complexNouns = this.fetchComplexNouns('nouns'); + const fullEntities = entities.map(item => + item.join(' ') + ); + + this.entities = patchList(fullEntities, complexNouns); + this.list = patchList(fullEntities, this.list); + + debug.verbose('Message: ', this); + callback(this); + }); + }); + } + + // We only want to lemmatize the nouns, verbs, adverbs and adjectives. + lemma(callback) { + const itor = function (hash, next) { + const word = hash[0].toLowerCase(); + const tag = Utils.pennToWordnet(hash[1]); + + // console.log(word, tag); + // next(null, [word]); + + if (tag) { + try { + Lemmer.lemmatize(`${word}#${tag}`, next); + } catch (e) { + console.log('Caught in Excption', e); + // This is probably because it isn't an english word. + next(null, [word]); + } + } else { + // Some words don't have a tag ie: like, to. + next(null, [word]); + } + }; + + async.map(this.taggedWords, itor, (err, lemWords) => { + const result = _.map(_.flatten(lemWords), lemWord => + lemWord.split('#')[0] + ); + callback(err, result); + }); + } + + checkMath() { + let numCount = 0; + let oppCount = 0; + + for (let i = 0; i < this.taggedWords.length; i++) { + if (this.taggedWords[i][1] === 'CD') { + numCount += 1; + } + if (this.taggedWords[i][1] === 'SYM' || + math.mathTerms.indexOf(this.taggedWords[i][0]) !== -1) { + // Half is a number and not an opp + if (this.taggedWords[i][0] === 'half') { + numCount += 1; + } else { + oppCount += 1; + } + } + } + + // Augment the Qtype for Math Expressions + this.numericExp = (numCount >= 2 && oppCount >= 1); + this.halfNumericExp = (numCount === 1 && oppCount === 1); + + if (this.numericExp || this.halfNumericExp) { + this.questionType = 'NUM:expression'; + this.isQuestion = true; + } + } + + fetchCompareWords() { + return this.dict.fetch('pos', ['JJR', 'RBR']); + } + + fetchAdjectives() { + return this.dict.fetch('pos', ['JJ', 'JJR', 'JJS']); + } + + fetchAdverbs() { + return this.dict.fetch('pos', ['RB', 'RBR', 'RBS']); + } + + fetchNumbers() { + return this.dict.fetch('pos', ['CD']); + } + + fetchVerbs() { + return this.dict.fetch('pos', ['VB', 'VBN', 'VBD', 'VBZ', 'VBP', 'VBG']); + } + + fetchPronouns() { + return this.dict.fetch('pos', ['PRP', 'PRP$']); + } + + fetchNouns() { + return this.dict.fetch('pos', ['NN', 'NNS', 'NNP', 'NNPS']); + } + + // Fetch list looks for a list of items + // a or b + // a, b or c + fetchList() { + debug.verbose('Fetch list'); + const list = []; + if (/NNP? CC(?:\s*DT\s|\s)NNP?/.test(this.posString) || + /NNP? , NNP?/.test(this.posString) || + /NNP? CC(?:\s*DT\s|\s)JJ NNP?/.test(this.posString)) { + let sn = false; + for (let i = 0; i < this.taggedWords.length; i++) { + if (this.taggedWords[i + 1] && + (this.taggedWords[i + 1][1] === ',' || + this.taggedWords[i + 1][1] === 'CC' || + this.taggedWords[i + 1][1] === 'JJ')) { + sn = true; + } + if (this.taggedWords[i + 1] === undefined) { + sn = true; + } + if (sn && Utils.isTag(this.taggedWords[i][1], 'nouns')) { + list.push(this.taggedWords[i][0]); + sn = false; + } + } + } + return list; + } + + fetchDate() { + let date = null; + const months = ['january', 'february', 'march', + 'april', 'may', 'june', 'july', 'august', 'september', + 'october', 'november', 'december']; + + // http://rubular.com/r/SAw0nUqHJh + const regex = /([a-z]{3,10}\s+[\d]{1,2}\s?,?\s+[\d]{2,4}|[\d]{2}\/[\d]{2}\/[\d]{2,4})/i; + const match = this.clean.match(regex); + + if (match) { + debug.verbose('Date: ', match); + date = moment(Date.parse(match[0])); + } + + if (this.questionType === 'NUM:date' && date === null) { + debug.verbose('Try to resolve date'); + // TODO, in x months, x months ago, x months from now + if (_.includes(this.nouns, 'month')) { + if (this.dict.includes('next')) { + date = moment().add('M', 1); + } + if (this.dict.includes('last')) { + date = moment().subtract('M', 1); + } + } else if (Utils.inArray(this.nouns, months)) { + // IN month vs ON month + const p = Utils.inArray(this.nouns, months); + date = moment(`${this.nouns[p]} 1`, 'MMM D'); + } + } + + return date; + } + + // Pulls concepts from the bigram DB. + fetchNamedEntities(callback) { + const bigrams = ngrams.bigrams(this.taggedWords); + + const sentenceBigrams = _.map(bigrams, bigram => + _.map(bigram, item => item[0]) + ); + + const itor = (item, cb) => { + const bigramLookup = { subject: item.join(' '), predicate: 'isa', object: 'bigram' }; + this.factSystem.db.get(bigramLookup, (err, res) => { + if (err) { + debug.error(err); + } + + if (!_.isEmpty(res)) { + cb(err, true); + } else { + cb(err, false); + } + }); + }; + + async.filter(sentenceBigrams, itor, (err, res) => { + callback(res); + }); + } + + // This function will return proper nouns and group them together if they need be. + // This function will also return regular nonus or common nouns grouped as well. + // Rob Ellis and Brock returns ['Rob Ellis', 'Brock'] + // @tags - Array, Words with POS [[word, pos], [word, pos]] + // @lookupType String, "nouns" or "names" + fetchComplexNouns(lookupType) { + const tags = this.taggedWords; + const bigrams = ngrams.bigrams(tags); + let tester; + + // TODO: Might be able to get rid of this and use this.dict to get nouns/proper names + if (lookupType === 'names') { + tester = item => + item[1] === 'NNP' || item[1] === 'NNPS' + ; + } else { + tester = item => + item[1] === 'NN' || item[1] === 'NNS' || item[1] === 'NNP' || item[1] === 'NNPS' + ; + } + + const nouns = _.filter(_.map(tags, item => + tester(item) ? item[0] : null + ), Boolean); + + const nounBigrams = ngrams.bigrams(nouns); + + // Get a list of term + const neTest = _.map(bigrams, bigram => + _.map(bigram, item => tester(item)) + ); + + // TODO: Work out what this is + const thing = _.map(neTest, (item, key) => + _.every(item, _.identity) ? bigrams[key] : null + ); + + // Return full names from the list + const fullnames = _.map(_.filter(thing, Boolean), item => + (_.map(item, item2 => + item2[0] + )).join(' ') + ); + + debug.verbose(`Full names found from lookupType ${lookupType}: ${fullnames}`); + + const x = _.map(nounBigrams, item => + _.includes(fullnames, item.join(' ')) + ); + + // FIXME: This doesn't do anything (result not used) + // Filter X out of the bigrams or names? + _.filter(nounBigrams, (item, key) => { + if (x[key]) { + // Remove these from the names + nouns.splice(nouns.indexOf(item[0]), 1); + nouns.splice(nouns.indexOf(item[1]), 1); + return nouns; + } + }); + + return nouns.concat(fullnames); + } +} + +export default Message; diff --git a/lib/postParse.js b/src/bot/postParse.js similarity index 51% rename from lib/postParse.js rename to src/bot/postParse.js index a19c2a4a..91a5f2c0 100644 --- a/lib/postParse.js +++ b/src/bot/postParse.js @@ -1,6 +1,5 @@ -const _ = require("lodash") -const RE2 = require('re2') - +import _ from 'lodash'; +import RE2 from 're2'; /** * Insert replacements into `source` string @@ -15,46 +14,53 @@ const RE2 = require('re2') * @returns {string} */ const replaceOneOrMore = (basename, source, replacements) => { - const pronounsRE = new RE2(`<(${basename})([s0-${replacements.length}])?>`, 'g') + const pronounsRE = new RE2(`<(${basename})([s0-${replacements.length}])?>`, 'g'); if (pronounsRE.search(source) !== -1 && replacements.length !== 0) { return pronounsRE.replace(source, (c, p1, p2) => { if (p1 === 's') { - return `(${replacements.join('|')})` + return `(${replacements.join('|')})`; } else { - let index = Number.parseInt(p2) - index = index ? index - 1 : 0 - return `(${replacements[index]})` + let index = Number.parseInt(p2); + index = index ? index - 1 : 0; + return `(${replacements[index]})`; } - }) + }); } else { - return source + return source; } -} - +}; -// This function can be done after the first and contains the -// user object so it may be contextual to this user. -exports.postParse = function (regexp, message, user, callback) { +/** + * This function replaces syntax in the trigger such as: + * + * with the respective word in the user's message. + * + * This function can be done after the first and contains the + * user object so it may be contextual to this user. + */ +const postParse = function postParse(regexp, message, user, callback) { if (_.isNull(regexp)) { callback(null); } else { - // todo: this can all be done in a single pass - regexp = replaceOneOrMore('name', regexp, message.names) - regexp = replaceOneOrMore('noun', regexp, message.nouns) - regexp = replaceOneOrMore('adverb', regexp, message.adverbs) - regexp = replaceOneOrMore('verb', regexp, message.verbs) - regexp = replaceOneOrMore('pronoun', regexp, message.pronouns) - regexp = replaceOneOrMore('adjective', regexp, message.adjectives) + // TODO: this can all be done in a single pass + regexp = replaceOneOrMore('name', regexp, message.names); + regexp = replaceOneOrMore('noun', regexp, message.nouns); + regexp = replaceOneOrMore('adverb', regexp, message.adverbs); + regexp = replaceOneOrMore('verb', regexp, message.verbs); + regexp = replaceOneOrMore('pronoun', regexp, message.pronouns); + regexp = replaceOneOrMore('adjective', regexp, message.adjectives); - const inputOrReplyRE = new RE2('<(input|reply)([1-9])?>', 'g') + const inputOrReplyRE = new RE2('<(input|reply)([1-9])?>', 'g'); if (inputOrReplyRE.search(regexp) !== -1) { - const history = user.__history__ + const history = user.__history__; regexp = inputOrReplyRE.replace(regexp, (c, p1, p2) => { - const index = p2 ? Number.parseInt(p2) : 0 - return history[p1][index] ? history[p1][index].raw : c - }) + const index = p2 ? Number.parseInt(p2) : 0; + return history[p1][index] ? history[p1][index].raw : c; + }); } } callback(regexp); }; + +export default postParse; diff --git a/src/bot/processTags.js b/src/bot/processTags.js new file mode 100644 index 00000000..c2395819 --- /dev/null +++ b/src/bot/processTags.js @@ -0,0 +1,341 @@ +// TODO: Fix this documentation, options is incorrect +/** + * Parse the reply for additional tags, this is called once we have a reply candidate filtered out. + * + * @param {Object} replyObj - The Reply Object + * @param {string} replyObj.id - This is the 8 digit id mapping back to the ss parsed json + * @param {array} replyObj.stars - All of the matched values + * @param {string} replyObj.topic - The Topic name we matched on + * @param {Object} replyObj.reply - This is the Mongo Reply Gambit + * @param {string} replyObj.trigger - The input string of the gambit the user matched with their message + * @param {string} replyObj.trigger_id - The trigger id (8 digit) + * @param {string} replyObj.trigger_id2 - The trigger id (mongo id) + * + * @param {Object} options + * @param {Object} options.user - The user object + * @param {Object} options.system - Extra cached items that are loaded async during load-time + * @param {Object} options.message - The original message object + * + * @param {array} options.system.plugins - An array of plugins loaded from the plugin folder + * @param {Object} options.system.scope - All of the data available to `this` inside of the plugin during execution + * @param {number} options.depth - Counter of how many times this function is called recursively. + * + * Replies can have the following: + * Basic (captured text) subsitution ie: `I like ` + * Input (parts of speech) subsitution ie: `I like ` + * Expanding terms using wordnet ie: `I like ~sport` + * Alternate terms to choose at random ie: `I like (baseball|hockey)` + * Custom functions that can be called ie: `I like ^chooseSport()` + * Redirects to another reply ie: `I like {@sport}` + */ + +import _ from 'lodash'; +import replace from 'async-replace'; +import RE2 from 're2'; +import debuglog from 'debug-levels'; + +import Utils from './utils'; +import processHelpers from './reply/common'; +import regexes from './regexes'; + +import inlineRedirect from './reply/inlineRedirect'; +import topicRedirect from './reply/topicRedirect'; +import respond from './reply/respond'; +import customFunction from './reply/customFunction'; + +const debug = debuglog('SS:ProcessTags'); + +// xxx: RegExp instead of RE2 because it is passed to async-replace +const WORDNET_REGEX = /(~)(\w[\w]+)/g; + +const processReplyTags = function processReplyTags(replyObj, options, callback) { + const system = options.system; + + debug.verbose('Depth: ', options.depth); + + let replyString = replyObj.reply.reply; + debug.info("Reply '%s'", replyString); + + // Let's set the currentTopic to whatever we matched on, providing it isn't already set + // The reply text might override that later. + if (_.isEmpty(options.user.pendingTopic)) { + options.user.setTopic(replyObj.topic); + } + + // If the reply has {topic=newTopic} syntax, get newTopic and the cleaned string. + const { replyString: cleanedTopicReply, newTopic } = processHelpers.topicSetter(replyString); + replyString = cleanedTopicReply; + + if (newTopic !== '') { + debug.verbose('New topic found: ', newTopic); + options.user.setTopic(newTopic); + } + + // Appends replyObj.stars to stars + const stars = ['']; + stars.push(...replyObj.stars); + + // Expand captures + replyString = new RE2('', 'ig').replace(replyString, (match, param) => { + const index = param ? Number.parseInt(param) : 1; + return index < stars.length ? stars[index] : match; + }); + + // So this is to address GH-207, pulling the stars out of the history and + // feeding them forward into new replies. It allows us to save a tiny bit of + // context though a conversation cycle. + const matches = regexes.pcaptures.match(replyString); + if (matches) { + // TODO: xxx: handle captures within captures, but only 1 level deep + for (let i = 0; i < matches.length; i++) { + const match = regexes.pcapture.match(matches[i]); + const historyPtr = +match[1] - 1; + const starPtr = +match[2] - 1; + if (options.user.__history__.stars[historyPtr] && options.user.__history__.stars[historyPtr][starPtr]) { + const term = options.user.__history__.stars[historyPtr][starPtr]; + replyString = replyString.replace(matches[i], term); + } else { + debug.verbose('Attempted to use previous capture data, but none was found in user history.'); + } + } + } + + // clean up the reply by unescaping newlines and hashes + replyString = new RE2('\\\\(n|#)', 'ig') + .replace(Utils.trim(replyString), (match, param) => + param === '#' ? '#' : '\n' + ); + + let clearConvoBit = false; + // There SHOULD only be 0 or 1. + const clearMatch = regexes.clear.match(replyString); + if (clearMatch) { + debug.verbose('Adding Clear Conversation Bit'); + replyString = replyString.replace(clearMatch[0], ''); + replyString = replyString.trim(); + clearConvoBit = true; + } + + let mbit = null; + const continueMatch = regexes.continue.match(replyString); + if (continueMatch) { + debug.verbose('Adding CONTINUE Conversation Bit'); + replyString = replyString.replace(continueMatch[0], ''); + replyString = replyString.trim(); + mbit = false; + } + + const endMatch = regexes.end.match(replyString); + if (endMatch) { + debug.verbose('Adding END Conversation Bit'); + replyString = replyString.replace(endMatch[0], ''); + replyString = replyString.trim(); + mbit = true; + } + + // and + // Special case, we have no items in the history yet. + // This could only happen if we are trying to match the first input. + // Kinda edgy. + + const message = options.message; + if (!_.isNull(message)) { + replyString = new RE2('', 'ig').replace(replyString, message.clean); + } + + replyString = new RE2('<(input|reply)([1-9]?)>', 'ig') + .replace(replyString, (match, param1, param2) => { + const data = param1 === 'input' ? options.user.__history__.input : options.user.__history__.reply; + return data[param2 ? Number.parseInt(param2) - 1 : 0]; + }); + + replace(replyString, WORDNET_REGEX, processHelpers.wordnetReplace, (err, wordnetReply) => { + const originalReply = replyString; + replyString = wordnetReply; + + // Inline redirector. + const redirectMatch = regexes.redirect.match(replyString); + const topicRedirectMatch = regexes.topic.match(replyString); + let respondMatch = regexes.respond.match(replyString); + const customFunctionMatch = regexes.customFn.match(replyString); + + let match = false; + if (redirectMatch || topicRedirectMatch || respondMatch || customFunctionMatch) { + const obj = []; + obj.push({ name: 'redirectMatch', index: (redirectMatch) ? redirectMatch.index : -1 }); + obj.push({ name: 'topicRedirectMatch', index: (topicRedirectMatch) ? topicRedirectMatch.index : -1 }); + obj.push({ name: 'respondMatch', index: (respondMatch) ? respondMatch.index : -1 }); + obj.push({ name: 'customFunctionMatch', index: (customFunctionMatch) ? customFunctionMatch.index : -1 }); + + match = _.result(_.find(_.sortBy(obj, 'index'), chr => + chr.index >= 0 + ), 'name'); + debug.verbose(`Augmenting function found: ${match}`); + } + + const augmentCallbackHandle = function augmentCallbackHandle(err, replyString, messageProps, getReplyObject, mbit1) { + if (err) { + // If we get an error, we back out completely and reject the reply. + debug.verbose('We got an error back from one of the handlers', err); + return callback(err, {}); + } else { + let newReplyObject; + if (_.isEmpty(getReplyObject)) { + newReplyObject = replyObj; + newReplyObject.reply.reply = replyString; + + // This is a new bit to stop us from matching more. + if (mbit !== null) { + newReplyObject.breakBit = mbit; + } + // If the function has the bit set, override the existing one + if (mbit1 !== null) { + newReplyObject.breakBit = mbit1; + } + + // Clear the conversation thread (this is on the next cycle) + newReplyObject.clearConvo = clearConvoBit; + } else { + // TODO: we flush everything except stars.. + + debug.verbose('getReplyObject', getReplyObject); + newReplyObject = replyObj; + newReplyObject.reply = getReplyObject.reply; + newReplyObject.topic = getReplyObject.topicName; + // update the root id with the reply id (it may have changed in respond) + newReplyObject.id = getReplyObject.reply.id; + + // This is a new bit to stop us from matching more. + if (mbit !== null) { + newReplyObject.breakBit = mbit; + } + // If the function has the bit set, override the existing one + if (mbit1 !== null) { + newReplyObject.breakBit = mbit1; + } + + if (getReplyObject.clearConvo === true) { + newReplyObject.clearConvo = getReplyObject.clearConvo; + } else { + newReplyObject.clearConvo = clearConvoBit; + } + + if (getReplyObject.subReplies) { + if (newReplyObject.subReplies && _.isArray(newReplyObject.subReplies)) { + newReplyObject.subReplies.concat(getReplyObject.subReplies); + } else { + newReplyObject.subReplies = getReplyObject.subReplies; + } + } + + // We also want to transfer forward any message props too + if (getReplyObject.props) { + newReplyObject.props = getReplyObject.props; + } + + newReplyObject.minMatchSet = getReplyObject.minMatchSet; + } + + debug.verbose('Return back to replies to re-process for more tags', newReplyObject); + // Okay Lets call this function again + return processReplyTags(newReplyObject, options, callback); + } + }; + + // TODO: Fix this replyOptions object + // This is the options for the (get)reply function, used for recursive traversal. + const replyOptions = { + topic: replyObj.topic, + depth: options.depth + 1, + message, + system, + user: options.user, + }; + + if (redirectMatch && match === 'redirectMatch') { + return inlineRedirect(replyString, redirectMatch, replyOptions, augmentCallbackHandle); + } + + if (topicRedirectMatch && match === 'topicRedirectMatch') { + return topicRedirect(replyString, stars, topicRedirectMatch, replyOptions, augmentCallbackHandle); + } + + if (respondMatch && match === 'respondMatch') { + // In some edge cases you could name a topic with a ~ and wordnet will remove it. + // respond needs a topic so we re-try again with the origional reply. + if (respondMatch[1] === '') { + replyString = originalReply; + respondMatch = regexes.respond.match(replyString); + } + + return respond(replyString, respondMatch, replyOptions, augmentCallbackHandle); + } + + if (customFunctionMatch && match === 'customFunctionMatch') { + return customFunction(replyString, customFunctionMatch, replyOptions, augmentCallbackHandle); + } + + // Using global callback and user. + const afterHandle = function afterHandle(callback) { + return (err, finalReply) => { + if (err) { + console.log(err); + } + + // This will update the reply with wordnet replaced changes and alternates + finalReply = processHelpers.processAlternates(finalReply); + + const msgStateMatch = regexes.state.match(finalReply); + if (msgStateMatch && finalReply.indexOf('delay') === -1) { + for (let i = 0; i < msgStateMatch.length; i++) { + const stateObj = processHelpers.addStateData(msgStateMatch[i]); + debug.verbose('Found Conversation State', stateObj); + options.user.conversationState = _.merge(options.user.conversationState, stateObj); + options.user.markModified('conversationState'); + finalReply = finalReply.replace(msgStateMatch[i], ''); + } + finalReply = finalReply.trim(); + } + + replyObj.reply.reply = Utils.decodeCommas(new RE2('\\\\s', 'g').replace(finalReply, ' ')); + + if (clearConvoBit && clearConvoBit === true) { + replyObj.clearConvo = clearConvoBit; + } + + // This is a new bit to stop us from matching more. + if (!replyObj.breakBit && mbit !== null) { + replyObj.breakBit = mbit; + } + + debug.verbose('Calling back with', replyObj); + + if (!replyObj.props && message.props) { + replyObj.props = message.props; + } else { + replyObj.props = _.merge(replyObj.props, message.props); + } + + callback(err, replyObj); + }; + }; + + replace(replyString, WORDNET_REGEX, processHelpers.wordnetReplace, afterHandle(callback)); + }); +}; + +const processThreadTags = function processThreadTags(string) { + const threads = []; + const strings = []; + string.split('\n').forEach((line) => { + const match = regexes.delay.match(line); + if (match) { + threads.push({ delay: match[1], string: Utils.trim(line.replace(match[0], '')) }); + } else { + strings.push(line); + } + }); + return [strings.join('\n'), threads]; +}; + +export default { processThreadTags, processReplyTags }; diff --git a/lib/regexes.js b/src/bot/regexes.js similarity index 90% rename from lib/regexes.js rename to src/bot/regexes.js index d94a023c..187a70fe 100644 --- a/lib/regexes.js +++ b/src/bot/regexes.js @@ -1,13 +1,12 @@ -const RE2 = require('re2') +import RE2 from 're2'; // Standard regular expressions that can be reused throughout the codebase // Also, easier to test now that they are all in one place // Of course this should all probably be replaced with a real parser ... -// todo: topic, customFn, and filter could all parse out the parameters instead of returning them as a single string - -module.exports = { +// TODO: topic, customFn, and filter could all parse out the parameters instead of returning them as a single string +export default { redirect: new RE2('\\{@(.+?)\\}'), topic: new RE2('\\^topicRedirect\\(\\s*([~\\w<>\\s]*),([~\\w<>\\s]*)\\s*\\)'), respond: new RE2('\\^respond\\(\\s*([\\w~]*)\\s*\\)'), @@ -38,7 +37,7 @@ module.exports = { trailing: new RE2('[ \\t]+$'), oneInner: new RE2('[ \\t]', 'g'), oneLeading: new RE2('^[ \\t]'), - oneTrailing: new RE2('[ \\t]$') + oneTrailing: new RE2('[ \\t]$'), }, whitespace: { @@ -47,6 +46,5 @@ module.exports = { trailing: new RE2('\s+$'), oneLeading: new RE2('^\s'), oneTrailing: new RE2('\s$'), - } - -} + }, +}; diff --git a/src/bot/reply/common.js b/src/bot/reply/common.js new file mode 100644 index 00000000..4bca4e37 --- /dev/null +++ b/src/bot/reply/common.js @@ -0,0 +1,137 @@ +import debuglog from 'debug-levels'; + +import Utils from '../utils'; +import wordnet from './wordnet'; + +const debug = debuglog('SS:ProcessHelpers'); + +const getTopic = function getTopic(chatSystem, name, cb) { + if (name) { + chatSystem.Topic.findOne({ name }, (err, topicData) => { + if (!topicData) { + cb(new Error(`No topic found for the topic name "${name}"`)); + } else { + debug.verbose('Getting topic data for', topicData); + cb(err, { id: topicData._id, name, type: 'TOPIC' }); + } + }); + } else { + cb(null, null); + } +}; + +// TODO - Topic Setter should have its own property +/** + * This function checks if the reply has "{topic=newTopic}" in the response, + * and returns an array of the reply and the topic name found. + * + * For example, the reply: + * + * - Me too! {topic=animals} + * + * Would return ['Me too!', 'animals']. + * + * @param {String} reply - The reply string you want to check for a topic setter. + */ +const topicSetter = function topicSetter(replyString) { + const TOPIC_REGEX = /\{topic=(.+?)\}/i; + let match = replyString.match(TOPIC_REGEX); + let depth = 0; + let newTopic = ''; + + while (match) { + depth += 1; + if (depth >= 50) { + debug.verbose('Infinite loop looking for topic tag!'); + break; + } + newTopic = match[1]; + replyString = replyString.replace(new RegExp(`{topic=${Utils.quotemeta(newTopic)}}`, 'ig'), ''); + replyString = replyString.trim(); + match = replyString.match(TOPIC_REGEX); // Look for more + } + debug.verbose(`New topic to set: ${newTopic}. Cleaned reply string: ${replyString}`); + return { replyString, newTopic }; +}; + +const processAlternates = function processAlternates(reply) { + // Reply Alternates. + let match = reply.match(/\(\((.+?)\)\)/); + let giveup = 0; + while (match) { + debug.verbose('Reply has Alternates'); + + giveup += 1; + if (giveup >= 50) { + debug.verbose('Infinite loop when trying to process optionals in trigger!'); + return ''; + } + + const parts = match[1].split('|'); + const opts = []; + for (let i = 0; i < parts.length; i++) { + opts.push(parts[i].trim()); + } + + const resp = Utils.getRandomInt(0, opts.length - 1); + reply = reply.replace(new RegExp(`\\(\\(\\s*${Utils.quotemeta(match[1])}\\s*\\)\\)`), opts[resp]); + match = reply.match(/\(\((.+?)\)\)/); + } + + return reply; +}; + +// Handle WordNet in Replies +const wordnetReplace = function wordnetReplace(match, sym, word, p3, offset, done) { + wordnet.lookup(word, sym, (err, words) => { + if (err) { + console.log(err); + } + + words = words.map(item => item.replace(/_/g, ' ')); + + debug.verbose('Wordnet Replies', words); + const resp = Utils.pickItem(words); + done(null, resp); + }); +}; + +const addStateData = function addStateData(data) { + const KEYVALG_REGEX = /\s*([a-z0-9]{2,20})\s*=\s*([a-z0-9\'\"]{2,20})\s*/ig; + const KEYVALI_REGEX = /([a-z0-9]{2,20})\s*=\s*([a-z0-9\'\"]{2,20})/i; + + // Do something with the state + const items = data.match(KEYVALG_REGEX); + const stateData = {}; + + for (let i = 0; i < items.length; i++) { + const x = items[i].match(KEYVALI_REGEX); + const key = x[1]; + let val = x[2]; + + // for strings + val = val.replace(/[\"\']/g, ''); + + if (/^[\d]+$/.test(val)) { + val = +val; + } + + if (val === 'true' || val === 'false') { + switch (val.toLowerCase().trim()) { + case 'true': val = true; break; + case 'false': val = false; break; + } + } + stateData[key] = val; + } + + return stateData; +}; + +export default { + addStateData, + getTopic, + processAlternates, + topicSetter, + wordnetReplace, +}; diff --git a/src/bot/reply/customFunction.js b/src/bot/reply/customFunction.js new file mode 100644 index 00000000..7f33deda --- /dev/null +++ b/src/bot/reply/customFunction.js @@ -0,0 +1,89 @@ +import _ from 'lodash'; +import async from 'async'; +import debuglog from 'debug-levels'; + +import Utils from '../utils'; + +const debug = debuglog('SS:Reply:customFunction'); + +const customFunction = function customFunction(reply, match, options, callback) { + const plugins = options.system.plugins; + // Important to create a new scope object otherwise we could leak data + const scope = _.merge({}, options.system.scope); + scope.message_props = options.system.extraScope; + scope.message = options.message; + scope.user = options.user; + + let mbit = null; + + // We use async to capture multiple matches in the same reply + return async.whilst(() => match, + (cb) => { + // Call Function here + const main = match[0]; + const pluginName = Utils.trim(match[1]); + const partsStr = Utils.trim(match[2]); + const parts = partsStr.split(','); + + debug.verbose('-- Function Arguments --', parts); + const args = []; + for (let i = 0; i < parts.length; i++) { + if (parts[i] !== '') { + args.push(Utils.decodeCommas(parts[i].trim())); + } + } + + if (plugins[pluginName]) { + // SubReply is the results of the object coming back + // TODO. Subreply should be optional and could be undefined, or null + args.push((err, subreply, matchBit) => { + let replyStr; + + if (_.isPlainObject(subreply)) { + if (subreply.hasOwnProperty('text')) { + replyStr = subreply.text; + delete subreply.text; + } + + if (subreply.hasOwnProperty('reply')) { + replyStr = subreply.reply; + delete subreply.reply; + } + scope.message.props = _.assign(scope.message.props, subreply); + } else { + replyStr = subreply; + } + + match = false; + reply = reply.replace(main, replyStr); + match = reply.match(/\^(\w+)\(([~\w<>,\s]*)\)/); + mbit = matchBit; + if (err) { + cb(err); + } else { + cb(); + } + }); + + debug.verbose('Calling Plugin Function', pluginName); + plugins[pluginName].apply(scope, args); + } else if (pluginName === 'topicRedirect' || pluginName === 'respond') { + debug.verbose('Existing, we have a systemFunction', pluginName); + match = false; + cb(null, ''); + } else { + // If a function is missing, we kill the line and return empty handed + console.log(`WARNING:\nCustom Function (${pluginName}) was not found. Your script may not behave as expected`); + debug.verbose('Custom Function not-found', pluginName); + match = false; + cb(true, ''); + } + }, + (err) => { + debug.verbose('Callback from custom function', err); + return callback(err, reply, scope.message.props, {}, mbit); + } + ); +}; + +export default customFunction; diff --git a/src/bot/reply/inlineRedirect.js b/src/bot/reply/inlineRedirect.js new file mode 100644 index 00000000..1b6fedf6 --- /dev/null +++ b/src/bot/reply/inlineRedirect.js @@ -0,0 +1,63 @@ +import async from 'async'; +import debuglog from 'debug-levels'; + +import Message from '../message'; +import Utils from '../utils'; +import processHelpers from './common'; +import getReply from '../getReply'; + +const debug = debuglog('SS:Reply:inline'); + +const inlineRedirect = function inlineRedirect(reply, redirectMatch, options, callback) { + return async.whilst(() => redirectMatch, (cb) => { + const target = redirectMatch[1]; + debug.verbose(`Inline redirection to: '${target}'`); + + // if we have a special topic, reset it to the previous one + // in order to preserve the context for inline redirection + if (options.topic === '__pre__' || options.topic === '__post__') { + if (options.user.__history__.topic.length) { + options.topic = options.user.__history__.topic[0]; + } + } + + processHelpers.getTopic(options.system.chatSystem, options.topic, (err, topicData) => { + const messageOptions = { + factSystem: options.system.factSystem, + }; + + Message.createMessage(target, messageOptions, (replyMessageObject) => { + debug.verbose('replyMessageObject', replyMessageObject); + + options.pendingTopics = []; + options.pendingTopics.push(topicData); + + getReply(replyMessageObject, options, (err, subreply) => { + if (err) { + console.log(err); + } + + debug.verbose('subreply', subreply); + + if (subreply) { + const rd1 = new RegExp(`\\{@${Utils.quotemeta(target)}\\}`, 'i'); + reply = reply.replace(rd1, subreply.string); + redirectMatch = reply.match(/\{@(.+?)\}/); + } else { + redirectMatch = false; + reply = reply.replace(new RegExp(`\\{@${Utils.quotemeta(target)}\\}`, 'i'), ''); + } + + cb((options.depth === 50) ? 'Depth Error' : null); + }); // getReply + }); // Message + }); + }, + (err) => { + debug.verbose('CallBack from inline redirect', Utils.trim(reply)); + return callback(err, Utils.trim(reply), options.message.props, {}); + } + ); +}; + +export default inlineRedirect; diff --git a/src/bot/reply/respond.js b/src/bot/reply/respond.js new file mode 100644 index 00000000..e305e0a6 --- /dev/null +++ b/src/bot/reply/respond.js @@ -0,0 +1,71 @@ +import async from 'async'; +import debuglog from 'debug-levels'; + +import processHelpers from './common'; +import Utils from '../utils'; +import getReply from '../getReply'; + +const debug = debuglog('SS:Reply:Respond'); + +const RESPOND_REGEX = /\^respond\(\s*([\w~]*)\s*\)/; + +const respond = function respond(reply, respondMatch, options, callback) { + let replyObj = {}; + + return async.whilst(() => respondMatch, + (cb) => { + const newTopic = Utils.trim(respondMatch[1]); + debug.verbose('Topic Check with new Topic: %s', newTopic); + + processHelpers.getTopic(options.system.chatSystem, newTopic, (err, topicData) => { + options.pendingTopics = []; + options.pendingTopics.push(topicData); + + getReply(options.message, options, (err, subreply) => { + if (err) { + console.log(err); + } + + // The topic is not set correctly in getReply! + debug.verbose('CallBack from respond topic (getReplyObj)', subreply); + + if (subreply && subreply.replyId) { + debug.verbose('subreply', subreply); + // We need to do a lookup on subreply.replyId and flash the entire reply. + options.system.chatSystem.Reply.findById(subreply.replyId) + .exec((err, fullReply) => { + if (err) { + debug.error(err); + } + + debug.verbose('fullReply', fullReply); + + debug.verbose('Setting the topic to the matched one'); + options.user.setTopic(newTopic); + + reply = fullReply.reply || ''; + replyObj = subreply; + replyObj.reply = fullReply; + replyObj.topicName = newTopic; + respondMatch = reply.match(RESPOND_REGEX); + + cb((options.depth === 50) ? 'Depth Error' : null); + }); + } else { + respondMatch = false; + reply = ''; + replyObj = {}; + + cb((options.depth === 50) ? 'Depth Error' : null); + } + }); + }); + }, + (err) => { + debug.verbose('CallBack from Respond Function', replyObj); + return callback(err, Utils.trim(reply), options.message.props, replyObj); + } + ); +}; + +export default respond; diff --git a/src/bot/reply/topicRedirect.js b/src/bot/reply/topicRedirect.js new file mode 100644 index 00000000..e7632c59 --- /dev/null +++ b/src/bot/reply/topicRedirect.js @@ -0,0 +1,111 @@ +import async from 'async'; +import debuglog from 'debug-levels'; + +import processHelpers from './common'; +import Message from '../message'; +import Utils from '../utils'; +import getReply from '../getReply'; + +const debug = debuglog('SS:Reply:topicRedirect'); + +const TOPIC_REGEX = /\^topicRedirect\(\s*([~\w<>\s]*),([~\w<>\s]*)\s*\)/; + +const topicRedirect = function topicRedirect(reply, stars, redirectMatch, options, callback) { + let replyObj = {}; + + // Undefined, unless it is being passed back + let mbit; + + return async.whilst(() => redirectMatch, + (cb) => { + const main = Utils.trim(redirectMatch[0]); + const topic = Utils.trim(redirectMatch[1]); + const target = Utils.trim(redirectMatch[2]); + + debug.verbose('Topic Redirection to: %s topic: %s', target, topic); + options.user.setTopic(topic); + + // Here we are looking for gambits in the NEW topic. + processHelpers.getTopic(options.system.chatSystem, topic, (err, topicData) => { + if (err) { + /* + In this case the topic does not exist, we want to just pretend it wasn't + provided and reply with whatever else is there. + */ + redirectMatch = reply.match(TOPIC_REGEX); + reply = Utils.trim(reply.replace(main, '')); + debug.verbose('Invalid Topic', reply); + return cb(null); + } + + const messageOptions = { + facts: options.system.factSystem, + }; + + Message.createMessage(target, messageOptions, (replyMessageObject) => { + options.pendingTopics = []; + options.pendingTopics.push(topicData); + + // Pass the stars (captured wildcards) forward + replyMessageObject.stars = stars.slice(1); + + getReply(replyMessageObject, options, (err, subreply) => { + if (err) { + cb(null); + } + + if (subreply) { + // We need to do a lookup on subreply.replyId and flash the entire reply. + debug.verbose('CallBack from topicRedirect', subreply); + options.system.chatSystem.Reply.findById(subreply.replyId) + .exec((err, fullReply) => { + if (err) { + console.log('No SubReply ID found', err); + } + + // This was changed as a result of gh-236 + // reply = reply.replace(main, fullReply.reply); + reply = reply.replace(main, subreply.string); + replyObj = subreply; + + debug.verbose('SubReply', subreply); + debug.verbose('fullReply', fullReply); + + if ((fullReply === null && !replyObj.reply) || err) { + debug.verbose('Something bad happened upstream'); + cb('upstream error'); + } else { + // Override the subreply string with the new complex one + replyObj.string = reply; + + replyObj.reply = fullReply; + replyObj.reply.reply = reply; + + // Lets capture this data too for better logs + replyObj.minMatchSet = subreply.minMatchSet; + + // This may be set before the redirect. + mbit = replyObj.breakBit; + + redirectMatch = reply.match(TOPIC_REGEX); + cb((options.depth === 50) ? 'Depth Error' : null); + } + }); + } else { + redirectMatch = false; + reply = reply.replace(main, ''); + replyObj = {}; + cb((options.depth === 50) ? 'Depth Error' : null); + } + }); // getReply + }); // Message + }); + }, + (err) => { + debug.verbose('CallBack from topic redirect', reply, replyObj); + return callback(err, Utils.trim(reply), options.message.props, replyObj, mbit); + } + ); +}; + +export default topicRedirect; diff --git a/src/bot/reply/wordnet.js b/src/bot/reply/wordnet.js new file mode 100644 index 00000000..35867c27 --- /dev/null +++ b/src/bot/reply/wordnet.js @@ -0,0 +1,101 @@ +// This is a shim for wordnet lookup. +// http://wordnet.princeton.edu/wordnet/man/wninput.5WN.html + +import natural from 'natural'; +import async from 'async'; +import _ from 'lodash'; + +const wordnet = new natural.WordNet(); + +const define = function define(word, cb) { + wordnet.lookup(word, (results) => { + if (!_.isEmpty(results)) { + cb(null, results[0].def); + } else { + cb(`No results for wordnet definition of '${word}'`); + } + }); +}; + +// Does a word lookup +// @word can be a word or a word/pos to filter out unwanted types +const lookup = function lookup(word, pointerSymbol = '~', cb) { + let pos = null; + + const match = word.match(/~(\w)$/); + if (match) { + pos = match[1]; + word = word.replace(match[0], ''); + } + + const synets = []; + + wordnet.lookup(word, (results) => { + results.forEach((result) => { + result.ptrs.forEach((part) => { + if (pos !== null && part.pos === pos && part.pointerSymbol === pointerSymbol) { + synets.push(part); + } else if (pos === null && part.pointerSymbol === pointerSymbol) { + synets.push(part); + } + }); + }); + + const itor = (word, next) => { + wordnet.get(word.synsetOffset, word.pos, (sub) => { + next(null, sub.lemma); + }); + }; + + async.map(synets, itor, (err, items) => { + items = _.uniq(items); + items = items.map(x => x.replace(/_/g, ' ')); + cb(err, items); + }); + }); +}; + +// Used to explore a word or concept +// Spits out lots of info on the word +const explore = function explore(word, cb) { + let ptrs = []; + + wordnet.lookup(word, (results) => { + for (let i = 0; i < results.length; i++) { + ptrs.push(results[i].ptrs); + } + + ptrs = _.uniq(_.flatten(ptrs)); + ptrs = _.map(ptrs, item => ({ pos: item.pos, sym: item.pointerSymbol })); + + ptrs = _.chain(ptrs) + .groupBy('pos') + .map((value, key) => { + return { + pos: key, + ptr: _.uniq(_.map(value, 'sym')), + }; + }) + .value(); + + const itor = (item, next) => { + const itor2 = (ptr, next2) => { + lookup(`${word}~${item.pos}`, ptr, (err, res) => { + if (err) { + console.error(err); + } + console.log(word, item.pos, ':', ptr, res.join(', ')); + next2(); + }); + }; + async.map(item.ptr, itor2, next); + }; + async.each(ptrs, itor, () => cb()); + }); +}; + +export default { + define, + explore, + lookup, +}; diff --git a/src/bot/utils.js b/src/bot/utils.js new file mode 100644 index 00000000..549f77e5 --- /dev/null +++ b/src/bot/utils.js @@ -0,0 +1,270 @@ +import _ from 'lodash'; +import fs from 'fs'; +import string from 'string'; +import debuglog from 'debug-levels'; +import pos from 'parts-of-speech'; +import RE2 from 're2'; +import regexes from './regexes'; + +const debug = debuglog('SS:Utils'); +const Lex = pos.Lexer; + +//-------------------------- + +const encodeCommas = s => (s ? regexes.commas.replace(s, '') : s); + +const encodedCommasRE = new RE2('', 'g'); +const decodeCommas = s => (s ? encodedCommasRE.replace(s, '') : s); + +// TODO: rename to normlize to avoid confusion with string.trim() semantics +/** + * Remove extra whitespace from a string, while preserving new lines. + * @param {string} text - the string to tidy up + */ +const trim = (text = '') => regexes.space.inner.replace(regexes.whitespace.both.replace(text, ''), ' '); + +const wordSepRE = new RE2('[\\s*#_|]+'); +/** + * Count the number of real words in a string + * @param {string} text - the text to count + * @returns {number} the number of words in `text` + */ +const wordCount = text => wordSepRE.split(text).filter(w => w.length > 0).length; + +// If needed, switch to _ or lodash +// Array.prototype.chunk = function (chunkSize) { +// var R = []; +// for (var i = 0; i < this.length; i += chunkSize) { +// R.push(this.slice(i, i + chunkSize)); +// } +// return R; +// }; + +// Contains with value being list +const inArray = function inArray(list, value) { + if (_.isArray(value)) { + let match = false; + for (let i = 0; i < value.length; i++) { + if (_.includes(list, value[i]) > 0) { + match = _.indexOf(list, value[i]); + } + } + return match; + } else { + return _.indexOf(list, value); + } +}; + +const sentenceSplit = function sentenceSplit(message) { + const lexer = new Lex(); + const bits = lexer.lex(message); + let R = []; + const L = []; + for (let i = 0; i < bits.length; i++) { + if (bits[i] === '.') { + // Push the punct + R.push(bits[i]); + L.push(R.join(' ')); + R = []; + } else if (bits[i] === ',' && + R.length >= 3 && + _.includes(['who', 'what', 'where', 'when', 'why'], bits[i + 1]) + ) { + R.push(bits[i]); + L.push(R.join(' ')); + R = []; + } else { + R.push(bits[i]); + } + } + + // if we havd left over R, push it into L (no punct was found) + if (R.length !== 0) { + L.push(R.join(' ')); + } + + return L; +}; + +const commandsRE = new RE2('[\\\\.+?${}=!:]', 'g'); +const nonCommandsRE = new RE2('[\\\\.+*?\\[^\\]$(){}=!<>|:]', 'g'); +/** + * Escape a string sp that it can be used in a regular expression. + * @param {string} string - the string to escape + * @param {boolean} commands - + */ +const quotemeta = (string, commands = false) => (commands ? commandsRE : nonCommandsRE).replace(string, c => `\\${c}`); + +const cleanArray = function cleanArray(actual) { + const newArray = []; + for (let i = 0; i < actual.length; i++) { + if (actual[i]) { + newArray.push(actual[i]); + } + } + return newArray; +}; + +const aRE = new RE2('^(([bcdgjkpqtuvwyz]|onc?e|onetime)$|e[uw]|uk|ur[aeiou]|use|ut([^t])|uni(l[^l]|[a-ko-z]))', 'i'); +const anRE = new RE2('^([aefhilmnorsx]$|hono|honest|hour|heir|[aeiou])', 'i'); +const upcaseARE = new RE2('^(UN$)'); +const upcaseANRE = new RE2('^$'); +const dashSpaceRE = new RE2('[- ]'); +const indefiniteArticlerize = (word) => { + const first = dashSpaceRE.split(word, 2)[0]; + const prefix = (anRE.test(first) || upcaseARE.test(first)) && !(aRE.test(first) || upcaseANRE.test(first)) ? 'an' : 'a'; + return `${prefix} ${word}`; +}; + +const indefiniteList = (list) => { + const n = list.map(indefiniteArticlerize); + if (n.length > 1) { + const last = n.pop(); + return `${n.join(', ')} and ${last}`; + } else { + return n.join(', '); + } +}; + +const getRandomInt = function getRandomInt(min, max) { + return Math.floor(Math.random() * ((max - min) + 1)) + min; +}; + +const underscoresRE = new RE2('_', 'g'); +const pickItem = function pickItem(arr) { + // TODO - Item may have a wornet suffix meal~2 or meal~n + const ind = getRandomInt(0, arr.length - 1); + return _.isString(arr[ind]) ? underscoresRE.replace(arr[ind], ' ') : arr[ind]; +}; + +// Capital first letter, and add period. +const makeSentense = function makeSentense(string) { + return `${string.charAt(0).toUpperCase() + string.slice(1)}.`; +}; + +const tags = { + wword: ['WDT', 'WP', 'WP$', 'WRB'], + nouns: ['NN', 'NNP', 'NNPS', 'NNS'], + verbs: ['VB', 'VBD', 'VBG', 'VBN', 'VBP', 'VBZ'], + adjectives: ['JJ', 'JJR', 'JJS'], +}; + +const isTag = function isTag(posTag, wordClass) { + return !!(tags[wordClass].indexOf(posTag) > -1); +}; + +const mkdirSync = function mkdirSync(path) { + try { + fs.mkdirSync(path); + } catch (e) { + if (e.code !== 'EEXIST') { + throw e; + } + } +}; + +const genId = function genId() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + for (let i = 0; i < 8; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +}; + +/** + * Search each string in `strings` for `` tags and replace them with values from `caps`. + * + * Replacement is positional so `` replaces with `caps[1]` and so on, with `` also replacing from `caps[1]`. + * Empty `strings` are removed from the result. + * + * @param {Array} strings - text to search for `` tags + * @param {Array} caps - replacement text + */ +const replaceCapturedText = (strings, caps) => { + const encoded = caps.map(s => encodeCommas(s)); + return strings + .filter(s => !_.isEmpty(s)) + .map(s => regexes.captures.replace(s, (m, p1) => encoded[Number.parseInt(p1 || 1)])); +}; + +const walk = function walk(dir, done) { + if (fs.statSync(dir).isFile()) { + debug.verbose('Expected directory, found file, simulating directory with only one file: %s', dir); + return done(null, [dir]); + } + + let results = []; + fs.readdir(dir, (err1, list) => { + if (err1) { + return done(err1); + } + let pending = list.length; + if (!pending) { + return done(null, results); + } + list.forEach((file) => { + file = `${dir}/${file}`; + fs.stat(file, (err2, stat) => { + if (err2) { + console.log(err2); + } + + if (stat && stat.isDirectory()) { + const cbf = function cbf(err3, res) { + results = results.concat(res); + pending -= 1; + if (!pending) { + done(err3, results); + } + }; + + walk(file, cbf); + } else { + results.push(file); + pending -= 1; + if (!pending) { + done(null, results); + } + } + }); + }); + }); +}; + +const pennToWordnet = function pennToWordnet(pennTag) { + if (string(pennTag).startsWith('J')) { + return 'a'; + } else if (string(pennTag).startsWith('V')) { + return 'v'; + } else if (string(pennTag).startsWith('N')) { + return 'n'; + } else if (string(pennTag).startsWith('R')) { + return 'r'; + } else { + return null; + } +}; + +export default { + cleanArray, + encodeCommas, + decodeCommas, + genId, + getRandomInt, + inArray, + indefiniteArticlerize, + indefiniteList, + isTag, + makeSentense, + mkdirSync, + pennToWordnet, + pickItem, + quotemeta, + replaceCapturedText, + sentenceSplit, + trim, + walk, + wordCount, +}; diff --git a/src/plugins/alpha.js b/src/plugins/alpha.js new file mode 100644 index 00000000..2a5070de --- /dev/null +++ b/src/plugins/alpha.js @@ -0,0 +1,139 @@ +import rhyme from 'rhymes'; +import syllablistic from 'syllablistic'; +import debuglog from 'debug'; +import _ from 'lodash'; + +const debug = debuglog('AlphaPlugins'); + +const getRandomInt = (min, max) => Math.floor(Math.random() * ((max - min) + 1)) + min; + +// TODO: deprecate oppisite and replace with opposite +const oppisite = function oppisite(word, cb) { + debug('oppisite', word); + + this.facts.db.get({ subject: word, predicate: 'opposite' }, (err, opp) => { + if (!_.isEmpty(opp)) { + let oppositeWord = opp[0].object; + oppositeWord = oppositeWord.replace(/_/g, ' '); + cb(null, oppositeWord); + } else { + cb(null, ''); + } + }); +}; + +const rhymes = function rhymes(word, cb) { + debug('rhyming', word); + + const rhymedWords = rhyme(word); + const i = getRandomInt(0, rhymedWords.length - 1); + + if (rhymedWords.length !== 0) { + cb(null, rhymedWords[i].word.toLowerCase()); + } else { + cb(null, null); + } +}; + +const syllable = (word, cb) => cb(null, syllablistic.text(word)); + +const letterLookup = function letterLookup(cb) { + let reply = ''; + + const lastWord = this.message.lemWords.slice(-1)[0]; + debug('--LastWord', lastWord); + debug('LemWords', this.message.lemWords); + const alpha = 'abcdefghijklmonpqrstuvwxyz'.split(''); + const pos = alpha.indexOf(lastWord); + debug('POS', pos); + if (this.message.lemWords.indexOf('before') !== -1) { + if (alpha[pos - 1]) { + reply = alpha[pos - 1].toUpperCase(); + } else { + reply = "Don't be silly, there is nothing before A"; + } + } else if (this.message.lemWords.indexOf('after') !== -1) { + if (alpha[pos + 1]) { + reply = alpha[pos + 1].toUpperCase(); + } else { + reply = 'haha, funny.'; + } + } else { + const i = this.message.lemWords.indexOf('letter'); + const loc = this.message.lemWords[i - 1]; + + if (loc === 'first') { + reply = 'It is A.'; + } else if (loc === 'last') { + reply = 'It is Z.'; + } else { + // Number or word number + // 1st, 2nd, 3rd, 4th or less then 99 + if ((loc === 'st' || loc === 'nd' || loc === 'rd' || loc === 'th') && this.message.numbers.length !== 0) { + const num = parseInt(this.message.numbers[0]); + if (num > 0 && num <= 26) { + reply = `It is ${alpha[num - 1].toUpperCase()}`; + } else { + reply = 'seriously...'; + } + } + } + } + cb(null, reply); +}; + +const wordLength = function wordLength(cap, cb) { + if (typeof cap === 'string') { + const parts = cap.split(' '); + if (parts.length === 1) { + cb(null, cap.length); + } else if (parts[0].toLowerCase() === 'the' && parts.length === 3) { + // name bill, word bill + cb(null, parts.pop().length); + } else if (parts[0] === 'the' && parts[1].toLowerCase() === 'alphabet') { + cb(null, '26'); + } else if (parts[0] === 'my' && parts.length === 2) { + // Varible lookup + const lookup = parts[1]; + this.user.getVar(lookup, (e, v) => { + if (v !== null && v.length) { + cb(null, `There are ${v.length} letters in your ${lookup}.`); + } else { + cb(null, "I don't know"); + } + }); + } else if (parts[0] == 'this' && parts.length == 2) { + // this phrase, this sentence + cb(null, `That phrase has ${this.message.raw.length} characters. I think.`); + } else { + cb(null, 'I think there is about 10 characters. :)'); + } + } else { + cap(null, ''); + } +}; + +const nextNumber = function nextNumber(cb) { + let reply = ''; + const num = this.message.numbers.slice(-1)[0]; + + if (num) { + if (this.message.lemWords.indexOf('before') !== -1) { + reply = parseInt(num) - 1; + } + if (this.message.lemWords.indexOf('after') !== -1) { + reply = parseInt(num) + 1; + } + } + + cb(null, reply); +}; + +export default { + letterLookup, + nextNumber, + oppisite, + rhymes, + syllable, + wordLength, +}; diff --git a/src/plugins/compare.js b/src/plugins/compare.js new file mode 100644 index 00000000..1608cc24 --- /dev/null +++ b/src/plugins/compare.js @@ -0,0 +1,248 @@ +import debuglog from 'debug'; +import _ from 'lodash'; +import async from 'async'; + +import history from '../bot/history'; +import Utils from '../bot/utils'; + +const debug = debuglog('Compare Plugin'); + +const createFact = function createFact(s, v, o, cb) { + this.user.memory.create(s, v, o, false, () => { + this.facts.db.get({ subject: v, predicate: 'opposite' }, (e, r) => { + if (r.length !== 0) { + this.user.memory.create(o, r[0].object, s, false, () => { + cb(null, ''); + }); + } else { + cb(null, ''); + } + }); + }); +}; + +const findOne = function findOne(haystack, arr) { + return arr.some(v => (haystack.indexOf(v) >= 0)); +}; + +const resolveAdjective = function resolveAdjective(cb) { + const candidates = history(this.user, { names: true }); + const message = this.message; + const userFacts = this.user.memory.db; + const botFacts = this.facts.db; + + const getOpp = function getOpp(term, callback) { + botFacts.search({ + subject: term, + predicate: 'opposite', + object: botFacts.v('opp'), + }, (e, oppResult) => { + if (!_.isEmpty(oppResult)) { + callback(null, oppResult[0].opp); + } else { + callback(null, null); + } + }); + }; + + const negatedTerm = function negatedTerm(msg, names, cb) { + // Are we confused about what we are looking for??! + // Could be "least tall" negated terms + if (_.contains(msg.adjectives, 'least') && msg.adjectives.length === 2) { + // We need to flip the adjective to the oppisite and do a lookup. + const cmpWord = _.without(msg.adjectives, 'least'); + getOpp(cmpWord[0], (err, oppWord) => { + // TODO - What if there is no oppWord? + // TODO - What if we have more than 2 names? + + debug('Lookup', oppWord, names); + if (names.length === 2) { + const pn1 = names[0].toLowerCase(); + const pn2 = names[1].toLowerCase(); + + userFacts.get({ subject: pn1, predicate: oppWord, object: pn2 }, (e, r) => { + // r is treated as 'truthy' + if (!_.isEmpty(r)) { + cb(null, `${_.capitalize(pn1)} is ${oppWord}er.`); + } else { + cb(null, `${_.capitalize(pn2)} is ${oppWord}er.`); + } + }); + } else { + cb(null, `${_.capitalize(names)} is ${oppWord}er.`); + } + }); + } else { + // We have no idea what they are searching for + cb(null, '???'); + } + }; + + // This will return the adjective from the message, or the oppisite term in some cases + // "least short" => tall + // "less tall" => short + let baseWord = null; + const getAdjective = function getAdjective(m, cb) { + let cmpWord; + + if (findOne(m.adjectives, ['least', 'less'])) { + cmpWord = _.first(_.difference(m.adjectives, ['least', 'less'])); + baseWord = cmpWord; + getOpp(cmpWord, cb); + } else { + cb(null, (m.compareWords[0]) ? m.compareWords[0] : m.adjectives[0]); + } + }; + + // We may want to roll though all the candidates?!? + // These examples are somewhat forced. (over fitted) + if (candidates) { + const prevMessage = candidates[0]; + + if (prevMessage && prevMessage.names.length === 1) { + cb(null, `It is ${prevMessage.names[0]}.`); + } else if (prevMessage && prevMessage.names.length > 1) { + // This could be: + // Jane is older than Janet. Who is the youngest? + // Jane is older than Janet. Who is the younger A or B? + // My parents are John and Susan. What is my mother called? + + if ((message.compareWords.length === 1 || message.adjectives.length === 1)) { + const handle = (e, cmpTerms) => { + const compareWord = cmpTerms[0]; + const compareWord2 = cmpTerms[1]; + + debug('CMP ', compareWord, compareWord2); + + botFacts.get({ subject: compareWord, predicate: 'opposite', object: compareWord2 }, (e, oppResult) => { + debug('Looking for Opp of', compareWord, oppResult); + + // Jane is older than Janet. Who is the older Jane or Janet? + if (!_.isEmpty(message.names)) { + debug('We have names', message.names); + // Make sure we say a name they are looking for. + const nameOne = message.names[0].toLowerCase(); + + userFacts.get({ subject: nameOne, predicate: compareWord }, (e, result) => { + if (_.isEmpty(result)) { + // So the fact is wrong, lets try the other way round + + userFacts.get({ object: nameOne, predicate: compareWord }, (e, result) => { + debug('RES', result); + + if (!_.isEmpty(result)) { + if (message.names.length === 2 && result[0].subject === message.names[1]) { + cb(null, `${_.capitalize(result[0].subject)} is ${compareWord}er than ${_.capitalize(result[0].object)}.`); + } else if (message.names.length === 2 && result[0].subject !== message.names[1]) { + // We can guess or do something more clever? + cb(null, `${_.capitalize(message.names[1])} is ${compareWord}er than ${_.capitalize(result[0].object)}.`); + } else { + cb(null, `${Utils.pickItem(message.names)} is ${compareWord}er?`); + } + } else { + // Lets do it again if we have another name + cb(null, `${Utils.pickItem(message.names)} is ${compareWord}er?`); + } + }); + } else { + // This could be a <-> b <-> c (is a << c ?) + userFacts.search([ + { subject: nameOne, predicate: compareWord, object: userFacts.v('f') }, + { subject: userFacts.v('f'), predicate: compareWord, object: userFacts.v('v') }, + ], (err, results) => { + if (!_.isEmpty(results)) { + if (results[0].v === message.names[1].toLowerCase()) { + cb(null, `${_.capitalize(message.names[0])} is ${compareWord}er than ${_.capitalize(message.names[1])}.`); + } else { + // Test this + cb(null, `${_.capitalize(message.names[1])} is ${compareWord}er than ${_.capitalize(message.names[0])}.`); + } + } else { + // Test this block + cb(null, `${Utils.pickItem(message.names)} is ${compareWord}er?`); + } + }); + } + }); + } else { + debug('NO NAMES'); + // Which of them is the ? + // This message has NO names + // Jane is older than Janet. **Who is the older?** + // Jane is older than Janet. **Who is the youngest?** + + // We pre-lemma the adjactives, so we need to fetch the raw word from the dict. + // We could have "Who is the oldest" + // If the word has been flipped, it WONT be in the dictionary, but we have a cache of it + const fullCompareWord = (baseWord) ? + message.dict.findByLem(baseWord).word : + message.dict.findByLem(compareWord).word; + + + // Looking for an end term + if (fullCompareWord.indexOf('est') > 0) { + userFacts.search([ + { subject: userFacts.v('oldest'), + predicate: compareWord, + object: userFacts.v('rand1') }, + { subject: userFacts.v('oldest'), + predicate: compareWord, + object: userFacts.v('rand2') }, + ], (err, results) => { + if (!_.isEmpty(results)) { + cb(null, `${_.capitalize(results[0].oldest)} is the ${compareWord}est.`); + } else { + // Pick one. + cb(null, `${_.capitalize(Utils.pickItem(prevMessage.names))} is the ${compareWord}est.`); + } + }); + } else { + if (!_.isEmpty(oppResult)) { + // They are oppisite, but lets check to see if we have a true fact + + userFacts.get({ subject: prevMessage.names[0].toLowerCase(), predicate: compareWord }, (e, result) => { + if (!_.isEmpty(result)) { + if (message.qSubType === 'YN') { + cb(null, `Yes, ${_.capitalize(result[0].object)} is ${compareWord}er.`); + } else { + cb(null, `${_.capitalize(result[0].object)} is ${compareWord}er than ${prevMessage.names[0]}.`); + } + } else { + if (message.qSubType === 'YN') { + cb(null, `Yes, ${_.capitalize(prevMessage.names[1])} is ${compareWord}er.`); + } else { + cb(null, `${_.capitalize(prevMessage.names[1])} is ${compareWord}er than ${prevMessage.names[0]}.`); + } + } + }); + } else if (compareWord === compareWord2) { + // They are the same adjectives + // No names. + if (message.qSubType === 'YN') { + cb(null, `Yes, ${_.capitalize(prevMessage.names[0])} is ${compareWord}er.`); + } else { + cb(null, `${_.capitalize(prevMessage.names[0])} is ${compareWord}er than ${prevMessage.names[1]}.`); + } + } else { + // not opposite terms. + cb(null, "Those things don't make sense to compare."); + } + } + } + }); + }; + + async.map([message, prevMessage], getAdjective, handle); + } else { + negatedTerm(message, prevMessage.names, cb); + } + } + } else { + cb(null, '??'); + } +}; + +export default { + createFact, + resolveAdjective, +}; diff --git a/src/plugins/math.js b/src/plugins/math.js new file mode 100644 index 00000000..7305a49a --- /dev/null +++ b/src/plugins/math.js @@ -0,0 +1,99 @@ +/* + Math functions for + - evaluating expressions + - converting functions + - sequence functions +*/ + +const math = require('../bot/math'); +const roman = require('roman-numerals'); +const debug = require('debug')('mathPlugin'); + +const evaluateExpression = function evaluateExpression(cb) { + if (this.message.numericExp || (this.message.halfNumericExp && this.user.prevAns)) { + const answer = math.parse(this.message.cwords, this.user.prevAns); + let suggestedReply; + if (answer) { + this.user.prevAns = answer; + console.log('Prev', this.user); + suggestedReply = `I think it is ${answer}`; + } else { + suggestedReply = 'What do I look like, a computer?'; + } + cb(null, suggestedReply); + } else { + cb(true, ''); + } +}; + +const numToRoman = function numToRoman(cb) { + const suggest = `I think it is ${roman.toRoman(this.message.numbers[0])}`; + cb(null, suggest); +}; + +const numToHex = function numToHex(cb) { + const suggest = `I think it is ${parseInt(this.message.numbers[0], 10).toString(16)}`; + cb(null, suggest); +}; + +const numToBinary = function numToBinary(cb) { + const suggest = `I think it is ${parseInt(this.message.numbers[0], 10).toString(2)}`; + cb(null, suggest); +}; + +const numMissing = function numMissing(cb) { + // What number are missing 1, 3, 5, 7 + if (this.message.lemWords.indexOf('missing') !== -1 && this.message.numbers.length !== 0) { + const numArray = this.message.numbers.sort(); + const mia = []; + for (let i = 1; i < numArray.length; i++) { + if (numArray[i] - numArray[i - 1] !== 1) { + const x = numArray[i] - numArray[i - 1]; + let j = 1; + while (j < x) { + mia.push(parseFloat(numArray[i - 1]) + j); + j += 1; + } + } + } + const s = mia.sort((a, b) => (a - b)); + cb(null, `I think it is ${s.join(' ')}`); + } else { + cb(true, ''); + } +}; + +// Sequence +const numSequence = function numSequence(cb) { + if (this.message.lemWords.indexOf('sequence') !== -1 && this.message.numbers.length !== 0) { + debug('Finding the next number in the series'); + let numArray = this.message.numbers.map(item => parseInt(item)); + numArray = numArray.sort((a, b) => (a - b)); + + let suggest; + if (math.arithGeo(numArray) === 'Arithmetic') { + let x; + for (let i = 1; i < numArray.length; i++) { + x = numArray[i] - numArray[i - 1]; + } + suggest = `I think it is ${parseInt(numArray.pop()) + x}`; + } else if (math.arithGeo(numArray) === 'Geometric') { + const a = numArray[1]; + const r = a / numArray[0]; + suggest = `I think it is ${numArray.pop() * r}`; + } + + cb(null, suggest); + } else { + cb(true, ''); + } +}; + +export default { + evaluateExpression, + numMissing, + numSequence, + numToBinary, + numToHex, + numToRoman, +}; diff --git a/plugins/message.js b/src/plugins/message.js similarity index 82% rename from plugins/message.js rename to src/plugins/message.js index 4015e900..2f841826 100644 --- a/plugins/message.js +++ b/src/plugins/message.js @@ -1,15 +1,17 @@ -var debug = require("debug")("Message Plugin"); -var history = require("../lib/history"); -var _ = require("lodash"); +// import debuglog from 'debug'; +// import _ from 'lodash'; -exports.addMessageProp = function(key, value, cb) { +// import history from '../bot/history'; - if (key !== "" && value !== "") { +// const debug = debuglog('Message Plugin'); + +const addMessageProp = function addMessageProp(key, value, cb) { + if (key !== '' && value !== '') { this.message.props[key] = value; } - cb(null, ""); -} + cb(null, ''); +}; /* @@ -72,4 +74,6 @@ exports.addMessageProp = function(key, value, cb) { // } else { // return null; // } -// } \ No newline at end of file +// } + +export default { addMessageProp }; diff --git a/src/plugins/reason.js b/src/plugins/reason.js new file mode 100644 index 00000000..348f9910 --- /dev/null +++ b/src/plugins/reason.js @@ -0,0 +1,350 @@ +import _ from 'lodash'; +import debuglog from 'debug'; +import moment from 'moment'; + +import history from '../bot/history'; +import Utils from '../bot/utils'; +import wd from '../bot/reply/wordnet'; + +const debug = debuglog('Reason Plugin'); + +exports.hasName = function hasName(bool, cb) { + this.user.getVar('name', (e, name) => { + if (name !== null) { + cb(null, (bool === 'true')); + } else { + // We have no name + cb(null, (bool === 'false')); + } + }); +}; + +exports.has = function has(value, cb) { + this.user.getVar(value, (e, uvar) => { + cb(null, (uvar === undefined)); + }); +}; + +exports.findLoc = function findLoc(cb) { + const candidates = history(this.user, { names: true }); + if (!_.isEmpty(candidates)) { + debug('history candidates', candidates); + const c = candidates[0]; + let suggest; + + if (c.names.length === 1) { + suggest = `In ${c.names[0]}`; + } else if (c.names.length === 2) { + suggest = `In ${c.names[0]}, ${c.names[1]}.`; + } else { + suggest = `In ${Utils.pickItem(c.names)}`; + } + + cb(null, suggest); + } else { + cb(null, "I'm not sure where you lived."); + } +}; + +exports.tooAdjective = function tooAdjective(cb) { + // what is/was too small? + const message = this.message; + const candidates = history(this.user, { adjectives: message.adjectives }); + debug('adj candidates', candidates); + let suggest = ''; + + if (candidates.length !== 0 && candidates[0].cNouns.length !== 0) { + const choice = candidates[0].cNouns.filter(item => (item.length >= 3)); + const too = (message.adverbs.indexOf('too') !== -1) ? 'too ' : ''; + suggest = `The ${choice.pop()} was ${too}${message.adjectives[0]}.`; + // suggest = "The " + choice.pop() + " was too " + message.adjectives[0] + "."; + } + + cb(null, suggest); +}; + +exports.usedFor = function usedFor(cb) { + const that = this; + this.cnet.usedForForward(that.message.nouns[0], (e, r) => { + if (!_.isEmpty(r)) { + const res = (r) ? Utils.makeSentense(r[0].sentense) : ''; + cb(null, res); + } else { + cb(null, ''); + } + }); +}; + +exports.resolveFact = function resolveFact(cb) { + // Resolve this + const message = this.message; + const t1 = message.nouns[0]; + const t2 = message.adjectives[0]; + + this.cnet.resolveFact(t1, t2, (err, res) => { + if (res) { + cb(null, 'It sure is.'); + } else { + cb(null, "I'm not sure."); + } + }); +}; + + +exports.putA = function putA(cb) { + const thing = (this.message.entities[0]) ? this.message.entities[0] : this.message.nouns[0]; + + if (thing) { + this.cnet.putConcept(thing, (e, putThing) => { + if (putThing) { + cb(null, Utils.makeSentense(Utils.indefiniteArticlerize(putThing))); + } else { + cb(null, ''); + } + }); + } +}; + +exports.isA = function isA(cb) { + const that = this; + let thing = (that.message.entities[0]) ? that.message.entities[0] : that.message.nouns[0]; + const userfacts = that.user.memory.db; + const userID = that.user.name; + + if (thing) { + this.cnet.isAForward(thing, (e, r) => { + if (!_.isEmpty(r)) { + const res = (r) ? Utils.makeSentense(r[0].sentense) : ''; + cb(null, res); + } else { + // Lets try wordnet + wd.define(thing, (err, result) => { + if (err) { + cb(null, ''); + } else { + cb(null, result); + } + }); + } + }); + } else { + thing = ''; + // my x is adj => what is adj + if (that.message.adverbs[0]) { + thing = that.message.adverbs[0]; + } else { + thing = that.message.adjectives[0]; + } + userfacts.get({ object: thing, predicate: userID }, (err, list) => { + if (!_.isEmpty(list)) { + // Because it came from userID it must be his + cb(null, `You said your ${list[0].subject} is ${thing}.`); + } else { + // find example of thing? + cb(null, ''); + } + }); + } +}; + +exports.colorLookup = function colorLookup(cb) { + const that = this; + const message = this.message; + const things = message.entities.filter(item => ((item !== 'color') ? item : null)); + let suggest = ''; + const facts = that.facts.db; + const userfacts = that.user.memory.db; + const botfacts = that.botfacts.db; + const userID = that.user.name; + + // TODO: This could be improved adjectives may be empty + const thing = (things.length === 1) ? things[0] : message.adjectives[0]; + + if (thing !== '' && message.pnouns.length === 0) { + // What else is green (AKA Example of green) OR + // What color is a tree? + + const fthing = thing.toLowerCase().replace(' ', '_'); + + // ISA on thing + facts.get({ object: fthing, predicate: 'color' }, (err, list) => { + if (!_.isEmpty(list)) { + const thingOfColor = Utils.pickItem(list); + const toc = thingOfColor.subject.replace(/_/g, ' '); + + cb(null, Utils.makeSentense(`${Utils.indefiniteArticlerize(toc)} is ${fthing}`)); + } else { + facts.get({ subject: fthing, predicate: 'color' }, (err, list) => { + if (!_.isEmpty(list)) { + suggest = `It is ${list[0].object}.`; + cb(null, suggest); + } else { + that.cnet.resolveFact('color', thing, (err, res) => { + if (res) { + suggest = `It is ${res}.`; + } else { + suggest = 'It depends, maybe brown?'; + } + cb(null, suggest); + }); + } + }); + } + }); + } else if (message.pronouns.length !== 0) { + // Your or My color? + // TODO: Lookup a saved or cached value. + + // what color is my car + // what is my favoirute color + if (message.pronouns.indexOf('my') !== -1) { + // my car is x + userfacts.get({ subject: message.nouns[1], predicate: userID }, (err, list) => { + if (!_.isEmpty(list)) { + const color = list[0].object; + const lookup = message.nouns[1]; + const toSay = [`Your ${lookup} is ${color}.`]; + + facts.get({ object: color, predicate: 'color' }, (err, list) => { + if (!_.isEmpty(list)) { + const thingOfColor = Utils.pickItem(list); + const toc = thingOfColor.subject.replace(/_/g, ' '); + toSay.push(`Your ${lookup} is the same color as a ${toc}.`); + } + cb(null, Utils.pickItem(toSay)); + }); + } else { + // my fav color - we need + const pred = message.entities[0]; + userfacts.get({ subject: thing, predicate: pred }, (err, list) => { + debug('!!!!', list); + if (!_.isEmpty(list)) { + const color = list[0].object; + cb(null, `Your ${thing} ${pred} is ${color}.`); + } else { + cb(null, `You never told me what color your ${thing} is.`); + } + }); + } + }); + } else if (message.pronouns.indexOf('your') !== -1) { + // Do I have a /thing/ and if so, what color could or would it be? + + botfacts.get({ subject: thing, predicate: 'color' }, (err, list) => { + if (!_.isEmpty(list)) { + const thingOfColor = Utils.pickItem(list); + const toc = thingOfColor.object.replace(/_/g, ' '); + cb(null, `My ${thing} color is ${toc}.`); + } else { + debug('---', { subject: thing, predicate: 'color' }); + // Do I make something up or just continue? + cb(null, ''); + } + }); + } + } else { + suggest = 'It is blue-green in color.'; + cb(null, suggest); + } +}; + +exports.makeChoice = function makeChoice(cb) { + if (!_.isEmpty(this.message.list)) { + // Save the choice so we can refer to our decision later + const sect = _.difference(this.message.entities, this.message.list); + // So I believe sect[0] is the HEAD noun + + if (sect.length === 0) { + // What do you like? + const choice = Utils.pickItem(this.message.list); + cb(null, `I like ${choice}.`); + } else { + // Which do you like? + this.cnet.filterConcepts(this.message.list, sect[0], (err, results) => { + const choice = Utils.pickItem(results); + cb(null, `I like ${choice}.`); + }); + } + } else { + cb(null, ''); + } +}; + +exports.findMoney = function findMoney(cb) { + const candidates = history(this.user, { nouns: this.message.nouns, money: true }); + if (candidates.length !== 0) { + cb(null, `It would cost $${candidates[0].numbers[0]}.`); + } else { + cb(null, 'Not sure.'); + } +}; + +exports.findDate = function findDate(cb) { + const candidates = history(this.user, { date: true }); + if (candidates.length !== 0) { + debug('DATE', candidates[0]); + cb(null, `It is in ${moment(candidates[0].date).format('MMMM')}.`); + } else { + cb(null, 'Not sure.'); + } +}; + +exports.locatedAt = function locatedAt(cb) { + debug('LocatedAt'); + const args = Array.prototype.slice.call(arguments); + let place; + + if (args.length === 2) { + place = args[0]; + cb = args[1]; + } else { + cb = args[0]; + // Pull the place from the history + const reply = this.user.getLastReply(); + if (reply && reply.nouns.length !== 0); + place = reply.nouns.pop(); + } + + // var thing = entities.filter(function(item){if (item != "name") return item }) + this.cnet.atLocationReverse(place, (err, results) => { + if (!_.isEmpty(results)) { + const itemFound = Utils.pickItem(results); + cb(null, Utils.makeSentense(`you might find ${Utils.indefiniteArticlerize(itemFound.c1_text)} at ${Utils.indefiniteArticlerize(place)}`)); + } else { + cb(null, ''); + } + }); +}; + +// TODO: deprecate for acquireGoods +exports.aquireGoods = function aquireGoods(cb) { + // Do you own a + const that = this; + const message = that.message; + const thing = (message.entities[0]) ? message.entities[0] : message.nouns[0]; + const botfacts = that.botfacts.db; + const cnet = that.cnet; + let reason = ''; + + botfacts.get({ subject: thing, predicate: 'ownedby', object: 'bot' }, (err, list) => { + debug('!!!', list); + if (!_.isEmpty(list)) { + // Lets find out more about it. + cb(null, 'Yes'); + } else { + // find example of thing? + // what is it? + cnet.usedForForward(thing, (err, res) => { + if (res) { + reason = Utils.pickItem(res); + reason = reason.frame2; + botfacts.put({ subject: thing, predicate: 'ownedby', object: 'bot' }, (err, list) => { + cb(null, `Yes, I used it for ${reason}.`); + }); + } else { + cb(null, 'NO'); + } + }); + } + }); +}; diff --git a/src/plugins/test.js b/src/plugins/test.js new file mode 100644 index 00000000..866e52a3 --- /dev/null +++ b/src/plugins/test.js @@ -0,0 +1,99 @@ +import _ from 'lodash'; + +// This is used in a test to verify fall though works +// TODO: Move this into a fixture. +const bail = function bail(cb) { + cb(true, null); +}; + +const one = function one(cb) { + cb(null, 'one'); +}; + +const num = function num(n, cb) { + cb(null, n); +}; + +const changetopic = function changetopic(n, cb) { + this.user.setTopic(n); + cb(null, ''); +}; + +const changefunctionreply = function changefunctionreply(newtopic, cb) { + cb(null, `{topic=${newtopic}}`); +}; + +const doSomething = function doSomething(cb) { + console.log('this.message.raw', this.message.raw); + cb(null, 'function'); +}; + +const breakFunc = function breakFunc(cb) { + cb(null, '', true); +}; + +const nobreak = function nobreak(cb) { + cb(null, '', false); +}; + +const objparam1 = function objparam1(cb) { + const data = { + text: 'world', + attachments: [ + { + text: 'Optional text that appears *within* the attachment', + }, + ], + }; + cb(null, data); +}; + +const objparam2 = function objparam2(cb) { + cb(null, { test: 'hello', text: 'world' }); +}; + + +const showScope = function showScope(cb) { + cb(null, `${this.message_props.key} ${this.user.id} ${this.message.clean}`); +}; + +const word = function word(word1, word2, cb) { + cb(null, word1 === word2); +}; + +const hasFirstName = function hasFirstName(bool, cb) { + this.user.getVar('firstName', (e, name) => { + if (name !== null) { + cb(null, (bool === 'true')); + } else { + cb(null, (bool === 'false')); + } + }); +}; + +const getUserId = function getUserId(cb) { + const userID = this.user.id; + const that = this; + // console.log("CMP1", _.isEqual(userID, that.user.id)); + return that.bot.getUser('userB', (err, user) => { + console.log('CMP2', _.isEqual(userID, that.user.id)); + cb(null, that.user.id); + }); +}; + +export default { + bail, + breakFunc, + doSomething, + changefunctionreply, + changetopic, + getUserId, + hasFirstName, + nobreak, + num, + objparam1, + objparam2, + one, + showScope, + word, +}; diff --git a/src/plugins/time.js b/src/plugins/time.js new file mode 100644 index 00000000..e022b1ca --- /dev/null +++ b/src/plugins/time.js @@ -0,0 +1,96 @@ +import moment from 'moment'; + +const COEFF = 1000 * 60 * 5; + +const getSeason = function getSeason() { + const now = moment(); + now.dayOfYear(); + const doy = now.dayOfYear(); + + if (doy > 80 && doy < 172) { + return 'spring'; + } else if (doy > 172 && doy < 266) { + return 'summer'; + } else if (doy > 266 && doy < 357) { + return 'fall'; + } else if (doy < 80 || doy > 357) { + return 'winter'; + } + return 'unknown'; +}; + +exports.getDOW = function getDOW(cb) { + cb(null, moment().format('dddd')); +}; + +exports.getDate = function getDate(cb) { + cb(null, moment().format('ddd, MMMM Do')); +}; + +exports.getDateTomorrow = function getDateTomorrow(cb) { + const date = moment().add('d', 1).format('ddd, MMMM Do'); + cb(null, date); +}; + +exports.getSeason = function getSeason(cb) { + cb(null, getSeason()); +}; + +exports.getTime = function getTime(cb) { + const date = new Date(); + const rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); + const time = moment(rounded).format('h:mm'); + cb(null, `The time is ${time}`); +}; + +exports.getGreetingTimeOfDay = function getGreetingTimeOfDay(cb) { + const date = new Date(); + const rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); + const time = moment(rounded).format('H'); + let tod; + if (time < 12) { + tod = 'morning'; + } else if (time < 17) { + tod = 'afternoon'; + } else { + tod = 'evening'; + } + + cb(null, tod); +}; + +exports.getTimeOfDay = function getTimeOfDay(cb) { + const date = new Date(); + const rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); + const time = moment(rounded).format('H'); + let tod; + if (time < 12) { + tod = 'morning'; + } else if (time < 17) { + tod = 'afternoon'; + } else { + tod = 'night'; + } + + cb(null, tod); +}; + +exports.getDayOfWeek = function getDayOfWeek(cb) { + cb(null, moment().format('dddd')); +}; + +exports.getMonth = function getMonth(cb) { + let reply = ''; + if (this.message.words.indexOf('next') !== -1) { + reply = moment().add('M', 1).format('MMMM'); + } else if (this.message.words.indexOf('previous') !== -1) { + reply = moment().subtract('M', 1).format('MMMM'); + } else if (this.message.words.indexOf('first') !== -1) { + reply = 'January'; + } else if (this.message.words.indexOf('last') !== -1) { + reply = 'December'; + } else { + reply = moment().format('MMMM'); + } + cb(null, reply); +}; diff --git a/src/plugins/user.js b/src/plugins/user.js new file mode 100644 index 00000000..199f5fd5 --- /dev/null +++ b/src/plugins/user.js @@ -0,0 +1,97 @@ +import _ from 'lodash'; +import debuglog from 'debug'; + +const debug = debuglog('SS:UserFacts'); + +const save = function save(key, value, cb) { + const memory = this.user.memory; + const userId = this.user.id; + + if (arguments.length !== 3) { + console.log('WARNING\nValue not found in save function.'); + if (_.isFunction(value)) { + cb = value; + value = ''; + } + } + + memory.db.get({ subject: key, predicate: userId }, (err, results) => { + if (!_.isEmpty(results)) { + memory.db.del(results[0], () => { + memory.db.put({ subject: key, predicate: userId, object: value }, () => { + cb(null, ''); + }); + }); + } else { + memory.db.put({ subject: key, predicate: userId, object: value }, (err) => { + cb(null, ''); + }); + } + }); +}; + +const hasItem = function hasItem(key, bool, cb) { + const memory = this.user.memory; + const userId = this.user.id; + + debug('getVar', key, bool, userId); + memory.db.get({ subject: key, predicate: userId }, (err, res) => { + if (!_.isEmpty(res)) { + cb(null, (bool === 'true')); + } else { + cb(null, (bool === 'false')); + } + }); +}; + +const get = function get(key, cb) { + const memory = this.user.memory; + const userId = this.user.id; + + debug('getVar', key, userId); + + memory.db.get({ subject: key, predicate: userId }, (err, res) => { + if (res && res.length !== 0) { + cb(err, res[0].object); + } else { + cb(err, ''); + } + }); +}; + +const createUserFact = function createUserFact(s, v, o, cb) { + this.user.memory.create(s, v, o, false, () => { + cb(null, ''); + }); +}; + +const known = function known(bool, cb) { + const memory = this.user.memory; + const name = (this.message.names && !_.isEmpty(this.message.names)) ? this.message.names[0] : ''; + memory.db.get({ subject: name.toLowerCase() }, (err, res1) => { + memory.db.get({ object: name.toLowerCase() }, (err, res2) => { + if (_.isEmpty(res1) && _.isEmpty(res2)) { + cb(null, (bool === 'false')); + } else { + cb(null, (bool === 'true')); + } + }); + }); +}; + +const inTopic = function inTopic(topic, cb) { + if (topic === this.user.currentTopic) { + cb(null, 'true'); + } else { + cb(null, 'false'); + } +}; + +export default { + createUserFact, + get, + hasItem, + inTopic, + known, + save, +}; diff --git a/src/plugins/wordnet.js b/src/plugins/wordnet.js new file mode 100644 index 00000000..6a20bb19 --- /dev/null +++ b/src/plugins/wordnet.js @@ -0,0 +1,18 @@ +import wd from '../bot/reply/wordnet'; + +const wordnetDefine = function wordnetDefine(cb) { + const args = Array.prototype.slice.call(arguments); + let word; + + if (args.length === 2) { + word = args[0]; + } else { + word = this.message.words.pop(); + } + + wd.define(word, (err, result) => { + cb(null, `The Definition of ${word} is ${result}`); + }); +}; + +export default { wordnetDefine }; diff --git a/src/plugins/words.js b/src/plugins/words.js new file mode 100644 index 00000000..e4dbeee9 --- /dev/null +++ b/src/plugins/words.js @@ -0,0 +1,40 @@ +import pluralize from 'pluralize'; +import debuglog from 'debug'; +import utils from '../bot/utils'; + +const debug = debuglog('Word Plugin'); + +const plural = function plural(word, cb) { + // Sometimes WordNet will give us more then one word + let reply; + const parts = word.split(' '); + + if (parts.length === 2) { + reply = `${pluralize.plural(parts[0])} ${parts[1]}`; + } else { + reply = pluralize.plural(word); + } + + cb(null, reply); +}; + +const not = function not(word, cb) { + const words = word.split('|'); + const results = utils.inArray(this.message.words, words); + debug('RES', results); + cb(null, (results === false)); +}; + +const lowercase = function lowercase(word, cb) { + if (word) { + cb(null, word.toLowerCase()); + } else { + cb(null, ''); + } +}; + +export default { + lowercase, + not, + plural, +}; diff --git a/test/capture.js b/test/capture.js index 99ed2189..2518c769 100644 --- a/test/capture.js +++ b/test/capture.js @@ -1,35 +1,35 @@ -/*global describe, it, bot, before, after */ -var mocha = require("mocha"); -var should = require("should"); -var help = require("./helpers"); +/* global describe, it, before, after */ -// The bulk of these tests now live in ss-parser, that script manages the -// input capture infterface. +import mocha from 'mocha'; +import should from 'should'; +import helpers from './helpers'; -describe('Super Script Capture System', function(){ +// The bulk of these tests now live in ss-parser - that script manages the +// input capture interface. - before(help.before("capture")); +describe('SuperScript Capture System', () => { + before(helpers.before('capture')); - describe('Previous Capture should return previous capture tag', function(){ - it("Previous capture", function(done) { - bot.reply("user1", "previous capture one interface", function(err, reply) { - reply.string.should.eql("previous capture test one interface"); - bot.reply("user1", "previous capture two", function(err, reply) { - reply.string.should.eql("previous capture test two interface"); + describe('Previous Capture should return previous capture tag', () => { + it('Previous capture', (done) => { + helpers.getBot().reply('user1', 'previous capture one interface', (err, reply) => { + reply.string.should.eql('previous capture test one interface'); + helpers.getBot().reply('user1', 'previous capture two', (err, reply) => { + reply.string.should.eql('previous capture test two interface'); done(); }); }); }); }); - describe("Match ", function() { - it("It should capture the last thing said", function(done) { - bot.reply("user1", "capture input", function(err, reply) { - reply.string.should.eql("capture input"); + describe('Match ', () => { + it('It should capture the last thing said', (done) => { + helpers.getBot().reply('user1', 'capture input', (err, reply) => { + reply.string.should.eql('capture input'); done(); }); }); }); - after(help.after); + after(helpers.after); }); diff --git a/test/continue.js b/test/continue.js index 2c445585..c7916dd5 100644 --- a/test/continue.js +++ b/test/continue.js @@ -1,113 +1,110 @@ -var mocha = require("mocha"); -var should = require("should"); -var help = require("./helpers"); +/* global describe, it, before, after */ +import mocha from 'mocha'; +import should from 'should'; +import helpers from './helpers'; -describe('Super Script Continue System aka Conversation', function(){ +describe('SuperScript Continue System aka Conversation', () => { + before(helpers.before('continue')); - before(help.before("continue")); - - describe('Dynamic Conversations', function() { - it("set some conversation state", function(done) { - bot.reply("user1", "__start__", function(err, reply) { - bot.getUser("user1", function(err, user) { - reply.string.should.eql("match here"); + describe('Dynamic Conversations', () => { + it('set some conversation state', (done) => { + helpers.getBot().reply('user1', '__start__', (err, reply) => { + helpers.getBot().getUser('user1', (err, user) => { + reply.string.should.eql('match here'); user.conversationState.id.should.eql(123); - bot.reply("user1", "I really hope this works!", function(err, reply) { - reply.string.should.eql("winning"); + helpers.getBot().reply('user1', 'I really hope this works!', (err, reply) => { + reply.string.should.eql('winning'); done(); }); }); - }); }); - it("and again", function(done) { - bot.reply("user1", "__start__", function(err, reply) { - bot.reply("user1", "boo ya", function(err, reply) { - bot.getUser("user1", function(err, user) { - reply.string.should.eql("YES"); + it('and again', (done) => { + helpers.getBot().reply('user1', '__start__', (err, reply) => { + helpers.getBot().reply('user1', 'boo ya', (err, reply) => { + helpers.getBot().getUser('user1', (err, user) => { + reply.string.should.eql('YES'); done(); }); }); }); }); - - }); - describe('Match and continue', function(){ - it("should continue", function(done) { - bot.reply("user1", "i went to highschool", function(err, reply) { - reply.string.should.eql("did you finish ?"); - bot.reply("user1", "then what happened?", function(err, reply2) { - ["i went to university", "what was it like?"].should.containEql(reply2.string); + describe('Match and continue', () => { + it('should continue', (done) => { + helpers.getBot().reply('user1', 'i went to highschool', (err, reply) => { + reply.string.should.eql('did you finish ?'); + helpers.getBot().reply('user1', 'then what happened?', (err, reply2) => { + ['i went to university', 'what was it like?'].should.containEql(reply2.string); done(); }); }); }); - it("should continue 2 - yes", function(done) { - bot.reply("user1", "i like to travel", function(err, reply) { - reply.string.should.eql("have you been to Madird?"); - bot.reply("user1", "yes it is the capital of spain!", function(err, reply2) { - reply2.string.should.eql("Madird is amazing."); + it('should continue 2 - yes', (done) => { + helpers.getBot().reply('user1', 'i like to travel', (err, reply) => { + reply.string.should.eql('have you been to Madird?'); + helpers.getBot().reply('user1', 'yes it is the capital of spain!', (err, reply2) => { + reply2.string.should.eql('Madird is amazing.'); done(); }); }); }); - it("should continue 3 - no", function(done) { - bot.reply("user1", "i like to travel", function(err, reply) { - reply.string.should.eql("have you been to Madird?"); - bot.reply("user1", "never", function(err, reply2) { - reply2.string.should.eql("Madird is my favorite city."); + it('should continue 3 - no', (done) => { + helpers.getBot().reply('user1', 'i like to travel', (err, reply) => { + reply.string.should.eql('have you been to Madird?'); + helpers.getBot().reply('user1', 'never', (err, reply2) => { + reply2.string.should.eql('Madird is my favorite city.'); done(); }); }); }); // These two are testing sorted gambits in replies. - it("should continue Sorted - A", function(done) { - bot.reply("user1", "something random", function(err, reply) { - bot.reply("user1", "red", function(err, reply2) { - reply2.string.should.eql("red is mine too."); + it('should continue Sorted - A', (done) => { + helpers.getBot().reply('user1', 'something random', (err, reply) => { + helpers.getBot().reply('user1', 'red', (err, reply2) => { + reply2.string.should.eql('red is mine too.'); done(); }); }); }); - it("should continue Sorted - B", function(done) { - bot.reply("user1", "something random", function(err, reply) { - bot.reply("user1", "blue", function(err, reply2) { - reply2.string.should.eql("I hate that color."); + it('should continue Sorted - B', (done) => { + helpers.getBot().reply('user1', 'something random', (err, reply) => { + helpers.getBot().reply('user1', 'blue', (err, reply2) => { + reply2.string.should.eql('I hate that color.'); done(); }); }); }); - it("GH-84 - compound reply convo.", function(done) { - bot.reply("user1", "test complex", function(err, reply) { - reply.string.should.eql("reply test super compound"); - bot.reply("user1", "cool", function(err, reply) { - reply.string.should.eql("it works"); + it('GH-84 - compound reply convo.', (done) => { + helpers.getBot().reply('user1', 'test complex', (err, reply) => { + reply.string.should.eql('reply test super compound'); + helpers.getBot().reply('user1', 'cool', (err, reply) => { + reply.string.should.eql('it works'); done(); }); }); }); }); - describe("GH-133", function() { - it("Threaded Conversation", function(done) { - bot.reply("user1", "conversation", function(err, reply) { - reply.string.should.eql("Are you happy?"); + describe('GH-133', () => { + it('Threaded Conversation', (done) => { + helpers.getBot().reply('user1', 'conversation', (err, reply) => { + reply.string.should.eql('Are you happy?'); // This is the reply to the conversation - bot.reply("user1", "yes", function(err, reply) { - reply.string.should.eql("OK, so you are happy"); + helpers.getBot().reply('user1', 'yes', (err, reply) => { + reply.string.should.eql('OK, so you are happy'); // Something else wont match because we are still in the conversation - bot.reply("user1", "something else", function(err, reply) { + helpers.getBot().reply('user1', 'something else', (err, reply) => { reply.string.should.eql("OK, so you don't know"); done(); }); @@ -116,20 +113,20 @@ describe('Super Script Continue System aka Conversation', function(){ }); // NB: I changed the user to user2 here to clear the thread. - // GH-162 - it.skip("Threaded Conversation 2", function(done) { - bot.reply("user2", "start", function(err, reply) { - reply.string.should.eql("What is your name?"); + // FIXME: GH-162 + it.skip('Threaded Conversation 2', (done) => { + helpers.getBot().reply('user2', 'start', (err, reply) => { + reply.string.should.eql('What is your name?'); - bot.reply("user2", "My name is Marius Ursache", function(err, reply) { - reply.string.should.eql("So your first name is Marius?"); + helpers.getBot().reply('user2', 'My name is Marius Ursache', (err, reply) => { + reply.string.should.eql('So your first name is Marius?'); - bot.reply("user2", "Yes", function(err, reply) { + helpers.getBot().reply('user2', 'Yes', (err, reply) => { reply.string.should.eql("That's a nice name."); // We are still stuck in the conversation here, so we repeat the question again - bot.reply("user2", "something else", function(err, reply) { - reply.string.should.eql("okay nevermind"); + helpers.getBot().reply('user2', 'something else', (err, reply) => { + reply.string.should.eql('okay nevermind'); done(); }); }); @@ -139,39 +136,34 @@ describe('Super Script Continue System aka Conversation', function(){ }); - describe('GH-152 - dont match sub-reply', function() { - it("Should not match", function(done) { - - bot.reply("user3", "lastreply two", function (err, reply) { - reply.string.should.eql(""); + describe('GH-152 - dont match sub-reply', () => { + it('Should not match', (done) => { + helpers.getBot().reply('user3', 'lastreply two', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - }); - describe('Match and continue KEEP', function() { - - it("Should be even more awesome", function(done){ - - bot.reply("user3", "new conversation", function (err, reply) { - reply.string.should.eql("What is your name?"); + describe('Match and continue KEEP', () => { + it('Should be even more awesome', (done) => { + helpers.getBot().reply('user3', 'new conversation', (err, reply) => { + reply.string.should.eql('What is your name?'); - bot.reply("user3", "My name is Rob", function (err, reply) { - reply.string.should.eql("So your first name is Rob?"); + helpers.getBot().reply('user3', 'My name is Rob', (err, reply) => { + reply.string.should.eql('So your first name is Rob?'); - bot.reply("user3", "yes", function (err, reply) { - reply.string.should.eql("Okay good."); + helpers.getBot().reply('user3', 'yes', (err, reply) => { + reply.string.should.eql('Okay good.'); - bot.reply("user3", "break out", function (err, reply) { - reply.string.should.eql("okay nevermind"); + helpers.getBot().reply('user3', 'break out', (err, reply) => { + reply.string.should.eql('okay nevermind'); // We should have exhausted "okay nevermind" and break out completely - bot.reply("user3", "break out", function (err, reply) { - reply.string.should.eql("okay we are free"); + helpers.getBot().reply('user3', 'break out', (err, reply) => { + reply.string.should.eql('okay we are free'); done(); }); - }); }); }); @@ -179,19 +171,18 @@ describe('Super Script Continue System aka Conversation', function(){ }); }); - describe("GH-207 Pass stars forward", function() { - it("should pass stars forward", function(done) { - bot.reply("user4", "start 2 foo or win", function(err, reply) { - reply.string.should.eql("reply 2 foo"); + describe('GH-207 Pass stars forward', () => { + it('should pass stars forward', (done) => { + helpers.getBot().reply('user4', 'start 2 foo or win', (err, reply) => { + reply.string.should.eql('reply 2 foo'); - bot.reply("user4", "second match bar", function(err, reply) { - reply.string.should.eql("reply 3 bar foo win"); + helpers.getBot().reply('user4', 'second match bar', (err, reply) => { + reply.string.should.eql('reply 3 bar foo win'); done(); }); - }); }); }); - after(help.after); + after(helpers.after); }); diff --git a/test/convo.js b/test/convo.js index cda54b9a..1c18e087 100644 --- a/test/convo.js +++ b/test/convo.js @@ -1,39 +1,39 @@ -var mocha = require("mocha"); -var should = require("should"); -var help = require("./helpers"); +/* global describe, it, before, after */ -describe('Super Script Conversation', function(){ +import mocha from 'mocha'; +import should from 'should'; +import helpers from './helpers'; - before(help.before("convo")); +describe('SuperScript Conversation', () => { + before(helpers.before('convo')); - describe('Volley', function() { - - it("should have volley", function(done) { - bot.reply("user1", "Can you skip rope?", function(err, reply) { - bot.getUser("user1", function(e, user){ + describe('Volley', () => { + it('should have volley', (done) => { + helpers.getBot().reply('user1', 'Can you skip rope?', (err, reply) => { + helpers.getBot().getUser('user1', (e, user) => { user.volley.should.eql(0); - done(); + done(); }); }); }); - it("should have volley 1", function(done) { - bot.reply("user1", "Can you jump rope?", function(err, reply) { - bot.getUser("user1", function(e, user){ + it('should have volley 1', (done) => { + helpers.getBot().reply('user1', 'Can you jump rope?', (err, reply) => { + helpers.getBot().getUser('user1', (e, user) => { user.volley.should.eql(1); user.rally.should.eql(1); - bot.reply("user1", "Have you done it lately?", function(err, reply) { - bot.getUser("user1", function(e, user){ + helpers.getBot().reply('user1', 'Have you done it lately?', (err, reply) => { + helpers.getBot().getUser('user1', (e, user) => { user.volley.should.eql(0); user.rally.should.eql(0); done(); }); }); - }); + }); }); }); }); - - after(help.after); -}); \ No newline at end of file + + after(helpers.after); +}); diff --git a/test/fixtures/cache/reason.json b/test/fixtures/cache/reason.json deleted file mode 100644 index 2149e86c..00000000 --- a/test/fixtures/cache/reason.json +++ /dev/null @@ -1 +0,0 @@ -{"topics":{"random":{"flags":[],"keywords":[]},"__pre__":{"flags":["keep"],"keywords":[],"filter":null}},"gambits":{"4BAC4OYO":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["d1YLkdcG"],"redirect":null,"trigger":"Can you skip rope","raw":"Can you skip rope"},"MLGPBJ7y":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["U6HV7IYt"],"redirect":null,"trigger":"Can you jump rope","raw":"Can you jump rope"},"X9SFbOhP":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["byh2Py3t"],"redirect":null,"trigger":"(?:.*?)","raw":"*"},"GAY8iw9H":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["NIP4rWgd"],"redirect":null,"trigger":"(?:.*\\s?)bathroom","raw":"* bathroom"},"WZmlSQ5i":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":"YN","filter":false},"replys":["jJgrP69V"],"redirect":null,"trigger":"(?:.*?)","raw":"*"},"NuwkZIWJ":{"topic":"random","options":{"isQuestion":true,"qType":"HUM","isConditional":false,"qSubType":false,"filter":false},"replys":["RnXk1muy"],"redirect":null,"trigger":"(?:.*?)","raw":"*"},"ibVBE9qF":{"topic":"random","options":{"isQuestion":true,"qType":"NUM","isConditional":false,"qSubType":false,"filter":false},"replys":["eLJLKHIX"],"redirect":null,"trigger":"(?:.*?)","raw":"*"},"3kwVTcOm":{"topic":"random","options":{"isQuestion":true,"qType":"HUM:ind","isConditional":false,"qSubType":false,"filter":false},"replys":["0LEza2QP"],"redirect":null,"trigger":"(?:.*?)","raw":"*"},"6tpnIBef":{"topic":"__pre__","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["2v96Rolk"],"redirect":null,"trigger":"(?:.*?)","raw":"*"},"wUHstcyP":{"topic":"random","options":{"isQuestion":true,"qType":"NUM:expression","isConditional":false,"qSubType":false,"filter":false},"replys":["MZit9oHM"],"redirect":null,"trigger":"(?:.*?)","raw":"*"},"vVBGyRZK":{"topic":"random","options":{"isQuestion":true,"qType":"DESC:def","isConditional":false,"qSubType":false,"filter":false},"replys":["Dixzadae"],"redirect":null,"trigger":"(?:.*\\s?)\\broman (\\bnumerial\\b|\\bnumeral\\b)\\s?\\b(?:.*\\s?)","raw":"* roman (numerial|numeral) *"},"WyBM1ief":{"topic":"random","options":{"isQuestion":true,"qType":"DESC:def","isConditional":false,"qSubType":false,"filter":false},"replys":["4pbo02XN"],"redirect":null,"trigger":"(?:.*\\s?)\\b(\\bhex\\b|\\bhexdecimal\\b)\\s?\\b(?:.*\\s?)","raw":"* (hex|hexdecimal) *"},"ZiJExDn9":{"topic":"random","options":{"isQuestion":true,"qType":"DESC:def","isConditional":false,"qSubType":false,"filter":false},"replys":["6UnN4dAA"],"redirect":null,"trigger":"(?:.*\\s?)\\bbinary\\b(?:.*\\s?)","raw":"* binary *"},"lOkDP7Nb":{"topic":"random","options":{"isQuestion":true,"qType":"NUM:other","isConditional":false,"qSubType":false,"filter":false},"replys":["me4Tt5BH"],"redirect":null,"trigger":"(?:.*\\s?)\\b(\\bmissing\\b)\\s?\\b(?:.*\\s?)","raw":"* (missing) *"},"xt6JLvet":{"topic":"random","options":{"isQuestion":true,"qType":"NUM:other","isConditional":false,"qSubType":false,"filter":false},"replys":["K47fhQL7"],"redirect":null,"trigger":"(?:.*\\s?)\\b(\\bsequence\\b)\\s?\\b(?:.*\\s?)","raw":"* (sequence) *"},"fpe4hgl6":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["3hG5oFBl"],"redirect":null,"trigger":"(?:\\s*if\\s*|\\s*)(?:\\s*is\\s*|\\s*be\\s*|\\s*)(?:\\s*more\\s*|\\s*less\\s*|\\s*) (\\bthen\\b|\\bthan\\b)\\s? ~extensions (?:\\s*is\\s*|\\s*be\\s*|\\s*) (\\bthen\\b|\\bthan\\b)\\s? ","raw":"[if] [is|be] [more|less] (then|than) ~extensions [is|be] (then|than) "},"QntRRIvW":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["TiPBmfSJ"],"redirect":null,"trigger":"(?:\\s*if\\s*|\\s*)(?:\\s*is\\s*|\\s*be\\s*|\\s*)(?:\\s*more\\s*|\\s*less\\s*|\\s*) (\\bthen\\b|\\bthan\\b)\\s? ","raw":"[if] [is|be] [more|less] (then|than) "},"AlJz2jD0":{"topic":"random","options":{"isQuestion":true,"qType":"HUM:ind","isConditional":false,"qSubType":false,"filter":false},"replys":["OH87Bfsh"],"redirect":null,"trigger":"(\\s?(?:[\\w-:]*\\??\\.?\\,?\\s*\\~?\\(?\\)?){0,3})who(\\s?(?:[\\w-:]*\\??\\.?\\,?\\s*\\~?\\(?\\)?){0,2})(?:\\s* or \\s*|\\s*)","raw":"*~3 who *~2 [ or ]"},"WoBKADZJ":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["nMsNX1DS"],"redirect":null,"trigger":"which(\\s?(?:[\\w-:]*\\??\\.?\\,?\\s*\\~?\\(?\\)?){0,4})(?:\\s* or \\s*|\\s*)","raw":"which *~4 [ or ]"},"dGLhWnJk":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["CRQr67jI"],"redirect":null,"trigger":"(?:.*\\s?)\\bfriend(?:\\s*named\\s*|\\s*)\\b(?:.*\\s?)","raw":"* friend [named] *"},"EbfOaoas":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["G6QqKsXJ"],"redirect":null,"trigger":"(?:.*\\s?)(?:.*\\s?)likes(?:.*\\s?)play (\\S+(?:\\s+\\S+){0})","raw":"* * likes * play *1"},"wBICPT4e":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":"WH","filter":"^has(friend)"},"replys":["mBbF5mpa"],"redirect":null,"trigger":"(?:.*\\s?)\\bis the name(\\s?(?:[\\w-:]*\\??\\.?\\,?\\s*\\~?\\(?\\)?){0,2})friend\\b(?:.*\\s?)","raw":"* is the name *~2 friend *"},"RzIeVLj9":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["md6ZtmOw"],"redirect":null,"trigger":"(?:.*\\s?)\\bput a\\b(?:.*\\s?)","raw":"* put a *"},"HglMZdQk":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["JZhkkPSk","P8mJdSh7"],"redirect":null,"trigger":"(?:.*\\s?)my name is ","raw":"* my name is "},"jHzWXF3U":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":"WH","filter":false},"replys":["hvEYVKdl"],"redirect":null,"trigger":"(?:.*\\s?)your name","raw":"* your name"},"0tIPhY72":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":"WH","filter":false},"replys":["vDeqz6es"],"redirect":null,"trigger":"(?:.*\\s?)you live","raw":"* you live"},"kBMWgjR6":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":"WH","filter":false},"replys":["la6at0E9"],"redirect":null,"trigger":"(?:.*\\s?)I live","raw":"* I live"},"0bXx7raW":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["RU296XAu"],"redirect":null,"trigger":"I live(?:.*\\s?)","raw":"I live *"},"YywV3uU5":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":"WH","filter":false},"replys":["gACFNmO8"],"redirect":null,"trigger":"(?:.*\\s?)","raw":"* "},"naSJ2Mmo":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":"WH","filter":false},"replys":["TuAp4vuR"],"redirect":null,"trigger":"(?:.*\\s?)\\bI do with a\\b(?:.*\\s?)","raw":"* I do with a *"},"Ym4jMJr0":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":"WH","filter":false},"replys":["xTqvHEHg"],"redirect":null,"trigger":"(?:.*\\s?)is a(?:.*\\s?)used for","raw":"* is a * used for"},"ZHG1HcrS":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["uJq3TZs6"],"redirect":null,"trigger":"what is a(\\s?(?:[\\w-:]*\\??\\.?\\,?\\s*\\~?\\(?\\)?){0,2})","raw":"what is a *~2"},"q4gZQjwu":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["WfHazW1g"],"redirect":null,"trigger":"what be (\\S+(?:\\s+\\S+){0})","raw":"what be *1"},"YB8IP6HI":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["5RAK9BOI"],"redirect":null,"trigger":"(?:.*\\s?)name(?:.*\\s?)find (\\bon\\b|\\bat\\b)\\s?(?:\\s*a\\s*|\\s*the\\s*|\\s*)(\\s?(?:[\\w-:]*\\??\\.?\\,?\\s*\\~?\\(?\\)?){0,2})","raw":"* name * find (on|at) [a|the] *~2"},"XPUNYyAu":{"topic":"random","options":{"isQuestion":true,"qType":"ENTY:color","isConditional":false,"qSubType":false,"filter":false},"replys":["idqVFC9e"],"redirect":null,"trigger":"(?:.*?)","raw":"*"},"HW1btHGx":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["lg0GbksD"],"redirect":null,"trigger":"what(?:\\s*else\\s*|\\s*)is ","raw":"what [else] is "},"3wSlGK24":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["CMvUCkJW"],"redirect":null,"trigger":"my is (\\b\\b|\\b\\b)\\s?","raw":"my is (|)"},"mN3bzt6e":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["NKQake8p"],"redirect":null,"trigger":"my favorite color is (\\S+(?:\\s+\\S+){0})","raw":"my favorite color is *1"},"Lr0T4Xzz":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["F3PtfyAw"],"redirect":null,"trigger":"what be (\\b\\b|\\b\\b)\\s?","raw":"what be (|)"},"POEasmQ5":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["tByjgsjn"],"redirect":null,"trigger":"is(?:\\s*the\\s*|\\s*) ","raw":"is [the] "},"sx17uAMy":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":"CH","filter":false},"replys":["yQoQJ17Y"],"redirect":null,"trigger":"(?:.*\\s?)\\bprefer\\b(?:.*\\s?)","raw":"* prefer *"},"xlOpP0Ey":{"topic":"random","options":{"isQuestion":true,"qType":"NUM:money","isConditional":false,"qSubType":false,"filter":false},"replys":["0GByj26l"],"redirect":null,"trigger":"(?:.*?)","raw":"*"},"J78xe3XD":{"topic":"random","options":{"isQuestion":true,"qType":"NUM:date","isConditional":false,"qSubType":false,"filter":false},"replys":["gp7pO0at"],"redirect":null,"trigger":"(?:.*\\s?)birthday","raw":"* birthday"},"4TAa906w":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["mNXPk8Cj"],"redirect":null,"trigger":"(?:.*\\s?)\\bdo you (\\bprepossess\\b)\\b(?:.*\\s?)","raw":"* do you ~own *"},"VvO8XWBf":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["TQ2dfqxA"],"redirect":null,"trigger":"my(\\s?(?:[\\w-:]*\\??\\.?\\,?\\s*\\~?\\(?\\)?){0,2})(?:\\s*likes to\\s*|\\s*) (\\S+(?:\\s+\\S+){0})","raw":"my *~2 [likes to] *1"},"SkN1qBWe":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["tUwROHqX"],"redirect":null,"trigger":"I (?:\\s*my\\s*|\\s*a\\s*|\\s*)~family_members(?:\\s*named\\s*|\\s*called\\s*|\\s*)","raw":"I [my|a] ~family_members [named|called] "},"JU3XhCpE":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["jPhGpcW1"],"redirect":null,"trigger":" is my ","raw":" is my "},"0paSFMJI":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["20SDkKHC"],"redirect":null,"trigger":"my ~family_members is ","raw":"my ~family_members is "},"LnUbCbkn":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["aPpWhv0G"],"redirect":null,"trigger":"my is(?:\\s*called\\s*|\\s*named\\s*|\\s*)","raw":"my is [called|named] "},"TfS9tTX1":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["h0IEAgIH"],"redirect":null,"trigger":"my (\\bgender role\\b|\\bposition\\b|\\braison d'etre\\b|\\bbit part\\b|\\bheavy\\b|\\bhero\\b|\\bingenue\\b|\\btitle role\\b|\\bheroine\\b|\\bvillain\\b|\\bcapacity\\b|\\bhat\\b|\\bportfolio\\b|\\bstead\\b|\\bsecond fiddle\\b) likes to play (\\S+(?:\\s+\\S+){0})","raw":"my ~role likes to play *1"},"POm1AApz":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["4VxQi4yM"],"redirect":null,"trigger":"I have (\\S+(?:\\s+\\S+){0}) (\\bkid\\b|\\bkids\\b|\\bchild\\b|\\bchildren\\b|\\bbabys\\b|\\bbabies\\b|\\bteenager\\b|\\bson\\b|\\bsons\\b|\\bdaughter\\b|\\bdaughters\\b|\\bcousin\\b|\\bfriend\\b)\\s?(?:.*\\s?)","raw":"I have *1 (kid|kids|child|children|babys|babies|teenager|son|sons|daughter|daughters|cousin|friend) *"},"SPa69Qc2":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["ttu05IL2","UGqdUWK3"],"redirect":null,"trigger":"(?:.*\\s?)hanging out with ","raw":"* hanging out with "},"nBQtdrdg":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["rEVmkaU1"],"redirect":null,"trigger":"make mad","raw":"make mad"},"z7BjORjt":{"topic":"random","options":{"isQuestion":false,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["BtvAbNtz","qYrt5gjk"],"redirect":null,"trigger":"look","raw":"look"},"mxnw88SW":{"topic":"random","options":{"isQuestion":true,"qType":false,"isConditional":false,"qSubType":false,"filter":false},"replys":["IB4hNrbt"],"redirect":null,"trigger":"who is she","raw":"who is she"}},"convos":{},"replys":{"d1YLkdcG":"yep","U6HV7IYt":"yep, can you?","byh2Py3t":"We should hang out sometime.","NIP4rWgd":"{keep} Down the hall on the left","jJgrP69V":"{keep} a","RnXk1muy":"{keep} b","eLJLKHIX":"{keep} c","0LEza2QP":"{keep} a","2v96Rolk":"^resolvePronouns()","MZit9oHM":"^evaluateExpression()","Dixzadae":"^numToRoman()","4pbo02XN":"^numToHex()","6UnN4dAA":"^numToBinary()","me4Tt5BH":"^numMissing()","K47fhQL7":"^numSequence()","3hG5oFBl":"^createFact(,,) ^createFact(,,)","TiPBmfSJ":"^createFact(,,)","OH87Bfsh":"^resolveAdjective()","nMsNX1DS":"^resolveAdjective()","CRQr67jI":"^save(friend,)","G6QqKsXJ":"^save(,)","mBbF5mpa":"Your friends name is ^get(friend).","md6ZtmOw":"^putA()","JZhkkPSk":"{^hasName(false)} ^save(name,) Nice to meet you, .","P8mJdSh7":"{^hasName(true)} I know, you already told me your name.","hvEYVKdl":"My name is Brit.","vDeqz6es":"I live in Vancouver.","la6at0E9":"^findLoc()","RU296XAu":"Do you like it there?","gACFNmO8":"^tooAdjective()","TuAp4vuR":"^usedFor()","xTqvHEHg":"^usedFor()","uJq3TZs6":"^isA()","WfHazW1g":"^isA()","5RAK9BOI":"^locatedAt()","idqVFC9e":"^colorLookup()","lg0GbksD":"^colorLookup()","CMvUCkJW":"^save(,)","NKQake8p":"^createUserFact(favorite, color, )","F3PtfyAw":"^tooAdjective()","tByjgsjn":"^resolveFact()","yQoQJ17Y":"^makeChoice()","0GByj26l":"^findMoney()","gp7pO0at":"^findDate()","mNXPk8Cj":"^aquireGoods()","TQ2dfqxA":"^createUserFact(,,)","tUwROHqX":"^createUserFact(i,,)^createUserFact(i,,)^createUserFact(,isa,)","jPhGpcW1":"^createUserFact(,isa,)","20SDkKHC":"^createUserFact(,isa,)","aPpWhv0G":"^createUserFact(,isa,)","h0IEAgIH":"^createUserFact(,like,to_play_)^createUserFact(,like,)^createUserFact(,play,)","4VxQi4yM":"^createUserFact(,have,)","ttu05IL2":"{^known(true)} Thats cool","UGqdUWK3":"{^known(false)} Who is ?","rEVmkaU1":"^save(bikerAngry, true)","BtvAbNtz":"{^hasItem(bikerAngry,true)} A big biker dude. He seems angry. Better check your wallet is safe.","qYrt5gjk":"{^hasItem(bikerAngry,false)} A big biker dude. He seems pretty chill. Maybe he knows where we are?","IB4hNrbt":"I'm not sure"},"checksums":{"./test/fixtures/reason/main.ss":"d6be39b7efa8a9b8263d3142bd6256825206b350"}} \ No newline at end of file diff --git a/test/fixtures/script/script.ss b/test/fixtures/script/script.ss index 118afe1d..fb323239 100644 --- a/test/fixtures/script/script.ss +++ b/test/fixtures/script/script.ss @@ -340,9 +340,9 @@ > topic:keep:system generic - + + __simple__ - - ^break() + - ^breakFunc() + * - no match @@ -363,4 +363,3 @@ + __B__ - ^showScope() < topic - diff --git a/test/fixtures/topicsystem/main.ss b/test/fixtures/topicsystem/main.ss index efa12b90..6a2c5e7d 100644 --- a/test/fixtures/topicsystem/main.ss +++ b/test/fixtures/topicsystem/main.ss @@ -28,7 +28,7 @@ - ^save(key, value) + force break - - ^break() + - ^breakFunc() + force continue - ^nobreak() force one @@ -38,7 +38,7 @@ < topic > topic:keep outdoors ( fishing hunting camping ) ^sometest() - + + I like to * - i like to spend time outdoors @@ -55,7 +55,7 @@ + I like to spend time * - fishing - + + I like to * - me too @@ -81,7 +81,7 @@ + test respond - {END} - + + __something__ - Something diff --git a/test/helpers.js b/test/helpers.js index 538d408d..5712fc55 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -1,16 +1,18 @@ -/* global gFacts:true, bot:true, Promise */ +import fs from 'fs'; +import _ from 'lodash'; +import async from 'async'; +import sfacts from 'sfacts'; +import parser from 'ss-parser'; -var script = require("../index"); -var sfact = require("sfacts"); -var fs = require("fs"); -var rmdir = require("rmdir"); -var async = require("async"); -var mongoose = require("mongoose"); -var mergex = require("deepmerge"); +import SuperScript from '../src/bot/index'; -var data, botData, bootstrap; +let bot; -data = [ +const getBot = function getBot() { + return bot; +}; + +const data = [ // './test/fixtures/concepts/bigrams.tbl', // Used in Reason tests // './test/fixtures/concepts/trigrams.tbl', // './test/fixtures/concepts/concepts.top', @@ -19,142 +21,101 @@ data = [ // './test/fixtures/concepts/opp.tbl' ]; -botData = [ +/* const botData = [ './test/fixtures/concepts/botfacts.tbl', - './test/fixtures/concepts/botown.tbl' -]; - -exports.bootstrap = bootstrap = function(cb) { - sfact.load(data, 'factsystem', function(err, facts){ - gFacts = facts; + './test/fixtures/concepts/botown.tbl', +];*/ + +// If you want to use data in tests, then use bootstrap +const bootstrap = function bootstrap(cb) { + sfacts.load('testFactSystem', data, false, (err, facts) => { + if (err) { + console.error(err); + } cb(null, facts); }); }; -var removeModel = function(name) { - return new Promise(function(resolve, reject){ - mongoose.connection.models[name].remove(function(error, removed) { - if(error) { - return reject(error); - } - delete mongoose.connection.models[name]; - resolve(removed); - }); - }); -}; - -exports.after = function(end) { - - var itor = function(item, next) { - fs.exists(item, function (exists) { - if (exists) { - rmdir(item, next); - } else { - next(); - } - }); - }; +const after = function after(end) { if (bot) { - bot.factSystem.db.close(function(){ - // Kill the globals - gFacts = null; + bot.factSystem.db.close(() => { + // Kill the globals and remove any fact systems bot = null; - async.each(['./factsystem', './systemDB'], itor, function(){ - Promise.all(Object.keys(mongoose.connection.models).map(removeModel)).then(function(){ - end(); - }, function(error) { - console.log(error.trace); - throw error; - }); - //mongoose.connection.models = {}; - //mongoose.connection.db.dropDatabase(); - //end(); - }); + async.each(['testFactSystem'], (item, next) => { + sfacts.clean(item, next); + }, () => end()); }); } else { end(); } - }; -var importFilePath = function(path, facts, callback) { - if(!mongoose.connection.readyState) { - mongoose.connect('mongodb://localhost/superscriptDB'); - } - var TopicSystem = require("../lib/topics/index")(mongoose, facts); - TopicSystem.importerFile(path, callback); - - // This is here in case you want to see what exactly was imported. - // TopicSystem.importerFile(path, function () { - // Topic.find({name: 'random'}, "gambits") - // .populate("gambits") - // .exec(function (err, mgambits) { - // console.log("------", err, mgambits); - // callback(); - // }); - // }); - -}; - -exports.before = function(file) { - - var options = { - scope: {} +const before = function before(file) { + const options = { + factSystem: { + name: 'testFactSystem', + clean: false, + }, }; - return function(done) { - var fileCache = './test/fixtures/cache/'+ file +'.json'; - fs.exists(fileCache, function (exists) { - + const afterParse = (fileCache, result, callback) => { + fs.exists(`${__dirname}/fixtures/cache`, (exists) => { if (!exists) { - bootstrap(function(err, facts) { - var parse = require("ss-parser")(facts); - parse.loadDirectory('./test/fixtures/' + file, function(err, result) { - options.factSystem = facts; - options.mongoose = mongoose; + fs.mkdirSync(`${__dirname}/fixtures/cache`); + } + return fs.writeFile(fileCache, JSON.stringify(result), (err) => { + if (err) { + return callback(err); + } + options.importFile = fileCache; + return SuperScript(options, (err, botInstance) => { + if (err) { + return callback(err); + } + bot = botInstance; + return callback(); + }); + }); + }); + }; - fs.writeFile(fileCache, JSON.stringify(result), function (err) { - // Load the topic file into the MongoDB - importFilePath(fileCache, facts, function() { - new script(options, function(err, botx) { - bot = botx; - done(); - }); - }); - }); + return (done) => { + const fileCache = `${__dirname}/fixtures/cache/${file}.json`; + fs.exists(fileCache, (exists) => { + if (!exists) { + bootstrap((err, factSystem) => { + parser.loadDirectory(`${__dirname}/fixtures/${file}`, { factSystem }, (err, result) => { + if (err) { + done(err); + } + afterParse(fileCache, result, done); }); }); } else { - console.log("Loading Cached Script"); - var contents = fs.readFileSync(fileCache, 'utf-8'); + console.log(`Loading cached script from ${fileCache}`); + let contents = fs.readFileSync(fileCache, 'utf-8'); contents = JSON.parse(contents); - bootstrap(function(err, facts) { - options.factSystem = facts; - options.mongoose = mongoose; - - var sums = contents.checksums; - var parse = require("ss-parser")(facts); - var start = new Date().getTime(); - var results; - - parse.loadDirectory('./test/fixtures/' + file, sums, function(err, result) { - results = mergex(contents, result); - fs.writeFile(fileCache, JSON.stringify(results), function (err) { - // facts.createUserDBWithData('botfacts', botData, function(err, botfacts){ - // options.botfacts = botfacts; - bot = null; - importFilePath(fileCache, facts, function() { - new script(options, function(err, botx) { - bot = botx; - done(); - }); // new bot - }); // import file - // }); // create user - }); // write file - }); // Load files to parse + bootstrap((err, factSystem) => { + if (err) { + done(err); + } + const checksums = contents.checksums; + parser.loadDirectory(`${__dirname}/fixtures/${file}`, { factSystem, cache: checksums }, (err, result) => { + if (err) { + done(err); + } + const results = _.merge(contents, result); + afterParse(fileCache, results, done); + }); }); } }); }; }; + +export default { + after, + before, + getBot, +}; diff --git a/test/qtype.js b/test/qtype.js index c91580de..0b56e648 100644 --- a/test/qtype.js +++ b/test/qtype.js @@ -1,52 +1,51 @@ -var mocha = require("mocha"); -var should = require("should"); -var help = require("./helpers"); +/* global describe, it, before, after */ -describe('Super Script QType Matching', function(){ +import mocha from 'mocha'; +import should from 'should'; +import helpers from './helpers'; - before(help.before("qtype")); +describe('SuperScript QType Matching', () => { + before(helpers.before('qtype')); - describe('Simple Question Matching (qSubType)', function(){ - xit("should reply to simple string", function(done) { - bot.reply("user1", "which way to the bathroom?", function(err, reply) { - reply.string.should.eql("Down the hall on the left"); + describe('Simple Question Matching (qSubType)', () => { + xit('should reply to simple string', (done) => { + helpers.getBot().reply('user1', 'which way to the bathroom?', (err, reply) => { + reply.string.should.eql('Down the hall on the left'); done(); }); }); - it("should not match", function(done) { - bot.reply("user1", "My mom cleans the bathroom.", function(err, reply) { - reply.string.should.eql(""); + it('should not match', (done) => { + helpers.getBot().reply('user1', 'My mom cleans the bathroom.', (err, reply) => { + reply.string.should.eql(''); done(); }); }); }); - describe('Advanced Question Matching (qType)', function(){ - it("should reply to QType string YN QType", function(done) { - bot.reply("user1", "Do you like to clean?", function(err, reply) { - reply.string.should.eql("a"); + describe('Advanced Question Matching (qType)', () => { + it('should reply to QType string YN QType', (done) => { + helpers.getBot().reply('user1', 'Do you like to clean?', (err, reply) => { + reply.string.should.eql('a'); done(); }); }); // HUN:ind should be ordered higher up the queue. - it("should reply to QType string B (fine grained)", function(done) { - bot.reply("user1", "Who can clean the house?", function(err, reply) { - reply.string.should.eql("a"); + it('should reply to QType string B (fine grained)', (done) => { + helpers.getBot().reply('user1', 'Who can clean the house?', (err, reply) => { + reply.string.should.eql('a'); done(); }); }); - it("should reply to QType string C", function(done) { - bot.reply("user1", "How fast can you clean?", function(err, reply) { - reply.string.should.eql("c"); + it('should reply to QType string C', (done) => { + helpers.getBot().reply('user1', 'How fast can you clean?', (err, reply) => { + reply.string.should.eql('c'); done(); }); }); - }); - after(help.after); - -}); \ No newline at end of file + after(helpers.after); +}); diff --git a/test/redirect.js b/test/redirect.js index 70c0169f..f44cd27d 100644 --- a/test/redirect.js +++ b/test/redirect.js @@ -1,141 +1,141 @@ -var mocha = require("mocha"); -var should = require("should"); -var help = require("./helpers"); +/* global describe, it, before, after */ -describe('Super Script Redirects', function(){ +import mocha from 'mocha'; +import should from 'should'; +import helpers from './helpers'; - before(help.before("redirect")); +describe('SuperScript Redirects', () => { + before(helpers.before('redirect')); - describe('Dont trim whitespace from redirect (GH-92)', function(){ - xit("this needs to work..", function(done) { - bot.reply("user1", "GitHub issue 92", function(err, reply) { - reply.string.should.eql("testing redirects one thing two thing"); + describe('Dont trim whitespace from redirect (GH-92)', () => { + xit('this needs to work..', (done) => { + helpers.getBot().reply('user1', 'GitHub issue 92', (err, reply) => { + reply.string.should.eql('testing redirects one thing two thing'); done(); }); }); }); - describe('Redirect Interface', function(){ - xit("should redirect on match", function(done) { - bot.reply("user1", "testing redirects", function(err, reply) { - reply.string.should.eql("redirect test pass"); + describe('Redirect Interface', () => { + xit('should redirect on match', (done) => { + helpers.getBot().reply('user1', 'testing redirects', (err, reply) => { + reply.string.should.eql('redirect test pass'); done(); }); }); }); - describe('Inline Redirect Interface', function(){ - it("should redirect on match", function(done) { - bot.reply("user1", "this is an inline redirect", function(err, reply) { - reply.string.should.eql("lets redirect to redirect test pass"); + describe('Inline Redirect Interface', () => { + it('should redirect on match', (done) => { + helpers.getBot().reply('user1', 'this is an inline redirect', (err, reply) => { + reply.string.should.eql('lets redirect to redirect test pass'); done(); }); }); }); - describe('Inline Redirect two message in one reply', function(){ - it("should redirect on match complex message", function(done) { - bot.reply("user1", "this is an complex redirect", function(err, reply) { - reply.string.should.eql("this game is made up of 2 teams"); + describe('Inline Redirect two message in one reply', () => { + it('should redirect on match complex message', (done) => { + helpers.getBot().reply('user1', 'this is an complex redirect', (err, reply) => { + reply.string.should.eql('this game is made up of 2 teams'); done(); }); }); }); - describe('Inline Redirect Interface nested inline redirects', function(){ - it("should redirect on match complex nested message", function(done) { - bot.reply("user1", "this is an nested redirect", function(err, reply) { - reply.string.should.eql("this message contains secrets"); + describe('Inline Redirect Interface nested inline redirects', () => { + it('should redirect on match complex nested message', (done) => { + helpers.getBot().reply('user1', 'this is an nested redirect', (err, reply) => { + reply.string.should.eql('this message contains secrets'); done(); }); }); }); - describe('Inline Redirect recurrsion!', function(){ - it("should redirect should save itself", function(done) { - bot.reply("user1", "this is a bad idea", function(err, reply) { + describe('Inline Redirect recurrsion!', () => { + it('should redirect should save itself', (done) => { + helpers.getBot().reply('user1', 'this is a bad idea', (err, reply) => { reply.string.should.not.be.empty; done(); }); }); }); - describe('Inline Redirect with function GH-81', function(){ - it("should parse function and redirect", function(done) { - bot.reply("user1", "tell me a random fact", function(err, reply) { + describe('Inline Redirect with function GH-81', () => { + it('should parse function and redirect', (done) => { + helpers.getBot().reply('user1', 'tell me a random fact', (err, reply) => { reply.string.should.not.be.empty; reply.string.should.containEql("Okay, here's a fact: one . Would you like me to tell you another fact?"); done(); }); }); - it("should parse function and redirect", function(done) { - bot.reply("user1", "tell me a random fact two", function(err, reply) { - reply.string.should.not.be.empty; - reply.string.should.containEql("Okay, here's a fact. one Would you like me to tell you another fact?"); - done(); - }); - }); + it('should parse function and redirect', (done) => { + helpers.getBot().reply('user1', 'tell me a random fact two', (err, reply) => { + reply.string.should.not.be.empty; + reply.string.should.containEql("Okay, here's a fact. one Would you like me to tell you another fact?"); + done(); + }); + }); }); - describe('Redirect to new topic', function(){ - + describe('Redirect to new topic', () => { // GH-156 - it("if redirect does not exist - don't crash", function(done) { - bot.reply("user1", "test missing topic", function(err, reply) { - reply.string.should.eql("Test OK."); + it("if redirect does not exist - don't crash", (done) => { + helpers.getBot().reply('user1', 'test missing topic', (err, reply) => { + reply.string.should.eql('Test OK.'); done(); }); }); // GH-227 - it("Missing function", function(done) { - bot.reply("user1", "issue 227", function(err, reply) { - reply.string.should.eql("oneIs it hot"); + it('Missing function', (done) => { + helpers.getBot().reply('user1', 'issue 227', (err, reply) => { + reply.string.should.eql('oneIs it hot'); done(); }); }); - it("should redirect to new topic", function(done) { - bot.reply("user1", "hello", function(err, reply) { - reply.string.should.eql("Is it hot"); + it('should redirect to new topic', (done) => { + helpers.getBot().reply('user1', 'hello', (err, reply) => { + reply.string.should.eql('Is it hot'); done(); }); }); - it("should redirect to new topic dynamically", function(done) { - bot.reply("user1", "i like school", function(err, reply) { + it('should redirect to new topic dynamically', (done) => { + helpers.getBot().reply('user1', 'i like school', (err, reply) => { reply.string.should.eql("I'm majoring in CS."); done(); }); }); - it("should redirect to new topic Inline", function(done) { - bot.reply("user1", "topic redirect test", function(err, reply) { - reply.string.should.eql("Say this. Say that."); + it('should redirect to new topic Inline', (done) => { + helpers.getBot().reply('user1', 'topic redirect test', (err, reply) => { + reply.string.should.eql('Say this. Say that.'); done(); }); }); - xit("should redirect forward capture", function(done) { - bot.reply("user1", "topic redirect to fishsticks", function(err, reply) { - reply.string.should.eql("Capture forward fishsticks"); + xit('should redirect forward capture', (done) => { + helpers.getBot().reply('user1', 'topic redirect to fishsticks', (err, reply) => { + reply.string.should.eql('Capture forward fishsticks'); done(); }); }); }); - describe('Set topic through plugin and match gambit in the topic in next reply', function(){ - it("should redirect to system topic", function(done) { - bot.reply("user1", "topic set systest", function(err, r1) { - r1.string.should.eql("Setting systest."); - bot.reply("user1", "where am I", function(err, r2) { - r2.string.should.eql("In systest."); + describe('Set topic through plugin and match gambit in the topic in next reply', () => { + it('should redirect to system topic', (done) => { + helpers.getBot().reply('user1', 'topic set systest', (err, r1) => { + r1.string.should.eql('Setting systest.'); + helpers.getBot().reply('user1', 'where am I', (err, r2) => { + r2.string.should.eql('In systest.'); done(); }); }); }); }); - after(help.after); -}); \ No newline at end of file + after(helpers.after); +}); diff --git a/test/script.js b/test/script.js index 23a9321c..920c3805 100644 --- a/test/script.js +++ b/test/script.js @@ -1,118 +1,119 @@ -var mocha = require("mocha"); -var should = require("should"); -var help = require("./helpers"); -var async = require("async"); -var Utils = require("../lib/utils"); +/* global describe, it, before, after */ -describe('SuperScript Scripting + Style Interface', function(){ - before(help.before("script")); +import mocha from 'mocha'; +import should from 'should'; +import async from 'async'; - describe('Simple star Interface *', function(){ +import helpers from './helpers'; +import Utils from '../src/bot/utils'; - it("Unscaped", function(done) { - bot.reply("user1", "+ this is unscaped", function(err, reply) { - reply.string.should.eql("This should pass"); +describe('SuperScript Scripting + Style Interface', () => { + before(helpers.before('script')); + + describe('Simple star Interface *', () => { + it('Unscaped', (done) => { + helpers.getBot().reply('user1', '+ this is unscaped', (err, reply) => { + reply.string.should.eql('This should pass'); done(); }); }); - it("should reply to simple string", function(done) { - bot.reply("user1", "This is a test", function(err, reply) { - reply.string.should.eql("Test should pass one"); + it('should reply to simple string', (done) => { + helpers.getBot().reply('user1', 'This is a test', (err, reply) => { + reply.string.should.eql('Test should pass one'); done(); }); }); - it("should match single star", function(done) { - bot.reply("user1", "Should match single star", function(err, reply) { - ["pass 1", "pass 2", "pass 3"].should.containEql(reply.string); + it('should match single star', (done) => { + helpers.getBot().reply('user1', 'Should match single star', (err, reply) => { + ['pass 1', 'pass 2', 'pass 3'].should.containEql(reply.string); done(); }); }); - it("should allow empty star - new behaviour", function(done) { - bot.reply("user1", "Should match single", function(err, reply) { - ["pass 1", "pass 2", "pass 3"].should.containEql(reply.string); + it('should allow empty star - new behaviour', (done) => { + helpers.getBot().reply('user1', 'Should match single', (err, reply) => { + ['pass 1', 'pass 2', 'pass 3'].should.containEql(reply.string); done(); }); }); - it("should match double star", function(done) { - bot.reply("user1", "Should match single star two", function(err, reply) { - ["pass 1", "pass 2", "pass 3"].should.containEql(reply.string); + it('should match double star', (done) => { + helpers.getBot().reply('user1', 'Should match single star two', (err, reply) => { + ['pass 1', 'pass 2', 'pass 3'].should.containEql(reply.string); done(); }); }); - it("capture in reply", function(done) { - bot.reply("user1", "connect the win", function(err, reply) { - reply.string.should.eql("Test should pass"); + it('capture in reply', (done) => { + helpers.getBot().reply('user1', 'connect the win', (err, reply) => { + reply.string.should.eql('Test should pass'); done(); }); }); - it("leading star", function(done) { - bot.reply("user1", "my bone", function(err, reply) { - reply.string.should.eql("win 1"); + it('leading star', (done) => { + helpers.getBot().reply('user1', 'my bone', (err, reply) => { + reply.string.should.eql('win 1'); done(); }); }); - it("trailing star", function(done) { - bot.reply("user1", "bone thug", function(err, reply) { - reply.string.should.eql("win 1"); + it('trailing star', (done) => { + helpers.getBot().reply('user1', 'bone thug', (err, reply) => { + reply.string.should.eql('win 1'); done(); }); }); - it("star star", function(done) { - bot.reply("user1", "my bone thug", function(err, reply) { - reply.string.should.eql("win 1"); + it('star star', (done) => { + helpers.getBot().reply('user1', 'my bone thug', (err, reply) => { + reply.string.should.eql('win 1'); done(); }); }); - it("star star empty", function(done) { - bot.reply("user1", "bone", function(err, reply) { - reply.string.should.eql("win 1"); + it('star star empty', (done) => { + helpers.getBot().reply('user1', 'bone', (err, reply) => { + reply.string.should.eql('win 1'); done(); }); }); - }); - describe('Exact length star interface *n', function(){ - it("should match *2 star - Zero case", function(done) { - bot.reply("user1", "It is hot out", function(err, reply) { - reply.string.should.eql(""); + describe('Exact length star interface *n', () => { + it('should match *2 star - Zero case', (done) => { + helpers.getBot().reply('user1', 'It is hot out', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - it("should match *2 star - One case", function(done) { - bot.reply("user1", "It is one hot out", function(err, reply) { - reply.string.should.eql(""); + it('should match *2 star - One case', (done) => { + helpers.getBot().reply('user1', 'It is one hot out', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - it("should match *2 star - Two case", function(done) { - bot.reply("user1", "It is one two hot out", function(err, reply) { - reply.string.should.eql("Test three should pass"); + it('should match *2 star - Two case', (done) => { + helpers.getBot().reply('user1', 'It is one two hot out', (err, reply) => { + reply.string.should.eql('Test three should pass'); done(); }); }); - it("should match *2 star - Three case", function(done) { - bot.reply("user1", "It is one two three hot out", function(err, reply) { - reply.string.should.eql(""); + it('should match *2 star - Three case', (done) => { + helpers.getBot().reply('user1', 'It is one two three hot out', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - it("should match *1 star - End case", function(done) { - bot.reply("user1", "fixedwidth define love", function(err, reply) { - reply.string.should.eql("Test endstar should pass"); + it('should match *1 star - End case', (done) => { + helpers.getBot().reply('user1', 'fixedwidth define love', (err, reply) => { + reply.string.should.eql('Test endstar should pass'); done(); }); }); @@ -120,110 +121,109 @@ describe('SuperScript Scripting + Style Interface', function(){ // min max *(1-2) - describe('Mix stars for Mix and Max', function(){ - it("min max star - Zero", function(done) { - bot.reply("user1", "min max", function(err, reply) { - reply.string.should.eql(""); + describe('Mix stars for Mix and Max', () => { + it('min max star - Zero', (done) => { + helpers.getBot().reply('user1', 'min max', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - it("min max star - one", function(done) { - bot.reply("user1", "min max one", function(err, reply) { - reply.string.should.eql("min max test"); + it('min max star - one', (done) => { + helpers.getBot().reply('user1', 'min max one', (err, reply) => { + reply.string.should.eql('min max test'); done(); }); }); - it("min max star - two", function(done) { - bot.reply("user1", "min max one two", function(err, reply) { - reply.string.should.eql("min max test"); + it('min max star - two', (done) => { + helpers.getBot().reply('user1', 'min max one two', (err, reply) => { + reply.string.should.eql('min max test'); done(); }); }); - it("min max star - three", function(done) { - bot.reply("user1", "min max one two three", function(err, reply) { - reply.string.should.eql(""); + it('min max star - three', (done) => { + helpers.getBot().reply('user1', 'min max one two three', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - it("min max star ~emo - gh-221", function(done) { - bot.reply("user1", "hello test test", function(err, reply) { - reply.string.should.eql("emo reply"); + it('min max star ~emo - gh-221', (done) => { + helpers.getBot().reply('user1', 'hello test test', (err, reply) => { + reply.string.should.eql('emo reply'); done(); }); }); - it.skip("min max star - four", function(done) { - bot.reply("user1", "test one. two. three.", function(err, reply) { - reply.string.should.eql("test one. two. three."); + it.skip('min max star - four', (done) => { + helpers.getBot().reply('user1', 'test one. two. three.', (err, reply) => { + reply.string.should.eql('test one. two. three.'); done(); }); }); - }); - describe('Variable length star interface *~n', function() { - it("should match *~2 star - End case", function(done) { - bot.reply("user1", "define love", function(err, reply) { - reply.string.should.eql("Test endstar should pass"); + describe('Variable length star interface *~n', () => { + it('should match *~2 star - End case', (done) => { + helpers.getBot().reply('user1', 'define love', (err, reply) => { + reply.string.should.eql('Test endstar should pass'); done(); }); }); - it("should match *~2 star - Empty", function(done) { - bot.reply("user1", "var length", function(err, reply) { - ["pass 1"].should.containEql(reply.string); + it('should match *~2 star - Empty', (done) => { + helpers.getBot().reply('user1', 'var length', (err, reply) => { + ['pass 1'].should.containEql(reply.string); done(); }); }); - it("should match *~2 star - Zero Star", function(done) { - bot.reply("user1", "It is hot out 2", function(err, reply) { - ["pass 1","pass 2","pass 3"].should.containEql(reply.string); + it('should match *~2 star - Zero Star', (done) => { + helpers.getBot().reply('user1', 'It is hot out 2', (err, reply) => { + ['pass 1', 'pass 2', 'pass 3'].should.containEql(reply.string); done(); }); }); - it("should match *~2 star - One Star", function(done) { - bot.reply("user1", "It is a hot out 2", function(err, reply) { - ["pass 1","pass 2","pass 3"].should.containEql(reply.string); + it('should match *~2 star - One Star', (done) => { + helpers.getBot().reply('user1', 'It is a hot out 2', (err, reply) => { + ['pass 1', 'pass 2', 'pass 3'].should.containEql(reply.string); done(); }); }); - it("should match *~2 star - Two Star", function(done) { - bot.reply("user1", "It is a b hot out 2", function(err, reply) { - ["pass 1","pass 2","pass 3"].should.containEql(reply.string); + it('should match *~2 star - Two Star', (done) => { + helpers.getBot().reply('user1', 'It is a b hot out 2', (err, reply) => { + ['pass 1', 'pass 2', 'pass 3'].should.containEql(reply.string); done(); }); }); - it("should match *~2 star - Three Star (fail)", function(done) { - bot.reply("user1", "It is a b c d hot out2", function(err, reply) { - reply.string.should.eql(""); + it('should match *~2 star - Three Star (fail)', (done) => { + helpers.getBot().reply('user1', 'It is a b c d hot out2', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - it("should match *~2 star - Return the resuling Star", function(done) { - bot.reply("user1", "It is foo bar cold out", function(err, reply) { - reply.string.should.eql("Two star result foo bar"); + it('should match *~2 star - Return the resuling Star', (done) => { + helpers.getBot().reply('user1', 'It is foo bar cold out', (err, reply) => { + reply.string.should.eql('Two star result foo bar'); done(); }); }); }); - describe('Replies can be repeated accross triggers', function(){ - it("Replies accross trigger should pass", function(done) { - bot.reply("user1", "trigger one", function(err, reply) { - reply.string.should.eql("generic reply"); - bot.reply("user1", "trigger two", function(err, reply) { - reply.string.should.eql("generic reply"); + describe('Replies can be repeated accross triggers', () => { + it('Replies accross trigger should pass', (done) => { + helpers.getBot().reply('user1', 'trigger one', (err, reply) => { + reply.string.should.eql('generic reply'); + helpers.getBot().reply('user1', 'trigger two', (err, reply) => { + reply.string.should.eql('generic reply'); done(); }); }); @@ -232,295 +232,285 @@ describe('SuperScript Scripting + Style Interface', function(){ // We exausted this reply in the last test. // NB: this test will fail if run on its own. // We lost this functionality when we started walking the tree. - it.skip("Should pass 2", function(done) { - bot.reply("user1", "trigger one", function(err, reply) { - reply.string.should.eql(""); + it.skip('Should pass 2', (done) => { + helpers.getBot().reply('user1', 'trigger one', (err, reply) => { + reply.string.should.eql(''); done(); }); }); }); - describe('Alternates Interface (a|b)', function() { - it("should match a or b - Not empty", function(done) { - bot.reply("user1", "what is it", function(err, reply) { - reply.string.should.eql(""); + describe('Alternates Interface (a|b)', () => { + it('should match a or b - Not empty', (done) => { + helpers.getBot().reply('user1', 'what is it', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - it("should match a or b - should be A", function(done) { - bot.reply("user1", "what day is it", function(err, reply) { - reply.string.should.eql("Test four should pass"); + it('should match a or b - should be A', (done) => { + helpers.getBot().reply('user1', 'what day is it', (err, reply) => { + reply.string.should.eql('Test four should pass'); done(); }); }); - it("should match a or b - should be B", function(done) { - bot.reply("user1", "what week is it", function(err, reply) { - reply.string.should.eql("Test four should pass"); + it('should match a or b - should be B', (done) => { + helpers.getBot().reply('user1', 'what week is it', (err, reply) => { + reply.string.should.eql('Test four should pass'); done(); }); }); - it("should match a or b - word boundries A", function(done) { - bot.reply("user1", "what weekend is it", function(err, reply) { - reply.string.should.eql(""); + it('should match a or b - word boundries A', (done) => { + helpers.getBot().reply('user1', 'what weekend is it', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - it("should match a or b - word boundries B", function(done) { - bot.reply("user1", "this or that", function(err, reply) { - reply.string.should.eql("alter boundry test"); + it('should match a or b - word boundries B', (done) => { + helpers.getBot().reply('user1', 'this or that', (err, reply) => { + reply.string.should.eql('alter boundry test'); done(); }); }); - it("should match a or b - word boundries C", function(done) { - bot.reply("user1", "favorite", function(err, reply) { - reply.string.should.eql(""); + it('should match a or b - word boundries C', (done) => { + helpers.getBot().reply('user1', 'favorite', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - it("should match a or b - word boundries D", function(done) { - bot.reply("user1", "this a should e", function(err, reply) { - reply.string.should.eql("alter boundry test 2"); + it('should match a or b - word boundries D', (done) => { + helpers.getBot().reply('user1', 'this a should e', (err, reply) => { + reply.string.should.eql('alter boundry test 2'); done(); }); }); - }); - describe('Optionals Interface [a|b|c]', function(){ - it("should match empty case", function(done) { - bot.reply("user1", "i have a car", function(err, reply) { - reply.string.should.eql("Test five should pass"); + describe('Optionals Interface [a|b|c]', () => { + it('should match empty case', (done) => { + helpers.getBot().reply('user1', 'i have a car', (err, reply) => { + reply.string.should.eql('Test five should pass'); done(); }); }); - it("should match a", function(done) { - bot.reply("user1", "i have a red car", function(err, reply) { - reply.string.should.eql("Test five should pass"); + it('should match a', (done) => { + helpers.getBot().reply('user1', 'i have a red car', (err, reply) => { + reply.string.should.eql('Test five should pass'); done(); }); }); - it("should match b", function(done) { - bot.reply("user1", "i have a blue car", function(err, reply) { - reply.string.should.eql("Test five should pass"); + it('should match b', (done) => { + helpers.getBot().reply('user1', 'i have a blue car', (err, reply) => { + reply.string.should.eql('Test five should pass'); done(); }); }); - it("should match c", function(done) { - bot.reply("user1", "i have a green car", function(err, reply) { - reply.string.should.eql("Test five should pass"); + it('should match c', (done) => { + helpers.getBot().reply('user1', 'i have a green car', (err, reply) => { + reply.string.should.eql('Test five should pass'); done(); }); }); - it("should not match d", function(done) { - bot.reply("user1", "i have a black car", function(err, reply) { - reply.string.should.eql(""); + it('should not match d', (done) => { + helpers.getBot().reply('user1', 'i have a black car', (err, reply) => { + reply.string.should.eql(''); done(); }); }); }); - describe('Expand with WordNet', function() { - it("should reply to simple string", function(done) { - bot.reply("user1", "I love shoes", function(err, reply) { - reply.string.should.eql("Wordnet test one"); + describe('Expand with WordNet', () => { + it('should reply to simple string', (done) => { + helpers.getBot().reply('user1', 'I love shoes', (err, reply) => { + reply.string.should.eql('Wordnet test one'); done(); }); }); - it("should not expand user-defined concepts greedly (word boundry protection)", function(done) { - bot.reply("user1", "I love ballball", function(err, reply) { - reply.string.should.eql(""); + it('should not expand user-defined concepts greedly (word boundry protection)', (done) => { + helpers.getBot().reply('user1', 'I love ballball', (err, reply) => { + reply.string.should.eql(''); done(); }); }); // This works, but I dont like having to import the DB every time - it.skip("should expand user-defined concepts too", function(done) { - bot.reply("user1", "I love basketball", function(err, reply) { - reply.string.should.eql("Term expanded"); + it.skip('should expand user-defined concepts too', (done) => { + helpers.getBot().reply('user1', 'I love basketball', (err, reply) => { + reply.string.should.eql('Term expanded'); done(); }); }); // To match lemma version of wordnet expanded terms, make sure the whole line is lemmed. - it.skip("should match both text and lemma", function(done) { - bot.reply("user1", "My brother is fat", function(err, reply) { - reply.string.should.eql("Ouch"); - bot.reply("user1", "My brothers is fat", function(err, reply) { - reply.string.should.eql("Ouch"); + it.skip('should match both text and lemma', (done) => { + helpers.getBot().reply('user1', 'My brother is fat', (err, reply) => { + reply.string.should.eql('Ouch'); + helpers.getBot().reply('user1', 'My brothers is fat', (err, reply) => { + reply.string.should.eql('Ouch'); done(); }); - }); }); - }); - describe('Replies can have Optionals too!', function(){ - it("replies with optionals", function(done) { - bot.reply("user1", "this reply is random", function(err, reply) { - ["yes this reply is awesome","yes this reply is random"].should.containEql(reply.string); + describe('Replies can have Optionals too!', () => { + it('replies with optionals', (done) => { + helpers.getBot().reply('user1', 'this reply is random', (err, reply) => { + ['yes this reply is awesome', 'yes this reply is random'].should.containEql(reply.string); done(); }); }); - it("replies with wordnet", function(done) { - bot.reply("user1", "reply with wordnet", function(err, reply) { - ["i cotton people","i prefer people", "i care for people", "i love people", "i please people"].should.containEql(reply.string); + it('replies with wordnet', (done) => { + helpers.getBot().reply('user1', 'reply with wordnet', (err, reply) => { + ['i cotton people', 'i prefer people', 'i care for people', 'i love people', 'i please people'].should.containEql(reply.string); done(); }); }); }); - describe('Sub-Replies', function(){ - it("Sub replies 1", function(done) { - bot.reply("user1", "redirect_rainbow", function(err, reply) { - - var r = { string: 'red', + describe('Sub-Replies', () => { + it('Sub replies 1', (done) => { + helpers.getBot().reply('user1', 'redirect_rainbow', (err, reply) => { + const r = { string: 'red', topicName: 'rainbow', subReplies: - [ { delay: '500', string: 'orange' }, + [{ delay: '500', string: 'orange' }, { delay: '500', string: 'yellow' }, { delay: '500', string: 'green' }, { delay: '500', string: 'blue' }, - { delay: '500', string: 'and black?' } ] }; + { delay: '500', string: 'and black?' }] }; reply.should.containDeep(r); done(); }); }); - it("Sub replies 2", function(done) { - bot.reply("user1", "how many colors in the rainbow", function(err, reply) { - - var r = { string: '', + it('Sub replies 2', (done) => { + helpers.getBot().reply('user1', 'how many colors in the rainbow', (err, reply) => { + const r = { string: '', topicName: 'rainbow', subReplies: - [ { delay: '500', string: 'lots' } ] }; + [{ delay: '500', string: 'lots' }] }; reply.should.containDeep(r); done(); }); }); - - }); - describe('Custom functions', function(){ - - it("should call a custom function with hyphen", function(done) { - bot.reply("user1", "error with function thirty-two", function(err, reply) { - reply.string.should.eql("thirty-two"); + describe('Custom functions', () => { + it('should call a custom function with hyphen', (done) => { + helpers.getBot().reply('user1', 'error with function thirty-two', (err, reply) => { + reply.string.should.eql('thirty-two'); done(); }); }); - it("should call a custom function", function(done) { - bot.reply("user1", "custom function", function(err, reply) { - reply.string.should.eql("The Definition of function is perform duties attached to a particular office or place or function"); + it('should call a custom function', (done) => { + helpers.getBot().reply('user1', 'custom function', (err, reply) => { + reply.string.should.eql('The Definition of function is perform duties attached to a particular office or place or function'); done(); }); }); - it("should continue if error is passed into callback", function(done) { - bot.reply("user1", "custom 3 function", function(err, reply) { - reply.string.should.eql("backup plan"); + it('should continue if error is passed into callback', (done) => { + helpers.getBot().reply('user1', 'custom 3 function', (err, reply) => { + reply.string.should.eql('backup plan'); done(); }); }); - it("pass a param into custom function", function(done) { - bot.reply("user1", "custom 5 function", function(err, reply) { - reply.string.should.eql("he likes this"); + it('pass a param into custom function', (done) => { + helpers.getBot().reply('user1', 'custom 5 function', (err, reply) => { + reply.string.should.eql('he likes this'); done(); }); }); - it("pass a param into custom function1", function(done) { - bot.reply("user1", "custom 6 function", function(err, reply) { - ["he cottons this","he prefers this", "he cares for this", "he loves this", "he pleases this"].should.containEql(reply.string); + it('pass a param into custom function1', (done) => { + helpers.getBot().reply('user1', 'custom 6 function', (err, reply) => { + ['he cottons this', 'he prefers this', 'he cares for this', 'he loves this', 'he pleases this'].should.containEql(reply.string); done(); }); }); - it("the same function twice with different params", function(done) { - bot.reply("user1", "custom 8 function", function(err, reply) { - reply.string.should.eql("4 + 3 = 7"); + it('the same function twice with different params', (done) => { + helpers.getBot().reply('user1', 'custom 8 function', (err, reply) => { + reply.string.should.eql('4 + 3 = 7'); done(); }); }); - it("should not freak out if function does not exist", function(done) { - bot.reply("user1", "custom4 function", function(err, reply) { - reply.string.should.eql("one + one = 2"); + it('should not freak out if function does not exist', (done) => { + helpers.getBot().reply('user1', 'custom4 function', (err, reply) => { + reply.string.should.eql('one + one = 2'); done(); }); }); - it("function in multi-line reply", function(done) { - bot.reply("user1", "custom9 function", function(err, reply) { - reply.string.should.eql("a\nb\none\n\nmore"); + it('function in multi-line reply', (done) => { + helpers.getBot().reply('user1', 'custom9 function', (err, reply) => { + reply.string.should.eql('a\nb\none\n\nmore'); done(); }); }); - }); // I moved this to 5 times because there was a odd chance that we could hit the keep message 2/3rds of the time - describe('Reply Flags', function() { - - it("Keep Flag 2", function(done) { - bot.reply("user1", "reply flags 2", function(err, reply) { - reply.string.should.eql("keep this"); - bot.reply("user1", "reply flags 2", function(err, reply) { - reply.string.should.eql("keep this"); + describe('Reply Flags', () => { + it('Keep Flag 2', (done) => { + helpers.getBot().reply('user1', 'reply flags 2', (err, reply) => { + reply.string.should.eql('keep this'); + helpers.getBot().reply('user1', 'reply flags 2', (err, reply) => { + reply.string.should.eql('keep this'); done(); }); }); }); }); - describe('Custom functions 2 - plugin related', function(){ - it("Alpha Length 1", function(done) { - bot.reply("user1", "How many characters in the word socks?", function(err, reply) { - reply.string.should.eql("5"); + describe('Custom functions 2 - plugin related', () => { + it('Alpha Length 1', (done) => { + helpers.getBot().reply('user1', 'How many characters in the word socks?', (err, reply) => { + reply.string.should.eql('5'); done(); }); }); - it("Alpha Length 2", function(done) { - bot.reply("user1", "How many characters in the name Bill?", function(err, reply) { - reply.string.should.eql("4"); + it('Alpha Length 2', (done) => { + helpers.getBot().reply('user1', 'How many characters in the name Bill?', (err, reply) => { + reply.string.should.eql('4'); done(); }); }); - it("Alpha Length 3", function(done) { - bot.reply("user1", "How many characters in the Alphabet?", function(err, reply) { - reply.string.should.eql("26"); + it('Alpha Length 3', (done) => { + helpers.getBot().reply('user1', 'How many characters in the Alphabet?', (err, reply) => { + reply.string.should.eql('26'); done(); }); }); - it("Alpha Length 4", function(done) { - bot.reply("suser1", "blank", function(err, reply) { - bot.getUser("suser1", function(err, u){ - u.setVar("name", "Bill", function(){ - bot.reply("suser1", "How many characters in my name?", function(err, reply) { - reply.string.should.eql("There are 4 letters in your name."); + it('Alpha Length 4', (done) => { + helpers.getBot().reply('suser1', 'blank', (err, reply) => { + helpers.getBot().getUser('suser1', (err, u) => { + u.setVar('name', 'Bill', () => { + helpers.getBot().reply('suser1', 'How many characters in my name?', (err, reply) => { + reply.string.should.eql('There are 4 letters in your name.'); done(); }); }); @@ -528,58 +518,57 @@ describe('SuperScript Scripting + Style Interface', function(){ }); }); - it("Alpha Lookup 1", function(done) { - bot.reply("user1", "What letter comes after B", function(err, reply) { - reply.string.should.eql("C"); + it('Alpha Lookup 1', (done) => { + helpers.getBot().reply('user1', 'What letter comes after B', (err, reply) => { + reply.string.should.eql('C'); done(); }); }); - it("Alpha Lookup 2", function(done) { - bot.reply("user1", "What letter comes before Z", function(err, reply) { - reply.string.should.eql("Y"); + it('Alpha Lookup 2', (done) => { + helpers.getBot().reply('user1', 'What letter comes before Z', (err, reply) => { + reply.string.should.eql('Y'); done(); }); }); - it("Alpha Lookup 3", function(done) { - bot.reply("user1", "What is the last letter in the alphabet?", function(err, reply) { - reply.string.should.eql("It is Z."); + it('Alpha Lookup 3', (done) => { + helpers.getBot().reply('user1', 'What is the last letter in the alphabet?', (err, reply) => { + reply.string.should.eql('It is Z.'); done(); }); }); - it("Alpha Lookup 4", function(done) { - bot.reply("user1", "What is the first letter of the alphabet?", function(err, reply) { - reply.string.should.eql("It is A."); + it('Alpha Lookup 4', (done) => { + helpers.getBot().reply('user1', 'What is the first letter of the alphabet?', (err, reply) => { + reply.string.should.eql('It is A.'); done(); }); }); - }); - describe('Custom functions 3 - user fact system', function(){ - it("Should save and recall 1", function(done) { - bot.reply("userX", "My name is Bob", function(err, reply) { - reply.string.should.eql("Hi Bob."); - bot.getUser("userX", function(err, u1){ - u1.getVar('name', function(err, name){ - name.should.eql("Bob"); + describe('Custom functions 3 - user fact system', () => { + it('Should save and recall 1', (done) => { + helpers.getBot().reply('userX', 'My name is Bob', (err, reply) => { + reply.string.should.eql('Hi Bob.'); + helpers.getBot().getUser('userX', (err, u1) => { + u1.getVar('name', (err, name) => { + name.should.eql('Bob'); done(); }); }); }); }); - it("Should save and recall 2", function(done) { - bot.reply("suser2", "My name is Ken", function(err, reply) { - reply.string.should.eql("Hi Ken."); - bot.getUser("userX", function(err, u1){ - bot.getUser("suser2", function(err, u2){ - u1.getVar("name", function(err, res){ - res.should.eql("Bob"); - u2.getVar("name", function(err, res){ - res.should.eql("Ken"); + it('Should save and recall 2', (done) => { + helpers.getBot().reply('suser2', 'My name is Ken', (err, reply) => { + reply.string.should.eql('Hi Ken.'); + helpers.getBot().getUser('userX', (err, u1) => { + helpers.getBot().getUser('suser2', (err, u2) => { + u1.getVar('name', (err, res) => { + res.should.eql('Bob'); + u2.getVar('name', (err, res) => { + res.should.eql('Ken'); done(); }); }); @@ -587,25 +576,24 @@ describe('SuperScript Scripting + Style Interface', function(){ }); }); }); - }); - describe.skip('Custom functions 4 - user topic change', function(){ - it("Change topic", function(done) { - bot.reply("user3", "call function with new topic", function(err, reply) { - bot.reply("user3", "i like fish", function(err, reply) { - reply.string.should.eql("me too"); + describe.skip('Custom functions 4 - user topic change', () => { + it('Change topic', (done) => { + helpers.getBot().reply('user3', 'call function with new topic', (err, reply) => { + helpers.getBot().reply('user3', 'i like fish', (err, reply) => { + reply.string.should.eql('me too'); done(); }); }); }); - it("Change topic 2", function(done) { - bot.reply("user4", "reply with a new topic from function", function(err, reply) { - bot.getUser("user4", function(err, user){ - user.currentTopic.should.eql("fish"); - bot.reply("user4", "i like fish", function(err, reply) { - reply.string.should.eql("me too"); + it('Change topic 2', (done) => { + helpers.getBot().reply('user4', 'reply with a new topic from function', (err, reply) => { + helpers.getBot().getUser('user4', (err, user) => { + user.currentTopic.should.eql('fish'); + helpers.getBot().reply('user4', 'i like fish', (err, reply) => { + reply.string.should.eql('me too'); done(); }); }); @@ -614,290 +602,283 @@ describe('SuperScript Scripting + Style Interface', function(){ }); - describe('Filter functions', function(){ - it("Trigger function", function(done) { - bot.reply("scuser5", "trigger filter function", function(err, reply) { - reply.string.should.eql(""); - bot.reply("scuser5", "trigger filler function", function(err, reply) { - reply.string.should.eql("trigger filter reply"); + describe('Filter functions', () => { + it('Trigger function', (done) => { + helpers.getBot().reply('scuser5', 'trigger filter function', (err, reply) => { + reply.string.should.eql(''); + helpers.getBot().reply('scuser5', 'trigger filler function', (err, reply) => { + reply.string.should.eql('trigger filter reply'); done(); }); }); }); }); - describe('Should parse subfolder', function(){ - it("Item in folder should exist", function(done) { - bot.topicSystem.topic.findOne({name:'suba'}, function(e,res){ + describe('Should parse subfolder', () => { + it('Item in folder should exist', (done) => { + helpers.getBot().chatSystem.Topic.findOne({ name: 'suba' }, (e, res) => { res.should.not.be.false; done(); }); }); }); - describe('Emo reply', function(){ - it("Emo Hello 1", function(done) { - bot.reply("user1", "Hello", function(err, reply) { - reply.string.should.eql("Hello"); + describe('Emo reply', () => { + it('Emo Hello 1', (done) => { + helpers.getBot().reply('user1', 'Hello', (err, reply) => { + reply.string.should.eql('Hello'); done(); }); }); }); - describe('Filter on Replies', function(){ - it("should save knowledge", function(done) { - bot.reply("r1user1", "okay my name is Adam.", function(err, reply) { - reply.string.should.containEql("Nice to meet you, Adam."); - bot.reply("r1user1", "okay my name is Adam.", function(err, reply1) { - reply1.string.should.containEql("I know, you already told me your name."); + describe('Filter on Replies', () => { + it('should save knowledge', (done) => { + helpers.getBot().reply('r1user1', 'okay my name is Adam.', (err, reply) => { + reply.string.should.containEql('Nice to meet you, Adam.'); + helpers.getBot().reply('r1user1', 'okay my name is Adam.', (err, reply1) => { + reply1.string.should.containEql('I know, you already told me your name.'); done(); }); }); }); }); - describe('Augment reply Object', function(){ - it("Should have replyProp", function(done) { - bot.reply("user1", "Can you smile?", function(err, reply) { - reply.string.should.eql("Sure can."); - reply.emoji.should.eql("smile"); + describe('Augment reply Object', () => { + it('Should have replyProp', (done) => { + helpers.getBot().reply('user1', 'Can you smile?', (err, reply) => { + reply.string.should.eql('Sure can.'); + reply.emoji.should.eql('smile'); done(); }); }); - it("Augment callback 1", function(done) { - bot.reply("user1", "object param one", function(err, reply) { - reply.string.should.eql("world"); - reply.attachments.should.eql([ { text: 'Optional text that appears *within* the attachment' } ]); + it('Augment callback 1', (done) => { + helpers.getBot().reply('user1', 'object param one', (err, reply) => { + reply.string.should.eql('world'); + reply.attachments.should.eql([{ text: 'Optional text that appears *within* the attachment' }]); done(); }); }); - it("Augment callback 2", function(done) { - bot.reply("user1", "object param two", function(err, reply) { - reply.string.should.eql("world"); - reply.foo.should.eql("bar"); + it('Augment callback 2', (done) => { + helpers.getBot().reply('user1', 'object param two', (err, reply) => { + reply.string.should.eql('world'); + reply.foo.should.eql('bar'); done(); }); }); // Params though redirects & Merge - it("Augment callback 3", function(done) { - bot.reply("user1", "object param three", function(err, reply) { - reply.string.should.eql("world"); - reply.foo.should.eql("bar"); - reply.attachments.should.eql([ { text: 'Optional text that appears *within* the attachment' } ]); + it('Augment callback 3', (done) => { + helpers.getBot().reply('user1', 'object param three', (err, reply) => { + reply.string.should.eql('world'); + reply.foo.should.eql('bar'); + reply.attachments.should.eql([{ text: 'Optional text that appears *within* the attachment' }]); done(); }); }); - }); - describe('Create Gambit Helper', function(){ - it("contains concept", function(done) { - bot.reply("user1", "my husband likes fish", function(err, reply) { + describe('Create Gambit Helper', () => { + it('contains concept', (done) => { + helpers.getBot().reply('user1', 'my husband likes fish', (err, reply) => { done(); }); }); }); - describe('Wrapping lines', function(){ - it("should continue onto the next line", function(done){ - bot.reply("user1", "tell me a poem", function(err, reply) { - reply.string.should.eql("Little Miss Muffit sat on her tuffet,\nIn a nonchalant sort of way.\nWith her forcefield around her,\nThe Spider, the bounder,\nIs not in the picture today."); + describe('Wrapping lines', () => { + it('should continue onto the next line', (done) => { + helpers.getBot().reply('user1', 'tell me a poem', (err, reply) => { + reply.string.should.eql('Little Miss Muffit sat on her tuffet,\nIn a nonchalant sort of way.\nWith her forcefield around her,\nThe Spider, the bounder,\nIs not in the picture today.'); done(); }); }); }); - describe('Normalize Trigger', function(){ - it("should be expanded before trying to match", function(done){ - bot.reply("user1", "it is all good in the hood", function(err, reply) { - reply.string.should.eql("normalize trigger test"); + describe('Normalize Trigger', () => { + it('should be expanded before trying to match', (done) => { + helpers.getBot().reply('user1', 'it is all good in the hood', (err, reply) => { + reply.string.should.eql('normalize trigger test'); done(); }); }); - it("should be expanded before trying to match contract form", function(done){ - bot.reply("user1", "it's all good in the hood two", function(err, reply) { - reply.string.should.eql("normalize trigger test"); + it('should be expanded before trying to match contract form', (done) => { + helpers.getBot().reply('user1', "it's all good in the hood two", (err, reply) => { + reply.string.should.eql('normalize trigger test'); done(); }); }); - it("message should exist after normalize", function(done){ - bot.reply("user1", "then", function(err, reply) { - reply.string.should.eql(""); + it('message should exist after normalize', (done) => { + helpers.getBot().reply('user1', 'then', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - }); - describe('Mix case test', function(){ - it("should match all capitals", function(done){ - bot.reply("user1", "this is all capitals", function(err, reply) { - reply.string.should.eql("Test six should pass"); + describe('Mix case test', () => { + it('should match all capitals', (done) => { + helpers.getBot().reply('user1', 'this is all capitals', (err, reply) => { + reply.string.should.eql('Test six should pass'); done(); }); }); - it("should match some capitals", function(done){ - bot.reply("user1", "this IS ALL capitals", function(err, reply) { - reply.string.should.eql("Test six should pass"); + it('should match some capitals', (done) => { + helpers.getBot().reply('user1', 'this IS ALL capitals', (err, reply) => { + reply.string.should.eql('Test six should pass'); done(); }); }); - it("should match with or without puct - 1", function(done){ - bot.reply("user1", "Do you have a clue?", function(err, reply) { - reply.string.should.eql("Test seven should pass"); + it('should match with or without puct - 1', (done) => { + helpers.getBot().reply('user1', 'Do you have a clue?', (err, reply) => { + reply.string.should.eql('Test seven should pass'); done(); }); }); - it("should match with or without puct - 2", function(done){ - bot.reply("user1", "Do you have a cause", function(err, reply) { - reply.string.should.eql("Test seven should pass"); + it('should match with or without puct - 2', (done) => { + helpers.getBot().reply('user1', 'Do you have a cause', (err, reply) => { + reply.string.should.eql('Test seven should pass'); done(); }); }); - it("should match with extra spaces mixed in", function(done){ - bot.reply("user1", "Do you have a condition", function(err, reply) { - reply.string.should.eql("Test seven should pass"); + it('should match with extra spaces mixed in', (done) => { + helpers.getBot().reply('user1', 'Do you have a condition', (err, reply) => { + reply.string.should.eql('Test seven should pass'); done(); }); }); - it("should allow spaces at the end of replies", function(done){ - bot.reply("user1", "spaced out", function(err, reply) { - reply.string.should.eql("note the space "); + it('should allow spaces at the end of replies', (done) => { + helpers.getBot().reply('user1', 'spaced out', (err, reply) => { + reply.string.should.eql('note the space '); done(); }); }); - }); - describe('Style - burst related', function(){ - it("should removed bursted commas", function(done){ - bot.reply("user1", "John is older than Mary, and Mary is older than Sarah", function(err, reply) { - reply.string.should.eql("Test eight should pass"); + describe('Style - burst related', () => { + it('should removed bursted commas', (done) => { + helpers.getBot().reply('user1', 'John is older than Mary, and Mary is older than Sarah', (err, reply) => { + reply.string.should.eql('Test eight should pass'); done(); }); }); - it("should removed bursted commas 2", function(done){ - bot.reply("user1", "Is it morning, noon, night?", function(err, reply) { - reply.string.should.eql("Test nine should pass"); + it('should removed bursted commas 2', (done) => { + helpers.getBot().reply('user1', 'Is it morning, noon, night?', (err, reply) => { + reply.string.should.eql('Test nine should pass'); done(); }); }); - it("should removed quotes", function(done){ - bot.reply("user1", 'remove quotes around "car"?', function(err, reply) { - reply.string.should.eql("Test ten should pass"); + it('should removed quotes', (done) => { + helpers.getBot().reply('user1', 'remove quotes around "car"?', (err, reply) => { + reply.string.should.eql('Test ten should pass'); done(); }); }); - it("should keep reply quotes", function(done){ - bot.reply("user1", "reply quotes", function(err, reply) { + it('should keep reply quotes', (done) => { + helpers.getBot().reply('user1', 'reply quotes', (err, reply) => { reply.string.should.eql('Test "eleven" should pass'); done(); }); }); - it("dont burst urls", function(done){ - Utils.sentenceSplit("should not burst http://google.com").should.have.length(1); - Utils.sentenceSplit("should not burst 19bdnznUXdHEOlp0Pnp9JY0rug6VuA2R3zK4AACdFzhE").should.have.length(1); - Utils.sentenceSplit("burst test should pass rob@silentrob.me").should.have.length(1); + it('dont burst urls', (done) => { + Utils.sentenceSplit('should not burst http://google.com').should.have.length(1); + Utils.sentenceSplit('should not burst 19bdnznUXdHEOlp0Pnp9JY0rug6VuA2R3zK4AACdFzhE').should.have.length(1); + Utils.sentenceSplit('burst test should pass rob@silentrob.me').should.have.length(1); done(); }); }); - describe('Keep the current topic when a special topic is matched', function(){ - it("Should redirect to the first gambit", function(done) { - bot.reply("user1", "first flow match", function(err, reply) { - reply.string.should.eql("You are in the first reply."); + describe('Keep the current topic when a special topic is matched', () => { + it('Should redirect to the first gambit', (done) => { + helpers.getBot().reply('user1', 'first flow match', (err, reply) => { + reply.string.should.eql('You are in the first reply.'); - bot.reply("user1", "second flow match", function(err, reply) { - reply.string.should.eql("You are in the second reply. You are in the first reply."); + helpers.getBot().reply('user1', 'second flow match', (err, reply) => { + reply.string.should.eql('You are in the second reply. You are in the first reply.'); done(); }); }); }); - it("Should redirect to the first gambit after matching __pre__", function(done) { - bot.reply("user1", "first flow match", function(err, reply) { - reply.string.should.eql("You are in the first reply."); + it('Should redirect to the first gambit after matching __pre__', (done) => { + helpers.getBot().reply('user1', 'first flow match', (err, reply) => { + reply.string.should.eql('You are in the first reply.'); - bot.reply("user1", "flow redirection test", function(err, reply) { - reply.string.should.eql("Going back. You are in the first reply."); + helpers.getBot().reply('user1', 'flow redirection test', (err, reply) => { + reply.string.should.eql('Going back. You are in the first reply.'); done(); }); }); }); }); - describe("gh-173", function(){ - it("should keep topic though sequence", function(done){ - bot.reply("user1", "name", function(err, reply) { - reply.string.should.eql("What is your first name?"); - reply.topicName.should.eql("set_name"); + describe('gh-173', () => { + it('should keep topic though sequence', (done) => { + helpers.getBot().reply('user1', 'name', (err, reply) => { + reply.string.should.eql('What is your first name?'); + reply.topicName.should.eql('set_name'); - bot.reply("user1", "Bob", function(err, reply) { - reply.topicName.should.eql("set_name"); - reply.string.should.eql("Ok Bob, what is your last name?"); + helpers.getBot().reply('user1', 'Bob', (err, reply) => { + reply.topicName.should.eql('set_name'); + reply.string.should.eql('Ok Bob, what is your last name?'); - bot.reply("user1", "Hope", function(err, reply) { + helpers.getBot().reply('user1', 'Hope', (err, reply) => { // this is where we FOUND the reply - reply.topicName.should.eql("set_name"); + reply.topicName.should.eql('set_name'); // the new topic (pending topic should now be random) - bot.getUser("user1", function(err, user){ - user.getTopic().should.eql("random"); + helpers.getBot().getUser('user1', (err, user) => { + user.getTopic().should.eql('random'); done(); }); }); - }); }); }); }); - describe("scope creep!", function(){ - - it("pass scope into redirect", function(done) { - bot.reply("user1", "scope though redirect", function(err, reply) { + describe('scope creep!', () => { + it('pass scope into redirect', (done) => { + helpers.getBot().reply('user1', 'scope though redirect', (err, reply) => { reply.string.should.eql('A user1 __B__'); done(); }, { - key: "A" + key: 'A', }); }); - it("dont leak scope", function(done) { - + it('dont leak scope', (done) => { async.parallel([ - function(callback){ - bot.reply("userA", "generic message", function(err, reply) { - callback(null, reply.string); - }, { - key: "A" - }); - - }, - function(callback){ - bot.reply("userB", "generic message two", function(err, reply) { - callback(null, reply.string); - }, { - key: "B" - }); - } + function (callback) { + helpers.getBot().reply('userA', 'generic message', (err, reply) => { + callback(null, reply.string); + }, { + key: 'A', + }); + }, + function (callback) { + helpers.getBot().reply('userB', 'generic message two', (err, reply) => { + callback(null, reply.string); + }, { + key: 'B', + }); + }, ], // optional callback - function(err, results){ + (err, results) => { results.should.containEql('generic reply A userA generic message'); results.should.containEql('generic reply B userB generic message two'); @@ -906,34 +887,32 @@ describe('SuperScript Scripting + Style Interface', function(){ }); }); - describe('Direct Reply', function() { - it("should return reply", function(done) { - bot.directReply("user1", "generic", "__simple__", function(err, reply) { - reply.string.should.eql(""); + describe('Direct Reply', () => { + it('should return reply', (done) => { + helpers.getBot().directReply('user1', 'generic', '__simple__', (err, reply) => { + reply.string.should.eql(''); done(); }); }); }); - describe.skip('GH-243', function() { - it("Should pass data back into filter function on input", function(done) { - bot.reply("user1", "filter by logic", function(err, reply) { - reply.string.should.eql("logic"); + describe.skip('GH-243', () => { + it('Should pass data back into filter function on input', (done) => { + helpers.getBot().reply('user1', 'filter by logic', (err, reply) => { + reply.string.should.eql('logic'); done(); }); }); - it("Should pass data back into filter function on input 2", function(done) { - bot.reply("user1", "filter by ai", function(err, reply) { - reply.string.should.eql("ai"); + it('Should pass data back into filter function on input 2', (done) => { + helpers.getBot().reply('user1', 'filter by ai', (err, reply) => { + reply.string.should.eql('ai'); done(); }); }); - }); - after(help.after); - + after(helpers.after); }); diff --git a/test/subs.js b/test/subs.js index 402b89db..397aadcb 100644 --- a/test/subs.js +++ b/test/subs.js @@ -1,49 +1,49 @@ -var mocha = require("mocha"); -var should = require("should"); -var help = require("./helpers"); - -describe('SuperScript substitution Interface', function(){ - - before(help.before("substitution")); - - describe('Message Subs', function(){ - it("name subsitution", function(done) { - bot.reply("user1", "Rob is here", function(err, reply) { - reply.string.should.eql("hi Rob"); +/* global describe, it, before, after */ + +import mocha from 'mocha'; +import should from 'should'; +import helpers from './helpers'; + +describe('SuperScript substitution Interface', () => { + before(helpers.before('substitution')); + + describe('Message Subs', () => { + it('name subsitution', (done) => { + helpers.getBot().reply('user1', 'Rob is here', (err, reply) => { + reply.string.should.eql('hi Rob'); done(); }); }); - it("name subsitution - 2", function(done) { - bot.reply("user1", "Rob is taller than Heather", function(err, reply) { - reply.string.should.eql("Heather is shorter than Rob"); + it('name subsitution - 2', (done) => { + helpers.getBot().reply('user1', 'Rob is taller than Heather', (err, reply) => { + reply.string.should.eql('Heather is shorter than Rob'); done(); }); }); - it("name subsitution - 3", function(done) { - bot.reply("user1", "Rob Ellis is taller than Heather Allen", function(err, reply) { - reply.string.should.eql("Heather Allen is shorter than Rob Ellis"); + it('name subsitution - 3', (done) => { + helpers.getBot().reply('user1', 'Rob Ellis is taller than Heather Allen', (err, reply) => { + reply.string.should.eql('Heather Allen is shorter than Rob Ellis'); done(); }); }); - it("name subsitution - 4", function(done) { - bot.reply("user1", "Rob is taller than Rob", function(err, reply) { - reply.string.should.eql(""); + it('name subsitution - 4', (done) => { + helpers.getBot().reply('user1', 'Rob is taller than Rob', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - it("verb pronoun noun subsitution ", function(done) { - bot.reply("user1", "She ran to Vancouver", function(err, reply) { - reply.string.should.eql("okay"); + it('verb pronoun noun subsitution ', (done) => { + helpers.getBot().reply('user1', 'She ran to Vancouver', (err, reply) => { + reply.string.should.eql('okay'); done(); }); }); - }); - - after(help.after); + }); + after(helpers.after); }); diff --git a/test/test-regexes.js b/test/test-regexes.js index d6fd215e..5ea3e2f7 100644 --- a/test/test-regexes.js +++ b/test/test-regexes.js @@ -1,64 +1,63 @@ -const should = require('should') +/* global describe, it, before, after */ -const regexes = require('../lib/regexes') +import should from 'should'; +import regexes from '../src/bot/regexes'; -describe('The shared regular expressions', function() { +describe('The shared regular expressions', () => { + it('redirect should match mustachioed reply names', () => { + const m = regexes.redirect.match('{@__greeting__} How are you today?'); + m[1].should.equal('__greeting__'); + m.index.should.equal(0); + }); - it('redirect should match mustachioed reply names', function() { - const m = regexes.redirect.match('{@__greeting__} How are you today?') - m[1].should.equal('__greeting__') - m.index.should.equal(0) - }) + it('redirect should not match mustachioed bare words', () => { + const m = regexes.redirect.match('{keep} Hi, good to see you!'); + should.not.exist(m); + }); - it('redirect should not match mustachioed bare words', function() { - const m = regexes.redirect.match('{keep} Hi, good to see you!') - should.not.exist(m) - }) - - it('topic should match “^topicRedirect” expressions', function() { - const m = regexes.topic.match('hello ^topicRedirect(topicName,trigger) ') + it('topic should match “^topicRedirect” expressions', () => { + const m = regexes.topic.match('hello ^topicRedirect(topicName,trigger) '); // const m = 'hello ^topicRedirect(topicName,trigger) '.match(regexes.topic) - m[1].should.equal('topicName') - m[2].should.equal('trigger') - m.index.should.equal(6) - }) - - it('respond should match “^respond(topicName)” expressions', function() { - const m = regexes.respond.match('hello ^respond(topicName) ') - m[1].should.equal('topicName') - m.index.should.equal(6) - }) - - it('customFn should match “^functionName(arg1,)” expressions', function() { - const m = regexes.customFn.match('the weather is ^getWeather(today,) today') - m[1].should.equal('getWeather') - m[2].should.equal('today,') - m.index.should.equal(15) - }) - - it('wordnet should match “I ~like ~sport” expressions', function() { - const m = regexes.wordnet.match('I ~like ~sport') - m.should.deepEqual(['~like', '~sport']) - }) - - it('state should match “{keep} some {state}” expressions', function() { - const m = regexes.state.match('{keep} some {state}') - m.should.deepEqual(['{keep}', '{state}']) - }) - - it('filters should match “hello ^filterName(foo,, baz) !” expressions', function() { - const m = regexes.filter.match('hello ^filterName(foo,, baz) !') - m.length.should.equal(3) - m[1].should.equal('filterName') - m[2].should.equal('foo,, baz') - m.index.should.equal(6) - }) - - it('delay should match “this {delay = 400}” expressions', function() { - regexes.delay.match('this {delay = 400}')[1].should.equal('400') - regexes.delay.match('{delay=300} testing')[1].should.equal('300') - regexes.delay.match('{ delay =300} test')[1].should.equal('300') - regexes.delay.match('{ delay =300 } test')[1].should.equal('300') - }) - -}) + m[1].should.equal('topicName'); + m[2].should.equal('trigger'); + m.index.should.equal(6); + }); + + it('respond should match “^respond(topicName)” expressions', () => { + const m = regexes.respond.match('hello ^respond(topicName) '); + m[1].should.equal('topicName'); + m.index.should.equal(6); + }); + + it('customFn should match “^functionName(arg1,)” expressions', () => { + const m = regexes.customFn.match('the weather is ^getWeather(today,) today'); + m[1].should.equal('getWeather'); + m[2].should.equal('today,'); + m.index.should.equal(15); + }); + + it('wordnet should match “I ~like ~sport” expressions', () => { + const m = regexes.wordnet.match('I ~like ~sport'); + m.should.deepEqual(['~like', '~sport']); + }); + + it('state should match “{keep} some {state}” expressions', () => { + const m = regexes.state.match('{keep} some {state}'); + m.should.deepEqual(['{keep}', '{state}']); + }); + + it('filters should match “hello ^filterName(foo,, baz) !” expressions', () => { + const m = regexes.filter.match('hello ^filterName(foo,, baz) !'); + m.length.should.equal(3); + m[1].should.equal('filterName'); + m[2].should.equal('foo,, baz'); + m.index.should.equal(6); + }); + + it('delay should match “this {delay = 400}” expressions', () => { + regexes.delay.match('this {delay = 400}')[1].should.equal('400'); + regexes.delay.match('{delay=300} testing')[1].should.equal('300'); + regexes.delay.match('{ delay =300} test')[1].should.equal('300'); + regexes.delay.match('{ delay =300 } test')[1].should.equal('300'); + }); +}); diff --git a/test/topicflags.js b/test/topicflags.js index 4c6fe175..b165c016 100644 --- a/test/topicflags.js +++ b/test/topicflags.js @@ -1,19 +1,20 @@ -var mocha = require("mocha"); -var should = require("should"); -var help = require("./helpers"); +/* global describe, it, before, after */ -// We need to revisit userConnect -describe('Super Script Topics', function(){ +import mocha from 'mocha'; +import should from 'should'; +import helpers from './helpers'; - before(help.before("topicflags")); +// We need to revisit userConnect +describe('SuperScript Topics', () => { + before(helpers.before('topicflags')); - describe('Topic Functions', function(){ + describe('Topic Functions', () => { // This test is failing and Im not sure if random or system topics should be included - it.skip("should fetch a list of topics", function(done){ - bot.findOrCreateUser("user1", function(err, user){ - var message = {lemString: "hello world"}; + it.skip('should fetch a list of topics', (done) => { + helpers.getBot().findOrCreateUser('user1', (err, user) => { + const message = { lemString: 'hello world' }; - bot.topicSystem.topic.findPendingTopicsForUser(user, message, function(e,topics) { + helpers.getBot().chatSystem.Topic.findPendingTopicsForUser(user, message, (e, topics) => { topics.should.not.be.empty; topics.should.have.length(7); done(); @@ -21,51 +22,49 @@ describe('Super Script Topics', function(){ }); }); - it("find topic by Name", function(done){ - bot.topicSystem.topic.findByName('random', function(err, topic){ + it('find topic by Name', (done) => { + helpers.getBot().chatSystem.Topic.findByName('random', (err, topic) => { topic.should.not.be.empty; done(); }); }); }); - - describe('Topics - System', function(){ - it("topic should have system flag", function(done){ - bot.reply("user1", "this is a system topic", function(err, reply){ + + describe('Topics - System', () => { + it('topic should have system flag', (done) => { + helpers.getBot().reply('user1', 'this is a system topic', (err, reply) => { reply.string.should.be.empty; done(); }); }); // Re-check this - it("Go to hidden topic indirectly", function(done){ - bot.reply("user1", "why did you run", function(err, reply){ + it('Go to hidden topic indirectly', (done) => { + helpers.getBot().reply('user1', 'why did you run', (err, reply) => { // This really just makes sure the reply is not accesses directly - reply.string.should.eql("to get away from someone"); - reply.topicName.should.eql("system_why"); + reply.string.should.eql('to get away from someone'); + reply.topicName.should.eql('system_why'); done(); }); }); - it("topic recurrsion with respond", function(done){ - bot.reply("user1", "test recursion", function(err, reply){ - reply.string.should.eql(""); + it('topic recurrsion with respond', (done) => { + helpers.getBot().reply('user1', 'test recursion', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - }); - describe('Topic - sort', function(){ - - it("topic should not be orderd by default", function(done) { - bot.reply("user1", "this should catch some", function(err, reply) { - bot.topicSystem.topic.findByName('random', function(err, topic) { - topic.createGambit({input:'this should catch some more'}, function(er, gam) { - gam.addReply({reply: "New Reply"}, function(err, rep) { - topic.sortGambits(function() { - bot.reply("user1", "this should catch some more", function(err, reply) { - reply.string.should.eql("New Reply"); + describe('Topic - sort', () => { + it('topic should not be orderd by default', (done) => { + helpers.getBot().reply('user1', 'this should catch some', (err, reply) => { + helpers.getBot().chatSystem.Topic.findByName('random', (err, topic) => { + topic.createGambit({ input: 'this should catch some more' }, (er, gam) => { + gam.addReply({ reply: 'New Reply' }, (err, rep) => { + topic.sortGambits(() => { + helpers.getBot().reply('user1', 'this should catch some more', (err, reply) => { + reply.string.should.eql('New Reply'); done(); }); }); @@ -73,114 +72,104 @@ describe('Super Script Topics', function(){ }); }); }); - }); }); - describe('Topic Flow', function() { - - it("topic flow 0", function(done) { - bot.reply("user1", "respond test", function(err, reply) { - reply.string.should.eql("final"); + describe('Topic Flow', () => { + it('topic flow 0', (done) => { + helpers.getBot().reply('user1', 'respond test', (err, reply) => { + reply.string.should.eql('final'); done(); }); }); - it("topic flow 1", function(done){ - bot.reply("user 10", "testing hidden", function(err, reply) { - reply.string.should.eql("some reply"); + it('topic flow 1', (done) => { + helpers.getBot().reply('user 10', 'testing hidden', (err, reply) => { + reply.string.should.eql('some reply'); - bot.reply("user 10", "yes", function(err, reply) { - reply.string.should.eql("this should work."); + helpers.getBot().reply('user 10', 'yes', (err, reply) => { + reply.string.should.eql('this should work.'); done(); }); - }); }); - it("topic flow 2", function(done){ - bot.reply("user2", "testing hidden", function(err, reply) { - reply.string.should.eql("some reply"); + it('topic flow 2', (done) => { + helpers.getBot().reply('user2', 'testing hidden', (err, reply) => { + reply.string.should.eql('some reply'); - bot.reply("user2", "lets not go on", function(err, reply) { - reply.string.should.eql("end"); + helpers.getBot().reply('user2', 'lets not go on', (err, reply) => { + reply.string.should.eql('end'); done(); }); - }); }); - }); - describe('Topics - NoStay Flag', function() { - it("topic should have keep flag", function(done){ - bot.reply("User1", "testing nostay", function(err, reply) { - reply.string.should.eql("topic test pass"); - bot.reply("User1", "something else", function(err, reply) { - reply.string.should.eql("reply in random"); + describe('Topics - NoStay Flag', () => { + it('topic should have keep flag', (done) => { + helpers.getBot().reply('User1', 'testing nostay', (err, reply) => { + reply.string.should.eql('topic test pass'); + helpers.getBot().reply('User1', 'something else', (err, reply) => { + reply.string.should.eql('reply in random'); done(); }); }); }); - }); - describe('Topics - Keep', function() { - - it("topic should have keep flag", function(done){ - bot.topicSystem.topic.findByName('keeptopic', function(err, t) { + describe('Topics - Keep', () => { + it('topic should have keep flag', (done) => { + helpers.getBot().chatSystem.Topic.findByName('keeptopic', (err, t) => { t.keep.should.be.true; done(); }); }); - it("should keep topic for reuse", function(done){ - bot.reply("user1", "set topic to keeptopic", function(err, reply) { - reply.string.should.eql("Okay we are going to keeptopic"); + it('should keep topic for reuse', (done) => { + helpers.getBot().reply('user1', 'set topic to keeptopic', (err, reply) => { + reply.string.should.eql('Okay we are going to keeptopic'); - bot.getUser("user1", function(err, cu){ - cu.getTopic().should.eql("keeptopic"); - bot.reply("user1", "i have one thing to say", function(err, reply) { - reply.string.should.eql("topic test pass"); - bot.reply("user1", "i have one thing to say", function(err, reply) { - reply.string.should.eql("topic test pass"); + helpers.getBot().getUser('user1', (err, cu) => { + cu.getTopic().should.eql('keeptopic'); + helpers.getBot().reply('user1', 'i have one thing to say', (err, reply) => { + reply.string.should.eql('topic test pass'); + helpers.getBot().reply('user1', 'i have one thing to say', (err, reply) => { + reply.string.should.eql('topic test pass'); done(); }); }); - }); }); }); - - it("should not repeat itself", function(done){ + + it('should not repeat itself', (done) => { // Manually reset the topic - bot.findOrCreateUser("user1", function(err, user){ - user.currentTopic = "random"; + helpers.getBot().findOrCreateUser('user1', (err, user) => { + user.currentTopic = 'random'; - bot.reply("user1", "set topic to dry", function(err, reply) { + helpers.getBot().reply('user1', 'set topic to dry', (err, reply) => { // Now in dry topic - bot.getUser("user1", function(err, su) { - ct = su.getTopic(); - ct.should.eql("dry"); + helpers.getBot().getUser('user1', (err, su) => { + const ct = su.getTopic(); + ct.should.eql('dry'); - bot.reply("user1", "this is a dry topic", function(err, reply) { - reply.string.should.eql("dry topic test pass"); + helpers.getBot().reply('user1', 'this is a dry topic', (err, reply) => { + reply.string.should.eql('dry topic test pass'); // Say it again... - bot.reply("user1", "this is a dry topic", function(err, reply) { - + helpers.getBot().reply('user1', 'this is a dry topic', (err, reply) => { // If something was said, we don't say it again - reply.string.should.eql(""); + reply.string.should.eql(''); done(); }); }); - }); }); }); }); }); - after(help.after); -}); \ No newline at end of file + after(helpers.after); +}); diff --git a/test/topichooks.js b/test/topichooks.js index 70b937f2..7a588695 100644 --- a/test/topichooks.js +++ b/test/topichooks.js @@ -1,35 +1,35 @@ -var mocha = require("mocha"); -var should = require("should"); -var help = require("./helpers"); +/* global describe, it, before, after */ -// Testing topics that include and mixin other topics. -describe('Super Script Topic Hooks', function(){ +import mocha from 'mocha'; +import should from 'should'; +import helpers from './helpers'; - before(help.before("topichooks")); +// Testing topics that include and mixin other topics. +describe('SuperScript Topic Hooks', () => { + before(helpers.before('topichooks')); - describe('Pre/Post Topic Hooks', function() { - it("pre topic should be called", function(done) { - bot.topicSystem.topic.findOne({name:'__pre__'}, function(err, res){ + describe('Pre/Post Topic Hooks', () => { + it('pre topic should be called', (done) => { + helpers.getBot().chatSystem.Topic.findOne({ name: '__pre__' }, (err, res) => { res.gambits.should.have.lengthOf(1); done(); }); }); - it("post topic should be called", function(done) { - bot.topicSystem.topic.findOne({name:'__post__'}, function(err, res){ - res.gambits.should.have.lengthOf(1) + it('post topic should be called', (done) => { + helpers.getBot().chatSystem.Topic.findOne({ name: '__post__' }, (err, res) => { + res.gambits.should.have.lengthOf(1); done(); }); }); - xit("normal topic should be called", function(done) { - bot.topicSystem.topic.findOne({name:'random'}, function(err, res){ - res.gambits.should.have.lengthOf(1) + xit('normal topic should be called', (done) => { + helpers.getBot().chatSystem.Topic.findOne({ name: 'random' }, (err, res) => { + res.gambits.should.have.lengthOf(1); done(); }); }); }); - after(help.after); - -}); \ No newline at end of file + after(helpers.after); +}); diff --git a/test/topicsystem.js b/test/topicsystem.js index 97061fe5..fed06cf7 100644 --- a/test/topicsystem.js +++ b/test/topicsystem.js @@ -1,6 +1,8 @@ -var mocha = require("mocha"); -var should = require("should"); -var help = require("./helpers"); +/* global describe, it, before, after */ + +import mocha from 'mocha'; +import should from 'should'; +import helpers from './helpers'; /* @@ -11,71 +13,73 @@ var help = require("./helpers"); */ // Testing topics that include and mixin other topics. -describe('SuperScript TopicsSystem', function(){ - - before(help.before("topicsystem")); +describe('SuperScript TopicsSystem', () => { + before(helpers.before('topicsystem')); - describe('TopicSystem', function() { - it("Should skip empty replies until it finds a match", function(done){ - bot.reply("testing topic system", function(err, reply){ - ["we like it","i hate it"].should.containEql(reply.string); + describe('TopicSystem', () => { + it('Should skip empty replies until it finds a match', (done) => { + helpers.getBot().reply('testing topic system', (err, reply) => { + ['we like it', 'i hate it'].should.containEql(reply.string); done(); }); }); - - it("Should break in function with third param", function(done){ - bot.reply("userx", "force break", function(err, reply){ - reply.string.should.eql(""); + + it('Should break in function with third param', (done) => { + helpers.getBot().reply('userx', 'force break', (err, reply) => { + reply.string.should.eql(''); done(); }); }); - it("Should continue in function with third param", function(done){ - bot.reply("userx", "force continue", function(err, reply){ - reply.string.should.eql("force one force two"); + it('Should continue in function with third param', (done) => { + helpers.getBot().reply('userx', 'force continue', (err, reply) => { + reply.string.should.eql('force one force two'); done(); }); }); - it("Should continue with a {CONTINUE} tag", function(done){ - bot.reply("userx", "break with continue", function(err, reply){ - reply.string.should.eql("ended test passed"); + it('Should continue with a {CONTINUE} tag', (done) => { + helpers.getBot().reply('userx', 'break with continue', (err, reply) => { + reply.string.should.eql('ended test passed'); done(); }); }); - - }); - - // Test Single gambit - describe.skip('Test Gambit', function () { + // Test Single gambit + describe('Test Gambit', () => { // this is a testing input for the editor // We want a string in and false or matches out - it("Should try string agaist gambit", function(done){ - bot.message("i like to build fires", function(err, msg){ - bot.topicSystem.gambit.findOne({input:'I like to *'}, function(e,g){ - g.doesMatch(msg, function (e,r) { - r.should.exist; - done(); + it('Should try string agaist gambit', (done) => { + helpers.getBot().message('i like to build fires', (err, msg) => { + helpers.getBot().chatSystem.Gambit.findOne({ input: 'I like to *' }, (e, g) => { + helpers.getBot().getUser('user1', (err, user) => { + const options = { user }; + g.doesMatch(msg, options, (e, r) => { + r.should.exist; + done(); + }); }); }); }); }); - it("update gambit test", function (done) { - bot.topicSystem.gambit.findOrCreate({input: 'this is a create test'}, function (er, gam) { - gam.save(function(){ - bot.message("this is a create test", function (err, msg) { - gam.doesMatch(msg, function (e, r) { - r.should.exist; - gam.input = 'this is a create *~2'; - gam.save(function () { - bot.message("this is a create hello world", function (err, msg) { - gam.doesMatch(msg, function (e, r) { - r[1].should.eql(' hello world'); - done(); + it('update gambit test', (done) => { + helpers.getBot().chatSystem.Gambit.findOrCreate({ input: 'this is a create test' }, (er, gam) => { + gam.save(() => { + helpers.getBot().message('this is a create test', (err, msg) => { + helpers.getBot().getUser('user1', (err, user) => { + const options = { user }; + gam.doesMatch(msg, options, (e, r) => { + r.should.exist; + gam.input = 'this is a create *~2'; + gam.save(() => { + helpers.getBot().message('this is a create hello world', (err, msg) => { + gam.doesMatch(msg, options, (e, r) => { + r[1].should.eql('hello world'); + done(); + }); }); }); }); @@ -84,45 +88,46 @@ describe('SuperScript TopicsSystem', function(){ }); }); }); - }); // Test Entire topic for Match - describe.skip('Test Topic', function() { + describe('Test Topic', () => { // this is a testing input for the editor // We want a string in and false or matches out - it("Should try string agaist topic", function(done){ - bot.message("I like to play outside", function(err, msg){ - bot.topicSystem.topic.findOne({name: 'outdoors'}, function(e,topic){ - topic.doesMatch(msg, function (e,r) { - r.should.not.be.empty; - r[0].input.should.containEql('I like to *'); - done(); + it('Should try string agaist topic', (done) => { + helpers.getBot().message('I like to play outside', (err, msg) => { + helpers.getBot().chatSystem.Topic.findOne({ name: 'outdoors' }, (e, topic) => { + const options = {}; + helpers.getBot().getUser('user1', (err, user) => { + options.user = user; + topic.doesMatch(msg, options, (e, r) => { + r.should.not.be.empty; + r[0].input.should.containEql('I like to *'); + done(); + }); }); }); }); - }); }); - describe('TopicDiscovery', function() { - it("Should find the right topic", function(done){ - bot.reply("i like to hunt", function(err, reply){ - reply.string.should.containEql("i like to spend time outdoors"); + describe('TopicDiscovery', () => { + it('Should find the right topic', (done) => { + helpers.getBot().reply('i like to hunt', (err, reply) => { + reply.string.should.containEql('i like to spend time outdoors'); - bot.reply("i like to fish", function(err, reply){ - reply.string.should.containEql("me too"); + helpers.getBot().reply('i like to fish', (err, reply) => { + reply.string.should.containEql('me too'); done(); }); - }); }); }); // it("Post Order Topics", function(done){ - // bot.reply("I like to spend time fishing", function(err, reply){ + // helpers.getBot().reply("I like to spend time fishing", function(err, reply){ // console.log(reply); // reply.string.should.containEql("fishing"); // done(); @@ -130,43 +135,40 @@ describe('SuperScript TopicsSystem', function(){ // }); - describe("log-debug", function() { - it("Should show steps - redirect", function(done) { - bot.reply("user", "generic redirect", function(err, reply) { - reply.debug.matched_gambit[0].topic.should.containEql("random"); - reply.debug.matched_gambit[0].subset[0].topic.should.containEql("test"); + describe.skip('log-debug', () => { + it('Should show steps - redirect', (done) => { + helpers.getBot().reply('user', 'generic redirect', (err, reply) => { + reply.debug.matched_gambit[0].topic.should.containEql('random'); + reply.debug.matched_gambit[0].subset[0].topic.should.containEql('test'); done(); }); }); - it("Should show steps - respond", function(done) { - bot.reply("user", "generic respond", function(err, reply) { - reply.debug.matched_gambit[0].topic.should.containEql("random"); - reply.debug.matched_gambit[0].subset[0].topic.should.containEql("test"); + it('Should show steps - respond', (done) => { + helpers.getBot().reply('user', 'generic respond', (err, reply) => { + reply.debug.matched_gambit[0].topic.should.containEql('random'); + reply.debug.matched_gambit[0].subset[0].topic.should.containEql('test'); done(); }); }); - }); - describe("gh-240", function() { - it("should stop with topicRedirect", function(done) { - bot.reply("user", "test empty", function(err, reply) { - reply.string.should.containEql(""); + describe('gh-240', () => { + it('should stop with topicRedirect', (done) => { + helpers.getBot().reply('user', 'test empty', (err, reply) => { + reply.string.should.containEql(''); done(); }); }); - - it("should stop with respond", function(done) { - bot.reply("user", "test respond", function(err, reply) { - reply.string.should.containEql(""); + + it('should stop with respond', (done) => { + helpers.getBot().reply('user', 'test respond', (err, reply) => { + reply.string.should.containEql(''); done(); }); }); - - }); - after(help.after); -}); \ No newline at end of file + after(helpers.after); +}); diff --git a/test/unit/history.js b/test/unit/history.js index 05f61126..68543db1 100644 --- a/test/unit/history.js +++ b/test/unit/history.js @@ -1,76 +1,83 @@ -var mocha = require("mocha"); -var should = require("should"); -var moment = require("moment"); - -var History = require("../../lib/history"); -var Users = require("../../lib/users"); - -var Message = require("../../lib/message"); -var norm = require("node-normalizer"); -var qtypes = require("qtypes"); -var Concepts = require("../../lib/concepts"); -var cnet = require("conceptnet")({host:'127.0.0.1', user:'root', pass:''}); - -describe('History Lookup Interface', function(){ - - var user, normalize, q; - var data = ['./data/names.top']; - - before(function(done){ - norm.loadData(function(){ - normalize = norm; - new qtypes(function(question) { - q = question; - user = Users.findOrCreate("testuser"); - Concepts.readFiles(data, function(facts) { - concept = facts; - done(); - }); +import mocha from 'mocha'; +import should from 'should'; + +import History from '../../src/bot/history'; +import Message from '../../src/bot/message'; +import connect from '../../src/bot/db/connect'; +import createFactSystem from '../../src/bot/factSystem'; +import createChatSystem from '../../src/bot/chatSystem'; + +describe.skip('History Lookup Interface', () => { + let user; + let factSystem; + + before((done) => { + const db = connect('mongodb://localhost/', 'testHistory'); + const data = [ + // './data/names.top' + ]; + const options = { + name: 'testHistoryFacts', + clean: true, + importData: data, + }; + createFactSystem(options, (err, facts) => { + factSystem = facts; + const chatSystem = createChatSystem(db, factSystem); + const findProps = { id: 'testuser' }; + const createProps = { + currentTopic: 'random', + status: 0, + conversation: 0, + volley: 0, + rally: 0, + }; + chatSystem.User.findOrCreate(findProps, createProps, (err, newUser) => { + user = newUser; + done(); }); - }); + }); }); - it("simple history recall", function(done){ - new Message("I have two sons", q, normalize, cnet, concept, function(msgObj) { - user.updateHistory(msgObj, ""); - new Message("How many sons do I have?", q, normalize, cnet, concept, function(msgObj2) { - var h = History(user, { nouns: msgObj2.nouns} ); + it('simple history recall', (done) => { + Message.createMessage('I have two sons', { factSystem }, (msgObj) => { + user.updateHistory(msgObj, ''); + Message.createMessage('How many sons do I have?', { factSystem }, (msgObj2) => { + const h = History(user, { nouns: msgObj2.nouns }); h.length.should.eql(1); - h[0].numbers[0].should.eql("2"); + h[0].numbers[0].should.eql('2'); done(); }); }); }); - it("money history recall", function(done){ - new Message("I have twenty-five bucks in my wallet", q, normalize, cnet, concept, function(msgObj) { - new Message("I ate a 3 course meal to days ago, it was amazing.", q, normalize, cnet, concept, function(msgObjx) { + it('money history recall', (done) => { + Message.createMessage('I have twenty-five bucks in my wallet', { factSystem }, (msgObj) => { + Message.createMessage('I ate a 3 course meal to days ago, it was amazing.', { factSystem }, (msgObjx) => { + user.updateHistory(msgObj, ''); + user.updateHistory(msgObjx, ''); + + Message.createMessage('How much money do I have?', { factSystem }, (msgObj2) => { + const h = History(user, { money: true }); - user.updateHistory(msgObj, ""); - user.updateHistory(msgObjx, ""); - - new Message("How much money do I have?", q, normalize, cnet, concept, function(msgObj2) { - var h = History(user, { money: true } ); - h.length.should.eql(1); - h[0].numbers[0].should.eql("25"); + h[0].numbers[0].should.eql('25'); done(); }); }); }); }); - it("money history recall with noun filter", function(done){ - new Message("A loaf of bread cost $4.50", q, normalize, cnet, concept, function(msgObj) { - new Message("A good bike cost like $1,000.00 bucks.", q, normalize, cnet, concept, function(msgObjx) { + it('money history recall with noun filter', (done) => { + Message.createMessage('A loaf of bread cost $4.50', { factSystem }, (msgObj) => { + Message.createMessage('A good bike cost like $1,000.00 bucks.', { factSystem }, (msgObjx) => { + user.updateHistory(msgObj, ''); + user.updateHistory(msgObjx, ''); - user.updateHistory(msgObj, ""); - user.updateHistory(msgObjx, ""); - - new Message("How much is a loaf of bread?", q, normalize, cnet, concept, function(msgObj2) { - var h = History(user, { money: true, nouns: msgObj2.nouns } ); + Message.createMessage('How much is a loaf of bread?', { factSystem }, (msgObj2) => { + const h = History(user, { money: true, nouns: msgObj2.nouns }); h.length.should.eql(1); - h[0].numbers[0].should.eql("4.50"); + h[0].numbers[0].should.eql('4.50'); done(); }); }); @@ -78,40 +85,38 @@ describe('History Lookup Interface', function(){ }); - it("date history recall", function(done){ - new Message("next month is important", q, normalize, cnet, concept, function(msgObj) { - user.updateHistory(msgObj, ""); - new Message("When is my birthday?", q, normalize, cnet, concept, function(msgObj2) { - var h = History(user, { date: true } ); + it('date history recall', (done) => { + Message.createMessage('next month is important', { factSystem }, (msgObj) => { + user.updateHistory(msgObj, ''); + Message.createMessage('When is my birthday?', { factSystem }, (msgObj2) => { + const h = History(user, { date: true }); h.length.should.eql(1); // date should be a moment object - h[0].date.format("MMMM").should.eql("July"); + h[0].date.format('MMMM').should.eql('July'); done(); }); }); }); - + // Name lookup with QType Resolver (ENTY:sport) // My friend Bob likes to play tennis. What game does Bob like to play? // What is the name of my friend who likes to play tennis? - it("memory problem 1", function(done){ - new Message("My friend Bob likes to play tennis.", q, normalize, cnet, concept, function(msgObj) { - user.updateHistory(msgObj, ""); - new Message("What game does Bob like to play?", q, normalize, cnet, concept, function(msgObj2) { - + it('memory problem 1', (done) => { + Message.createMessage('My friend Bob likes to play tennis.', { factSystem }, (msgObj) => { + user.updateHistory(msgObj, ''); + Message.createMessage('What game does Bob like to play?', { factSystem }, (msgObj2) => { // AutoReply ENTY:Sport - var h = History(user, { nouns: msgObj2.nouns }); + const h = History(user, { nouns: msgObj2.nouns }); h.length.should.eql(1); - cnet.resolveFacts(h[0].cNouns, "sport", function(err, res) { - + cnet.resolveFacts(h[0].cNouns, 'sport', (err, res) => { res.length.should.eql(1); - res[0].should.eql("tennis"); + res[0].should.eql('tennis'); // update the history with the new msg obj. - user.updateHistory(msgObj2, ""); + user.updateHistory(msgObj2, ''); - new Message("What is the name of my friend who likes to play tennis?", q, normalize, cnet, concept, function(msgObj3) { - var h2 = History(user, { nouns: msgObj3.nouns }); + Message.createMessage('What is the name of my friend who likes to play tennis?', { factSystem }, (msgObj3) => { + const h2 = History(user, { nouns: msgObj3.nouns }); h2.length.should.eql(1); h2[0].names[0].should.eql('Bob'); done(); @@ -123,23 +128,20 @@ describe('History Lookup Interface', function(){ // My friend John likes to fish for trout. What does John like to fish for? // What is the name of my friend who fishes for trout? - it("memory problem 2", function(done){ - new Message("My friend John likes to fish for trout.", q, normalize, cnet, concept, function(msgObj) { - user.updateHistory(msgObj, ""); - new Message("What does John like to fish for?", q, normalize, cnet, concept, function(msgObj2) { - - var h = History(user, { nouns: msgObj2.nouns }); + it('memory problem 2', (done) => { + Message.createMessage('My friend John likes to fish for trout.', { factSystem }, (msgObj) => { + user.updateHistory(msgObj, ''); + Message.createMessage('What does John like to fish for?', { factSystem }, (msgObj2) => { + const h = History(user, { nouns: msgObj2.nouns }); h.length.should.eql(1); - cnet.resolveFacts(h[0].cNouns, "food", function(err, res) { + cnet.resolveFacts(h[0].cNouns, 'food', (err, res) => { res.length.should.eql(2); - res.should.containEql("fish"); - res.should.containEql("trout"); - new Message("What is the name of my friend who fishes for trout?", q, normalize, cnet, concept, function(msgObj3) { - - var h2 = History(user, { nouns: msgObj3.nouns }); + res.should.containEql('fish'); + res.should.containEql('trout'); + Message.createMessage('What is the name of my friend who fishes for trout?', { factSystem }, (msgObj3) => { + const h2 = History(user, { nouns: msgObj3.nouns }); h2[0].names[0].should.eql('John'); done(); - }); }); }); @@ -148,26 +150,23 @@ describe('History Lookup Interface', function(){ // The ball was hit by Bill. What did Bill hit? // Who hit the ball? - it("memory problem 3", function(done){ - new Message("The ball was hit by Jack.", q, normalize, cnet, concept, function(msgObj) { - user.updateHistory(msgObj, ""); - new Message("What did Jack hit?", q, normalize, cnet, concept, function(msgObj2) { - var h = History(user, { nouns: msgObj2.nouns }); + it('memory problem 3', (done) => { + Message.createMessage('The ball was hit by Jack.', { factSystem }, (msgObj) => { + user.updateHistory(msgObj, ''); + Message.createMessage('What did Jack hit?', { factSystem }, (msgObj2) => { + const h = History(user, { nouns: msgObj2.nouns }); h.length.should.eql(1); // Answer types is WHAT, the answer is in the cnouns - h[0].cNouns[0].should.eql("ball"); + h[0].cNouns[0].should.eql('ball'); // Follow up question: Who hit the ball? - new Message("Who hit the ball?", q, normalize, cnet, concept, function(msgObj3) { + Message.createMessage('Who hit the ball?', { factSystem }, (msgObj3) => { // We know this is a HUM:ind, give me a name! - var h = History(user, { nouns: msgObj3.nouns }); - h[0].names[0].should.eql("Jack"); - done(); + const h = History(user, { nouns: msgObj3.nouns }); + h[0].names[0].should.eql('Jack'); + done(); }); }); }); }); - - }); - diff --git a/test/unit/message.js b/test/unit/message.js index 5724b58b..eb33a751 100644 --- a/test/unit/message.js +++ b/test/unit/message.js @@ -1,64 +1,60 @@ -var mocha = require("mocha"); -var should = require("should"); +import mocha from 'mocha'; +import should from 'should'; +import sfacts from 'sfacts'; -var norm = require("node-normalizer"); -var qtypes = require("qtypes"); -var cnet = require("conceptnet")({host:'127.0.0.1', user:'root', pass:''}); +import Message from '../../src/bot/message'; +import createFactSystem from '../../src/bot/factSystem'; -var Concepts = require("../../lib/concepts"); - -var data = ['./data/names.top', - './data/affect.top', - './data/adverbhierarchy.top', +const data = [ + /* './data/names.top', + './data/affect.top', + './data/adverbhierarchy.top', './data/verbhierarchy.top', - './data/concepts.top']; - -var Message = require("../../lib/message"); - -describe('Message Interface', function(){ - - var normalize, questions, concept; - - before(function(done){ - norm.loadData(function(){ - // Why didn't I pass this back in the CB?!? - normalize = norm; - new qtypes(function(question) { - questions = question; - - Concepts.readFiles(data, function(facts) { - concept = facts; - done(); - }); - }); - }); - }); + './data/concepts.top',*/ +]; + +describe('Message Interface', () => { + let factSystem; + + before((done) => { + const options = { + name: 'testMessage', + clean: true, + importData: data, + }; + createFactSystem(options, (err, facts) => { + factSystem = facts; + done(err); + }); + }); - it("should parse names and nouns from message 1", function(done){ - new Message("Rob Ellis and Heather know Ashley, Brooklyn and Sydney.", questions, normalize, cnet, concept, function(mo){ + // FIXME: Currently returning [ 'Heather', 'Sydney', 'Rob Ellis', 'Ashley Brooklyn' ] + it.skip('should parse names and nouns from message 1', (done) => { + Message.createMessage('Rob Ellis and Heather know Ashley, Brooklyn and Sydney.', { factSystem }, (mo) => { mo.names.should.be.instanceof(Array).and.have.lengthOf(5); mo.nouns.should.be.instanceof(Array).and.have.lengthOf(6); done(); }); }); - it("should parse names and nouns from message 2 - this pulls names from scripted concepts since they are not NNP's", function(done){ - new Message("heather knows Ashley, brooklyn and sydney.", questions, normalize, cnet, concept, function(mo){ + // FIXME: Some tests are skipped because 'Concepts' no longer exists: this needs looking at + it.skip("should parse names and nouns from message 2 - this pulls names from scripted concepts since they are not NNP's", (done) => { + Message.createMessage('heather knows Ashley, brooklyn and sydney.', { factSystem }, (mo) => { mo.names.should.be.instanceof(Array).and.have.lengthOf(4); done(); }); }); - it("should parse names and nouns from message 3 - some NN NN should burst", function(done){ - new Message("My friend steve likes to play tennis", questions, normalize, cnet, concept, function(mo){ + it.skip('should parse names and nouns from message 3 - some NN NN should burst', (done) => { + Message.createMessage('My friend steve likes to play tennis', { factSystem }, (mo) => { mo.nouns.should.be.instanceof(Array).and.have.lengthOf(3); mo.names.should.be.instanceof(Array).and.have.lengthOf(1); done(); }); }); - it("should have nouns with names filters out (cNouns)", function(done){ - new Message("My friend Bob likes to play tennis", questions, normalize, cnet, concept, function(mo){ + it('should have nouns with names filters out (cNouns)', (done) => { + Message.createMessage('My friend Bob likes to play tennis', { factSystem }, (mo) => { mo.nouns.should.be.instanceof(Array).and.have.lengthOf(3); mo.names.should.be.instanceof(Array).and.have.lengthOf(1); mo.cNouns.should.be.instanceof(Array).and.have.lengthOf(2); @@ -66,86 +62,93 @@ describe('Message Interface', function(){ }); }); - it("should find compare", function(done){ - new Message("So do you like dogs or cats.", questions, normalize, cnet, concept, function(mo){ - mo.qSubType.should.eql("CH"); + it('should find compare', (done) => { + Message.createMessage('So do you like dogs or cats.', { factSystem }, (mo) => { + mo.questionSubType.should.eql('CH'); done(); }); - }); + }); - it("should find compare words 2", function(done){ - new Message("What is bigger a dog or cat?", questions, normalize, cnet, concept, function(mo){ - mo.qSubType.should.eql("CH"); + it('should find compare words 2', (done) => { + Message.createMessage('What is bigger a dog or cat?', { factSystem }, (mo) => { + mo.questionSubType.should.eql('CH'); done(); }); - }); + }); - it("should find context", function(done){ - new Message("They are going on holidays", questions, normalize, cnet, concept, function(mo){ - mo.pnouns.should.have.includeEql("they"); + it('should find context', (done) => { + Message.createMessage('They are going on holidays', { factSystem }, (mo) => { + mo.pnouns.should.containEql('they'); done(); }); - }); + }); - it("should convert to numeric form 1", function(done){ - new Message("what is one plus twenty-one", questions, normalize, cnet, concept, function(mo){ - mo.numbers.should.eql(["1", "21"]); + it('should convert to numeric form 1', (done) => { + Message.createMessage('what is one plus twenty-one', { factSystem }, (mo) => { + mo.numbers.should.eql(['1', '21']); mo.numericExp.should.be.true; done(); }); - }); + }); - it("should convert to numeric form 2", function(done){ - new Message("what is one plus three hundred and forty-five", questions, normalize, cnet, concept, function(mo){ - mo.numbers.should.eql(["1", "345"]); + it('should convert to numeric form 2', (done) => { + Message.createMessage('what is one plus three hundred and forty-five', { factSystem }, (mo) => { + mo.numbers.should.eql(['1', '345']); mo.numericExp.should.be.true; done(); }); - }); + }); - it("should convert to numeric form 3", function(done){ - new Message("five hundred thousand and three hundred and forty-five", questions, normalize, cnet, concept, function(mo){ - mo.numbers.should.eql(["500345"]); + it('should convert to numeric form 3', (done) => { + Message.createMessage('five hundred thousand and three hundred and forty-five', { factSystem }, (mo) => { + mo.numbers.should.eql(['500345']); done(); }); - }); + }); - it("should convert to numeric form 4", function(done){ + it('should convert to numeric form 4', (done) => { // This this actually done lower down in the stack. (normalizer) - var mo = new Message("how much is 1,000,000", questions, normalize, cnet, concept, function(mo){ + const mo = Message.createMessage('how much is 1,000,000', { factSystem }, (mo) => { mo.numericExp.should.be.false; - mo.numbers.should.eql(["1000000"]); + mo.numbers.should.eql(['1000000']); done(); }); - }); + }); - it("should find expression", function(done){ - new Message("one plus one = two", questions, normalize, cnet, concept, function(mo){ + it('should find expression', (done) => { + Message.createMessage('one plus one = two', { factSystem }, (mo) => { mo.numericExp.should.be.true; done(); }); - }); + }); - it("should find Date Obj", function(done){ - new Message("If I was born on February 23 1980 how old am I", questions, normalize, cnet, concept, function(mo){ - mo.date.should.not.be.empty + it('should find Date Obj', (done) => { + Message.createMessage('If I was born on February 23 1980 how old am I', { factSystem }, (mo) => { + mo.date.should.not.be.empty; done(); }); }); - it("should find Concepts", function(done){ - new Message("tell that bitch to fuck off", questions, normalize, cnet, concept, function(mo){ + it.skip('should find Concepts', (done) => { + Message.createMessage('tell that bitch to fuck off', { factSystem }, (mo) => { mo.sentiment.should.eql(-7); done(); }); }); - it.skip("should find concepts 2", function(done){ - new Message("I watched a movie last week with my brother.", questions, normalize, cnet, concept, function(mo){ - + it.skip('should find concepts 2', (done) => { + Message.createMessage('I watched a movie last week with my brother.', { factSystem }, (mo) => { done(); }); }); - -}); \ No newline at end of file + after((done) => { + if (factSystem) { + factSystem.db.close(() => { + sfacts.clean('testMessage', done); + }); + } else { + done(); + } + }); +}); diff --git a/test/unit/utils.js b/test/unit/utils.js index 2ac235c5..cd995b21 100644 --- a/test/unit/utils.js +++ b/test/unit/utils.js @@ -1,63 +1,60 @@ -var mocha = require("mocha"); -var should = require("should"); +import mocha from 'mocha'; +import should from 'should'; -var utils = require("../../lib/utils"); +import utils from '../../src/bot/utils'; -describe("Util Helpers", function() { - - it("should not care about sentences with no punctuation", function() { - utils.sentenceSplit("Hello world").should.eql([ 'Hello world' ]) +describe('Util Helpers', () => { + it('should not care about sentences with no punctuation', () => { + utils.sentenceSplit('Hello world').should.eql(['Hello world']); }); - it("should simple split", function() { - utils.sentenceSplit("Hello world.").should.eql([ 'Hello world .' ]) + it('should simple split', () => { + utils.sentenceSplit('Hello world.').should.eql(['Hello world .']); }); - it("should double split", function() { - utils.sentenceSplit("Hello world. Hello wild world.").should.eql([ 'Hello world .', 'Hello wild world .' ]) + it('should double split', () => { + utils.sentenceSplit('Hello world. Hello wild world.').should.eql(['Hello world .', 'Hello wild world .']); }); - it("should indicate article", function() { - utils.indefiniteArticlerize("banana").should.equal("a banana") - utils.indefiniteArticlerize("apple").should.equal("an apple") - utils.indefiniteArticlerize("hour").should.equal("an hour") + it('should indicate article', () => { + utils.indefiniteArticlerize('banana').should.equal('a banana'); + utils.indefiniteArticlerize('apple').should.equal('an apple'); + utils.indefiniteArticlerize('hour').should.equal('an hour'); }); - it("should indicate article", function() { - utils.indefiniteList(["pear", "banana", "apple"]).should.eql("a pear, a banana and an apple") + it('should indicate article', () => { + utils.indefiniteList(['pear', 'banana', 'apple']).should.eql('a pear, a banana and an apple'); }); - it('should escape mustaches', function() { - utils.quotemeta('hello{world}', true).should.equal('hello\\{world\\}') - utils.quotemeta('hello{world}', false).should.equal('hello\\{world\\}') - }) - - it('should only escape pipes when not in commands mode', function() { - utils.quotemeta('hello|world', true).should.equal('hello|world') - utils.quotemeta('hello|world', false).should.equal('hello\\|world') - }) + it('should escape mustaches', () => { + utils.quotemeta('hello{world}', true).should.equal('hello\\{world\\}'); + utils.quotemeta('hello{world}', false).should.equal('hello\\{world\\}'); + }); - it('should trim space from string', function() { - utils.trim(' hello \t\tworld ').should.equal('hello world') - }) + it('should only escape pipes when not in commands mode', () => { + utils.quotemeta('hello|world', true).should.equal('hello|world'); + utils.quotemeta('hello|world', false).should.equal('hello\\|world'); + }); - it('should preserve newlines in strings', function() { - utils.trim(' hello \n world ').should.equal('hello \n world') - }) + it('should trim space from string', () => { + utils.trim(' hello \t\tworld ').should.equal('hello world'); + }); - it('should count words', function() { - utils.wordCount('hello_world#this is a very*odd*string').should.equal(8) - }) + it('should preserve newlines in strings', () => { + utils.trim(' hello \n world ').should.equal('hello \n world'); + }); - it('should replace captured text', function() { - const parts = ['hello ', '', 'how are you today', ', meet '] - const stars = ['', 'Dave', 'feeling', 'Sally'] - const replaced = utils.replaceCapturedText(parts, stars) - replaced.length.should.equal(3) - replaced[0].should.equal('hello Dave') - replaced[1].should.equal('how are you feeling today') - replaced[2].should.equal('Dave, meet Sally') - }) + it('should count words', () => { + utils.wordCount('hello_world#this is a very*odd*string').should.equal(8); + }); + it('should replace captured text', () => { + const parts = ['hello ', '', 'how are you today', ', meet ']; + const stars = ['', 'Dave', 'feeling', 'Sally']; + const replaced = utils.replaceCapturedText(parts, stars); + replaced.length.should.equal(3); + replaced[0].should.equal('hello Dave'); + replaced[1].should.equal('how are you feeling today'); + replaced[2].should.equal('Dave, meet Sally'); + }); }); - diff --git a/test/unit/wordnet.js b/test/unit/wordnet.js index 09db085b..7974461a 100644 --- a/test/unit/wordnet.js +++ b/test/unit/wordnet.js @@ -1,45 +1,46 @@ -var mocha = require("mocha"); -var should = require("should"); - -var wordnet = require("../../lib/wordnet"); - -wordnet.lookup("milk", "~", function(e,r) { - console.log(e,r); +import mocha from 'mocha'; +import should from 'should'; + +import wordnet from '../../src/bot/reply/wordnet'; + +describe('Wordnet Interface', () => { + it('should have have lookup and explore function', (done) => { + wordnet.lookup.should.be.Function; + wordnet.explore.should.be.Function; + done(); + }); + + it('should perform lookup correctly', (done) => { + wordnet.lookup('like', '@', (err, results) => { + should.not.exist(err); + results.should.not.be.empty; + results.should.have.length(3); + done(); + }); + }); + + it('should perform lookup correctly', (done) => { + wordnet.lookup('like~v', '@', (err, results) => { + should.not.exist(err); + results.should.not.be.empty; + results.should.have.length(2); + done(); + }); + }); + + it('should refine to POS', (done) => { + wordnet.lookup('milk', '~', (err, results) => { + should.not.exist(err); + results.should.not.be.empty; + results.should.have.length(25); + done(); + }); + }); + + it('should explore a concept', (done) => { + wordnet.explore('job', (err, results) => { + console.log(results); + done(); + }); + }); }); - - -// describe('Wordnet Interface', function(){ - -// it("should have have a lookup function", function(done){ -// wordnet.lookup.should.be.Function; -// wordnet.explore.should.be.Function; -// done() -// }); - -// it("should have have perform lookup", function(done){ -// wordnet.lookup("like", "@", function(err, results){ -// should.not.exist(err); -// results.should.not.be.empty; -// results.should.have.length(3); -// done(); -// }); -// }); - -// it("should refine to POS ", function(done){ -// wordnet.lookup("like~v", "@", function(err, results){ -// should.not.exist(err); -// results.should.not.be.empty; -// results.should.have.length(2) -// done(); -// }); -// }); - -// // not sure how I want to test this yet -// // it("should refine to POS ", function(done){ -// // wordnet.explore("job", function(err, results){ -// // done(); -// // }); -// // }); - - -// }); \ No newline at end of file diff --git a/test/user.js b/test/user.js index 9b0947b6..cbcff258 100644 --- a/test/user.js +++ b/test/user.js @@ -1,67 +1,58 @@ -var mocha = require("mocha"); -var should = require("should"); -var help = require("./helpers"); -var async = require("async"); -// This test needs to be manually run. -// We done the frist block to create the DB -// then the second to check to see if it works, -// I found this less work then figuring out how to setup a new -// process to spawn each block. -// In this suite, the after hook does not delete the DB -// so that will have to be torn down manually. +/* global describe, it, before, after */ -describe('Super Script User Persist', function(){ +import mocha from 'mocha'; +import should from 'should'; +import async from 'async'; +import helpers from './helpers'; - before(help.before("user")); +describe('SuperScript User Persist', () => { + before(helpers.before('user')); - describe('Get a list of users', function(){ - it("should return all users", function(done){ - bot.reply("userx", "hello world", function(err, reply){ - bot.getUsers(function(err, list){ + describe('Get a list of users', () => { + it('should return all users', (done) => { + helpers.getBot().reply('userx', 'hello world', (err, reply) => { + helpers.getBot().getUsers((err, list) => { list.should.not.be.empty; + list[0].id.should.eql('userx'); done(); }); }); }); }); - describe('Should save users session', function(){ - - it("should save user session", function(done) { - bot.reply("iuser3", "Save user token ABCD.", function(err, reply) { - reply.string.should.eql("User token ABCD has been saved."); + describe('Should save users session', () => { + it('should save user session', (done) => { + helpers.getBot().reply('iuser3', 'Save user token ABCD.', (err, reply) => { + reply.string.should.eql('User token ABCD has been saved.'); done(); }); }); - it("it remember my name", function(done) { + it('it remember my name', (done) => { // Call startup again (same as before hook) - bot.reply("iuser3", "Get user token", function(err, reply) { - reply.string.should.eql("Return ABCD"); + helpers.getBot().reply('iuser3', 'Get user token', (err, reply) => { + reply.string.should.eql('Return ABCD'); done(); }); }); }); - describe("Don't leak the user", function() { - var list = ["userA", "userB"]; + describe("Don't leak the user", () => { + const list = ['userA', 'userB']; - it("ask user A", function(done) { - var itor = function(user, next) { - bot.reply(user, "this is a test", function(err, reply) { - reply.string.should.eql("this is user " + user); + it('ask user A', (done) => { + const itor = function (user, next) { + helpers.getBot().reply(user, 'this is a test', (err, reply) => { + reply.string.should.eql(`this is user ${user}`); next(); - }); - } - async.each(list, itor, function() { + }); + }; + async.each(list, itor, () => { done(); }); - }); }); - after(help.after); - - -}); \ No newline at end of file + after(helpers.after); +});