From 820183989ec44af031d39453db013ac06dbefa19 Mon Sep 17 00:00:00 2001 From: Ben James Date: Mon, 21 Nov 2016 18:20:58 +0000 Subject: [PATCH 1/7] Add ability to have several bots on a single Mongo server (multitenancy) --- package.json | 1 + src/bot/chatSystem.js | 27 +++++++-- src/bot/db/helpers.js | 28 ++++----- src/bot/db/models/gambit.js | 23 ++++--- src/bot/db/models/reply.js | 7 ++- src/bot/db/models/topic.js | 20 ++++--- src/bot/db/models/user.js | 28 +++------ src/bot/factSystem.js | 22 ++++++- src/bot/index.js | 116 ++++++++++++++++++++---------------- src/bot/logger.js | 32 ++++++++++ test/helpers.js | 2 +- test/unit/message.js | 7 +-- 12 files changed, 191 insertions(+), 122 deletions(-) create mode 100644 src/bot/logger.js diff --git a/package.json b/package.json index 22dd2e31..e26d1a9a 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "lodash": "^4.16.5", "mkdirp": "^0.5.1", "moment": "^2.13.0", + "mongo-tenant": "^1.0.1", "mongoose": "^4.5.10", "mongoose-findorcreate": "^0.1.2", "natural": "^0.4.0", diff --git a/src/bot/chatSystem.js b/src/bot/chatSystem.js index 31ce27bf..481470f9 100644 --- a/src/bot/chatSystem.js +++ b/src/bot/chatSystem.js @@ -17,11 +17,23 @@ import createReplyModel from './db/models/reply'; import createTopicModel from './db/models/topic'; import createUserModel from './db/models/user'; -const createChatSystem = function createChatSystem(db, factSystem, logPath) { - const Gambit = createGambitModel(db, factSystem); - const Reply = createReplyModel(db); - const Topic = createTopicModel(db); - const User = createUserModel(db, factSystem, logPath); +let GambitCore = null; +let ReplyCore = null; +let TopicCore = null; +let UserCore = null; + +const createChatSystem = function createChatSystem(db) { + GambitCore = createGambitModel(db); + ReplyCore = createReplyModel(db); + TopicCore = createTopicModel(db); + UserCore = createUserModel(db); +}; + +const createChatSystemForTenant = function createChatSystemForTenant(tenantId = 'master') { + const Gambit = GambitCore.byTenant(tenantId); + const Reply = ReplyCore.byTenant(tenantId); + const Topic = TopicCore.byTenant(tenantId); + const User = UserCore.byTenant(tenantId); return { Gambit, @@ -31,4 +43,7 @@ const createChatSystem = function createChatSystem(db, factSystem, logPath) { }; }; -export default createChatSystem; +export default { + createChatSystem, + createChatSystemForTenant, +}; diff --git a/src/bot/db/helpers.js b/src/bot/db/helpers.js index 5b02abe9..c83849f9 100644 --- a/src/bot/db/helpers.js +++ b/src/bot/db/helpers.js @@ -10,8 +10,8 @@ import postParse from '../postParse'; const debug = debuglog('SS:Common'); -const _walkReplyParent = function _walkReplyParent(db, replyId, replyIds, cb) { - db.model('Reply').findById(replyId) +const _walkReplyParent = function _walkReplyParent(db, tenantId, replyId, replyIds, cb) { + db.model('Reply').byTenant(tenantId).findById(replyId) .populate('parent') .exec((err, reply) => { if (err) { @@ -23,7 +23,7 @@ const _walkReplyParent = function _walkReplyParent(db, replyId, replyIds, cb) { if (reply) { replyIds.push(reply._id); if (reply.parent && reply.parent.parent) { - _walkReplyParent(db, reply.parent.parent, replyIds, cb); + _walkReplyParent(db, tenantId, reply.parent.parent, replyIds, cb); } else { cb(null, replyIds); } @@ -33,8 +33,8 @@ const _walkReplyParent = function _walkReplyParent(db, replyId, replyIds, cb) { }); }; -const _walkGambitParent = function _walkGambitParent(db, gambitId, gambitIds, cb) { - db.model('Gambit').findOne({ _id: gambitId }) +const _walkGambitParent = function _walkGambitParent(db, tenantId, gambitId, gambitIds, cb) { + db.model('Gambit').byTenant(tenantId).findOne({ _id: gambitId }) .populate('parent') .exec((err, gambit) => { if (err) { @@ -44,7 +44,7 @@ const _walkGambitParent = function _walkGambitParent(db, gambitId, gambitIds, cb if (gambit) { gambitIds.push(gambit._id); if (gambit.parent && gambit.parent.parent) { - _walkGambitParent(db, gambit.parent.parent, gambitIds, cb); + _walkGambitParent(db, tenantId, gambit.parent.parent, gambitIds, cb); } else { cb(null, gambitIds); } @@ -56,7 +56,7 @@ const _walkGambitParent = function _walkGambitParent(db, gambitId, gambitIds, cb // 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) { +const findMatchingGambitsForMessage = function findMatchingGambitsForMessage(db, tenantId, type, id, message, options, callback) { // Let's query for Gambits const execHandle = function execHandle(err, gambitsParent) { if (err) { @@ -65,7 +65,7 @@ const findMatchingGambitsForMessage = function findMatchingGambitsForMessage(db, const populateGambits = function populateGambits(gambit, next) { debug.verbose('Populating gambit'); - db.model('Reply').populate(gambit, { path: 'replies' }, next); + db.model('Reply').byTenant(tenantId).populate(gambit, { path: 'replies' }, next); }; async.each(gambitsParent.gambits, populateGambits, (err) => { @@ -83,13 +83,13 @@ const findMatchingGambitsForMessage = function findMatchingGambitsForMessage(db, if (type === 'topic') { debug.verbose('Looking back Topic', id); - db.model('Topic').findOne({ _id: id }, 'gambits') + db.model('Topic').byTenant(tenantId).findOne({ _id: id }, 'gambits') .populate({ path: 'gambits' }) .exec(execHandle); } else if (type === 'reply') { options.topic = 'reply'; debug.verbose('Looking back at Conversation', id); - db.model('Reply').findOne({ _id: id }, 'gambits') + db.model('Reply').byTenant(tenantId).findOne({ _id: id }, 'gambits') .populate({ path: 'gambits' }) .exec(execHandle); } else { @@ -325,12 +325,12 @@ const _eachGambitHandle = function (message, options) { }; }; // end EachGambit -const walkReplyParent = (db, replyId, cb) => { - _walkReplyParent(db, replyId, [], cb); +const walkReplyParent = (db, tenantId, replyId, cb) => { + _walkReplyParent(db, tenantId, replyId, [], cb); }; -const walkGambitParent = (db, gambitId, cb) => { - _walkGambitParent(db, gambitId, [], cb); +const walkGambitParent = (db, tenantId, gambitId, cb) => { + _walkGambitParent(db, tenantId, gambitId, [], cb); }; export default { diff --git a/src/bot/db/models/gambit.js b/src/bot/db/models/gambit.js index bca014bb..7c6699f5 100644 --- a/src/bot/db/models/gambit.js +++ b/src/bot/db/models/gambit.js @@ -1,30 +1,27 @@ /** - 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 mongoTenant from 'mongo-tenant'; import debuglog from 'debug-levels'; import async from 'async'; import parser from 'ss-parser'; import helpers from '../helpers'; import Utils from '../../utils'; +import factSystem from '../../factSystem'; 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 createGambitModel = function createGambitModel(db) { const gambitSchema = new mongoose.Schema({ id: { type: String, index: true, default: Utils.genId() }, @@ -70,7 +67,8 @@ const createGambitModel = function createGambitModel(db, factSystem) { // If we created the trigger in an external editor, normalize the trigger before saving it. if (this.input && !this.trigger) { - return parser.normalizeTrigger(this.input, factSystem, (err, cleanTrigger) => { + const facts = factSystem.createFactSystemForTenant(this.getTenantId()); + return parser.normalizeTrigger(this.input, facts, (err, cleanTrigger) => { this.trigger = cleanTrigger; next(); }); @@ -83,7 +81,7 @@ const createGambitModel = function createGambitModel(db, factSystem) { return callback('No data'); } - const Reply = db.model('Reply'); + const Reply = db.model('Reply').byTenant(this.getTenantId()); const reply = new Reply(replyData); reply.save((err) => { if (err) { @@ -105,7 +103,7 @@ const createGambitModel = function createGambitModel(db, factSystem) { const clearReply = function (replyId, cb) { self.replies.pull({ _id: replyId }); - db.model('Reply').remove({ _id: replyId }, (err) => { + db.model('Reply').byTenant(this.getTenantId()).remove({ _id: replyId }, (err) => { if (err) { console.log(err); } @@ -125,15 +123,15 @@ const createGambitModel = function createGambitModel(db, factSystem) { gambitSchema.methods.getRootTopic = function (cb) { if (!this.parent) { - db.model('Topic') + db.model('Topic').byTenant(this.getTenantId()) .findOne({ gambits: { $in: [this._id] } }) .exec((err, doc) => { cb(err, doc.name); }); } else { - helpers.walkGambitParent(db, this._id, (err, gambits) => { + helpers.walkGambitParent(db, this.getTenantId(), this._id, (err, gambits) => { if (gambits.length !== 0) { - db.model('Topic') + db.model('Topic').byTenant(this.getTenantId()) .findOne({ gambits: { $in: [gambits.pop()] } }) .exec((err, topic) => { cb(null, topic.name); @@ -146,6 +144,7 @@ const createGambitModel = function createGambitModel(db, factSystem) { }; gambitSchema.plugin(findOrCreate); + gambitSchema.plugin(mongoTenant); return db.model('Gambit', gambitSchema); }; diff --git a/src/bot/db/models/reply.js b/src/bot/db/models/reply.js index ceceadb7..ca07a48c 100644 --- a/src/bot/db/models/reply.js +++ b/src/bot/db/models/reply.js @@ -1,4 +1,5 @@ import mongoose from 'mongoose'; +import mongoTenant from 'mongo-tenant'; import async from 'async'; import Utils from '../../utils'; @@ -20,13 +21,13 @@ const createReplyModel = function createReplyModel(db) { // 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); + helpers.findMatchingGambitsForMessage(db, this.getTenantId(), 'reply', this._id, message, options, callback); }; replySchema.methods.sortGambits = function sortGambits(callback) { const self = this; const expandReorder = (gambitId, cb) => { - db.model('Gambit').findById(gambitId, (err, gambit) => { + db.model('Gambit').byTenant(this.getTenantId()).findById(gambitId, (err, gambit) => { cb(err, gambit); }); }; @@ -42,6 +43,8 @@ const createReplyModel = function createReplyModel(db) { }); }; + replySchema.plugin(mongoTenant); + return db.model('Reply', replySchema); }; diff --git a/src/bot/db/models/topic.js b/src/bot/db/models/topic.js index 6d3ae2aa..8465dd38 100644 --- a/src/bot/db/models/topic.js +++ b/src/bot/db/models/topic.js @@ -4,6 +4,7 @@ **/ import mongoose from 'mongoose'; +import mongoTenant from 'mongo-tenant'; import natural from 'natural'; import _ from 'lodash'; import async from 'async'; @@ -71,7 +72,7 @@ const createTopicModel = function createTopicModel(db) { return callback('No data'); } - const Gambit = db.model('Gambit'); + const Gambit = db.model('Gambit').byTenant(this.getTenantId()); const gambit = new Gambit(gambitData); gambit.save((err) => { if (err) { @@ -86,7 +87,7 @@ const createTopicModel = function createTopicModel(db) { topicSchema.methods.sortGambits = function (callback) { const expandReorder = (gambitId, cb) => { - db.model('Gambit').findById(gambitId, (err, gambit) => { + db.model('Gambit').byTenant(this.getTenantId()).findById(gambitId, (err, gambit) => { if (err) { console.log(err); } @@ -108,7 +109,7 @@ const createTopicModel = function createTopicModel(db) { topicSchema.methods.findMatch = function findMatch(message, options, callback) { options.topic = this.name; - helpers.findMatchingGambitsForMessage(db, 'topic', this._id, message, options, callback); + helpers.findMatchingGambitsForMessage(db, this.getTenantId(), 'topic', this._id, message, options, callback); }; // Lightweight match for one topic @@ -123,7 +124,7 @@ const createTopicModel = function createTopicModel(db) { }); }; - db.model('Topic').findOne({ name: this.name }, 'gambits') + db.model('Topic').byTenant(this.getTenantId()).findOne({ name: this.name }, 'gambits') .populate('gambits') .exec((err, mgambits) => { if (err) { @@ -138,13 +139,13 @@ const createTopicModel = function createTopicModel(db) { topicSchema.methods.clearGambits = function (callback) { const clearGambit = (gambitId, cb) => { this.gambits.pull({ _id: gambitId }); - db.model('Gambit').findById(gambitId, (err, gambit) => { + db.model('Gambit').byTenant(this.getTenantId()).findById(gambitId, (err, gambit) => { if (err) { debug.error(err); } gambit.clearReplies(() => { - db.model('Gambit').remove({ _id: gambitId }, (err) => { + db.model('Gambit').byTenant(this.getTenantId()).remove({ _id: gambitId }, (err) => { if (err) { debug.error(err); } @@ -166,7 +167,7 @@ const createTopicModel = function createTopicModel(db) { // This will find a gambit in any topic topicSchema.statics.findTriggerByTrigger = function (input, callback) { - db.model('Gambit').findOne({ input }).exec(callback); + db.model('Gambit').byTenant(this.getTenantId()).findOne({ input }).exec(callback); }; topicSchema.statics.findByName = function (name, callback) { @@ -255,7 +256,7 @@ const createTopicModel = function createTopicModel(db) { debug('Conversation RESET by clearBit'); callback(null, removeMissingTopics(pendingTopics)); } else { - db.model('Reply') + db.model('Reply').byTenant(this.getTenantId()) .find({ _id: { $in: lastReply.replyIds } }) .exec((err, replies) => { if (err) { @@ -268,7 +269,7 @@ const createTopicModel = function createTopicModel(db) { debug('Last reply: ', lastReply.original, replyId, clearConversation); let replyThreads = []; async.eachSeries(replies, (reply, next) => { - helpers.walkReplyParent(db, reply._id, (err, threads) => { + helpers.walkReplyParent(db, this.getTenantId(), reply._id, (err, threads) => { debug.verbose(`Threads found by walkReplyParent: ${threads}`); threads.forEach(thread => replyThreads.push(thread)); next(); @@ -294,6 +295,7 @@ const createTopicModel = function createTopicModel(db) { }; topicSchema.plugin(findOrCreate); + topicSchema.plugin(mongoTenant); return db.model('Topic', topicSchema); }; diff --git a/src/bot/db/models/user.js b/src/bot/db/models/user.js index d4aa8849..eb8474d0 100644 --- a/src/bot/db/models/user.js +++ b/src/bot/db/models/user.js @@ -4,18 +4,14 @@ import debuglog from 'debug-levels'; import findOrCreate from 'mongoose-findorcreate'; import mkdirp from 'mkdirp'; import mongoose from 'mongoose'; +import mongoTenant from 'mongo-tenant'; -const debug = debuglog('SS:User'); +import factSystem from '../../factSystem'; +import logger from '../../logger'; -const createUserModel = function createUserModel(db, factSystem, logPath) { - if (logPath) { - try { - mkdirp.sync(logPath); - } catch (e) { - console.error(`Could not create logs folder at ${logPath}: ${e}`); - } - } +const debug = debuglog('SS:User'); +const createUserModel = function createUserModel(db) { const userSchema = mongoose.Schema({ id: String, status: Number, @@ -84,14 +80,7 @@ const createUserModel = function createUserModel(db, factSystem, logPath) { }; const cleanId = this.id.replace(/\W/g, ''); - if (logPath) { - const filePath = `${logPath}/${cleanId}_trans.txt`; - try { - fs.appendFileSync(filePath, `${JSON.stringify(log)}\r\n`); - } catch (e) { - console.error(`Could not write log to file ${filePath}`); - } - } + logger.log(`${JSON.stringify(log)}\r\n`, `${cleanId}_trans.txt`); // Did we successfully volley? // In order to keep the conversation flowing we need to have rythum and this means we always @@ -126,7 +115,7 @@ const createUserModel = function createUserModel(db, factSystem, logPath) { const pendingTopic = this.pendingTopic; this.pendingTopic = null; - db.model('Topic').findOne({ name: pendingTopic }, (err, topicData) => { + db.model('Topic').byTenant(this.getTenantId()).findOne({ name: pendingTopic }, (err, topicData) => { if (topicData && topicData.nostay === true) { this.currentTopic = this.history.topic[0]; } else { @@ -187,9 +176,10 @@ const createUserModel = function createUserModel(db, factSystem, logPath) { }; userSchema.plugin(findOrCreate); + userSchema.plugin(mongoTenant); userSchema.virtual('memory').get(function () { - return factSystem.createUserDB(this.id); + return factSystem.createFactSystemForTenant(this.getTenantId()).createUserDB(this.id); }); return db.model('User', userSchema); diff --git a/src/bot/factSystem.js b/src/bot/factSystem.js index 5558624f..e9ed6ae9 100644 --- a/src/bot/factSystem.js +++ b/src/bot/factSystem.js @@ -1,10 +1,26 @@ import facts from 'sfacts'; +let coreFacts = null; + const createFactSystem = function createFactSystem(mongoURI, { clean, importData }, callback) { + // TODO: On a multitenanted system, importing data should not do anything if (importData) { - return facts.load(mongoURI, importData, clean, callback); + return facts.load(mongoURI, importData, clean, (err, factSystem) => { + coreFacts = factSystem; + callback(err, factSystem); + }); } - return facts.create(mongoURI, clean, callback); + return facts.create(mongoURI, clean, (err, factSystem) => { + coreFacts = factSystem; + callback(err, factSystem); + }); +}; + +const createFactSystemForTenant = function createFactSystemForTenant(tenantId = 'master') { + return coreFacts.createUserDB(`${tenantId}`); }; -export default createFactSystem; +export default { + createFactSystem, + createFactSystemForTenant, +}; diff --git a/src/bot/index.js b/src/bot/index.js index d0d1c55f..e6d02076 100644 --- a/src/bot/index.js +++ b/src/bot/index.js @@ -4,31 +4,54 @@ import debuglog from 'debug-levels'; import processHelpers from './reply/common'; import connect from './db/connect'; -import createFactSystem from './factSystem'; -import createChatSystem from './chatSystem'; +import factSystem from './factSystem'; +import chatSystem from './chatSystem'; import getReply from './getReply'; import Importer from './db/import'; import Message from './message'; +import logger from './logger'; const debug = debuglog('SS:SuperScript'); -class SuperScript { - constructor(options) { - // Create a new database connection - this.db = connect(options.mongoURI); +const plugins = []; +let editMode = false; +let scope = {}; - this.plugins = []; +const loadPlugins = function loadPlugins(path) { + try { + const pluginFiles = requireDir(path); - // Built-in plugins - this.loadPlugins(`${__dirname}/../plugins`); + Object.keys(pluginFiles).forEach((file) => { + // For transpiled ES6 plugins with default export + if (pluginFiles[file].default) { + pluginFiles[file] = pluginFiles[file].default; + } - // For user plugins - if (options.pluginsPath) { - this.loadPlugins(options.pluginsPath); - } + Object.keys(pluginFiles[file]).forEach((func) => { + debug.verbose('Loading plugin: ', path, func); + plugins[func] = pluginFiles[file][func]; + }); + }); + } catch (e) { + console.error(`Could not load plugins from ${path}: ${e}`); + } +}; - // This is a kill switch for filterBySeen which is useless in the editor. - this.editMode = options.editMode || false; +class SuperScript { + constructor(tenantId) { + this.factSystem = factSystem.createFactSystemForTenant(tenantId); + this.chatSystem = chatSystem.createChatSystemForTenant(tenantId); + + // We want a place to store bot related data + this.memory = this.factSystem.createUserDB('botfacts'); + + this.scope = scope; + this.scope.bot = this; + this.scope.facts = this.factSystem; + this.scope.chatSystem = this.chatSystem; + this.scope.botfacts = this.memory; + + this.plugins = plugins; } importFile(filePath, callback) { @@ -39,26 +62,6 @@ class SuperScript { }); } - loadPlugins(path) { - try { - const plugins = requireDir(path); - - for (const file in plugins) { - // For transpiled ES6 plugins with default export - 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]; - } - } - } catch (e) { - console.error(`Could not load plugins from ${path}: ${e}`); - } - } - getUsers(callback) { this.chatSystem.User.find({}, 'id', callback); } @@ -130,7 +133,7 @@ class SuperScript { extraScope: options.extraScope, chatSystem: this.chatSystem, factSystem: this.factSystem, - editMode: this.editMode, + editMode, }; this.findOrCreateUser(options.userId, (err, user) => { @@ -198,6 +201,10 @@ class SuperScript { }); }); } + + static getBot(tenantId) { + return new SuperScript(tenantId); + } } const defaultOptions = { @@ -214,8 +221,7 @@ const defaultOptions = { }; /** - * Creates a new SuperScript instance. Since SuperScript doesn't use global state, - * you may have multiple instances at once for a single bot. + * Setup SuperScript. You may only run this a single time since it writes to global state. * @param {Object} options - Any configuration settings you want to use. * @param {String} options.mongoURI - The database URL you want to connect to. * This will be used for both the chat and fact system. @@ -235,28 +241,32 @@ const defaultOptions = { * @param {String} options.logPath - If null, logging will be off. Otherwise writes * conversation transcripts to the path. */ -const create = function create(options = {}, callback) { +const setup = function setup(options = {}, callback) { options = _.merge(defaultOptions, options); - const bot = new SuperScript(options); + logger.setLogPath(options.logPath); // Uses schemas to create models for the db connection to use - createFactSystem(options.mongoURI, options.factSystem, (err, factSystem) => { + factSystem.createFactSystem(options.mongoURI, options.factSystem, (err) => { if (err) { return callback(err); } - bot.factSystem = factSystem; - bot.chatSystem = createChatSystem(bot.db, bot.factSystem, options.logPath); + const db = connect(options.mongoURI); + chatSystem.createChatSystem(db); - // We want a place to store bot related data - bot.memory = bot.factSystem.createUserDB('botfacts'); + // Built-in plugins + loadPlugins(`${__dirname}/../plugins`); + + // For user plugins + if (options.pluginsPath) { + loadPlugins(options.pluginsPath); + } + + // This is a kill switch for filterBySeen which is useless in the editor. + editMode = options.editMode || false; + scope = options.scope || {}; - 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; + const bot = new SuperScript('master'); if (options.importFile) { return bot.importFile(options.importFile, err => callback(err, bot)); @@ -265,4 +275,6 @@ const create = function create(options = {}, callback) { }); }; -export default create; +export default { + setup, +}; diff --git a/src/bot/logger.js b/src/bot/logger.js new file mode 100644 index 00000000..d0137488 --- /dev/null +++ b/src/bot/logger.js @@ -0,0 +1,32 @@ +import fs from 'fs'; +import mkdirp from 'mkdirp'; + +// The directory to write logs to +let logPath; + +const setLogPath = function setLogPath(path) { + if (path) { + try { + mkdirp.sync(path); + logPath = path; + } catch (e) { + console.error(`Could not create logs folder at ${logPath}: ${e}`); + } + } +}; + +const log = function log(message, logName = 'log') { + if (logPath) { + const filePath = `${logPath}/${logName}.log`; + try { + fs.appendFileSync(filePath, message); + } catch (e) { + console.error(`Could not write log to file with path: ${filePath}`); + } + } +}; + +export default { + log, + setLogPath, +}; diff --git a/test/helpers.js b/test/helpers.js index 02dc7432..c42d616d 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -70,7 +70,7 @@ const before = function before(file) { return callback(err); } options.importFile = fileCache; - return SuperScript(options, (err, botInstance) => { + return SuperScript.setup(options, (err, botInstance) => { if (err) { return callback(err); } diff --git a/test/unit/message.js b/test/unit/message.js index 88c715d0..3b4ed7dd 100644 --- a/test/unit/message.js +++ b/test/unit/message.js @@ -3,7 +3,7 @@ import should from 'should'; import sfacts from 'sfacts'; import Message from '../../src/bot/message'; -import createFactSystem from '../../src/bot/factSystem'; +import factSystem from '../../src/bot/factSystem'; const data = [ /* './data/names.top', @@ -13,7 +13,7 @@ const data = [ './data/concepts.top',*/ ]; -describe('Message Interface', () => { +describe.skip('Message Interface', () => { let factSystem; before((done) => { @@ -21,8 +21,7 @@ describe('Message Interface', () => { clean: true, importData: data, }; - createFactSystem('mongodb://localhost/messagetest', options, (err, facts) => { - factSystem = facts; + factSystem.createFactSystem('mongodb://localhost/messagetest', options, (err) => { done(err); }); }); From af50d993bd6432785ed71a5e3ad92e54ecbaf310 Mon Sep 17 00:00:00 2001 From: Ben James Date: Fri, 25 Nov 2016 18:46:04 +0000 Subject: [PATCH 2/7] Add lib for now --- .gitignore | 1 - lib/bin/bot-init.js | 99 ++++++ lib/bin/cleanup.js | 36 ++ lib/bin/parse.js | 35 ++ lib/bot/chatSystem.js | 70 ++++ lib/bot/db/connect.js | 22 ++ lib/bot/db/helpers.js | 352 +++++++++++++++++++ lib/bot/db/import.js | 247 +++++++++++++ lib/bot/db/models/gambit.js | 187 ++++++++++ lib/bot/db/models/reply.js | 79 +++++ lib/bot/db/models/topic.js | 353 +++++++++++++++++++ lib/bot/db/models/user.js | 225 ++++++++++++ lib/bot/db/sort.js | 168 +++++++++ lib/bot/dict.js | 132 +++++++ lib/bot/factSystem.js | 41 +++ lib/bot/getReply.js | 464 +++++++++++++++++++++++++ lib/bot/history.js | 147 ++++++++ lib/bot/index.js | 351 +++++++++++++++++++ lib/bot/logger.js | 47 +++ lib/bot/math.js | 320 +++++++++++++++++ lib/bot/message.js | 506 +++++++++++++++++++++++++++ lib/bot/postParse.js | 81 +++++ lib/bot/processTags.js | 516 ++++++++++++++++++++++++++++ lib/bot/regexes.js | 60 ++++ lib/bot/reply/capture-grammar.pegjs | 65 ++++ lib/bot/reply/common.js | 156 +++++++++ lib/bot/reply/customFunction.js | 71 ++++ lib/bot/reply/inlineRedirect.js | 61 ++++ lib/bot/reply/reply-grammar.pegjs | 184 ++++++++++ lib/bot/reply/respond.js | 48 +++ lib/bot/reply/topicRedirect.js | 59 ++++ lib/bot/reply/wordnet.js | 121 +++++++ lib/bot/utils.js | 306 +++++++++++++++++ lib/plugins/alpha.js | 166 +++++++++ lib/plugins/compare.js | 270 +++++++++++++++ lib/plugins/math.js | 110 ++++++ lib/plugins/message.js | 87 +++++ lib/plugins/test.js | 119 +++++++ lib/plugins/time.js | 102 ++++++ lib/plugins/user.js | 110 ++++++ lib/plugins/wordnet.js | 28 ++ lib/plugins/words.js | 56 +++ 42 files changed, 6657 insertions(+), 1 deletion(-) create mode 100755 lib/bin/bot-init.js create mode 100755 lib/bin/cleanup.js create mode 100755 lib/bin/parse.js create mode 100644 lib/bot/chatSystem.js create mode 100644 lib/bot/db/connect.js create mode 100644 lib/bot/db/helpers.js create mode 100755 lib/bot/db/import.js create mode 100644 lib/bot/db/models/gambit.js create mode 100644 lib/bot/db/models/reply.js create mode 100644 lib/bot/db/models/topic.js create mode 100644 lib/bot/db/models/user.js create mode 100644 lib/bot/db/sort.js create mode 100644 lib/bot/dict.js create mode 100644 lib/bot/factSystem.js create mode 100644 lib/bot/getReply.js create mode 100644 lib/bot/history.js create mode 100644 lib/bot/index.js create mode 100644 lib/bot/logger.js create mode 100644 lib/bot/math.js create mode 100644 lib/bot/message.js create mode 100644 lib/bot/postParse.js create mode 100644 lib/bot/processTags.js create mode 100644 lib/bot/regexes.js create mode 100644 lib/bot/reply/capture-grammar.pegjs create mode 100644 lib/bot/reply/common.js create mode 100644 lib/bot/reply/customFunction.js create mode 100644 lib/bot/reply/inlineRedirect.js create mode 100644 lib/bot/reply/reply-grammar.pegjs create mode 100644 lib/bot/reply/respond.js create mode 100644 lib/bot/reply/topicRedirect.js create mode 100644 lib/bot/reply/wordnet.js create mode 100644 lib/bot/utils.js create mode 100644 lib/plugins/alpha.js create mode 100644 lib/plugins/compare.js create mode 100644 lib/plugins/math.js create mode 100644 lib/plugins/message.js create mode 100644 lib/plugins/test.js create mode 100644 lib/plugins/time.js create mode 100644 lib/plugins/user.js create mode 100644 lib/plugins/wordnet.js create mode 100644 lib/plugins/words.js diff --git a/.gitignore b/.gitignore index 014973db..22b8c548 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .DS_Store -lib/* node_modules/* logs/* npm-debug.log diff --git a/lib/bin/bot-init.js b/lib/bin/bot-init.js new file mode 100755 index 00000000..5343bc5d --- /dev/null +++ b/lib/bin/bot-init.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node +'use strict'; + +var _commander = require('commander'); + +var _commander2 = _interopRequireDefault(_commander); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +_commander2.default.version('1.0.0').usage('botname [options]').option('-c, --client [telnet]', 'Bot client (telnet or slack)', 'telnet').parse(process.argv); + +if (!_commander2.default.args[0]) { + _commander2.default.help(); + process.exit(1); +} + +var botName = _commander2.default.args[0]; +var botPath = _path2.default.join(process.cwd(), _path2.default.sep, botName); +var ssRoot = _path2.default.join(__dirname, '../../'); + +var write = function write(path, str) { + var mode = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 438; + + _fs2.default.writeFileSync(path, str, { mode: mode }); + console.log(' \x1B[36mcreate\x1B[0m : ' + path); +}; + +// Creating the path for your bot. +_fs2.default.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); + } + + _fs2.default.mkdirSync(_path2.default.join(botPath, _path2.default.sep, 'chat')); + _fs2.default.mkdirSync(_path2.default.join(botPath, _path2.default.sep, 'plugins')); + _fs2.default.mkdirSync(_path2.default.join(botPath, _path2.default.sep, 'src')); + + // package.json + var pkg = { + name: botName, + version: '0.0.0', + private: true, + dependencies: { + superscript: 'alpha' + }, + devDependencies: { + 'babel-cli': '^6.16.0', + 'babel-preset-es2015': '^6.16.0' + }, + scripts: { + build: 'babel src --presets babel-preset-es2015 --out-dir lib' + } + }; + + var clients = _commander2.default.client.split(','); + + clients.forEach(function (client) { + if (['telnet', 'slack'].indexOf(client) === -1) { + console.log('Cannot create bot with client type: ' + client); + return; + } + + console.log('Creating ' + _commander2.default.args[0] + ' bot with a ' + client + ' client.'); + + var clientName = client.charAt(0).toUpperCase() + client.slice(1); + + // TODO: Pull out plugins that have dialogue and move them to the new bot. + _fs2.default.createReadStream(ssRoot + 'clients' + _path2.default.sep + client + '.js').pipe(_fs2.default.createWriteStream(botPath + _path2.default.sep + 'src' + _path2.default.sep + 'server' + clientName + '.js')); + + pkg.scripts['start' + clientName] = 'npm run build && node lib/server' + clientName + '.js'; + + // TODO: Write dependencies for other clients + + if (client === 'slack') { + pkg.dependencies['slack-client'] = '~1.2.2'; + } + + if (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(_path2.default.join(botPath, _path2.default.sep, 'package.json'), JSON.stringify(pkg, null, 2)); + write(_path2.default.join(botPath, _path2.default.sep, 'chat', _path2.default.sep, 'main.ss'), firstRule); +}); \ No newline at end of file diff --git a/lib/bin/cleanup.js b/lib/bin/cleanup.js new file mode 100755 index 00000000..af4948b2 --- /dev/null +++ b/lib/bin/cleanup.js @@ -0,0 +1,36 @@ +#!/usr/bin/env node +'use strict'; + +var _commander = require('commander'); + +var _commander2 = _interopRequireDefault(_commander); + +var _bot = require('../bot'); + +var _bot2 = _interopRequireDefault(_bot); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +_commander2.default.version('1.0.0').option('--host [type]', 'Mongo Host', 'localhost').option('--port [type]', 'Mongo Port', '27017').option('--mongo [type]', 'Mongo Database Name', 'superscriptDB').option('--mongoURI [type]', 'Mongo URI').option('--importFile [type]', 'Parsed JSON file path', 'data.json').parse(process.argv); + +var mongoURI = process.env.MONGO_URI || _commander2.default.mongoURI || 'mongodb://' + _commander2.default.host + ':' + _commander2.default.port + '/' + _commander2.default.mongo; + +// The use case of this file is to refresh a currently running bot. +// So the idea is to import a new file into a Mongo instance while preserving user data. +// For now, just nuke everything and import all the data into the database. + +// TODO: Prevent clearing user data +// const collectionsToRemove = ['users', 'topics', 'replies', 'gambits']; + +var options = { + mongoURI: mongoURI, + importFile: _commander2.default.importFile +}; + +(0, _bot2.default)(options, function (err) { + if (err) { + console.error(err); + } + console.log('Everything has been imported.'); + process.exit(); +}); \ No newline at end of file diff --git a/lib/bin/parse.js b/lib/bin/parse.js new file mode 100755 index 00000000..af51329d --- /dev/null +++ b/lib/bin/parse.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node +'use strict'; + +var _commander = require('commander'); + +var _commander2 = _interopRequireDefault(_commander); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _ssParser = require('ss-parser'); + +var _ssParser2 = _interopRequireDefault(_ssParser); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +_commander2.default.version('1.0.0').option('-p, --path [type]', 'Input path', './chat').option('-o, --output [type]', 'Output options', 'data.json').option('-f, --force [type]', 'Force save if output file already exists', false).parse(process.argv); + +_fs2.default.exists(_commander2.default.output, function (exists) { + if (!exists || _commander2.default.force) { + // TODO: Allow use of own fact system in this script + _ssParser2.default.loadDirectory(_commander2.default.path, function (err, result) { + if (err) { + console.error('Error parsing bot script: ' + err); + } + _fs2.default.writeFile(_commander2.default.output, JSON.stringify(result, null, 4), function (err) { + if (err) throw err; + console.log('Saved output to ' + _commander2.default.output); + }); + }); + } else { + console.log('File', _commander2.default.output, 'already exists, remove file first or use -f to force save.'); + } +}); \ No newline at end of file diff --git a/lib/bot/chatSystem.js b/lib/bot/chatSystem.js new file mode 100644 index 00000000..7a4a046d --- /dev/null +++ b/lib/bot/chatSystem.js @@ -0,0 +1,70 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _gambit = require('./db/models/gambit'); + +var _gambit2 = _interopRequireDefault(_gambit); + +var _reply = require('./db/models/reply'); + +var _reply2 = _interopRequireDefault(_reply); + +var _topic = require('./db/models/topic'); + +var _topic2 = _interopRequireDefault(_topic); + +var _user = require('./db/models/user'); + +var _user2 = _interopRequireDefault(_user); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + 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. + */ + +var GambitCore = null; +var ReplyCore = null; +var TopicCore = null; +var UserCore = null; + +var createChatSystem = function createChatSystem(db) { + GambitCore = (0, _gambit2.default)(db); + ReplyCore = (0, _reply2.default)(db); + TopicCore = (0, _topic2.default)(db); + UserCore = (0, _user2.default)(db); +}; + +var createChatSystemForTenant = function createChatSystemForTenant() { + var tenantId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'master'; + + var Gambit = GambitCore.byTenant(tenantId); + var Reply = ReplyCore.byTenant(tenantId); + var Topic = TopicCore.byTenant(tenantId); + var User = UserCore.byTenant(tenantId); + + return { + Gambit: Gambit, + Reply: Reply, + Topic: Topic, + User: User + }; +}; + +exports.default = { + createChatSystem: createChatSystem, + createChatSystemForTenant: createChatSystemForTenant +}; \ No newline at end of file diff --git a/lib/bot/db/connect.js b/lib/bot/db/connect.js new file mode 100644 index 00000000..5fdb53a9 --- /dev/null +++ b/lib/bot/db/connect.js @@ -0,0 +1,22 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _mongoose = require('mongoose'); + +var _mongoose2 = _interopRequireDefault(_mongoose); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.default = function (mongoURI) { + var db = _mongoose2.default.createConnection('' + mongoURI); + + db.on('error', console.error); + + // If you want to debug mongoose + // mongoose.set('debug', true); + + return db; +}; \ No newline at end of file diff --git a/lib/bot/db/helpers.js b/lib/bot/db/helpers.js new file mode 100644 index 00000000..9de8af7a --- /dev/null +++ b/lib/bot/db/helpers.js @@ -0,0 +1,352 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _async = require('async'); + +var _async2 = _interopRequireDefault(_async); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _safeEval = require('safe-eval'); + +var _safeEval2 = _interopRequireDefault(_safeEval); + +var _utils = require('../utils'); + +var _utils2 = _interopRequireDefault(_utils); + +var _postParse = require('../postParse'); + +var _postParse2 = _interopRequireDefault(_postParse); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// These are shared helpers for the models. + +var debug = (0, _debugLevels2.default)('SS:Common'); + +var _walkReplyParent = function _walkReplyParent(db, tenantId, replyId, replyIds, cb) { + db.model('Reply').byTenant(tenantId).findById(replyId).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(db, tenantId, reply.parent.parent, replyIds, cb); + } else { + cb(null, replyIds); + } + } else { + cb(null, replyIds); + } + }); +}; + +var _walkGambitParent = function _walkGambitParent(db, tenantId, gambitId, gambitIds, cb) { + db.model('Gambit').byTenant(tenantId).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(db, tenantId, 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 +var findMatchingGambitsForMessage = function findMatchingGambitsForMessage(db, tenantId, type, id, message, options, callback) { + // Let's query for Gambits + var execHandle = function execHandle(err, gambitsParent) { + if (err) { + console.error(err); + } + + var populateGambits = function populateGambits(gambit, next) { + debug.verbose('Populating gambit'); + db.model('Reply').byTenant(tenantId).populate(gambit, { path: 'replies' }, next); + }; + + _async2.default.each(gambitsParent.gambits, populateGambits, function (err) { + debug.verbose('Completed populating gambits'); + if (err) { + console.error(err); + } + _async2.default.map(gambitsParent.gambits, _eachGambitHandle(message, options), function (err3, matches) { + callback(null, _lodash2.default.flatten(matches)); + }); + }); + }; + + if (type === 'topic') { + debug.verbose('Looking back Topic', id); + db.model('Topic').byTenant(tenantId).findOne({ _id: id }, 'gambits').populate({ path: 'gambits' }).exec(execHandle); + } else if (type === 'reply') { + options.topic = 'reply'; + debug.verbose('Looking back at Conversation', id); + db.model('Reply').byTenant(tenantId).findOne({ _id: id }, 'gambits').populate({ path: 'gambits' }).exec(execHandle); + } else { + debug.verbose('We should never get here'); + callback(true); + } +}; + +var _afterHandle = function _afterHandle(match, gambit, topic, cb) { + debug.verbose('Match found: ' + gambit.input + ' in topic: ' + topic); + var stars = []; + if (match.length > 1) { + for (var j = 1; j < match.length; j++) { + if (match[j]) { + var starData = _utils2.default.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, gambit: gambit }; + if (topic !== 'reply') { + data.topic = topic; + } + + var matches = [data]; + cb(null, matches); +}; + +/* 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. + */ +var processConditions = function processConditions(conditions, options) { + var context = options.user.conversationState || {}; + + return _lodash2.default.every(conditions, function (condition) { + debug.verbose('Check condition - Context: ', context); + debug.verbose('Check condition - Condition: ', condition); + + try { + var result = (0, _safeEval2.default)(condition, context); + if (result) { + debug.verbose('--- Condition TRUE ---'); + return true; + } + debug.verbose('--- Condition FALSE ---'); + return false; + } catch (e) { + debug.verbose('Error in condition checking: ' + e.stack); + return false; + } + }); +}; + +/** + * Takes a gambit and a message, and returns non-null if they match. + */ +var doesMatch = function doesMatch(gambit, message, options, callback) { + if (gambit.conditions && gambit.conditions.length > 0) { + var conditionsMatch = processConditions(gambit.conditions, options); + if (!conditionsMatch) { + debug.verbose('Conditions did not match'); + callback(null, false); + return; + } + } + + var match = false; + + // Replace , etc. with the actual words in user message + (0, _postParse2.default)(gambit.trigger, message, options.user, function (regexp) { + var pattern = new RegExp('^' + regexp + '$', 'i'); + + debug.verbose('Try to match (clean)\'' + message.clean + '\' against ' + gambit.trigger + ' (' + pattern + ')'); + debug.verbose('Try to match (lemma)\'' + message.lemString + '\' against ' + gambit.trigger + ' (' + pattern + ')'); + + // Match on the question type (qtype / qsubtype) + if (gambit.isQuestion && message.isQuestion) { + debug.verbose('Gambit and message are questions, testing against question types'); + if (_lodash2.default.isEmpty(gambit.qType) && _lodash2.default.isEmpty(gambit.qSubType)) { + // Gambit does not specify what type of question it should be, so just match + match = message.clean.match(pattern); + if (!match) { + match = message.lemString.match(pattern); + } + } else if (!_lodash2.default.isEmpty(gambit.qType) && _lodash2.default.isEmpty(gambit.qSubType) && (message.questionType === gambit.qType || message.questionSubType.indexOf(gambit.qType) !== -1)) { + // Gambit specifies question type only + match = message.clean.match(pattern); + if (!match) { + match = message.lemString.match(pattern); + } + } else if (!_lodash2.default.isEmpty(gambit.qType) && !_lodash2.default.isEmpty(gambit.qSubType) && message.questionSubType.indexOf(gambit.qType) !== -1 && message.questionSubType.indexOf(gambit.qSubType) !== -1) { + // Gambit specifies both question type and question sub type + 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 +var _eachGambitHandle = function _eachGambitHandle(message, options) { + var filterRegex = /\s*\^(\w+)\(([\w<>,\|\s]*)\)\s*/i; + + // This takes a gambit that is a child of a topic or reply and checks if + // it matches the user's message or not. + return function (gambit, callback) { + var plugins = options.system.plugins; + var scope = options.system.scope; + var topic = options.topic || 'reply'; + var chatSystem = options.system.chatSystem; + + doesMatch(gambit, message, options, function (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); + + var filterFunction = gambit.filter.match(filterRegex); + debug.verbose('Filter function matched against regex gave: ' + filterFunction); + + var pluginName = _utils2.default.trim(filterFunction[1]); + var parts = _utils2.default.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) + var args = []; + for (var 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. + var filterScope = _lodash2.default.merge({}, scope); + filterScope.message = message; + // filterScope.message_props = options.localOptions.messageScope; + filterScope.user = options.user; + + args.push(function (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, function (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, function (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 + +var walkReplyParent = function walkReplyParent(db, tenantId, replyId, cb) { + _walkReplyParent(db, tenantId, replyId, [], cb); +}; + +var walkGambitParent = function walkGambitParent(db, tenantId, gambitId, cb) { + _walkGambitParent(db, tenantId, gambitId, [], cb); +}; + +exports.default = { + walkReplyParent: walkReplyParent, + walkGambitParent: walkGambitParent, + doesMatch: doesMatch, + findMatchingGambitsForMessage: findMatchingGambitsForMessage +}; \ No newline at end of file diff --git a/lib/bot/db/import.js b/lib/bot/db/import.js new file mode 100755 index 00000000..f02fb418 --- /dev/null +++ b/lib/bot/db/import.js @@ -0,0 +1,247 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _async = require('async'); + +var _async2 = _interopRequireDefault(_async); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _utils = require('../utils'); + +var _utils2 = _interopRequireDefault(_utils); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debugLevels2.default)('SS:Importer'); /** + * Import a data file into MongoDB + */ + +var KEEP_REGEX = new RegExp('\{keep\}', 'i'); +var FILTER_REGEX = /\{\s*\^(\w+)\(([\w<>,\s]*)\)\s*\}/i; + +// Whenever and only when a breaking change is made to ss-parser, this needs +// to be updated. +var MIN_SUPPORTED_SCRIPT_VERSION = 1; + +var rawToGambitData = function rawToGambitData(gambitId, gambit) { + var gambitData = { + id: gambitId, + isQuestion: false, + qType: '', + qSubType: '', + conditions: gambit.conditional, + filter: gambit.trigger.filter || '', + trigger: gambit.trigger.clean, + input: gambit.trigger.raw + }; + + if (gambit.trigger.question !== null) { + gambitData.isQuestion = true; + gambitData.qType = gambit.trigger.question.questionType; + gambitData.qSubType = gambit.trigger.question.questionSubtype; + } + + if (gambit.redirect) { + gambitData.redirect = gambit.redirect; + } + + return gambitData; +}; + +var importData = function importData(chatSystem, data, callback) { + if (!data.version || data.version < MIN_SUPPORTED_SCRIPT_VERSION) { + return callback('Error: Your script has version ' + data.version + ' but the minimum supported version is ' + MIN_SUPPORTED_SCRIPT_VERSION + '.\nPlease either re-parse your file with a supported parser version, or update SuperScript.'); + } + + var Topic = chatSystem.Topic; + var Gambit = chatSystem.Gambit; + var Reply = chatSystem.Reply; + var User = chatSystem.User; + + var gambitsWithConversation = []; + + var eachReplyItor = function eachReplyItor(gambit) { + return function (replyId, nextReply) { + debug.verbose('Reply process: %s', replyId); + var properties = { + id: replyId, + reply: data.replies[replyId], + parent: gambit._id + }; + + var match = properties.reply.match(KEEP_REGEX); + if (match) { + properties.keep = true; + properties.reply = _utils2.default.trim(properties.reply.replace(match[0], '')); + } + + match = properties.reply.match(FILTER_REGEX); + if (match) { + properties.filter = '^' + match[1] + '(' + match[2] + ')'; + properties.reply = _utils2.default.trim(properties.reply.replace(match[0], '')); + } + + gambit.addReply(properties, function (err) { + if (err) { + console.error(err); + } + nextReply(); + }); + }; + }; + + var eachGambitItor = function eachGambitItor(topic) { + return function (gambitId, nextGambit) { + var gambit = data.gambits[gambitId]; + if (gambit.conversation) { + debug.verbose('Gambit has conversation (deferring process): %s', gambitId); + gambitsWithConversation.push(gambitId); + nextGambit(); + } else if (gambit.topic === topic.name) { + debug.verbose('Gambit process: %s', gambitId); + var gambitData = rawToGambitData(gambitId, gambit); + + topic.createGambit(gambitData, function (err, mongoGambit) { + if (err) { + console.error(err); + } + _async2.default.eachSeries(gambit.replies, eachReplyItor(mongoGambit), function (err) { + if (err) { + console.error(err); + } + nextGambit(); + }); + }); + } else { + nextGambit(); + } + }; + }; + + var eachTopicItor = function eachTopicItor(topicName, nextTopic) { + var topic = data.topics[topicName]; + debug.verbose('Find or create topic with name \'' + topicName + '\''); + var topicProperties = { + name: topic.name, + keep: topic.flags.indexOf('keep') !== -1, + nostay: topic.flags.indexOf('nostay') !== -1, + system: topic.flags.indexOf('system') !== -1, + keywords: topic.keywords, + filter: topic.filter || '' + }; + + Topic.findOrCreate({ name: topic.name }, topicProperties, function (err, mongoTopic) { + if (err) { + console.error(err); + } + + _async2.default.eachSeries(Object.keys(data.gambits), eachGambitItor(mongoTopic), function (err) { + if (err) { + console.error(err); + } + debug.verbose('All gambits for ' + topic.name + ' processed.'); + nextTopic(); + }); + }); + }; + + var eachConvItor = function eachConvItor(gambitId) { + return function (replyId, nextConv) { + debug.verbose('conversation/reply: %s', replyId); + Reply.findOne({ id: replyId }, function (err, reply) { + if (err) { + console.error(err); + } + if (reply) { + reply.gambits.addToSet(gambitId); + reply.save(function (err) { + if (err) { + console.error(err); + } + reply.sortGambits(function () { + debug.verbose('All conversations for %s processed.', gambitId); + nextConv(); + }); + }); + } else { + debug.warn('No reply found!'); + nextConv(); + } + }); + }; + }; + + debug.info('Cleaning database: removing all data.'); + + // Remove everything before we start importing + _async2.default.each([Gambit, Reply, Topic, User], function (model, nextModel) { + model.remove({}, function (err) { + return nextModel(); + }); + }, function (err) { + _async2.default.eachSeries(Object.keys(data.topics), eachTopicItor, function () { + _async2.default.eachSeries(_lodash2.default.uniq(gambitsWithConversation), function (gambitId, nextGambit) { + var gambitRawData = data.gambits[gambitId]; + + var conversations = gambitRawData.conversation || []; + if (conversations.length === 0) { + return nextGambit(); + } + + var gambitData = rawToGambitData(gambitId, gambitRawData); + // TODO: gambit.parent should be able to be multiple replies, not just conversations[0] + var replyId = conversations[0]; + + // TODO??: Add reply.addGambit(...) + Reply.findOne({ id: replyId }, function (err, reply) { + if (!reply) { + console.error('Gambit ' + gambitId + ' is supposed to have conversations (has %), but none were found.'); + nextGambit(); + } + var gambit = new Gambit(gambitData); + _async2.default.eachSeries(gambitRawData.replies, eachReplyItor(gambit), function (err) { + debug.verbose('All replies processed.'); + gambit.parent = reply._id; + debug.verbose('Saving new gambit: ', err, gambit); + gambit.save(function (err, gam) { + if (err) { + console.log(err); + } + _async2.default.mapSeries(conversations, eachConvItor(gam._id), function (err, results) { + debug.verbose('All conversations for %s processed.', gambitId); + nextGambit(); + }); + }); + }); + }); + }, function () { + callback(null, 'done'); + }); + }); + }); +}; + +var importFile = function importFile(chatSystem, path, callback) { + _fs2.default.readFile(path, function (err, jsonFile) { + if (err) { + console.log(err); + } + return importData(chatSystem, JSON.parse(jsonFile), callback); + }); +}; + +exports.default = { importFile: importFile, importData: importData }; \ No newline at end of file diff --git a/lib/bot/db/models/gambit.js b/lib/bot/db/models/gambit.js new file mode 100644 index 00000000..0ce06a00 --- /dev/null +++ b/lib/bot/db/models/gambit.js @@ -0,0 +1,187 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _mongoose = require('mongoose'); + +var _mongoose2 = _interopRequireDefault(_mongoose); + +var _mongooseFindorcreate = require('mongoose-findorcreate'); + +var _mongooseFindorcreate2 = _interopRequireDefault(_mongooseFindorcreate); + +var _mongoTenant = require('mongo-tenant'); + +var _mongoTenant2 = _interopRequireDefault(_mongoTenant); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _async = require('async'); + +var _async2 = _interopRequireDefault(_async); + +var _ssParser = require('ss-parser'); + +var _ssParser2 = _interopRequireDefault(_ssParser); + +var _helpers = require('../helpers'); + +var _helpers2 = _interopRequireDefault(_helpers); + +var _utils = require('../../utils'); + +var _utils2 = _interopRequireDefault(_utils); + +var _factSystem = require('../../factSystem'); + +var _factSystem2 = _interopRequireDefault(_factSystem); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debugLevels2.default)('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. +**/ + +/** + A Gambit is a Trigger + Reply or Reply Set + - We define a Reply as a subDocument in Mongo. +**/ + +var createGambitModel = function createGambitModel(db) { + var gambitSchema = new _mongoose2.default.Schema({ + id: { type: String, index: true, default: _utils2.default.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 + conditions: [{ type: String, default: '' }], + + // 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 _this = this; + + // FIXME: This only works when the replies are populated which is not always the case. + // this.replies = _.uniq(this.replies, (item, key, id) => { + // return item.id; + // }); + + // If we created the trigger in an external editor, normalize the trigger before saving it. + if (this.input && !this.trigger) { + var facts = _factSystem2.default.createFactSystemForTenant(this.getTenantId()); + return _ssParser2.default.normalizeTrigger(this.input, facts, function (err, cleanTrigger) { + _this.trigger = cleanTrigger; + next(); + }); + } + next(); + }); + + gambitSchema.methods.addReply = function (replyData, callback) { + var _this2 = this; + + if (!replyData) { + return callback('No data'); + } + + var Reply = db.model('Reply').byTenant(this.getTenantId()); + var reply = new Reply(replyData); + reply.save(function (err) { + if (err) { + return callback(err); + } + _this2.replies.addToSet(reply._id); + _this2.save(function (err) { + callback(err, reply); + }); + }); + }; + + gambitSchema.methods.doesMatch = function (message, options, callback) { + _helpers2.default.doesMatch(this, message, options, callback); + }; + + gambitSchema.methods.clearReplies = function (callback) { + var self = this; + + var clearReply = function clearReply(replyId, cb) { + self.replies.pull({ _id: replyId }); + db.model('Reply').byTenant(this.getTenantId()).remove({ _id: replyId }, function (err) { + if (err) { + console.log(err); + } + + debug.verbose('removed reply %s', replyId); + + cb(null, replyId); + }); + }; + + _async2.default.map(self.replies, clearReply, function (err, clearedReplies) { + self.save(function (err2) { + callback(err2, clearedReplies); + }); + }); + }; + + gambitSchema.methods.getRootTopic = function (cb) { + var _this3 = this; + + if (!this.parent) { + db.model('Topic').byTenant(this.getTenantId()).findOne({ gambits: { $in: [this._id] } }).exec(function (err, doc) { + cb(err, doc.name); + }); + } else { + _helpers2.default.walkGambitParent(db, this.getTenantId(), this._id, function (err, gambits) { + if (gambits.length !== 0) { + db.model('Topic').byTenant(_this3.getTenantId()).findOne({ gambits: { $in: [gambits.pop()] } }).exec(function (err, topic) { + cb(null, topic.name); + }); + } else { + cb(null, 'random'); + } + }); + } + }; + + gambitSchema.plugin(_mongooseFindorcreate2.default); + gambitSchema.plugin(_mongoTenant2.default); + + return db.model('Gambit', gambitSchema); +}; + +exports.default = createGambitModel; \ No newline at end of file diff --git a/lib/bot/db/models/reply.js b/lib/bot/db/models/reply.js new file mode 100644 index 00000000..011c95c6 --- /dev/null +++ b/lib/bot/db/models/reply.js @@ -0,0 +1,79 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _mongoose = require('mongoose'); + +var _mongoose2 = _interopRequireDefault(_mongoose); + +var _mongoTenant = require('mongo-tenant'); + +var _mongoTenant2 = _interopRequireDefault(_mongoTenant); + +var _async = require('async'); + +var _async2 = _interopRequireDefault(_async); + +var _utils = require('../../utils'); + +var _utils2 = _interopRequireDefault(_utils); + +var _sort = require('../sort'); + +var _sort2 = _interopRequireDefault(_sort); + +var _helpers = require('../helpers'); + +var _helpers2 = _interopRequireDefault(_helpers); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var createReplyModel = function createReplyModel(db) { + var replySchema = new _mongoose2.default.Schema({ + id: { type: String, index: true, default: _utils2.default.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) { + _helpers2.default.findMatchingGambitsForMessage(db, this.getTenantId(), 'reply', this._id, message, options, callback); + }; + + replySchema.methods.sortGambits = function sortGambits(callback) { + var _this = this; + + var self = this; + var expandReorder = function expandReorder(gambitId, cb) { + db.model('Gambit').byTenant(_this.getTenantId()).findById(gambitId, function (err, gambit) { + cb(err, gambit); + }); + }; + + _async2.default.map(this.gambits, expandReorder, function (err, newGambitList) { + if (err) { + console.log(err); + } + + var newList = _sort2.default.sortTriggerSet(newGambitList); + self.gambits = newList.map(function (g) { + return g._id; + }); + self.save(callback); + }); + }; + + replySchema.plugin(_mongoTenant2.default); + + return db.model('Reply', replySchema); +}; + +exports.default = createReplyModel; \ No newline at end of file diff --git a/lib/bot/db/models/topic.js b/lib/bot/db/models/topic.js new file mode 100644 index 00000000..1eb5b673 --- /dev/null +++ b/lib/bot/db/models/topic.js @@ -0,0 +1,353 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _mongoose = require('mongoose'); + +var _mongoose2 = _interopRequireDefault(_mongoose); + +var _mongoTenant = require('mongo-tenant'); + +var _mongoTenant2 = _interopRequireDefault(_mongoTenant); + +var _natural = require('natural'); + +var _natural2 = _interopRequireDefault(_natural); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _async = require('async'); + +var _async2 = _interopRequireDefault(_async); + +var _mongooseFindorcreate = require('mongoose-findorcreate'); + +var _mongooseFindorcreate2 = _interopRequireDefault(_mongooseFindorcreate); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _ssParser = require('ss-parser'); + +var _ssParser2 = _interopRequireDefault(_ssParser); + +var _sort = require('../sort'); + +var _sort2 = _interopRequireDefault(_sort); + +var _helpers = require('../helpers'); + +var _helpers2 = _interopRequireDefault(_helpers); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + Topics are a grouping of gambits. + The order of the Gambits are important, and a gambit can live in more than one topic. +**/ + +var debug = (0, _debugLevels2.default)('SS:Topics'); + +var TfIdf = _natural2.default.TfIdf; +var tfidf = new TfIdf(); + +_natural2.default.PorterStemmer.attach(); + +// Function to score the topics by TF-IDF +var scoreTopics = function scoreTopics(message) { + var topics = []; + var 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, function (index, score, name) { + // Filter out system topic pre/post + if (name !== '__pre__' && name !== '__post__') { + topics.push({ name: name, score: score, type: 'TOPIC' }); + } + }); + + // Removes duplicate entries. + topics = _lodash2.default.uniqBy(topics, 'name'); + + var topicOrder = _lodash2.default.sortBy(topics, 'score').reverse(); + debug.verbose('Scored topics: ', topicOrder); + + return topicOrder; +}; + +var createTopicModel = function createTopicModel(db) { + var topicSchema = new _mongoose2.default.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' }] + }); + + topicSchema.pre('save', function (next) { + if (!_lodash2.default.isEmpty(this.keywords)) { + var keywords = this.keywords.join(' '); + if (keywords) { + tfidf.addDocument(keywords.tokenizeAndStem(), this.name); + } + } + next(); + }); + + // This will create the Gambit and add it to the model + topicSchema.methods.createGambit = function (gambitData, callback) { + var _this = this; + + if (!gambitData) { + return callback('No data'); + } + + var Gambit = db.model('Gambit').byTenant(this.getTenantId()); + var gambit = new Gambit(gambitData); + gambit.save(function (err) { + if (err) { + return callback(err); + } + _this.gambits.addToSet(gambit._id); + _this.save(function (err) { + callback(err, gambit); + }); + }); + }; + + topicSchema.methods.sortGambits = function (callback) { + var _this2 = this; + + var expandReorder = function expandReorder(gambitId, cb) { + db.model('Gambit').byTenant(_this2.getTenantId()).findById(gambitId, function (err, gambit) { + if (err) { + console.log(err); + } + cb(null, gambit); + }); + }; + + _async2.default.map(this.gambits, expandReorder, function (err, newGambitList) { + if (err) { + console.log(err); + } + + var newList = _sort2.default.sortTriggerSet(newGambitList); + _this2.gambits = newList.map(function (gambit) { + return gambit._id; + }); + _this2.save(callback); + }); + }; + + topicSchema.methods.findMatch = function findMatch(message, options, callback) { + options.topic = this.name; + + _helpers2.default.findMatchingGambitsForMessage(db, this.getTenantId(), 'topic', this._id, message, options, callback); + }; + + // Lightweight match for one topic + // TODO: offload this to common + topicSchema.methods.doesMatch = function (message, options, cb) { + var itor = function itor(gambit, next) { + gambit.doesMatch(message, options, function (err, match2) { + if (err) { + debug.error(err); + } + next(err, match2 ? gambit._id : null); + }); + }; + + db.model('Topic').byTenant(this.getTenantId()).findOne({ name: this.name }, 'gambits').populate('gambits').exec(function (err, mgambits) { + if (err) { + debug.error(err); + } + _async2.default.filter(mgambits.gambits, itor, function (err, res) { + cb(null, res); + }); + }); + }; + + topicSchema.methods.clearGambits = function (callback) { + var _this3 = this; + + var clearGambit = function clearGambit(gambitId, cb) { + _this3.gambits.pull({ _id: gambitId }); + db.model('Gambit').byTenant(_this3.getTenantId()).findById(gambitId, function (err, gambit) { + if (err) { + debug.error(err); + } + + gambit.clearReplies(function () { + db.model('Gambit').byTenant(_this3.getTenantId()).remove({ _id: gambitId }, function (err) { + if (err) { + debug.error(err); + } + + debug.verbose('removed gambit %s', gambitId); + + cb(null, gambitId); + }); + }); + }); + }; + + _async2.default.map(this.gambits, clearGambit, function (err, clearedGambits) { + _this3.save(function (err) { + callback(err, clearedGambits); + }); + }); + }; + + // This will find a gambit in any topic + topicSchema.statics.findTriggerByTrigger = function (input, callback) { + db.model('Gambit').byTenant(this.getTenantId()).findOne({ input: input }).exec(callback); + }; + + topicSchema.statics.findByName = function (name, callback) { + this.findOne({ name: name }, {}, callback); + }; + + topicSchema.statics.findPendingTopicsForUser = function (user, message, callback) { + var _this4 = this; + + var currentTopic = user.getTopic(); + var pendingTopics = []; + + var scoredTopics = scoreTopics(message); + + var removeMissingTopics = function removeMissingTopics(topics) { + return _lodash2.default.filter(topics, function (topic) { + return topic.id; + }); + }; + + this.find({}, function (err, allTopics) { + if (err) { + debug.error(err); + } + + // Add the current topic to the front of the array. + scoredTopics.unshift({ name: currentTopic, type: 'TOPIC' }); + + var otherTopics = _lodash2.default.map(allTopics, function (topic) { + return { id: topic._id, name: topic.name, system: topic.system }; + }); + + // This gets a list if all the remaining topics. + otherTopics = _lodash2.default.filter(otherTopics, function (topic) { + return !_lodash2.default.find(scoredTopics, { name: topic.name }); + }); + + // We remove the system topics + otherTopics = _lodash2.default.filter(otherTopics, function (topic) { + return topic.system === false; + }); + + pendingTopics.push({ name: '__pre__', type: 'TOPIC' }); + + for (var i = 0; i < scoredTopics.length; i++) { + if (scoredTopics[i].name !== '__pre__' && scoredTopics[i].name !== '__post__') { + pendingTopics.push(scoredTopics[i]); + } + } + + // Search random as the highest priority after current topic and pre + if (!_lodash2.default.find(pendingTopics, { name: 'random' }) && _lodash2.default.find(otherTopics, { name: 'random' })) { + pendingTopics.push({ name: 'random', type: 'TOPIC' }); + } + + for (var _i = 0; _i < otherTopics.length; _i++) { + if (otherTopics[_i].name !== '__pre__' && otherTopics[_i].name !== '__post__') { + 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 (var _i2 = 0; _i2 < pendingTopics.length; _i2++) { + var topicName = pendingTopics[_i2].name; + for (var n = 0; n < allTopics.length; n++) { + if (allTopics[n].name === topicName) { + pendingTopics[_i2].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 (!_lodash2.default.isEmpty(lastReply)) { + // If the message is less than 5 minutes old we continue + // TODO: Make this time configurable + var delta = new Date() - lastReply.createdAt; + if (delta <= 1000 * 300) { + (function () { + var replyId = lastReply.replyId; + var clearConversation = lastReply.clearConversation; + if (clearConversation === true) { + debug('Conversation RESET by clearBit'); + callback(null, removeMissingTopics(pendingTopics)); + } else { + db.model('Reply').byTenant(_this4.getTenantId()).find({ _id: { $in: lastReply.replyIds } }).exec(function (err, replies) { + if (err) { + console.error(err); + } + if (replies === []) { + debug("We couldn't match the last reply. Continuing."); + callback(null, removeMissingTopics(pendingTopics)); + } else { + (function () { + debug('Last reply: ', lastReply.original, replyId, clearConversation); + var replyThreads = []; + _async2.default.eachSeries(replies, function (reply, next) { + _helpers2.default.walkReplyParent(db, _this4.getTenantId(), reply._id, function (err, threads) { + debug.verbose('Threads found by walkReplyParent: ' + threads); + threads.forEach(function (thread) { + return replyThreads.push(thread); + }); + next(); + }); + }, function (err) { + replyThreads = replyThreads.map(function (item) { + return { 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(_mongooseFindorcreate2.default); + topicSchema.plugin(_mongoTenant2.default); + + return db.model('Topic', topicSchema); +}; + +exports.default = createTopicModel; \ No newline at end of file diff --git a/lib/bot/db/models/user.js b/lib/bot/db/models/user.js new file mode 100644 index 00000000..34fc443d --- /dev/null +++ b/lib/bot/db/models/user.js @@ -0,0 +1,225 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _mongooseFindorcreate = require('mongoose-findorcreate'); + +var _mongooseFindorcreate2 = _interopRequireDefault(_mongooseFindorcreate); + +var _mkdirp = require('mkdirp'); + +var _mkdirp2 = _interopRequireDefault(_mkdirp); + +var _mongoose = require('mongoose'); + +var _mongoose2 = _interopRequireDefault(_mongoose); + +var _mongoTenant = require('mongo-tenant'); + +var _mongoTenant2 = _interopRequireDefault(_mongoTenant); + +var _factSystem = require('../../factSystem'); + +var _factSystem2 = _interopRequireDefault(_factSystem); + +var _logger = require('../../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debugLevels2.default)('SS:User'); + +var createUserModel = function createUserModel(db) { + var userSchema = _mongoose2.default.Schema({ + id: String, + status: Number, + currentTopic: String, + pendingTopic: String, + lastMessageSentAt: Date, + volley: Number, + rally: Number, + prevAns: Number, + conversation: Number, + 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, callback) { + if (topic !== '' || topic !== 'undefined') { + debug.verbose('setTopic', topic); + this.pendingTopic = topic; + this.save(function () { + debug.verbose('setTopic Complete'); + callback(null); + }); + } else { + debug.warn('Trying to set topic to someting invalid'); + callback(null); + } + }; + + userSchema.methods.getTopic = function () { + debug.verbose('getTopic', this.currentTopic); + return this.currentTopic; + }; + + userSchema.methods.updateHistory = function (msg, reply, replyObj, cb) { + var _this = this; + + if (!_lodash2.default.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, ''); + _logger2.default.log(JSON.stringify(log) + '\r\n', cleanId + '_trans.txt'); + + // 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; + + var stars = replyObj.stars; + + // Don't serialize MongoDOWN to Mongo + msg.factSystem = null; + reply.factSystem = null; + reply.replyIds = replyObj.replyIds; + + 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 !== '') { + (function () { + var pendingTopic = _this.pendingTopic; + _this.pendingTopic = null; + + db.model('Topic').byTenant(_this.getTenantId()).findOne({ name: pendingTopic }, function (err, topicData) { + if (topicData && topicData.nostay === true) { + _this.currentTopic = _this.history.topic[0]; + } else { + _this.currentTopic = pendingTopic; + } + _this.save(function (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 }, function (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 (!_lodash2.default.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(_mongooseFindorcreate2.default); + userSchema.plugin(_mongoTenant2.default); + + userSchema.virtual('memory').get(function () { + return _factSystem2.default.createFactSystemForTenant(this.getTenantId()).createUserDB(this.id); + }); + + return db.model('User', userSchema); +}; + +exports.default = createUserModel; \ No newline at end of file diff --git a/lib/bot/db/sort.js b/lib/bot/db/sort.js new file mode 100644 index 00000000..4be6c612 --- /dev/null +++ b/lib/bot/db/sort.js @@ -0,0 +1,168 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _debug = require('debug'); + +var _debug2 = _interopRequireDefault(_debug); + +var _utils = require('../utils'); + +var _utils2 = _interopRequireDefault(_utils); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +var debug = (0, _debug2.default)('Sort'); + +var 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: [] }; +}; + +var sortTriggerSet = function sortTriggerSet(gambits) { + var gambit = void 0; + var cnt = void 0; + var inherits = void 0; + + var lengthSort = function lengthSort(a, b) { + return b.length - a.length; + }; + + // Create a priority map. + var prior = { + 0: [] }; + + // Sort triggers by their weights. + for (var i = 0; i < gambits.length; i++) { + gambit = gambits[i]; + var match = gambit.input.match(/\{weight=(\d+)\}/i); + var weight = 0; + if (match && match[1]) { + weight = match[1]; + } + + if (!prior[weight]) { + prior[weight] = []; + } + prior[weight].push(gambit); + } + + var sortFwd = function sortFwd(a, b) { + return b - a; + }; + var sortRev = function sortRev(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 (var _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 (var j = 0; j < prior[p].length; j++) { + gambit = prior[p][j]; + + inherits = -1; + if (!track[inherits]) { + track[inherits] = initSortTrack(); + } + + if (gambit.qType) { + // Qtype included + cnt = gambit.qType.length; + debug('Has a qType with ' + gambit.qType.length + ' length.'); + + if (!track[inherits].qtype[cnt]) { + track[inherits].qtype[cnt] = []; + } + track[inherits].qtype[cnt].push(gambit); + } else if (gambit.input.indexOf('*') > -1) { + // Wildcard included. + cnt = _utils2.default.wordCount(gambit.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(gambit); + } else { + track[inherits].star.push(gambit); + } + } else if (gambit.input.indexOf('[') > -1) { + // Optionals included. + cnt = _utils2.default.wordCount(gambit.input); + debug('Has optionals with ' + cnt + ' words.'); + if (!track[inherits].option[cnt]) { + track[inherits].option[cnt] = []; + } + track[inherits].option[cnt].push(gambit); + } else { + // Totally atomic. + cnt = _utils2.default.wordCount(gambit.input); + debug('Totally atomic trigger and ' + cnt + ' words.'); + if (!track[inherits].atomic[cnt]) { + track[inherits].atomic[cnt] = []; + } + track[inherits].atomic[cnt].push(gambit); + } + } + + // 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 (var _j = 0; _j < trackSorted.length; _j++) { + var ip = trackSorted[_j]; + debug('ip=' + ip); + + var kinds = ['qtype', 'atomic', 'option', 'alpha', 'number', 'wild']; + for (var k = 0; k < kinds.length; k++) { + var kind = kinds[k]; + + var kindSorted = Object.keys(track[ip][kind]).sort(sortFwd); + + for (var l = 0; l < kindSorted.length; l++) { + var item = kindSorted[l]; + running.push.apply(running, _toConsumableArray(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, _toConsumableArray(underSorted)); + running.push.apply(running, _toConsumableArray(poundSorted)); + running.push.apply(running, _toConsumableArray(starSorted)); + } + } + return running; +}; + +exports.default = { + sortTriggerSet: sortTriggerSet +}; \ No newline at end of file diff --git a/lib/bot/dict.js b/lib/bot/dict.js new file mode 100644 index 00000000..bd3b9d0f --- /dev/null +++ b/lib/bot/dict.js @@ -0,0 +1,132 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var debug = (0, _debugLevels2.default)('SS:Dict'); + +var Dict = function () { + function Dict(wordArray) { + _classCallCheck(this, Dict); + + this.words = []; + + for (var i = 0; i < wordArray.length; i++) { + this.words.push({ word: wordArray[i], position: i }); + } + } + + _createClass(Dict, [{ + key: 'add', + value: function add(key, array) { + for (var i = 0; i < array.length; i++) { + this.words[i][key] = array[i]; + } + } + }, { + key: 'get', + value: function get(word) { + debug.verbose('Getting word from dictionary: ' + word); + for (var i = 0; i < this.words.length; i++) { + if (this.words[i].word === word || this.words[i].lemma === word) { + return this.words[i]; + } + } + return null; + } + }, { + key: 'contains', + value: function contains(word) { + for (var i = 0; i < this.words.length; i++) { + if (this.words[i].word === word || this.words[i].lemma === word) { + return true; + } + } + return false; + } + }, { + key: 'addHLC', + value: function addHLC(array) { + debug.verbose('Adding HLCs to dictionary: ' + 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.verbose('HLC extra or missing for word/phrase: ' + word); + extra.push(word); + } + } + return extra; + } + }, { + key: 'getHLC', + value: function getHLC(concept) { + for (var i = 0; i < this.words.length; i++) { + if (_lodash2.default.includes(this.words[i].hlc, concept)) { + return this.words[i]; + } + } + return null; + } + }, { + key: 'containsHLC', + value: function containsHLC(concept) { + for (var i = 0; i < this.words.length; i++) { + if (_lodash2.default.includes(this.words[i].hlc, concept)) { + return true; + } + } + return false; + } + }, { + key: 'fetch', + value: function fetch(list, thing) { + var results = []; + for (var i = 0; i < this.words.length; i++) { + if (_lodash2.default.isArray(thing)) { + if (_lodash2.default.includes(thing, this.words[i][list])) { + results.push(this.words[i].lemma); + } + } else if (_lodash2.default.isArray(this.words[i][list])) { + if (_lodash2.default.includes(this.words[i][list], thing)) { + results.push(this.words[i].lemma); + } + } + } + return results; + } + }, { + key: 'findByLem', + value: function findByLem(word) { + for (var i = 0; i < this.words.length; i++) { + if (this.words[i].lemma === word) { + return this.words[i]; + } + } + return null; + } + }]); + + return Dict; +}(); + +exports.default = Dict; \ No newline at end of file diff --git a/lib/bot/factSystem.js b/lib/bot/factSystem.js new file mode 100644 index 00000000..17f8adee --- /dev/null +++ b/lib/bot/factSystem.js @@ -0,0 +1,41 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _sfacts = require('sfacts'); + +var _sfacts2 = _interopRequireDefault(_sfacts); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var coreFacts = null; + +var createFactSystem = function createFactSystem(mongoURI, _ref, callback) { + var clean = _ref.clean, + importData = _ref.importData; + + // TODO: On a multitenanted system, importing data should not do anything + if (importData) { + return _sfacts2.default.load(mongoURI, importData, clean, function (err, factSystem) { + coreFacts = factSystem; + callback(err, factSystem); + }); + } + return _sfacts2.default.create(mongoURI, clean, function (err, factSystem) { + coreFacts = factSystem; + callback(err, factSystem); + }); +}; + +var createFactSystemForTenant = function createFactSystemForTenant() { + var tenantId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'master'; + + return coreFacts.createUserDB('' + tenantId); +}; + +exports.default = { + createFactSystem: createFactSystem, + createFactSystemForTenant: createFactSystemForTenant +}; \ No newline at end of file diff --git a/lib/bot/getReply.js b/lib/bot/getReply.js new file mode 100644 index 00000000..70e6db9c --- /dev/null +++ b/lib/bot/getReply.js @@ -0,0 +1,464 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _async = require('async'); + +var _async2 = _interopRequireDefault(_async); + +var _re = require('re2'); + +var _re2 = _interopRequireDefault(_re); + +var _regexes = require('./regexes'); + +var _regexes2 = _interopRequireDefault(_regexes); + +var _utils = require('./utils'); + +var _utils2 = _interopRequireDefault(_utils); + +var _processTags = require('./processTags'); + +var _processTags2 = _interopRequireDefault(_processTags); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debugLevels2.default)('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. +var topicItorHandle = function topicItorHandle(messageObject, options) { + var system = options.system; + + return function (topicData, callback) { + if (topicData.type === 'TOPIC') { + system.chatSystem.Topic.findOne({ _id: topicData.id }).populate('gambits').exec(function (err, topic) { + if (err) { + console.error(err); + } + if (topic) { + // 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(function (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); + } + }; +}; + +var 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 function (continueSearching, matchSet) { + debug.verbose('Continue searching: ' + continueSearching); + debug.verbose('Set of matches: ' + matchSet); + + // remove empties + matchSet = _lodash2.default.compact(matchSet); + + var minMatchSet = []; + var props = {}; + var clearConversation = false; + var lastTopicToMatch = null; + var lastStarSet = null; + var lastReplyId = null; + var replyString = ''; + var lastSubReplies = null; + var lastContinueMatching = null; + var lastReplyIds = 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 (!_lodash2.default.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 = _lodash2.default.assign(props, item.props); + lastTopicToMatch = item.topic; + lastStarSet = item.stars; + lastReplyId = item.reply._id; + lastSubReplies = item.subReplies; + lastContinueMatching = item.continueMatching; + lastReplyIds = item.replyIds; + + if (item.clearConversation) { + clearConversation = item.clearConversation; + } + } + + var threadsArr = []; + if (_lodash2.default.isEmpty(lastSubReplies)) { + threadsArr = _processTags2.default.processThreadTags(replyString); + } else { + threadsArr[0] = replyString; + threadsArr[1] = lastSubReplies; + } + + // only remove one trailing space (because spaces may have been added deliberately) + var replyStr = new _re2.default('(?:^[ \\t]+)|(?:[ \\t]$)').replace(threadsArr[0], ''); + + var cbdata = { + replyId: lastReplyId, + replyIds: lastReplyIds, + props: props, + clearConversation: clearConversation, + topicName: lastTopicToMatch, + minMatchSet: minMatchSet, + string: replyStr, + subReplies: threadsArr[1], + stars: lastStarSet, + continueMatching: lastContinueMatching + }; + + debug.verbose('afterHandle', cbdata); + + callback(null, cbdata); + }; +}; + +// This may be called several times, once for each topic. +var filterRepliesBySeen = function filterRepliesBySeen(filteredResults, options, callback) { + var system = options.system; + debug.verbose('filterRepliesBySeen', filteredResults); + var bucket = []; + + var eachResultItor = function eachResultItor(filteredResult, next) { + var topicName = filteredResult.topic; + system.chatSystem.Topic.findOne({ name: topicName }).exec(function (err, currentTopic) { + if (err) { + console.log(err); + } + + // var repIndex = filteredResult.id; + var replyId = filteredResult.reply._id; + var reply = filteredResult.reply; + var gambitId = 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 = options.user.history.topic[i]; + + if (topicItem !== undefined) { + // TODO: Come back to this and check names make sense + var pastGambit = options.user.history.reply[i]; + var 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(); + }); + }; + + _async2.default.each(filteredResults, eachResultItor, function () { + debug.verbose('Bucket of selected replies: ', bucket); + if (!_lodash2.default.isEmpty(bucket)) { + callback(null, _utils2.default.pickItem(bucket)); + } else { + callback(true); + } + }); +}; // end filterBySeen + +var filterRepliesByFunction = function filterRepliesByFunction(potentialReplies, options, callback) { + var filterHandle = function filterHandle(potentialReply, cb) { + var 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 !== '') { + var filterFunction = _regexes2.default.filter.match(potentialReply.reply.filter); + var pluginName = _utils2.default.trim(filterFunction[1]); + var partsStr = _utils2.default.trim(filterFunction[2]); + var args = _utils2.default.replaceCapturedText(partsStr.split(','), [''].concat(potentialReply.stars)); + + debug.verbose('Filter function found with plugin name: ' + pluginName); + + if (system.plugins[pluginName]) { + args.push(function (err, filterReply) { + if (err) { + console.log(err); + } + + if (filterReply === 'true' || filterReply === true) { + cb(err, true); + } else { + cb(err, false); + } + }); + + var filterScope = _lodash2.default.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 = _utils2.default.trim(potentialReply.reply.reply.replace(filterFunction[0], '')); + cb(null, true); + } + } else { + cb(null, true); + } + }; + + _async2.default.filter(potentialReplies, filterHandle, function (err, filteredReplies) { + debug.verbose('filterByFunction results: ', filteredReplies); + + filterRepliesBySeen(filteredReplies, options, function (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 { + _processTags2.default.processReplyTags(reply, options, function (err, replyObj) { + if (!_lodash2.default.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('Reply object after processing tags: ', replyObj); + + if (replyObj.continueMatching === false) { + debug.info('Continue matching is set to false: returning.'); + callback(true, replyObj); + } else if (replyObj.continueMatching === true || replyObj.reply.reply === '') { + debug.info('Continue matching is set to true or reply is not empty: continuing.'); + // By calling back with error set as 'true', we break out of async flow + // and return the reply to the user. + callback(null, replyObj); + } else { + debug.info('Reply is not empty: returning.'); + callback(true, replyObj); + } + } else { + debug.verbose('No reply object was received from processTags so check for more.'); + if (err) { + debug.verbose('There was an error in processTags', err); + } + callback(null, null); + } + }); + } + }); + }); +}; + +// Iterates through matched gambits +var matchItorHandle = function matchItorHandle(message, options) { + var system = options.system; + options.message = message; + + return function (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(function (err, gambitExpanded) { + if (err) { + console.log(err); + } + + match.gambit = gambitExpanded; + + match.gambit.getRootTopic(function (err, topic) { + if (err) { + console.log(err); + } + + var rootTopic = void 0; + if (match.topic) { + rootTopic = match.topic; + } else { + rootTopic = topic; + } + + var stars = match.stars; + if (!_lodash2.default.isEmpty(message.stars)) { + stars = message.stars; + } + + var potentialReplies = []; + + for (var i = 0; i < match.gambit.replies.length; i++) { + var reply = match.gambit.replies[i]; + var replyData = { + id: reply.id, + topic: rootTopic, + stars: stars, + reply: 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. + */ +var 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 (!_lodash2.default.isEmpty(options.pendingTopics)) { + debug.verbose('Using pre-set topic list via directReply, respond or topicRedirect'); + debug.info('Topics to check: ', options.pendingTopics.map(function (topic) { + return topic.name; + })); + afterFindPendingTopics(options.pendingTopics, messageObject, options, callback); + } else { + var chatSystem = options.system.chatSystem; + + // Find potential topics for the response based on the message (tfidfs) + chatSystem.Topic.findPendingTopicsForUser(options.user, messageObject, function (err, pendingTopics) { + if (err) { + console.log(err); + } + afterFindPendingTopics(pendingTopics, messageObject, options, callback); + }); + } +}; + +var 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. + _async2.default.mapSeries(pendingTopics, topicItorHandle(messageObject, options), function (err, results) { + if (err) { + console.error(err); + } + + // Remove the empty topics, and flatten the array down. + var matches = _lodash2.default.flatten(_lodash2.default.filter(results, function (n) { + return n; + })); + + // TODO - This sort should happen in the process sort logic. + // Try matching most specific question matches first + matches = matches.sort(function (a, b) { + var questionTypeA = a.gambit.qType || ''; + var questionSubTypeA = a.gambit.qSubType || ''; + var questionTypeB = b.gambit.qType || ''; + var questionSubTypeB = b.gambit.qSubType || ''; + return questionTypeA.concat(questionSubTypeA).length < questionTypeB.concat(questionSubTypeB).length; + }); + + debug.verbose('Matching gambits are: '); + matches.forEach(function (match) { + debug.verbose('Trigger: ' + match.gambit.input); + debug.verbose('Replies: ' + match.gambit.replies.map(function (reply) { + return reply.reply; + }).join('\n')); + }); + + // Was `eachSeries` + _async2.default.mapSeries(matches, matchItorHandle(messageObject, options), afterHandle(options.user, callback)); + }); +}; + +exports.default = getReply; \ No newline at end of file diff --git a/lib/bot/history.js b/lib/bot/history.js new file mode 100644 index 00000000..bc984bc4 --- /dev/null +++ b/lib/bot/history.js @@ -0,0 +1,147 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debugLevels2.default)('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 historyLookup(user, options) { + debug.verbose('History Lookup with', options); + + var candidates = []; + var nn = void 0; + + var moneyWords = function moneyWords(item) { + return item[1] === '$' || item[0] === 'quid' || item[0] === 'pounds' || item[0] === 'dollars' || item[0] === 'bucks' || item[0] === 'cost'; + }; + + var _loop = function _loop(i) { + var pobj = user.history.input[i]; + + if (pobj !== undefined) { + // TODO - See why we are getting a nested array. + if (Array.isArray(pobj)) { + pobj = pobj[0]; + } + + if (options.numbers || options.number) { + if (pobj.numbers.length !== 0) { + candidates.push(pobj); + } + } + + // Special case of number + if (options.money === true && options.nouns) { + if (pobj.numbers.length !== 0) { + var t = []; + if (_lodash2.default.any(pobj.taggedWords, moneyWords)) { + t.push(pobj); + + // Now filter out the nouns + for (var n = 0; n < t.length; n++) { + nn = _lodash2.default.any(t[n].nouns, function (item) { + for (var j = 0; j < options.nouns.length; j++) { + return options.nouns[i] === item ? true : false; + } + }); + } + + if (nn) { + candidates.push(pobj); + } + } + } + } else if (options.money && pobj) { + if (pobj.numbers.length !== 0) { + if (_lodash2.default.any(pobj.taggedWords, moneyWords)) { + candidates.push(pobj); + } + } + } else if (options.nouns && pobj) { + debug.verbose('Noun Lookup'); + if (_lodash2.default.isArray(options.nouns)) { + s = 0; + c = 0; + + + nn = _lodash2.default.any(pobj.nouns, function (item) { + var x = _lodash2.default.includes(options.nouns, item); + c++; + s = x ? s + 1 : s; + return x; + }); + + if (nn) { + pobj.score = s / c; + candidates.push(pobj); + } + } else if (pobj.nouns.length !== 0) { + candidates.push(pobj); + } + } else if (options.names && pobj) { + debug.verbose('Name Lookup'); + + if (_lodash2.default.isArray(options.names)) { + nn = _lodash2.default.any(pobj.names, function (item) { + return _lodash2.default.includes(options.names, item); + }); + if (nn) { + candidates.push(pobj); + } + } else if (pobj.names.length !== 0) { + candidates.push(pobj); + } + } else if (options.adjectives && pobj) { + debug.verbose('adjectives Lookup'); + if (_lodash2.default.isArray(options.adjectives)) { + s = 0; + c = 0; + + nn = _lodash2.default.any(pobj.adjectives, function (item) { + var x = _lodash2.default.includes(options.adjectives, item); + c++; + s = x ? s + 1 : s; + return x; + }); + + if (nn) { + pobj.score = s / c; + candidates.push(pobj); + } + } else if (pobj.adjectives.length !== 0) { + candidates.push(pobj); + } + } + + if (options.date && pobj) { + if (pobj.date !== null) { + candidates.push(pobj); + } + } + } + }; + + for (var i = 0; i < user.history.input.length; i++) { + var s; + var c; + + _loop(i); + } + + return candidates; +}; + +exports.default = historyLookup; \ No newline at end of file diff --git a/lib/bot/index.js b/lib/bot/index.js new file mode 100644 index 00000000..2b090e41 --- /dev/null +++ b/lib/bot/index.js @@ -0,0 +1,351 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _requireDir = require('require-dir'); + +var _requireDir2 = _interopRequireDefault(_requireDir); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _common = require('./reply/common'); + +var _common2 = _interopRequireDefault(_common); + +var _connect = require('./db/connect'); + +var _connect2 = _interopRequireDefault(_connect); + +var _factSystem = require('./factSystem'); + +var _factSystem2 = _interopRequireDefault(_factSystem); + +var _chatSystem = require('./chatSystem'); + +var _chatSystem2 = _interopRequireDefault(_chatSystem); + +var _getReply = require('./getReply'); + +var _getReply2 = _interopRequireDefault(_getReply); + +var _import = require('./db/import'); + +var _import2 = _interopRequireDefault(_import); + +var _message = require('./message'); + +var _message2 = _interopRequireDefault(_message); + +var _logger = require('./logger'); + +var _logger2 = _interopRequireDefault(_logger); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var debug = (0, _debugLevels2.default)('SS:SuperScript'); + +var plugins = []; +var editMode = false; +var scope = {}; + +var loadPlugins = function loadPlugins(path) { + try { + (function () { + var pluginFiles = (0, _requireDir2.default)(path); + + Object.keys(pluginFiles).forEach(function (file) { + // For transpiled ES6 plugins with default export + if (pluginFiles[file].default) { + pluginFiles[file] = pluginFiles[file].default; + } + + Object.keys(pluginFiles[file]).forEach(function (func) { + debug.verbose('Loading plugin: ', path, func); + plugins[func] = pluginFiles[file][func]; + }); + }); + })(); + } catch (e) { + console.error('Could not load plugins from ' + path + ': ' + e); + } +}; + +var SuperScript = function () { + function SuperScript(tenantId) { + _classCallCheck(this, SuperScript); + + this.factSystem = _factSystem2.default.createFactSystemForTenant(tenantId); + this.chatSystem = _chatSystem2.default.createChatSystemForTenant(tenantId); + + // We want a place to store bot related data + this.memory = this.factSystem.createUserDB('botfacts'); + + this.scope = scope; + this.scope.bot = this; + this.scope.facts = this.factSystem; + this.scope.chatSystem = this.chatSystem; + this.scope.botfacts = this.memory; + + this.plugins = plugins; + } + + _createClass(SuperScript, [{ + key: 'importFile', + value: function importFile(filePath, callback) { + _import2.default.importFile(this.chatSystem, filePath, function (err) { + console.log('Bot is ready for input!'); + debug.verbose('System loaded, waiting for replies'); + callback(err); + }); + } + }, { + key: 'getUsers', + value: function getUsers(callback) { + this.chatSystem.User.find({}, 'id', callback); + } + }, { + key: 'getUser', + value: function getUser(userId, callback) { + this.chatSystem.User.findOne({ id: userId }, callback); + } + }, { + key: 'findOrCreateUser', + value: function findOrCreateUser(userId, callback) { + var findProps = { id: userId }; + var 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 + + }, { + key: 'reply', + value: function 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); + var options = { + userId: userId, + extraScope: extraScope + }; + + this._reply(messageString, options, callback); + } + + // This is like doing a topicRedirect + + }, { + key: 'directReply', + value: function directReply(userId, topicName, messageString, callback) { + debug.log("[ New DirectReply - '%s']- %s", userId, messageString); + var options = { + userId: userId, + topicName: topicName, + extraScope: {} + }; + + this._reply(messageString, options, callback); + } + }, { + key: 'message', + value: function message(messageString, callback) { + var options = { + factSystem: this.factSystem + }; + + _message2.default.createMessage(messageString, options, function (msgObj) { + callback(null, msgObj); + }); + } + }, { + key: '_reply', + value: function _reply(messageString, options, callback) { + var _this = this; + + var 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: editMode + }; + + this.findOrCreateUser(options.userId, function (err, user) { + if (err) { + debug.error(err); + } + + var messageOptions = { + factSystem: _this.factSystem + }; + + _message2.default.createMessage(messageString, messageOptions, function (messageObject) { + _common2.default.getTopic(system.chatSystem, system.topicName, function (err, topicData) { + var options = { + user: user, + system: system, + depth: 0 + }; + + if (topicData) { + options.pendingTopics = [topicData]; + } + + (0, _getReply2.default)(messageObject, options, function (err, replyObj) { + // Convert the reply into a message object too. + var replyMessage = ''; + var messageOptions = { + factSystem: system.factSystem + }; + + if (replyObj) { + messageOptions.replyId = replyObj.replyId; + replyMessage = replyObj.string; + + if (replyObj.clearConversation) { + messageOptions.clearConversation = replyObj.clearConversation; + } + } else { + replyObj = {}; + console.log('There was no response matched.'); + } + + _message2.default.createMessage(replyMessage, messageOptions, function (replyMessageObject) { + user.updateHistory(messageObject, 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: replyMessage || '', // replyMessageObject.raw || "", + topicName: replyObj.topicName, + subReplies: replyObj.subReplies, + debug: log + }; + + var newClientObject = _lodash2.default.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); + }); + }); + }); + }); + }); + }); + } + }], [{ + key: 'getBot', + value: function getBot(tenantId) { + return new SuperScript(tenantId); + } + }]); + + return SuperScript; +}(); + +var defaultOptions = { + mongoURI: 'mongodb://localhost/superscriptDB', + importFile: null, + factSystem: { + clean: false, + importFiles: null + }, + scope: {}, + editMode: false, + pluginsPath: process.cwd() + '/plugins', + logPath: process.cwd() + '/logs' +}; + +/** + * Setup SuperScript. You may only run this a single time since it writes to global state. + * @param {Object} options - Any configuration settings you want to use. + * @param {String} options.mongoURI - The database URL you want to connect to. + * This will be used for both the chat and fact system. + * @param {String} options.importFile - Use this if you want to re-import your parsed + * '*.json' file. Otherwise SuperScript will use whatever it currently + * finds in the database. + * @param {Object} options.factSystem - Settings to use for the fact system. + * @param {Boolean} options.factSystem.clean - If you want to remove everything in the + * fact system upon launch. Otherwise SuperScript will keep facts from + * the last time it was run. + * @param {Array} options.factSystem.importFiles - Any additional data you want to + * import into the fact system. + * @param {Object} options.scope - Any extra scope you want to pass into your plugins. + * @param {Boolean} options.editMode - Used in the editor. + * @param {String} options.pluginsPath - A path to the plugins written by you. This loads + * the entire directory recursively. + * @param {String} options.logPath - If null, logging will be off. Otherwise writes + * conversation transcripts to the path. + */ +var setup = function setup() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var callback = arguments[1]; + + options = _lodash2.default.merge(defaultOptions, options); + _logger2.default.setLogPath(options.logPath); + + // Uses schemas to create models for the db connection to use + _factSystem2.default.createFactSystem(options.mongoURI, options.factSystem, function (err) { + if (err) { + return callback(err); + } + + var db = (0, _connect2.default)(options.mongoURI); + _chatSystem2.default.createChatSystem(db); + + // Built-in plugins + loadPlugins(__dirname + '/../plugins'); + + // For user plugins + if (options.pluginsPath) { + loadPlugins(options.pluginsPath); + } + + // This is a kill switch for filterBySeen which is useless in the editor. + editMode = options.editMode || false; + scope = options.scope || {}; + + var bot = new SuperScript('master'); + + if (options.importFile) { + return bot.importFile(options.importFile, function (err) { + return callback(err, bot); + }); + } + return callback(null, bot); + }); +}; + +exports.default = { + setup: setup +}; \ No newline at end of file diff --git a/lib/bot/logger.js b/lib/bot/logger.js new file mode 100644 index 00000000..90cfbb91 --- /dev/null +++ b/lib/bot/logger.js @@ -0,0 +1,47 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _mkdirp = require('mkdirp'); + +var _mkdirp2 = _interopRequireDefault(_mkdirp); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// The directory to write logs to +var logPath = void 0; + +var setLogPath = function setLogPath(path) { + if (path) { + try { + _mkdirp2.default.sync(path); + logPath = path; + } catch (e) { + console.error('Could not create logs folder at ' + logPath + ': ' + e); + } + } +}; + +var log = function log(message) { + var logName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'log'; + + if (logPath) { + var filePath = logPath + '/' + logName + '.log'; + try { + _fs2.default.appendFileSync(filePath, message); + } catch (e) { + console.error('Could not write log to file with path: ' + filePath); + } + } +}; + +exports.default = { + log: log, + setLogPath: setLogPath +}; \ No newline at end of file diff --git a/lib/bot/math.js b/lib/bot/math.js new file mode 100644 index 00000000..6439111e --- /dev/null +++ b/lib/bot/math.js @@ -0,0 +1,320 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _debug = require('debug'); + +var _debug2 = _interopRequireDefault(_debug); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/* eslint no-eval:0 */ +// TODO - Make this into its own project + +var debug = (0, _debug2.default)('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', '%']; + +var 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 +var parse = function parse(words) { + var prev = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + + debug('In parse with ', words); + var expression = []; + var newexpression = []; + var i = void 0; + var word = void 0; + + 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 +var convertWordsToNumbers = function convertWordsToNumbers(wordArray) { + var mult = { hundred: 100, thousand: 1000 }; + var results = []; + var i = void 0; + + 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 convertWordToNumber(word) { + var number = void 0; + var multipleOfTen = void 0; + var cardinalNumber = void 0; + + if (word !== undefined) { + if (word.indexOf('-') === -1) { + if (_lodash2.default.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; + } +}; + +var numberLookup = function numberLookup(number) { + var multipleOfTen = void 0; + 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 convertNumberToWord(number) { + if (number === 0) { + return 'zero'; + } + + if (number < 0) { + return 'negative ' + numberLookup(Math.abs(number)); + } + + return numberLookup(number); +}; + +var cardPlural = function cardPlural(wordNumber) { + return cardinalNumberPlural[wordNumber]; +}; + +var arithGeo = function arithGeo(arr) { + var ap = void 0; + var gp = void 0; + + 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 (var _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; +}; + +exports.default = { + arithGeo: arithGeo, + cardPlural: cardPlural, + convertWordToNumber: convertWordToNumber, + convertWordsToNumbers: convertWordsToNumbers, + mathTerms: mathTerms, + parse: parse +}; \ No newline at end of file diff --git a/lib/bot/message.js b/lib/bot/message.js new file mode 100644 index 00000000..79a97f73 --- /dev/null +++ b/lib/bot/message.js @@ -0,0 +1,506 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _qtypes = require('qtypes'); + +var _qtypes2 = _interopRequireDefault(_qtypes); + +var _partsOfSpeech = require('parts-of-speech'); + +var _partsOfSpeech2 = _interopRequireDefault(_partsOfSpeech); + +var _natural = require('natural'); + +var _natural2 = _interopRequireDefault(_natural); + +var _moment = require('moment'); + +var _moment2 = _interopRequireDefault(_moment); + +var _lemmer = require('lemmer'); + +var _lemmer2 = _interopRequireDefault(_lemmer); + +var _async = require('async'); + +var _async2 = _interopRequireDefault(_async); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _nodeNormalizer = require('node-normalizer'); + +var _nodeNormalizer2 = _interopRequireDefault(_nodeNormalizer); + +var _math = require('./math'); + +var _math2 = _interopRequireDefault(_math); + +var _dict = require('./dict'); + +var _dict2 = _interopRequireDefault(_dict); + +var _utils = require('./utils'); + +var _utils2 = _interopRequireDefault(_utils); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var debug = (0, _debugLevels2.default)('SS:Message'); +var ngrams = _natural2.default.NGrams; + +var patchList = function patchList(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 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 + +var Message = function () { + /** + * 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.clearConversation] - If you want to clear the conversation. + */ + function Message(message, options) { + _classCallCheck(this, Message); + + debug.verbose('Creating message from string: ' + message); + + this.id = _utils2.default.genId(); + + // If this message is based on a Reply. + if (options.replyId) { + this.replyId = options.replyId; + } + + if (options.clearConversation) { + this.clearConversation = options.clearConversation; + } + + 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 = _nodeNormalizer2.default.clean(message).trim(); + this.clean = cleanMessage(this.raw).trim(); + debug.verbose('Message before cleaning: ', message); + debug.verbose('Message after cleaning: ', this.clean); + + this.props = {}; + + var words = new _partsOfSpeech2.default.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 _dict2.default(words); + + words = _math2.default.convertWordsToNumbers(words); + this.taggedWords = new _partsOfSpeech2.default.Tagger().tag(words); + } + + _createClass(Message, [{ + key: 'finishCreating', + value: function finishCreating(callback) { + var _this = this; + + this.lemma(function (err, lemWords) { + if (err) { + console.log(err); + } + + _this.lemWords = lemWords; + _this.lemString = _this.lemWords.join(' '); + + _this.posWords = _this.taggedWords.map(function (hash) { + return hash[1]; + }); + _this.posString = _this.posWords.join(' '); + + _this.dict.add('lemma', _this.lemWords); + _this.dict.add('pos', _this.posWords); + + // Classify Question + _this.questionType = _qtypes2.default.questionType(_this.clean); + _this.questionSubType = _qtypes2.default.classify(_this.lemString); + _this.isQuestion = _qtypes2.default.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 = _lodash2.default.uniq(_this.names, function (name) { + return name.toLowerCase(); + }); + + // Nouns with Names removed. + var lowerCaseNames = _this.names.map(function (name) { + return name.toLowerCase(); + }); + + _this.cNouns = _lodash2.default.filter(_this.nouns, function (item) { + return !_lodash2.default.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(function (entities) { + var complexNouns = _this.fetchComplexNouns('nouns'); + var fullEntities = entities.map(function (item) { + return 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. + + }, { + key: 'lemma', + value: function lemma(callback) { + var itor = function itor(hash, next) { + var word = hash[0].toLowerCase(); + var tag = _utils2.default.pennToWordnet(hash[1]); + + // console.log(word, tag); + // next(null, [word]); + + if (tag) { + try { + _lemmer2.default.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]); + } + }; + + _async2.default.map(this.taggedWords, itor, function (err, lemWords) { + var result = _lodash2.default.map(_lodash2.default.flatten(lemWords), function (lemWord) { + return lemWord.split('#')[0]; + }); + callback(err, result); + }); + } + }, { + key: 'checkMath', + value: function checkMath() { + var numCount = 0; + var oppCount = 0; + + for (var i = 0; i < this.taggedWords.length; i++) { + if (this.taggedWords[i][1] === 'CD') { + numCount += 1; + } + if (this.taggedWords[i][1] === 'SYM' || _math2.default.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; + } + } + }, { + key: 'fetchCompareWords', + value: function fetchCompareWords() { + return this.dict.fetch('pos', ['JJR', 'RBR']); + } + }, { + key: 'fetchAdjectives', + value: function fetchAdjectives() { + return this.dict.fetch('pos', ['JJ', 'JJR', 'JJS']); + } + }, { + key: 'fetchAdverbs', + value: function fetchAdverbs() { + return this.dict.fetch('pos', ['RB', 'RBR', 'RBS']); + } + }, { + key: 'fetchNumbers', + value: function fetchNumbers() { + return this.dict.fetch('pos', ['CD']); + } + }, { + key: 'fetchVerbs', + value: function fetchVerbs() { + return this.dict.fetch('pos', ['VB', 'VBN', 'VBD', 'VBZ', 'VBP', 'VBG']); + } + }, { + key: 'fetchPronouns', + value: function fetchPronouns() { + return this.dict.fetch('pos', ['PRP', 'PRP$']); + } + }, { + key: 'fetchNouns', + value: function 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 + + }, { + key: 'fetchList', + value: function fetchList() { + debug.verbose('Fetch list'); + var 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)) { + var sn = false; + for (var 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 && _utils2.default.isTag(this.taggedWords[i][1], 'nouns')) { + list.push(this.taggedWords[i][0]); + sn = false; + } + } + } + return list; + } + }, { + key: 'fetchDate', + value: function fetchDate() { + var date = null; + var months = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december']; + + // http://rubular.com/r/SAw0nUqHJh + var regex = /([a-z]{3,10}\s+[\d]{1,2}\s?,?\s+[\d]{2,4}|[\d]{2}\/[\d]{2}\/[\d]{2,4})/i; + var match = this.clean.match(regex); + + if (match) { + debug.verbose('Date: ', match); + date = (0, _moment2.default)(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 (_lodash2.default.includes(this.nouns, 'month')) { + if (this.dict.includes('next')) { + date = (0, _moment2.default)().add('M', 1); + } + if (this.dict.includes('last')) { + date = (0, _moment2.default)().subtract('M', 1); + } + } else if (_utils2.default.inArray(this.nouns, months)) { + // IN month vs ON month + var p = _utils2.default.inArray(this.nouns, months); + date = (0, _moment2.default)(this.nouns[p] + ' 1', 'MMM D'); + } + } + + return date; + } + + // Pulls concepts from the bigram DB. + + }, { + key: 'fetchNamedEntities', + value: function fetchNamedEntities(callback) { + var _this2 = this; + + var bigrams = ngrams.bigrams(this.taggedWords); + + var sentenceBigrams = _lodash2.default.map(bigrams, function (bigram) { + return _lodash2.default.map(bigram, function (item) { + return item[0]; + }); + }); + + var itor = function itor(item, cb) { + var bigramLookup = { subject: item.join(' '), predicate: 'isa', object: 'bigram' }; + _this2.factSystem.db.get(bigramLookup, function (err, res) { + if (err) { + debug.error(err); + } + + if (!_lodash2.default.isEmpty(res)) { + cb(err, true); + } else { + cb(err, false); + } + }); + }; + + _async2.default.filter(sentenceBigrams, itor, function (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" + + }, { + key: 'fetchComplexNouns', + value: function fetchComplexNouns(lookupType) { + var tags = this.taggedWords; + var bigrams = ngrams.bigrams(tags); + var tester = void 0; + + // TODO: Might be able to get rid of this and use this.dict to get nouns/proper names + if (lookupType === 'names') { + tester = function tester(item) { + return item[1] === 'NNP' || item[1] === 'NNPS'; + }; + } else { + tester = function tester(item) { + return item[1] === 'NN' || item[1] === 'NNS' || item[1] === 'NNP' || item[1] === 'NNPS'; + }; + } + + var nouns = _lodash2.default.filter(_lodash2.default.map(tags, function (item) { + return tester(item) ? item[0] : null; + }), Boolean); + + var nounBigrams = ngrams.bigrams(nouns); + + // Get a list of term + var neTest = _lodash2.default.map(bigrams, function (bigram) { + return _lodash2.default.map(bigram, function (item) { + return tester(item); + }); + }); + + // TODO: Work out what this is + var thing = _lodash2.default.map(neTest, function (item, key) { + return _lodash2.default.every(item, _lodash2.default.identity) ? bigrams[key] : null; + }); + + // Return full names from the list + var fullnames = _lodash2.default.map(_lodash2.default.filter(thing, Boolean), function (item) { + return _lodash2.default.map(item, function (item2) { + return item2[0]; + }).join(' '); + }); + + debug.verbose('Full names found from lookupType ' + lookupType + ': ' + fullnames); + + var x = _lodash2.default.map(nounBigrams, function (item) { + return _lodash2.default.includes(fullnames, item.join(' ')); + }); + + // FIXME: This doesn't do anything (result not used) + // Filter X out of the bigrams or names? + _lodash2.default.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); + } + }], [{ + key: 'createMessage', + value: function createMessage(message, options, callback) { + if (!message) { + debug.verbose('Message received was empty, callback immediately'); + return callback({}); + } + + var messageObj = new Message(message, options); + messageObj.finishCreating(callback); + } + }]); + + return Message; +}(); + +exports.default = Message; \ No newline at end of file diff --git a/lib/bot/postParse.js b/lib/bot/postParse.js new file mode 100644 index 00000000..3190361b --- /dev/null +++ b/lib/bot/postParse.js @@ -0,0 +1,81 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _re = require('re2'); + +var _re2 = _interopRequireDefault(_re); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Insert replacements into `source` string + * + * - `` gets replaced by `(replacements[0])` + * - `` gets replaced by `(replacements[0]|replacements[1]|...)` + * - `` gets replaced by `(replacements[N])` + * + * @param {string} basename + * @param {string} source + * @param {Array} replacements + * @returns {string} + */ +var replaceOneOrMore = function replaceOneOrMore(basename, source, replacements) { + var pronounsRE = new _re2.default('<(' + basename + ')([s0-' + replacements.length + '])?>', 'g'); + if (pronounsRE.search(source) !== -1 && replacements.length !== 0) { + return pronounsRE.replace(source, function (c, p1, p2) { + if (p1 === 's') { + return '(' + replacements.join('|') + ')'; + } else { + var index = Number.parseInt(p2); + index = index ? index - 1 : 0; + return '(' + replacements[index] + ')'; + } + }); + } else { + return source; + } +}; + +/** + * 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. + */ +var postParse = function postParse(regexp, message, user, callback) { + if (_lodash2.default.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); + + var inputOrReplyRE = new _re2.default('<(input|reply)([1-9])?>', 'g'); + if (inputOrReplyRE.search(regexp) !== -1) { + (function () { + var history = user.history; + regexp = inputOrReplyRE.replace(regexp, function (c, p1, p2) { + var index = p2 ? Number.parseInt(p2) : 0; + return history[p1][index] ? history[p1][index].raw : c; + }); + })(); + } + } + + callback(regexp); +}; + +exports.default = postParse; \ No newline at end of file diff --git a/lib/bot/processTags.js b/lib/bot/processTags.js new file mode 100644 index 00000000..10e61417 --- /dev/null +++ b/lib/bot/processTags.js @@ -0,0 +1,516 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _async = require('async'); + +var _async2 = _interopRequireDefault(_async); + +var _re = require('re2'); + +var _re2 = _interopRequireDefault(_re); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _pegjs = require('pegjs'); + +var _pegjs2 = _interopRequireDefault(_pegjs); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _utils = require('./utils'); + +var _utils2 = _interopRequireDefault(_utils); + +var _common = require('./reply/common'); + +var _common2 = _interopRequireDefault(_common); + +var _regexes = require('./regexes'); + +var _regexes2 = _interopRequireDefault(_regexes); + +var _wordnet = require('./reply/wordnet'); + +var _wordnet2 = _interopRequireDefault(_wordnet); + +var _inlineRedirect = require('./reply/inlineRedirect'); + +var _inlineRedirect2 = _interopRequireDefault(_inlineRedirect); + +var _topicRedirect = require('./reply/topicRedirect'); + +var _topicRedirect2 = _interopRequireDefault(_topicRedirect); + +var _respond = require('./reply/respond'); + +var _respond2 = _interopRequireDefault(_respond); + +var _customFunction = require('./reply/customFunction'); + +var _customFunction2 = _interopRequireDefault(_customFunction); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// 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}` + */ + +var debug = (0, _debugLevels2.default)('SS:ProcessTags'); + +var grammar = _fs2.default.readFileSync(__dirname + '/reply/reply-grammar.pegjs', 'utf-8'); +// Change trace to true to debug peg +var parser = _pegjs2.default.generate(grammar, { trace: false }); + +var captureGrammar = _fs2.default.readFileSync(__dirname + '/reply/capture-grammar.pegjs', 'utf-8'); +// Change trace to true to debug peg +var captureParser = _pegjs2.default.generate(captureGrammar, { trace: false }); + +/* topicRedirect +/ respond +/ redirect +/ customFunction +/ newTopic +/ capture +/ previousCapture +/ clearConversation +/ continueSearching +/ endSearching +/ previousInput +/ previousReply +/ wordnetLookup +/ alternates +/ delay +/ setState +/ string*/ + +var processCapture = function processCapture(tag, replyObj, options) { + var starID = (tag.starID || 1) - 1; + debug.verbose('Processing capture: '); + var replacedCapture = starID < replyObj.stars.length ? replyObj.stars[starID] : ''; + debug.verbose('Replacing with "' + replacedCapture + '"'); + return replacedCapture; +}; + +var processPreviousCapture = function processPreviousCapture(tag, replyObj, options) { + // 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. + // TODO: handle captures within captures, but only 1 level deep + var starID = (tag.starID || 1) - 1; + var conversationID = (tag.conversationID || 1) - 1; + debug.verbose('Processing previous capture: '); + var replacedCapture = ''; + + if (options.user.history.stars[conversationID] && options.user.history.stars[conversationID][starID]) { + replacedCapture = options.user.history.stars[conversationID][starID]; + debug.verbose('Replacing with "' + replacedCapture + '"'); + } else { + debug.verbose('Attempted to use previous capture data, but none was found in user history.'); + } + return replacedCapture; +}; + +var processPreviousInput = function processPreviousInput(tag, replyObj, options) { + if (tag.inputID === null) { + debug.verbose('Processing previous input '); + // This means instead of , etc. so give the current input back + var _replacedInput = options.message.clean; + return _replacedInput; + } + + var inputID = (tag.inputID || 1) - 1; + debug.verbose('Processing previous input '); + var replacedInput = ''; + if (!options.user.history.input) { + // Nothing yet in the history + replacedInput = ''; + } else { + replacedInput = options.user.history.input[inputID].clean; + } + debug.verbose('Replacing with "' + replacedInput + '"'); + return replacedInput; +}; + +var processPreviousReply = function processPreviousReply(tag, replyObj, options) { + var replyID = (tag.replyID || 1) - 1; + debug.verbose('Processing previous reply '); + var replacedReply = ''; + if (!options.user.history.reply) { + // Nothing yet in the history + replacedReply = ''; + } else { + replacedReply = options.user.history.reply[replyID]; + } + debug.verbose('Replacing with "' + replacedReply + '"'); + return replacedReply; +}; + +var processCaptures = function processCaptures(tag, replyObj, options) { + switch (tag.type) { + case 'capture': + { + return processCapture(tag, replyObj, options); + } + case 'previousCapture': + { + return processPreviousCapture(tag, replyObj, options); + } + case 'previousInput': + { + return processPreviousInput(tag, replyObj, options); + } + case 'previousReply': + { + return processPreviousReply(tag, replyObj, options); + } + default: + { + console.error('Capture tag type does not exist: ' + tag.type); + return ''; + } + } +}; + +var preprocess = function preprocess(reply, replyObj, options) { + var captureTags = captureParser.parse(reply); + var cleanedReply = captureTags.map(function (tag) { + // Don't do anything to non-captures + if (typeof tag === 'string') { + return tag; + } + // It's a capture e.g. , so replace it with the captured star in replyObj.stars + return processCaptures(tag, replyObj, options); + }); + cleanedReply = cleanedReply.join(''); + return cleanedReply; +}; + +var postAugment = function postAugment(replyObject, tag, callback) { + return function (err, augmentedReplyObject) { + 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, ''); + } + + replyObject.continueMatching = augmentedReplyObject.continueMatching; + replyObject.clearConversation = augmentedReplyObject.clearConversation; + replyObject.topic = augmentedReplyObject.topicName; + replyObject.props = _lodash2.default.merge(replyObject.props, augmentedReplyObject.props); + + // Keep track of all the ids of all the triggers we go through via redirects + if (augmentedReplyObject.replyIds) { + augmentedReplyObject.replyIds.forEach(function (replyId) { + replyObject.replyIds.push(replyId); + }); + } + + if (augmentedReplyObject.subReplies) { + if (replyObject.subReplies) { + replyObject.subReplies = replyObject.subReplies.concat(augmentedReplyObject.subReplies); + } else { + replyObject.subReplies = augmentedReplyObject.subReplies; + } + } + + replyObject.minMatchSet = augmentedReplyObject.minMatchSet; + return callback(null, augmentedReplyObject.string); + }; +}; + +var processTopicRedirect = function processTopicRedirect(tag, replyObj, options, callback) { + debug.verbose('Processing topic redirect ^topicRedirect(' + tag.topicName + ',' + tag.topicTrigger + ')'); + options.depth = options.depth + 1; + (0, _topicRedirect2.default)(tag.topicName, tag.topicTrigger, options, postAugment(replyObj, tag, callback)); +}; + +var processRespond = function processRespond(tag, replyObj, options, callback) { + debug.verbose('Processing respond: ^respond(' + tag.topicName + ')'); + options.depth = options.depth + 1; + (0, _respond2.default)(tag.topicName, options, postAugment(replyObj, tag, callback)); +}; + +var processRedirect = function processRedirect(tag, replyObj, options, callback) { + debug.verbose('Processing inline redirect: {@' + tag.trigger + '}'); + options.depth = options.depth + 1; + (0, _inlineRedirect2.default)(tag.trigger, options, postAugment(replyObj, tag, callback)); +}; + +var processCustomFunction = function processCustomFunction(tag, replyObj, options, callback) { + if (tag.args === null) { + debug.verbose('Processing custom function: ^' + tag.functionName + '()'); + return (0, _customFunction2.default)(tag.functionName, [], replyObj, options, callback); + } + + // If there's a wordnet lookup as a parameter, expand it first + return _async2.default.map(tag.functionArgs, function (arg, next) { + if (typeof arg === 'string') { + return next(null, arg); + } + return processWordnetLookup(arg, replyObj, options, next); + }, function (err, args) { + if (err) { + console.error(err); + } + debug.verbose('Processing custom function: ^' + tag.functionName + '(' + args.join(', ') + ')'); + return (0, _customFunction2.default)(tag.functionName, args, replyObj, options, callback); + }); +}; + +var processNewTopic = function processNewTopic(tag, replyObj, options, callback) { + debug.verbose('Processing new topic: ' + tag.topicName); + var newTopic = tag.topicName; + options.user.setTopic(newTopic, function () { + return callback(null, ''); + }); +}; + +var processClearConversation = function processClearConversation(tag, replyObj, options, callback) { + debug.verbose('Processing clear conversation: setting clear conversation to true'); + replyObj.clearConversation = true; + callback(null, ''); +}; + +var processContinueSearching = function processContinueSearching(tag, replyObj, options, callback) { + debug.verbose('Processing continue searching: setting continueMatching to true'); + replyObj.continueMatching = true; + callback(null, ''); +}; + +var processEndSearching = function processEndSearching(tag, replyObj, options, callback) { + debug.verbose('Processing end searching: setting continueMatching to false'); + replyObj.continueMatching = false; + callback(null, ''); +}; + +var processWordnetLookup = function processWordnetLookup(tag, replyObj, options, callback) { + debug.verbose('Processing wordnet lookup for word: ~' + tag.term); + _wordnet2.default.lookup(tag.term, '~', function (err, words) { + if (err) { + console.error(err); + } + + words = words.map(function (item) { + return item.replace(/_/g, ' '); + }); + debug.verbose('Terms found in wordnet: ' + words); + + var replacedWordnet = _utils2.default.pickItem(words); + debug.verbose('Wordnet replaced term: ' + replacedWordnet); + callback(null, replacedWordnet); + }); +}; + +var processAlternates = function processAlternates(tag, replyObj, options, callback) { + debug.verbose('Processing alternates: ' + tag.alternates); + var alternates = tag.alternates; + var random = _utils2.default.getRandomInt(0, alternates.length - 1); + var result = alternates[random]; + callback(null, result); +}; + +var processDelay = function processDelay(tag, replyObj, options, callback) { + callback(null, '{delay=' + tag.delayLength + '}'); +}; + +var processSetState = function processSetState(tag, replyObj, options, callback) { + debug.verbose('Processing setState: ' + JSON.stringify(tag.stateToSet)); + var stateToSet = tag.stateToSet; + var newState = {}; + stateToSet.forEach(function (keyValuePair) { + var key = keyValuePair.key; + var value = keyValuePair.value; + + // Value is a string + value = value.replace(/["']/g, ''); + + // Value is an integer + if (/^[\d]+$/.test(value)) { + value = +value; + } + + // Value is a boolean + if (value === 'true') { + value = true; + } else if (value === 'false') { + value = false; + } + + newState[key] = value; + }); + debug.verbose('New state: ' + JSON.stringify(newState)); + options.user.conversationState = _lodash2.default.merge(options.user.conversationState, newState); + options.user.markModified('conversationState'); + callback(null, ''); +}; + +var processTag = function processTag(tag, replyObj, options, next) { + if (typeof tag === 'string') { + next(null, tag); + } else { + var tagType = tag.type; + switch (tagType) { + case 'topicRedirect': + { + processTopicRedirect(tag, replyObj, options, next); + break; + } + case 'respond': + { + processRespond(tag, replyObj, options, next); + break; + } + case 'customFunction': + { + processCustomFunction(tag, replyObj, options, next); + break; + } + case 'newTopic': + { + processNewTopic(tag, replyObj, options, next); + break; + } + case 'clearConversation': + { + processClearConversation(tag, replyObj, options, next); + break; + } + case 'continueSearching': + { + processContinueSearching(tag, replyObj, options, next); + break; + } + case 'endSearching': + { + processEndSearching(tag, replyObj, options, next); + break; + } + case 'wordnetLookup': + { + processWordnetLookup(tag, replyObj, options, next); + break; + } + case 'redirect': + { + processRedirect(tag, replyObj, options, next); + break; + } + case 'alternates': + { + processAlternates(tag, replyObj, options, next); + break; + } + case 'delay': + { + processDelay(tag, replyObj, options, next); + break; + } + case 'setState': + { + processSetState(tag, replyObj, options, next); + break; + } + default: + { + next('No such tag type: ' + tagType); + break; + } + } + } +}; + +var processReplyTags = function processReplyTags(replyObj, options, callback) { + debug.verbose('Depth: ', options.depth); + + var replyString = replyObj.reply.reply; + debug.info('Reply before processing reply tags: "' + replyString + '"'); + + options.topic = replyObj.topic; + + // Deals with captures as a preprocessing step (avoids tricksy logic having captures + // as function parameters) + var preprocessed = preprocess(replyString, replyObj, options); + var replyTags = parser.parse(preprocessed); + + replyObj.replyIds = [replyObj.reply._id]; + + _async2.default.mapSeries(replyTags, function (tag, next) { + if (typeof tag === 'string') { + next(null, tag); + } else { + processTag(tag, replyObj, options, next); + } + }, function (err, processedReplyParts) { + if (err) { + console.error('There was an error processing reply tags: ' + err); + } + + replyString = processedReplyParts.join('').trim(); + + replyObj.reply.reply = new _re2.default('\\\\s', 'g').replace(replyString, ' '); + + debug.verbose('Final reply object from processTags: ', replyObj); + + if (_lodash2.default.isEmpty(options.user.pendingTopic)) { + return options.user.setTopic(replyObj.topic, function () { + return callback(err, replyObj); + }); + } + + return callback(err, replyObj); + }); +}; + +var processThreadTags = function processThreadTags(string) { + var threads = []; + var strings = []; + string.split('\n').forEach(function (line) { + var match = _regexes2.default.delay.match(line); + if (match) { + threads.push({ delay: match[1], string: _utils2.default.trim(line.replace(match[0], '')) }); + } else { + strings.push(line); + } + }); + return [strings.join('\n'), threads]; +}; + +exports.default = { processThreadTags: processThreadTags, processReplyTags: processReplyTags }; \ No newline at end of file diff --git a/lib/bot/regexes.js b/lib/bot/regexes.js new file mode 100644 index 00000000..27023786 --- /dev/null +++ b/lib/bot/regexes.js @@ -0,0 +1,60 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _re = require('re2'); + +var _re2 = _interopRequireDefault(_re); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// 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 + +exports.default = { + redirect: new _re2.default('\\{@(.+?)\\}'), + topic: new _re2.default('\\^topicRedirect\\(\\s*([~\\w<>\\s]*),([~\\w<>\\s]*)\\s*\\)'), + respond: new _re2.default('\\^respond\\(\\s*([\\w~]*)\\s*\\)'), + + customFn: new _re2.default('\\^(\\w+)\\(([\\w<>%,\\s\\-&()"\';:$]*)\\)'), + wordnet: new _re2.default('(~)(\\w[\\w]+)', 'g'), + state: new _re2.default('{([^}]*)}', 'g'), + + filter: new _re2.default('\\^(\\w+)\\(([\\w<>,\\s]*)\\)', 'i'), + + delay: new _re2.default('{\\s*delay\\s*=\\s*(\\d+)\\s*}'), + + clear: new _re2.default('{\\s*clear\\s*}', 'i'), + continue: new _re2.default('{\\s*continue\\s*}', 'i'), + end: new _re2.default('{\\s*end\\s*}', 'i'), + + capture: new _re2.default('', 'i'), + captures: new _re2.default('', 'ig'), + pcapture: new _re2.default('', 'i'), + pcaptures: new _re2.default('', 'ig'), + + comma: new _re2.default(','), + commas: new _re2.default(',', 'g'), + + space: { + inner: new _re2.default('[ \\t]+', 'g'), + leading: new _re2.default('^[ \\t]+'), + trailing: new _re2.default('[ \\t]+$'), + oneInner: new _re2.default('[ \\t]', 'g'), + oneLeading: new _re2.default('^[ \\t]'), + oneTrailing: new _re2.default('[ \\t]$') + }, + + whitespace: { + both: new _re2.default('(?:^\\s+)|(?:\\s+$)', 'g'), + leading: new _re2.default('^\s+'), + trailing: new _re2.default('\s+$'), + oneLeading: new _re2.default('^\s'), + oneTrailing: new _re2.default('\s$') + } +}; \ No newline at end of file diff --git a/lib/bot/reply/capture-grammar.pegjs b/lib/bot/reply/capture-grammar.pegjs new file mode 100644 index 00000000..abd06f42 --- /dev/null +++ b/lib/bot/reply/capture-grammar.pegjs @@ -0,0 +1,65 @@ +start = captures + +capture + = "" + { + return { + type: "capture", + starID: starID + }; + } + +previousCapture + = "" + { + return { + type: "previousCapture", + starID, + conversationID + }; + } + +previousInput + = "" + { + return { + type: "previousInput", + inputID: inputID + } + } + +previousReply + = "" + { + return { + type: "previousReply", + replyID: replyID + } + } + +stringCharacter + = "\\" character:[<>] { return character; } + / character:[^<>] { return character; } + +string + = string:stringCharacter+ { return string.join(""); } + +captureType + = capture + / previousCapture + / previousInput + / previousReply + / string + +captures + = captureType* + +integer + = numbers:[0-9]+ + { return Number.parseInt(numbers.join("")); } + +ws "whitespace" + = [ \t] + +nl "newline" + = [\n\r] diff --git a/lib/bot/reply/common.js b/lib/bot/reply/common.js new file mode 100644 index 00000000..2660b749 --- /dev/null +++ b/lib/bot/reply/common.js @@ -0,0 +1,156 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _utils = require('../utils'); + +var _utils2 = _interopRequireDefault(_utils); + +var _wordnet = require('./wordnet'); + +var _wordnet2 = _interopRequireDefault(_wordnet); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debugLevels2.default)('SS:ProcessHelpers'); + +var getTopic = function getTopic(chatSystem, name, cb) { + if (name) { + chatSystem.Topic.findOne({ name: name }, function (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: 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. + */ +var topicSetter = function topicSetter(replyString) { + var TOPIC_REGEX = /\{topic=(.+?)\}/i; + var match = replyString.match(TOPIC_REGEX); + var depth = 0; + var 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=' + _utils2.default.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: replyString, newTopic: newTopic }; +}; + +var processAlternates = function processAlternates(reply) { + // Reply Alternates. + var match = reply.match(/\(\((.+?)\)\)/); + var 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 ''; + } + + var parts = match[1].split('|'); + var opts = []; + for (var i = 0; i < parts.length; i++) { + opts.push(parts[i].trim()); + } + + var resp = _utils2.default.getRandomInt(0, opts.length - 1); + reply = reply.replace(new RegExp('\\(\\(\\s*' + _utils2.default.quotemeta(match[1]) + '\\s*\\)\\)'), opts[resp]); + match = reply.match(/\(\((.+?)\)\)/); + } + + return reply; +}; + +// Handle WordNet in Replies +var wordnetReplace = function wordnetReplace(match, sym, word, p3, offset, done) { + _wordnet2.default.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 = _utils2.default.pickItem(words); + done(null, resp); + }); +}; + +var addStateData = function addStateData(data) { + 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; + + // 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; +}; + +exports.default = { + addStateData: addStateData, + getTopic: getTopic, + processAlternates: processAlternates, + topicSetter: topicSetter, + wordnetReplace: wordnetReplace +}; \ No newline at end of file diff --git a/lib/bot/reply/customFunction.js b/lib/bot/reply/customFunction.js new file mode 100644 index 00000000..1d6c3eaf --- /dev/null +++ b/lib/bot/reply/customFunction.js @@ -0,0 +1,71 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debugLevels2.default)('SS:Reply:customFunction'); + +var customFunction = function customFunction(functionName, functionArgs, replyObj, options, callback) { + var plugins = options.system.plugins; + // Important to create a new scope object otherwise we could leak data + var scope = _lodash2.default.merge({}, options.system.scope); + scope.extraScope = options.system.extraScope; + scope.message = options.message; + scope.user = options.user; + + if (plugins[functionName]) { + functionArgs.push(function (err, functionResponse, stopMatching) { + var reply = ''; + var props = {}; + if (err) { + console.error('Error in plugin function (' + functionName + '): ' + err); + return callback(err); + } + + if (_lodash2.default.isPlainObject(functionResponse)) { + if (functionResponse.text) { + reply = functionResponse.text; + delete functionResponse.text; + } + + if (functionResponse.reply) { + reply = functionResponse.reply; + delete functionResponse.reply; + } + + // There may be data, so merge it with the reply object + replyObj.props = _lodash2.default.merge(replyObj.props, functionResponse); + if (stopMatching !== undefined) { + replyObj.continueMatching = !stopMatching; + } + } else { + reply = functionResponse || ''; + if (stopMatching !== undefined) { + replyObj.continueMatching = !stopMatching; + } + } + + return callback(err, reply); + }); + + debug.verbose('Calling plugin function: ' + functionName); + plugins[functionName].apply(scope, functionArgs); + } else { + // If a function is missing, we kill the line and return empty handed + console.error('WARNING: Custom function (' + functionName + ') was not found. Your script may not behave as expected.'); + callback(true, ''); + } +}; + +exports.default = customFunction; \ No newline at end of file diff --git a/lib/bot/reply/inlineRedirect.js b/lib/bot/reply/inlineRedirect.js new file mode 100644 index 00000000..69b29ede --- /dev/null +++ b/lib/bot/reply/inlineRedirect.js @@ -0,0 +1,61 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _message = require('../message'); + +var _message2 = _interopRequireDefault(_message); + +var _common = require('./common'); + +var _common2 = _interopRequireDefault(_common); + +var _getReply = require('../getReply'); + +var _getReply2 = _interopRequireDefault(_getReply); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debugLevels2.default)('SS:Reply:inline'); + +var inlineRedirect = function inlineRedirect(triggerTarget, options, callback) { + debug.verbose('Inline redirection to: \'' + triggerTarget + '\''); + + // 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]; + } + } + + _common2.default.getTopic(options.system.chatSystem, options.topic, function (err, topicData) { + var messageOptions = { + factSystem: options.system.factSystem + }; + + _message2.default.createMessage(triggerTarget, messageOptions, function (redirectMessage) { + options.pendingTopics = [topicData]; + + (0, _getReply2.default)(redirectMessage, options, function (err, redirectReply) { + if (err) { + console.error(err); + } + + debug.verbose('Response from inlineRedirect: ', redirectReply); + if (redirectReply) { + return callback(null, redirectReply); + } + return callback(null, {}); + }); + }); + }); +}; + +exports.default = inlineRedirect; \ No newline at end of file diff --git a/lib/bot/reply/reply-grammar.pegjs b/lib/bot/reply/reply-grammar.pegjs new file mode 100644 index 00000000..63052cc3 --- /dev/null +++ b/lib/bot/reply/reply-grammar.pegjs @@ -0,0 +1,184 @@ +start = reply + +functionArg + = arg:[^),]+ { return arg.join(""); } + +topicRedirect + = "^topicRedirect(" ws* topicName:functionArg ws* "," ws* topicTrigger:functionArg ")" + { + return { + type: "topicRedirect", + topicName, + topicTrigger + } + } + +respond + = "^respond(" ws* topicName:functionArg ws* ")" + { + return { + type: "respond", + topicName: topicName + } + } + +redirect + = "{@" ws* trigger:[^}]+ ws* "}" + { + return { + type: "redirect", + trigger: trigger.join("") + } + } + +customFunctionArg + = ws* "[" arrayContents:[^\]]* "]" ws* + { return `[${arrayContents.join("") || ''}]`; } + / ws* "{" objectContents:[^}]* "}" ws* + { return `{${objectContents.join("") || ''}}`; } + / ws* wordnetLookup:wordnetLookup ws* + { return wordnetLookup; } + / ws* string:[^,)]+ ws* + { return string.join(""); } + +customFunctionArgs + = argFirst:customFunctionArg args:("," arg:customFunctionArg { return arg; })* + { return [argFirst].concat(args); } + +customFunction + = "^" !"topicRedirect" !"respond" name:[A-Za-z0-9_]+ "(" args:customFunctionArgs? ")" + { + return { + type: "customFunction", + functionName: name.join(""), + functionArgs: args + }; + } + +newTopic + = "{" ws* "topic" ws* "=" ws* topicName:[A-Za-z0-9~_]* ws* "}" + { + return { + type: "newTopic", + topicName: topicName.join("") + }; + } + +clearString + = "clear" + / "CLEAR" + +clearConversation + = "{" ws* clearString ws* "}" + { + return { + type: "clearConversation" + } + } + +continueString + = "continue" + / "CONTINUE" + +continueSearching + = "{" ws* continueString ws* "}" + { + return { + type: "continueSearching" + } + } + +endString + = "end" + / "END" + +endSearching + = "{" ws* endString ws* "}" + { + return { + type: "endSearching" + } + } + +wordnetLookup + = "~" term:[A-Za-z0-9_]+ + { + return { + type: "wordnetLookup", + term: term.join("") + } + } + +alternates + = "((" alternateFirst:[^|]+ alternates:("|" alternate:[^|)]+ { return alternate.join(""); })+ "))" + { + return { + type: "alternates", + alternates: [alternateFirst.join("")].concat(alternates) + } + } + +delay + = "{" ws* "delay" ws* "=" ws* delayLength:integer "}" + { + return { + type: "delay", + delayLength + } + } + +keyValuePair + = ws* key:[A-Za-z0-9_]+ ws* "=" ws* value:[A-Za-z0-9_'"]+ ws* + { + return { + key: key.join(""), + value: value.join("") + } + } + +setState + = "{" keyValuePairFirst:keyValuePair keyValuePairs:("," keyValuePair:keyValuePair { return keyValuePair; })* "}" + { + return { + type: "setState", + stateToSet: [keyValuePairFirst].concat(keyValuePairs) + } + } + +stringCharacter + = !"((" "\\" character:[n] { return `\n`; } + / !"((" "\\" character:[s] { return `\\s`; } + / !"((" "\\" character:. { return character; } + / !"((" character:[^^{<~] { return character; } + +string + = string:stringCharacter+ { return string.join(""); } + +replyToken + = topicRedirect + / respond + / redirect + / customFunction + / newTopic + / clearConversation + / continueSearching + / endSearching + / wordnetLookup + / alternates + / delay + / setState + / string + +reply + = tokens:replyToken* + { return tokens; } + +integer + = numbers:[0-9]+ + { return Number.parseInt(numbers.join("")); } + +ws "whitespace" + = [ \t] + +nl "newline" + = [\n\r] diff --git a/lib/bot/reply/respond.js b/lib/bot/reply/respond.js new file mode 100644 index 00000000..91feac95 --- /dev/null +++ b/lib/bot/reply/respond.js @@ -0,0 +1,48 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _common = require('./common'); + +var _common2 = _interopRequireDefault(_common); + +var _getReply = require('../getReply'); + +var _getReply2 = _interopRequireDefault(_getReply); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debugLevels2.default)('SS:Reply:Respond'); + +var respond = function respond(topicName, options, callback) { + debug.verbose('Responding to topic: ' + topicName); + + _common2.default.getTopic(options.system.chatSystem, topicName, function (err, topicData) { + if (err) { + console.error(err); + } + + options.pendingTopics = [topicData]; + + (0, _getReply2.default)(options.message, options, function (err, respondReply) { + if (err) { + console.error(err); + } + + debug.verbose('Callback from respond getReply: ', respondReply); + + if (respondReply) { + return callback(err, respondReply); + } + return callback(err, {}); + }); + }); +}; + +exports.default = respond; \ No newline at end of file diff --git a/lib/bot/reply/topicRedirect.js b/lib/bot/reply/topicRedirect.js new file mode 100644 index 00000000..d5b1a9ca --- /dev/null +++ b/lib/bot/reply/topicRedirect.js @@ -0,0 +1,59 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _common = require('./common'); + +var _common2 = _interopRequireDefault(_common); + +var _message = require('../message'); + +var _message2 = _interopRequireDefault(_message); + +var _getReply = require('../getReply'); + +var _getReply2 = _interopRequireDefault(_getReply); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debugLevels2.default)('SS:Reply:topicRedirect'); + +var topicRedirect = function topicRedirect(topicName, topicTrigger, options, callback) { + debug.verbose('Topic redirection to topic: ' + topicName + ', trigger: ' + topicTrigger); + + // Here we are looking for gambits in the NEW topic. + _common2.default.getTopic(options.system.chatSystem, topicName, function (err, topicData) { + if (err) { + console.error(err); + return callback(null, {}); + } + + var messageOptions = { + facts: options.system.factSystem + }; + + _message2.default.createMessage(topicTrigger, messageOptions, function (redirectMessage) { + options.pendingTopics = [topicData]; + + (0, _getReply2.default)(redirectMessage, options, function (err, redirectReply) { + if (err) { + console.error(err); + } + + debug.verbose('redirectReply', redirectReply); + if (redirectReply) { + return callback(null, redirectReply); + } + return callback(null, {}); + }); + }); + }); +}; + +exports.default = topicRedirect; \ No newline at end of file diff --git a/lib/bot/reply/wordnet.js b/lib/bot/reply/wordnet.js new file mode 100644 index 00000000..6c690c17 --- /dev/null +++ b/lib/bot/reply/wordnet.js @@ -0,0 +1,121 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _natural = require('natural'); + +var _natural2 = _interopRequireDefault(_natural); + +var _async = require('async'); + +var _async2 = _interopRequireDefault(_async); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var wordnet = new _natural2.default.WordNet(); // This is a shim for wordnet lookup. +// http://wordnet.princeton.edu/wordnet/man/wninput.5WN.html + +var define = function define(word, cb) { + wordnet.lookup(word, function (results) { + if (!_lodash2.default.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 +var lookup = function lookup(word) { + var pointerSymbol = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '~'; + var cb = arguments[2]; + + var pos = null; + + var match = word.match(/~(\w)$/); + if (match) { + pos = match[1]; + word = word.replace(match[0], ''); + } + + 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); + } + }); + }); + + var itor = function itor(word, next) { + wordnet.get(word.synsetOffset, word.pos, function (sub) { + next(null, sub.lemma); + }); + }; + + _async2.default.map(synets, itor, function (err, items) { + items = _lodash2.default.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 +var explore = function explore(word, cb) { + var ptrs = []; + + wordnet.lookup(word, function (results) { + for (var i = 0; i < results.length; i++) { + ptrs.push(results[i].ptrs); + } + + ptrs = _lodash2.default.uniq(_lodash2.default.flatten(ptrs)); + ptrs = _lodash2.default.map(ptrs, function (item) { + return { pos: item.pos, sym: item.pointerSymbol }; + }); + + ptrs = _lodash2.default.chain(ptrs).groupBy('pos').map(function (value, key) { + return { + pos: key, + ptr: _lodash2.default.uniq(_lodash2.default.map(value, 'sym')) + }; + }).value(); + + var itor = function itor(item, next) { + var itor2 = function itor2(ptr, next2) { + lookup(word + '~' + item.pos, ptr, function (err, res) { + if (err) { + console.error(err); + } + console.log(word, item.pos, ':', ptr, res.join(', ')); + next2(); + }); + }; + _async2.default.map(item.ptr, itor2, next); + }; + _async2.default.each(ptrs, itor, function () { + return cb(); + }); + }); +}; + +exports.default = { + define: define, + explore: explore, + lookup: lookup +}; \ No newline at end of file diff --git a/lib/bot/utils.js b/lib/bot/utils.js new file mode 100644 index 00000000..955a9dc7 --- /dev/null +++ b/lib/bot/utils.js @@ -0,0 +1,306 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _string = require('string'); + +var _string2 = _interopRequireDefault(_string); + +var _debugLevels = require('debug-levels'); + +var _debugLevels2 = _interopRequireDefault(_debugLevels); + +var _partsOfSpeech = require('parts-of-speech'); + +var _partsOfSpeech2 = _interopRequireDefault(_partsOfSpeech); + +var _re = require('re2'); + +var _re2 = _interopRequireDefault(_re); + +var _regexes = require('./regexes'); + +var _regexes2 = _interopRequireDefault(_regexes); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debugLevels2.default)('SS:Utils'); +var Lex = _partsOfSpeech2.default.Lexer; + +//-------------------------- + +var encodeCommas = function encodeCommas(s) { + return s ? _regexes2.default.commas.replace(s, '') : s; +}; + +var encodedCommasRE = new _re2.default('', 'g'); +var decodeCommas = function decodeCommas(s) { + return 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 + */ +var trim = function trim() { + var text = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + return _regexes2.default.space.inner.replace(_regexes2.default.whitespace.both.replace(text, ''), ' '); +}; + +var wordSepRE = new _re2.default('[\\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` + */ +var wordCount = function wordCount(text) { + return wordSepRE.split(text).filter(function (w) { + return 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 +var inArray = function inArray(list, value) { + if (_lodash2.default.isArray(value)) { + var match = false; + for (var i = 0; i < value.length; i++) { + if (_lodash2.default.includes(list, value[i]) > 0) { + match = _lodash2.default.indexOf(list, value[i]); + } + } + return match; + } else { + return _lodash2.default.indexOf(list, value); + } +}; + +var sentenceSplit = function sentenceSplit(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 && _lodash2.default.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; +}; + +var commandsRE = new _re2.default('[\\\\.+?${}=!:]', 'g'); +var nonCommandsRE = new _re2.default('[\\\\.+*?\\[^\\]$(){}=!<>|:]', 'g'); +/** + * Escape a string sp that it can be used in a regular expression. + * @param {string} string - the string to escape + * @param {boolean} commands - + */ +var quotemeta = function quotemeta(string) { + var commands = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + return (commands ? commandsRE : nonCommandsRE).replace(string, function (c) { + return '\\' + c; + }); +}; + +var cleanArray = function cleanArray(actual) { + var newArray = []; + for (var i = 0; i < actual.length; i++) { + if (actual[i]) { + newArray.push(actual[i]); + } + } + return newArray; +}; + +var aRE = new _re2.default('^(([bcdgjkpqtuvwyz]|onc?e|onetime)$|e[uw]|uk|ur[aeiou]|use|ut([^t])|uni(l[^l]|[a-ko-z]))', 'i'); +var anRE = new _re2.default('^([aefhilmnorsx]$|hono|honest|hour|heir|[aeiou])', 'i'); +var upcaseARE = new _re2.default('^(UN$)'); +var upcaseANRE = new _re2.default('^$'); +var dashSpaceRE = new _re2.default('[- ]'); +var indefiniteArticlerize = function indefiniteArticlerize(word) { + var first = dashSpaceRE.split(word, 2)[0]; + var prefix = (anRE.test(first) || upcaseARE.test(first)) && !(aRE.test(first) || upcaseANRE.test(first)) ? 'an' : 'a'; + return prefix + ' ' + word; +}; + +var indefiniteList = function indefiniteList(list) { + var n = list.map(indefiniteArticlerize); + if (n.length > 1) { + var last = n.pop(); + return n.join(', ') + ' and ' + last; + } else { + return n.join(', '); + } +}; + +var getRandomInt = function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +}; + +var underscoresRE = new _re2.default('_', 'g'); +var pickItem = function pickItem(arr) { + // TODO - Item may have a wornet suffix meal~2 or meal~n + var ind = getRandomInt(0, arr.length - 1); + return _lodash2.default.isString(arr[ind]) ? underscoresRE.replace(arr[ind], ' ') : arr[ind]; +}; + +// Capital first letter, and add period. +var makeSentense = function makeSentense(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'] +}; + +var isTag = function isTag(posTag, wordClass) { + return !!(tags[wordClass].indexOf(posTag) > -1); +}; + +var genId = function genId() { + 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 + */ +var replaceCapturedText = function replaceCapturedText(strings, caps) { + var encoded = caps.map(function (s) { + return encodeCommas(s); + }); + return strings.filter(function (s) { + return !_lodash2.default.isEmpty(s); + }).map(function (s) { + return _regexes2.default.captures.replace(s, function (m, p1) { + return encoded[Number.parseInt(p1 || 1)]; + }); + }); +}; + +var walk = function walk(dir, done) { + if (_fs2.default.statSync(dir).isFile()) { + debug.verbose('Expected directory, found file, simulating directory with only one file: %s', dir); + return done(null, [dir]); + } + + var results = []; + _fs2.default.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; + _fs2.default.stat(file, function (err2, stat) { + if (err2) { + console.log(err2); + } + + if (stat && stat.isDirectory()) { + var 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); + } + } + }); + }); + }); +}; + +var pennToWordnet = function pennToWordnet(pennTag) { + if ((0, _string2.default)(pennTag).startsWith('J')) { + return 'a'; + } else if ((0, _string2.default)(pennTag).startsWith('V')) { + return 'v'; + } else if ((0, _string2.default)(pennTag).startsWith('N')) { + return 'n'; + } else if ((0, _string2.default)(pennTag).startsWith('R')) { + return 'r'; + } else { + return null; + } +}; + +exports.default = { + cleanArray: cleanArray, + encodeCommas: encodeCommas, + decodeCommas: decodeCommas, + genId: genId, + getRandomInt: getRandomInt, + inArray: inArray, + indefiniteArticlerize: indefiniteArticlerize, + indefiniteList: indefiniteList, + isTag: isTag, + makeSentense: makeSentense, + pennToWordnet: pennToWordnet, + pickItem: pickItem, + quotemeta: quotemeta, + replaceCapturedText: replaceCapturedText, + sentenceSplit: sentenceSplit, + trim: trim, + walk: walk, + wordCount: wordCount +}; \ No newline at end of file diff --git a/lib/plugins/alpha.js b/lib/plugins/alpha.js new file mode 100644 index 00000000..08733993 --- /dev/null +++ b/lib/plugins/alpha.js @@ -0,0 +1,166 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _rhymes = require('rhymes'); + +var _rhymes2 = _interopRequireDefault(_rhymes); + +var _syllablistic = require('syllablistic'); + +var _syllablistic2 = _interopRequireDefault(_syllablistic); + +var _debug = require('debug'); + +var _debug2 = _interopRequireDefault(_debug); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debug2.default)('AlphaPlugins'); + +var getRandomInt = function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +}; + +// TODO: deprecate oppisite and replace with opposite +var oppisite = function oppisite(word, cb) { + debug('oppisite', word); + + this.facts.db.get({ subject: word, predicate: 'opposite' }, function (err, opp) { + if (!_lodash2.default.isEmpty(opp)) { + var oppositeWord = opp[0].object; + oppositeWord = oppositeWord.replace(/_/g, ' '); + cb(null, oppositeWord); + } else { + cb(null, ''); + } + }); +}; + +var rhymes = function rhymes(word, cb) { + debug('rhyming', word); + + var rhymedWords = (0, _rhymes2.default)(word); + var i = getRandomInt(0, rhymedWords.length - 1); + + if (rhymedWords.length !== 0) { + cb(null, rhymedWords[i].word.toLowerCase()); + } else { + cb(null, null); + } +}; + +var syllable = function syllable(word, cb) { + return cb(null, _syllablistic2.default.text(word)); +}; + +var letterLookup = function letterLookup(cb) { + 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); +}; + +var wordLength = function wordLength(cap, cb) { + var _this = this; + + 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) { + (function () { + // 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, ''); + } +}; + +var nextNumber = function nextNumber(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); +}; + +exports.default = { + letterLookup: letterLookup, + nextNumber: nextNumber, + oppisite: oppisite, + rhymes: rhymes, + syllable: syllable, + wordLength: wordLength +}; \ No newline at end of file diff --git a/lib/plugins/compare.js b/lib/plugins/compare.js new file mode 100644 index 00000000..7e454d96 --- /dev/null +++ b/lib/plugins/compare.js @@ -0,0 +1,270 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _debug = require('debug'); + +var _debug2 = _interopRequireDefault(_debug); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _async = require('async'); + +var _async2 = _interopRequireDefault(_async); + +var _history = require('../bot/history'); + +var _history2 = _interopRequireDefault(_history); + +var _utils = require('../bot/utils'); + +var _utils2 = _interopRequireDefault(_utils); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debug2.default)('Compare Plugin'); + +var createFact = function createFact(s, v, o, cb) { + var _this = this; + + this.user.memory.create(s, v, o, false, function () { + _this.facts.db.get({ subject: v, predicate: 'opposite' }, function (e, r) { + if (r.length !== 0) { + _this.user.memory.create(o, r[0].object, s, false, function () { + cb(null, ''); + }); + } else { + cb(null, ''); + } + }); + }); +}; + +var findOne = function findOne(haystack, arr) { + return arr.some(function (v) { + return haystack.indexOf(v) >= 0; + }); +}; + +var resolveAdjective = function resolveAdjective(cb) { + var candidates = (0, _history2.default)(this.user, { names: true }); + var message = this.message; + var userFacts = this.user.memory.db; + var botFacts = this.facts.db; + + var getOpp = function getOpp(term, callback) { + botFacts.search({ + subject: term, + predicate: 'opposite', + object: botFacts.v('opp') + }, function (e, oppResult) { + if (!_lodash2.default.isEmpty(oppResult)) { + callback(null, oppResult[0].opp); + } else { + callback(null, null); + } + }); + }; + + var negatedTerm = function negatedTerm(msg, names, cb) { + // Are we confused about what we are looking for??! + // Could be "least tall" negated terms + if (_lodash2.default.contains(msg.adjectives, 'least') && msg.adjectives.length === 2) { + // We need to flip the adjective to the oppisite and do a lookup. + var cmpWord = _lodash2.default.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) { + (function () { + var pn1 = names[0].toLowerCase(); + var pn2 = names[1].toLowerCase(); + + userFacts.get({ subject: pn1, predicate: oppWord, object: pn2 }, function (e, r) { + // r is treated as 'truthy' + if (!_lodash2.default.isEmpty(r)) { + cb(null, _lodash2.default.capitalize(pn1) + ' is ' + oppWord + 'er.'); + } else { + cb(null, _lodash2.default.capitalize(pn2) + ' is ' + oppWord + 'er.'); + } + }); + })(); + } else { + cb(null, _lodash2.default.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 + var baseWord = null; + var getAdjective = function getAdjective(m, cb) { + var cmpWord = void 0; + + if (findOne(m.adjectives, ['least', 'less'])) { + cmpWord = _lodash2.default.first(_lodash2.default.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) { + (function () { + 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 handle(e, cmpTerms) { + var compareWord = cmpTerms[0]; + var compareWord2 = cmpTerms[1]; + + debug('CMP ', compareWord, compareWord2); + + botFacts.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 (!_lodash2.default.isEmpty(message.names)) { + (function () { + debug('We have names', message.names); + // Make sure we say a name they are looking for. + var nameOne = message.names[0].toLowerCase(); + + userFacts.get({ subject: nameOne, predicate: compareWord }, function (e, result) { + if (_lodash2.default.isEmpty(result)) { + // So the fact is wrong, lets try the other way round + + userFacts.get({ object: nameOne, predicate: compareWord }, function (e, result) { + debug('RES', result); + + if (!_lodash2.default.isEmpty(result)) { + if (message.names.length === 2 && result[0].subject === message.names[1]) { + cb(null, _lodash2.default.capitalize(result[0].subject) + ' is ' + compareWord + 'er than ' + _lodash2.default.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, _lodash2.default.capitalize(message.names[1]) + ' is ' + compareWord + 'er than ' + _lodash2.default.capitalize(result[0].object) + '.'); + } else { + cb(null, _utils2.default.pickItem(message.names) + ' is ' + compareWord + 'er?'); + } + } else { + // Lets do it again if we have another name + cb(null, _utils2.default.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') }], function (err, results) { + if (!_lodash2.default.isEmpty(results)) { + if (results[0].v === message.names[1].toLowerCase()) { + cb(null, _lodash2.default.capitalize(message.names[0]) + ' is ' + compareWord + 'er than ' + _lodash2.default.capitalize(message.names[1]) + '.'); + } else { + // Test this + cb(null, _lodash2.default.capitalize(message.names[1]) + ' is ' + compareWord + 'er than ' + _lodash2.default.capitalize(message.names[0]) + '.'); + } + } else { + // Test this block + cb(null, _utils2.default.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) { + userFacts.search([{ subject: userFacts.v('oldest'), + predicate: compareWord, + object: userFacts.v('rand1') }, { subject: userFacts.v('oldest'), + predicate: compareWord, + object: userFacts.v('rand2') }], function (err, results) { + if (!_lodash2.default.isEmpty(results)) { + cb(null, _lodash2.default.capitalize(results[0].oldest) + ' is the ' + compareWord + 'est.'); + } else { + // Pick one. + cb(null, _lodash2.default.capitalize(_utils2.default.pickItem(prevMessage.names)) + ' is the ' + compareWord + 'est.'); + } + }); + } else { + if (!_lodash2.default.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 }, function (e, result) { + if (!_lodash2.default.isEmpty(result)) { + if (message.qSubType === 'YN') { + cb(null, 'Yes, ' + _lodash2.default.capitalize(result[0].object) + ' is ' + compareWord + 'er.'); + } else { + cb(null, _lodash2.default.capitalize(result[0].object) + ' is ' + compareWord + 'er than ' + prevMessage.names[0] + '.'); + } + } else { + if (message.qSubType === 'YN') { + cb(null, 'Yes, ' + _lodash2.default.capitalize(prevMessage.names[1]) + ' is ' + compareWord + 'er.'); + } else { + cb(null, _lodash2.default.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, ' + _lodash2.default.capitalize(prevMessage.names[0]) + ' is ' + compareWord + 'er.'); + } else { + cb(null, _lodash2.default.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."); + } + } + } + }); + }; + + _async2.default.map([message, prevMessage], getAdjective, handle); + } else { + negatedTerm(message, prevMessage.names, cb); + } + } + })(); + } else { + cb(null, '??'); + } +}; + +exports.default = { + createFact: createFact, + resolveAdjective: resolveAdjective +}; \ No newline at end of file diff --git a/lib/plugins/math.js b/lib/plugins/math.js new file mode 100644 index 00000000..fbf01902 --- /dev/null +++ b/lib/plugins/math.js @@ -0,0 +1,110 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/* + Math functions for + - evaluating expressions + - converting functions + - sequence functions +*/ + +var math = require('../bot/math'); +var roman = require('roman-numerals'); +var debug = require('debug')('mathPlugin'); + +var evaluateExpression = function evaluateExpression(cb) { + if (this.message.numericExp || this.message.halfNumericExp && this.user.prevAns) { + var answer = math.parse(this.message.cwords, this.user.prevAns); + var suggestedReply = void 0; + 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, ''); + } +}; + +var numToRoman = function numToRoman(cb) { + var suggest = 'I think it is ' + roman.toRoman(this.message.numbers[0]); + cb(null, suggest); +}; + +var numToHex = function numToHex(cb) { + var suggest = 'I think it is ' + parseInt(this.message.numbers[0], 10).toString(16); + cb(null, suggest); +}; + +var numToBinary = function numToBinary(cb) { + var suggest = 'I think it is ' + parseInt(this.message.numbers[0], 10).toString(2); + cb(null, suggest); +}; + +var numMissing = function numMissing(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 += 1; + } + } + } + var s = mia.sort(function (a, b) { + return a - b; + }); + cb(null, 'I think it is ' + s.join(' ')); + } else { + cb(true, ''); + } +}; + +// Sequence +var numSequence = function numSequence(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; + }); + + var suggest = void 0; + if (math.arithGeo(numArray) === 'Arithmetic') { + var x = void 0; + for (var 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') { + var a = numArray[1]; + var r = a / numArray[0]; + suggest = 'I think it is ' + numArray.pop() * r; + } + + cb(null, suggest); + } else { + cb(true, ''); + } +}; + +exports.default = { + evaluateExpression: evaluateExpression, + numMissing: numMissing, + numSequence: numSequence, + numToBinary: numToBinary, + numToHex: numToHex, + numToRoman: numToRoman +}; \ No newline at end of file diff --git a/lib/plugins/message.js b/lib/plugins/message.js new file mode 100644 index 00000000..5f701c11 --- /dev/null +++ b/lib/plugins/message.js @@ -0,0 +1,87 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +// import debuglog from 'debug'; +// import _ from 'lodash'; + +// import history from '../bot/history'; + +// const debug = debuglog('Message Plugin'); + +var addMessageProp = function addMessageProp(key, value, callback) { + if (key !== '' && value !== '') { + return callback(null, _defineProperty({}, key, value)); + } + + return callback(null, ''); +}; + +/* + + ## First Person (Single, Plural) + I, we + me, us + my/mine, our/ours + + ## Second Person (Single, Plural) + you, yous + + ## Third Person Single + he (masculine) + she (feminine) + it (neuter) + him (masculine) + her (feminine) + it (neuter) + his/his (masculine) + her/hers (feminine) + its/its (neuter) + + ## Third Person plural + they + them + their/theirs + +*/ +// exports.resolvePronouns = function(cb) { +// var message = this.message; +// var user = this.user; +// message.pronounMap = {}; + +// if (user['history']['input'].length !== 0) { +// console.log(message.pronouns); +// for (var i = 0; i < message.pronouns.length;i++) { +// var pn = message.pronouns[i]; +// var value = findPronoun(pn, user); +// message.pronounMap[pn] = value; +// } +// console.log(message.pronounMap) +// cb(null, ""); +// } else { +// for (var i = 0; i < message.pronouns.length;i++) { +// var pn = message.pronouns[i]; +// message.pronounMap[pn] = null; +// } +// console.log(message.pronounMap) +// cb(null, ""); +// } +// } + +// var findPronoun = function(pnoun, user) { +// console.log("Looking in history for", pnoun); + +// var candidates = history(user, { names: true }); +// if (!_.isEmpty(candidates)) { +// debug("history candidates", candidates); +// return candidates[0].names; +// } else { +// return null; +// } +// } + +exports.default = { addMessageProp: addMessageProp }; \ No newline at end of file diff --git a/lib/plugins/test.js b/lib/plugins/test.js new file mode 100644 index 00000000..f84275a0 --- /dev/null +++ b/lib/plugins/test.js @@ -0,0 +1,119 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// This is used in a test to verify fall though works +// TODO: Move this into a fixture. +var bail = function bail(cb) { + cb(true, null); +}; + +var one = function one(cb) { + cb(null, 'one'); +}; + +var num = function num(n, cb) { + cb(null, n); +}; + +var changetopic = function changetopic(n, cb) { + this.user.setTopic(n, function () { + return cb(null, ''); + }); +}; + +var changefunctionreply = function changefunctionreply(newtopic, cb) { + cb(null, '{topic=' + newtopic + '}'); +}; + +var doSomething = function doSomething(cb) { + console.log('this.message.raw', this.message.raw); + cb(null, 'function'); +}; + +var breakFunc = function breakFunc(cb) { + cb(null, '', true); +}; + +var nobreak = function nobreak(cb) { + cb(null, '', false); +}; + +var objparam1 = function objparam1(cb) { + var data = { + text: 'world', + attachments: [{ + text: 'Optional text that appears *within* the attachment' + }] + }; + cb(null, data); +}; + +var objparam2 = function objparam2(cb) { + cb(null, { test: 'hello', text: 'world' }); +}; + +var showScope = function showScope(cb) { + cb(null, this.extraScope.key + ' ' + this.user.id + ' ' + this.message.clean); +}; + +var word = function word(word1, word2, cb) { + cb(null, word1 === word2); +}; + +var hasFirstName = function hasFirstName(bool, cb) { + this.user.getVar('firstName', function (e, name) { + if (name !== null) { + cb(null, bool === 'true'); + } else { + cb(null, bool === 'false'); + } + }); +}; + +var getUserId = function getUserId(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', _lodash2.default.isEqual(userID, that.user.id)); + cb(null, that.user.id); + }); +}; + +var hasName = function hasName(bool, cb) { + this.user.getVar('name', function (e, name) { + if (name !== null) { + cb(null, bool === 'true'); + } else { + // We have no name + cb(null, bool === 'false'); + } + }); +}; + +exports.default = { + bail: bail, + breakFunc: breakFunc, + doSomething: doSomething, + changefunctionreply: changefunctionreply, + changetopic: changetopic, + getUserId: getUserId, + hasFirstName: hasFirstName, + hasName: hasName, + nobreak: nobreak, + num: num, + objparam1: objparam1, + objparam2: objparam2, + one: one, + showScope: showScope, + word: word +}; \ No newline at end of file diff --git a/lib/plugins/time.js b/lib/plugins/time.js new file mode 100644 index 00000000..dec6fd55 --- /dev/null +++ b/lib/plugins/time.js @@ -0,0 +1,102 @@ +'use strict'; + +var _moment = require('moment'); + +var _moment2 = _interopRequireDefault(_moment); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var COEFF = 1000 * 60 * 5; + +var getSeason = function getSeason() { + var now = (0, _moment2.default)(); + 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'; + } + return 'unknown'; +}; + +exports.getDOW = function getDOW(cb) { + cb(null, (0, _moment2.default)().format('dddd')); +}; + +exports.getDate = function getDate(cb) { + cb(null, (0, _moment2.default)().format('ddd, MMMM Do')); +}; + +exports.getDateTomorrow = function getDateTomorrow(cb) { + var date = (0, _moment2.default)().add('d', 1).format('ddd, MMMM Do'); + cb(null, date); +}; + +exports.getSeason = function getSeason(cb) { + cb(null, getSeason()); +}; + +exports.getTime = function getTime(cb) { + var date = new Date(); + var rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); + var time = (0, _moment2.default)(rounded).format('h:mm'); + cb(null, 'The time is ' + time); +}; + +exports.getGreetingTimeOfDay = function getGreetingTimeOfDay(cb) { + var date = new Date(); + var rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); + var time = (0, _moment2.default)(rounded).format('H'); + var tod = void 0; + if (time < 12) { + tod = 'morning'; + } else if (time < 17) { + tod = 'afternoon'; + } else { + tod = 'evening'; + } + + cb(null, tod); +}; + +exports.getTimeOfDay = function getTimeOfDay(cb) { + var date = new Date(); + var rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); + var time = (0, _moment2.default)(rounded).format('H'); + var tod = void 0; + if (time < 12) { + tod = 'morning'; + } else if (time < 17) { + tod = 'afternoon'; + } else { + tod = 'night'; + } + + cb(null, tod); +}; + +exports.getDayOfWeek = function getDayOfWeek(cb) { + cb(null, (0, _moment2.default)().format('dddd')); +}; + +exports.getMonth = function getMonth(cb) { + var reply = ''; + if (this.message.words.indexOf('next') !== -1) { + reply = (0, _moment2.default)().add('M', 1).format('MMMM'); + } else if (this.message.words.indexOf('previous') !== -1) { + reply = (0, _moment2.default)().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 = (0, _moment2.default)().format('MMMM'); + } + cb(null, reply); +}; \ No newline at end of file diff --git a/lib/plugins/user.js b/lib/plugins/user.js new file mode 100644 index 00000000..1891f584 --- /dev/null +++ b/lib/plugins/user.js @@ -0,0 +1,110 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _debug = require('debug'); + +var _debug2 = _interopRequireDefault(_debug); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debug2.default)('SS:UserFacts'); + +var save = function save(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 (_lodash2.default.isFunction(value)) { + cb = value; + value = ''; + } + } + + memory.db.get({ subject: key, predicate: userId }, function (err, results) { + if (!_lodash2.default.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, ''); + }); + } + }); +}; + +var hasItem = function hasItem(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 (err, res) { + if (!_lodash2.default.isEmpty(res)) { + cb(null, bool === 'true'); + } else { + cb(null, bool === 'false'); + } + }); +}; + +var get = function get(key, cb) { + var memory = this.user.memory; + var userId = this.user.id; + + debug('getVar', key, userId); + + memory.db.get({ subject: key, predicate: userId }, function (err, res) { + if (res && res.length !== 0) { + cb(err, res[0].object); + } else { + cb(err, ''); + } + }); +}; + +var createUserFact = function createUserFact(s, v, o, cb) { + this.user.memory.create(s, v, o, false, function () { + cb(null, ''); + }); +}; + +var known = function known(bool, cb) { + var memory = this.user.memory; + var name = this.message.names && !_lodash2.default.isEmpty(this.message.names) ? this.message.names[0] : ''; + memory.db.get({ subject: name.toLowerCase() }, function (err, res1) { + memory.db.get({ object: name.toLowerCase() }, function (err, res2) { + if (_lodash2.default.isEmpty(res1) && _lodash2.default.isEmpty(res2)) { + cb(null, bool === 'false'); + } else { + cb(null, bool === 'true'); + } + }); + }); +}; + +var inTopic = function inTopic(topic, cb) { + if (topic === this.user.currentTopic) { + cb(null, 'true'); + } else { + cb(null, 'false'); + } +}; + +exports.default = { + createUserFact: createUserFact, + get: get, + hasItem: hasItem, + inTopic: inTopic, + known: known, + save: save +}; \ No newline at end of file diff --git a/lib/plugins/wordnet.js b/lib/plugins/wordnet.js new file mode 100644 index 00000000..fbcb09c7 --- /dev/null +++ b/lib/plugins/wordnet.js @@ -0,0 +1,28 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _wordnet = require('../bot/reply/wordnet'); + +var _wordnet2 = _interopRequireDefault(_wordnet); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var wordnetDefine = function wordnetDefine(cb) { + var args = Array.prototype.slice.call(arguments); + var word = void 0; + + if (args.length === 2) { + word = args[0]; + } else { + word = this.message.words.pop(); + } + + _wordnet2.default.define(word, function (err, result) { + cb(null, 'The Definition of ' + word + ' is ' + result); + }); +}; + +exports.default = { wordnetDefine: wordnetDefine }; \ No newline at end of file diff --git a/lib/plugins/words.js b/lib/plugins/words.js new file mode 100644 index 00000000..cf9663af --- /dev/null +++ b/lib/plugins/words.js @@ -0,0 +1,56 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _pluralize = require('pluralize'); + +var _pluralize2 = _interopRequireDefault(_pluralize); + +var _debug = require('debug'); + +var _debug2 = _interopRequireDefault(_debug); + +var _utils = require('../bot/utils'); + +var _utils2 = _interopRequireDefault(_utils); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debug2.default)('Word Plugin'); + +var plural = function plural(word, cb) { + // Sometimes WordNet will give us more then one word + var reply = void 0; + var parts = word.split(' '); + + if (parts.length === 2) { + reply = _pluralize2.default.plural(parts[0]) + ' ' + parts[1]; + } else { + reply = _pluralize2.default.plural(word); + } + + cb(null, reply); +}; + +var not = function not(word, cb) { + var words = word.split('|'); + var results = _utils2.default.inArray(this.message.words, words); + debug('RES', results); + cb(null, results === false); +}; + +var lowercase = function lowercase(word, cb) { + if (word) { + cb(null, word.toLowerCase()); + } else { + cb(null, ''); + } +}; + +exports.default = { + lowercase: lowercase, + not: not, + plural: plural +}; \ No newline at end of file From 8c3991acdad45bb6fd7dd1bb4c7dc44ab19144da Mon Sep 17 00:00:00 2001 From: Ben James Date: Fri, 25 Nov 2016 22:49:41 +0000 Subject: [PATCH 3/7] Make model names less prone to collision (most likely User) --- lib/bot/db/helpers.js | 18 ++++++++++-------- lib/bot/db/modelNames.js | 13 +++++++++++++ lib/bot/db/models/gambit.js | 28 ++++++++++++++++------------ lib/bot/db/models/reply.js | 12 ++++++++---- lib/bot/db/models/topic.js | 32 +++++++++++++++++--------------- lib/bot/db/models/user.js | 8 ++++++-- lib/bot/index.js | 4 +++- src/bot/db/helpers.js | 11 ++++++----- src/bot/db/modelNames.js | 8 ++++++++ src/bot/db/models/gambit.js | 15 ++++++++------- src/bot/db/models/reply.js | 9 +++++---- src/bot/db/models/topic.js | 19 ++++++++++--------- src/bot/db/models/user.js | 5 +++-- src/bot/index.js | 2 +- 14 files changed, 114 insertions(+), 70 deletions(-) create mode 100644 lib/bot/db/modelNames.js create mode 100644 src/bot/db/modelNames.js diff --git a/lib/bot/db/helpers.js b/lib/bot/db/helpers.js index 9de8af7a..511b59e8 100644 --- a/lib/bot/db/helpers.js +++ b/lib/bot/db/helpers.js @@ -20,6 +20,10 @@ var _safeEval = require('safe-eval'); var _safeEval2 = _interopRequireDefault(_safeEval); +var _modelNames = require('./modelNames'); + +var _modelNames2 = _interopRequireDefault(_modelNames); + var _utils = require('../utils'); var _utils2 = _interopRequireDefault(_utils); @@ -30,12 +34,10 @@ var _postParse2 = _interopRequireDefault(_postParse); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -// These are shared helpers for the models. - -var debug = (0, _debugLevels2.default)('SS:Common'); +var debug = (0, _debugLevels2.default)('SS:Common'); // These are shared helpers for the models. var _walkReplyParent = function _walkReplyParent(db, tenantId, replyId, replyIds, cb) { - db.model('Reply').byTenant(tenantId).findById(replyId).populate('parent').exec(function (err, reply) { + db.model(_modelNames2.default.reply).byTenant(tenantId).findById(replyId).populate('parent').exec(function (err, reply) { if (err) { debug.error(err); } @@ -56,7 +58,7 @@ var _walkReplyParent = function _walkReplyParent(db, tenantId, replyId, replyIds }; var _walkGambitParent = function _walkGambitParent(db, tenantId, gambitId, gambitIds, cb) { - db.model('Gambit').byTenant(tenantId).findOne({ _id: gambitId }).populate('parent').exec(function (err, gambit) { + db.model(_modelNames2.default.gambit).byTenant(tenantId).findOne({ _id: gambitId }).populate('parent').exec(function (err, gambit) { if (err) { console.log(err); } @@ -85,7 +87,7 @@ var findMatchingGambitsForMessage = function findMatchingGambitsForMessage(db, t var populateGambits = function populateGambits(gambit, next) { debug.verbose('Populating gambit'); - db.model('Reply').byTenant(tenantId).populate(gambit, { path: 'replies' }, next); + db.model(_modelNames2.default.reply).byTenant(tenantId).populate(gambit, { path: 'replies' }, next); }; _async2.default.each(gambitsParent.gambits, populateGambits, function (err) { @@ -101,11 +103,11 @@ var findMatchingGambitsForMessage = function findMatchingGambitsForMessage(db, t if (type === 'topic') { debug.verbose('Looking back Topic', id); - db.model('Topic').byTenant(tenantId).findOne({ _id: id }, 'gambits').populate({ path: 'gambits' }).exec(execHandle); + db.model(_modelNames2.default.topic).byTenant(tenantId).findOne({ _id: id }, 'gambits').populate({ path: 'gambits' }).exec(execHandle); } else if (type === 'reply') { options.topic = 'reply'; debug.verbose('Looking back at Conversation', id); - db.model('Reply').byTenant(tenantId).findOne({ _id: id }, 'gambits').populate({ path: 'gambits' }).exec(execHandle); + db.model(_modelNames2.default.reply).byTenant(tenantId).findOne({ _id: id }, 'gambits').populate({ path: 'gambits' }).exec(execHandle); } else { debug.verbose('We should never get here'); callback(true); diff --git a/lib/bot/db/modelNames.js b/lib/bot/db/modelNames.js new file mode 100644 index 00000000..4bf18693 --- /dev/null +++ b/lib/bot/db/modelNames.js @@ -0,0 +1,13 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var names = { + gambit: 'ss_gambit', + reply: 'ss_reply', + topic: 'ss_topic', + user: 'ss_user' +}; + +exports.default = names; \ No newline at end of file diff --git a/lib/bot/db/models/gambit.js b/lib/bot/db/models/gambit.js index 0ce06a00..465a5dc5 100644 --- a/lib/bot/db/models/gambit.js +++ b/lib/bot/db/models/gambit.js @@ -28,6 +28,10 @@ var _ssParser = require('ss-parser'); var _ssParser2 = _interopRequireDefault(_ssParser); +var _modelNames = require('../modelNames'); + +var _modelNames2 = _interopRequireDefault(_modelNames); + var _helpers = require('../helpers'); var _helpers2 = _interopRequireDefault(_helpers); @@ -42,6 +46,11 @@ var _factSystem2 = _interopRequireDefault(_factSystem); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +/** + A Gambit is a Trigger + Reply or Reply Set + - We define a Reply as a subDocument in Mongo. +**/ + var debug = (0, _debugLevels2.default)('SS:Gambit'); /** @@ -49,11 +58,6 @@ var debug = (0, _debugLevels2.default)('SS:Gambit'); A trigger also contains one or more replies. **/ -/** - A Gambit is a Trigger + Reply or Reply Set - - We define a Reply as a subDocument in Mongo. -**/ - var createGambitModel = function createGambitModel(db) { var gambitSchema = new _mongoose2.default.Schema({ id: { type: String, index: true, default: _utils2.default.genId() }, @@ -80,10 +84,10 @@ var createGambitModel = function createGambitModel(db) { filter: { type: String, default: '' }, // An array of replies. - replies: [{ type: String, ref: 'Reply' }], + replies: [{ type: String, ref: _modelNames2.default.reply }], // Save a reference to the parent Reply, so we can walk back up the tree - parent: { type: String, ref: 'Reply' }, + parent: { type: String, ref: _modelNames2.default.reply }, // This will redirect anything that matches elsewhere. // If you want to have a conditional rediect use reply redirects @@ -118,7 +122,7 @@ var createGambitModel = function createGambitModel(db) { return callback('No data'); } - var Reply = db.model('Reply').byTenant(this.getTenantId()); + var Reply = db.model(_modelNames2.default.reply).byTenant(this.getTenantId()); var reply = new Reply(replyData); reply.save(function (err) { if (err) { @@ -140,7 +144,7 @@ var createGambitModel = function createGambitModel(db) { var clearReply = function clearReply(replyId, cb) { self.replies.pull({ _id: replyId }); - db.model('Reply').byTenant(this.getTenantId()).remove({ _id: replyId }, function (err) { + db.model(_modelNames2.default.reply).byTenant(this.getTenantId()).remove({ _id: replyId }, function (err) { if (err) { console.log(err); } @@ -162,13 +166,13 @@ var createGambitModel = function createGambitModel(db) { var _this3 = this; if (!this.parent) { - db.model('Topic').byTenant(this.getTenantId()).findOne({ gambits: { $in: [this._id] } }).exec(function (err, doc) { + db.model(_modelNames2.default.topic).byTenant(this.getTenantId()).findOne({ gambits: { $in: [this._id] } }).exec(function (err, doc) { cb(err, doc.name); }); } else { _helpers2.default.walkGambitParent(db, this.getTenantId(), this._id, function (err, gambits) { if (gambits.length !== 0) { - db.model('Topic').byTenant(_this3.getTenantId()).findOne({ gambits: { $in: [gambits.pop()] } }).exec(function (err, topic) { + db.model(_modelNames2.default.topic).byTenant(_this3.getTenantId()).findOne({ gambits: { $in: [gambits.pop()] } }).exec(function (err, topic) { cb(null, topic.name); }); } else { @@ -181,7 +185,7 @@ var createGambitModel = function createGambitModel(db) { gambitSchema.plugin(_mongooseFindorcreate2.default); gambitSchema.plugin(_mongoTenant2.default); - return db.model('Gambit', gambitSchema); + return db.model('ss_gambit', gambitSchema); }; exports.default = createGambitModel; \ No newline at end of file diff --git a/lib/bot/db/models/reply.js b/lib/bot/db/models/reply.js index 011c95c6..76024459 100644 --- a/lib/bot/db/models/reply.js +++ b/lib/bot/db/models/reply.js @@ -16,6 +16,10 @@ var _async = require('async'); var _async2 = _interopRequireDefault(_async); +var _modelNames = require('../modelNames'); + +var _modelNames2 = _interopRequireDefault(_modelNames); + var _utils = require('../../utils'); var _utils2 = _interopRequireDefault(_utils); @@ -36,11 +40,11 @@ var createReplyModel = function createReplyModel(db) { reply: { type: String, required: '{reply} is required.' }, keep: { type: Boolean, default: false }, filter: { type: String, default: '' }, - parent: { type: String, ref: 'Gambit' }, + parent: { type: String, ref: _modelNames2.default.gambit }, // Replies could referece other gambits // This forms the basis for the 'previous' - These are Children - gambits: [{ type: String, ref: 'Gambit' }] + gambits: [{ type: String, ref: _modelNames2.default.gambit }] }); // This method is similar to the topic.findMatch @@ -53,7 +57,7 @@ var createReplyModel = function createReplyModel(db) { var self = this; var expandReorder = function expandReorder(gambitId, cb) { - db.model('Gambit').byTenant(_this.getTenantId()).findById(gambitId, function (err, gambit) { + db.model(_modelNames2.default.gambit).byTenant(_this.getTenantId()).findById(gambitId, function (err, gambit) { cb(err, gambit); }); }; @@ -73,7 +77,7 @@ var createReplyModel = function createReplyModel(db) { replySchema.plugin(_mongoTenant2.default); - return db.model('Reply', replySchema); + return db.model(_modelNames2.default.reply, replySchema); }; exports.default = createReplyModel; \ No newline at end of file diff --git a/lib/bot/db/models/topic.js b/lib/bot/db/models/topic.js index 1eb5b673..e9657b0c 100644 --- a/lib/bot/db/models/topic.js +++ b/lib/bot/db/models/topic.js @@ -36,6 +36,10 @@ var _ssParser = require('ss-parser'); var _ssParser2 = _interopRequireDefault(_ssParser); +var _modelNames = require('../modelNames'); + +var _modelNames2 = _interopRequireDefault(_modelNames); + var _sort = require('../sort'); var _sort2 = _interopRequireDefault(_sort); @@ -46,12 +50,10 @@ var _helpers2 = _interopRequireDefault(_helpers); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -/** - Topics are a grouping of gambits. - The order of the Gambits are important, and a gambit can live in more than one topic. -**/ - -var debug = (0, _debugLevels2.default)('SS:Topics'); +var debug = (0, _debugLevels2.default)('SS:Topics'); /** + Topics are a grouping of gambits. + The order of the Gambits are important, and a gambit can live in more than one topic. + **/ var TfIdf = _natural2.default.TfIdf; var tfidf = new TfIdf(); @@ -89,7 +91,7 @@ var createTopicModel = function createTopicModel(db) { nostay: { type: Boolean, default: false }, filter: { type: String, default: '' }, keywords: { type: Array }, - gambits: [{ type: String, ref: 'Gambit' }] + gambits: [{ type: String, ref: _modelNames2.default.gambit }] }); topicSchema.pre('save', function (next) { @@ -110,7 +112,7 @@ var createTopicModel = function createTopicModel(db) { return callback('No data'); } - var Gambit = db.model('Gambit').byTenant(this.getTenantId()); + var Gambit = db.model(_modelNames2.default.gambit).byTenant(this.getTenantId()); var gambit = new Gambit(gambitData); gambit.save(function (err) { if (err) { @@ -127,7 +129,7 @@ var createTopicModel = function createTopicModel(db) { var _this2 = this; var expandReorder = function expandReorder(gambitId, cb) { - db.model('Gambit').byTenant(_this2.getTenantId()).findById(gambitId, function (err, gambit) { + db.model(_modelNames2.default.gambit).byTenant(_this2.getTenantId()).findById(gambitId, function (err, gambit) { if (err) { console.log(err); } @@ -166,7 +168,7 @@ var createTopicModel = function createTopicModel(db) { }); }; - db.model('Topic').byTenant(this.getTenantId()).findOne({ name: this.name }, 'gambits').populate('gambits').exec(function (err, mgambits) { + db.model(_modelNames2.default.topic).byTenant(this.getTenantId()).findOne({ name: this.name }, 'gambits').populate('gambits').exec(function (err, mgambits) { if (err) { debug.error(err); } @@ -181,13 +183,13 @@ var createTopicModel = function createTopicModel(db) { var clearGambit = function clearGambit(gambitId, cb) { _this3.gambits.pull({ _id: gambitId }); - db.model('Gambit').byTenant(_this3.getTenantId()).findById(gambitId, function (err, gambit) { + db.model(_modelNames2.default.gambit).byTenant(_this3.getTenantId()).findById(gambitId, function (err, gambit) { if (err) { debug.error(err); } gambit.clearReplies(function () { - db.model('Gambit').byTenant(_this3.getTenantId()).remove({ _id: gambitId }, function (err) { + db.model(_modelNames2.default.gambit).byTenant(_this3.getTenantId()).remove({ _id: gambitId }, function (err) { if (err) { debug.error(err); } @@ -209,7 +211,7 @@ var createTopicModel = function createTopicModel(db) { // This will find a gambit in any topic topicSchema.statics.findTriggerByTrigger = function (input, callback) { - db.model('Gambit').byTenant(this.getTenantId()).findOne({ input: input }).exec(callback); + db.model(_modelNames2.default.gambit).byTenant(this.getTenantId()).findOne({ input: input }).exec(callback); }; topicSchema.statics.findByName = function (name, callback) { @@ -301,7 +303,7 @@ var createTopicModel = function createTopicModel(db) { debug('Conversation RESET by clearBit'); callback(null, removeMissingTopics(pendingTopics)); } else { - db.model('Reply').byTenant(_this4.getTenantId()).find({ _id: { $in: lastReply.replyIds } }).exec(function (err, replies) { + db.model(_modelNames2.default.reply).byTenant(_this4.getTenantId()).find({ _id: { $in: lastReply.replyIds } }).exec(function (err, replies) { if (err) { console.error(err); } @@ -347,7 +349,7 @@ var createTopicModel = function createTopicModel(db) { topicSchema.plugin(_mongooseFindorcreate2.default); topicSchema.plugin(_mongoTenant2.default); - return db.model('Topic', topicSchema); + return db.model(_modelNames2.default.topic, topicSchema); }; exports.default = createTopicModel; \ No newline at end of file diff --git a/lib/bot/db/models/user.js b/lib/bot/db/models/user.js index 34fc443d..7ae1b8dd 100644 --- a/lib/bot/db/models/user.js +++ b/lib/bot/db/models/user.js @@ -32,6 +32,10 @@ var _mongoTenant = require('mongo-tenant'); var _mongoTenant2 = _interopRequireDefault(_mongoTenant); +var _modelNames = require('../modelNames'); + +var _modelNames2 = _interopRequireDefault(_modelNames); + var _factSystem = require('../../factSystem'); var _factSystem2 = _interopRequireDefault(_factSystem); @@ -151,7 +155,7 @@ var createUserModel = function createUserModel(db) { var pendingTopic = _this.pendingTopic; _this.pendingTopic = null; - db.model('Topic').byTenant(_this.getTenantId()).findOne({ name: pendingTopic }, function (err, topicData) { + db.model(_modelNames2.default.topic).byTenant(_this.getTenantId()).findOne({ name: pendingTopic }, function (err, topicData) { if (topicData && topicData.nostay === true) { _this.currentTopic = _this.history.topic[0]; } else { @@ -219,7 +223,7 @@ var createUserModel = function createUserModel(db) { return _factSystem2.default.createFactSystemForTenant(this.getTenantId()).createUserDB(this.id); }); - return db.model('User', userSchema); + return db.model(_modelNames2.default.user, userSchema); }; exports.default = createUserModel; \ No newline at end of file diff --git a/lib/bot/index.js b/lib/bot/index.js index 2b090e41..578bc1a1 100644 --- a/lib/bot/index.js +++ b/lib/bot/index.js @@ -83,7 +83,9 @@ var loadPlugins = function loadPlugins(path) { }; var SuperScript = function () { - function SuperScript(tenantId) { + function SuperScript() { + var tenantId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'master'; + _classCallCheck(this, SuperScript); this.factSystem = _factSystem2.default.createFactSystemForTenant(tenantId); diff --git a/src/bot/db/helpers.js b/src/bot/db/helpers.js index c83849f9..319d0727 100644 --- a/src/bot/db/helpers.js +++ b/src/bot/db/helpers.js @@ -5,13 +5,14 @@ import _ from 'lodash'; import debuglog from 'debug-levels'; import safeEval from 'safe-eval'; +import modelNames from './modelNames'; import Utils from '../utils'; import postParse from '../postParse'; const debug = debuglog('SS:Common'); const _walkReplyParent = function _walkReplyParent(db, tenantId, replyId, replyIds, cb) { - db.model('Reply').byTenant(tenantId).findById(replyId) + db.model(modelNames.reply).byTenant(tenantId).findById(replyId) .populate('parent') .exec((err, reply) => { if (err) { @@ -34,7 +35,7 @@ const _walkReplyParent = function _walkReplyParent(db, tenantId, replyId, replyI }; const _walkGambitParent = function _walkGambitParent(db, tenantId, gambitId, gambitIds, cb) { - db.model('Gambit').byTenant(tenantId).findOne({ _id: gambitId }) + db.model(modelNames.gambit).byTenant(tenantId).findOne({ _id: gambitId }) .populate('parent') .exec((err, gambit) => { if (err) { @@ -65,7 +66,7 @@ const findMatchingGambitsForMessage = function findMatchingGambitsForMessage(db, const populateGambits = function populateGambits(gambit, next) { debug.verbose('Populating gambit'); - db.model('Reply').byTenant(tenantId).populate(gambit, { path: 'replies' }, next); + db.model(modelNames.reply).byTenant(tenantId).populate(gambit, { path: 'replies' }, next); }; async.each(gambitsParent.gambits, populateGambits, (err) => { @@ -83,13 +84,13 @@ const findMatchingGambitsForMessage = function findMatchingGambitsForMessage(db, if (type === 'topic') { debug.verbose('Looking back Topic', id); - db.model('Topic').byTenant(tenantId).findOne({ _id: id }, 'gambits') + db.model(modelNames.topic).byTenant(tenantId).findOne({ _id: id }, 'gambits') .populate({ path: 'gambits' }) .exec(execHandle); } else if (type === 'reply') { options.topic = 'reply'; debug.verbose('Looking back at Conversation', id); - db.model('Reply').byTenant(tenantId).findOne({ _id: id }, 'gambits') + db.model(modelNames.reply).byTenant(tenantId).findOne({ _id: id }, 'gambits') .populate({ path: 'gambits' }) .exec(execHandle); } else { diff --git a/src/bot/db/modelNames.js b/src/bot/db/modelNames.js new file mode 100644 index 00000000..9cb23402 --- /dev/null +++ b/src/bot/db/modelNames.js @@ -0,0 +1,8 @@ +const names = { + gambit: 'ss_gambit', + reply: 'ss_reply', + topic: 'ss_topic', + user: 'ss_user', +}; + +export default names; diff --git a/src/bot/db/models/gambit.js b/src/bot/db/models/gambit.js index 7c6699f5..3d69dcda 100644 --- a/src/bot/db/models/gambit.js +++ b/src/bot/db/models/gambit.js @@ -10,6 +10,7 @@ import debuglog from 'debug-levels'; import async from 'async'; import parser from 'ss-parser'; +import modelNames from '../modelNames'; import helpers from '../helpers'; import Utils from '../../utils'; import factSystem from '../../factSystem'; @@ -47,10 +48,10 @@ const createGambitModel = function createGambitModel(db) { filter: { type: String, default: '' }, // An array of replies. - replies: [{ type: String, ref: 'Reply' }], + replies: [{ type: String, ref: modelNames.reply }], // Save a reference to the parent Reply, so we can walk back up the tree - parent: { type: String, ref: 'Reply' }, + parent: { type: String, ref: modelNames.reply }, // This will redirect anything that matches elsewhere. // If you want to have a conditional rediect use reply redirects @@ -81,7 +82,7 @@ const createGambitModel = function createGambitModel(db) { return callback('No data'); } - const Reply = db.model('Reply').byTenant(this.getTenantId()); + const Reply = db.model(modelNames.reply).byTenant(this.getTenantId()); const reply = new Reply(replyData); reply.save((err) => { if (err) { @@ -103,7 +104,7 @@ const createGambitModel = function createGambitModel(db) { const clearReply = function (replyId, cb) { self.replies.pull({ _id: replyId }); - db.model('Reply').byTenant(this.getTenantId()).remove({ _id: replyId }, (err) => { + db.model(modelNames.reply).byTenant(this.getTenantId()).remove({ _id: replyId }, (err) => { if (err) { console.log(err); } @@ -123,7 +124,7 @@ const createGambitModel = function createGambitModel(db) { gambitSchema.methods.getRootTopic = function (cb) { if (!this.parent) { - db.model('Topic').byTenant(this.getTenantId()) + db.model(modelNames.topic).byTenant(this.getTenantId()) .findOne({ gambits: { $in: [this._id] } }) .exec((err, doc) => { cb(err, doc.name); @@ -131,7 +132,7 @@ const createGambitModel = function createGambitModel(db) { } else { helpers.walkGambitParent(db, this.getTenantId(), this._id, (err, gambits) => { if (gambits.length !== 0) { - db.model('Topic').byTenant(this.getTenantId()) + db.model(modelNames.topic).byTenant(this.getTenantId()) .findOne({ gambits: { $in: [gambits.pop()] } }) .exec((err, topic) => { cb(null, topic.name); @@ -146,7 +147,7 @@ const createGambitModel = function createGambitModel(db) { gambitSchema.plugin(findOrCreate); gambitSchema.plugin(mongoTenant); - return db.model('Gambit', gambitSchema); + return db.model('ss_gambit', gambitSchema); }; export default createGambitModel; diff --git a/src/bot/db/models/reply.js b/src/bot/db/models/reply.js index ca07a48c..e9b9fea7 100644 --- a/src/bot/db/models/reply.js +++ b/src/bot/db/models/reply.js @@ -2,6 +2,7 @@ import mongoose from 'mongoose'; import mongoTenant from 'mongo-tenant'; import async from 'async'; +import modelNames from '../modelNames'; import Utils from '../../utils'; import Sort from '../sort'; import helpers from '../helpers'; @@ -12,11 +13,11 @@ const createReplyModel = function createReplyModel(db) { reply: { type: String, required: '{reply} is required.' }, keep: { type: Boolean, default: false }, filter: { type: String, default: '' }, - parent: { type: String, ref: 'Gambit' }, + parent: { type: String, ref: modelNames.gambit }, // Replies could referece other gambits // This forms the basis for the 'previous' - These are Children - gambits: [{ type: String, ref: 'Gambit' }], + gambits: [{ type: String, ref: modelNames.gambit }], }); // This method is similar to the topic.findMatch @@ -27,7 +28,7 @@ const createReplyModel = function createReplyModel(db) { replySchema.methods.sortGambits = function sortGambits(callback) { const self = this; const expandReorder = (gambitId, cb) => { - db.model('Gambit').byTenant(this.getTenantId()).findById(gambitId, (err, gambit) => { + db.model(modelNames.gambit).byTenant(this.getTenantId()).findById(gambitId, (err, gambit) => { cb(err, gambit); }); }; @@ -45,7 +46,7 @@ const createReplyModel = function createReplyModel(db) { replySchema.plugin(mongoTenant); - return db.model('Reply', replySchema); + return db.model(modelNames.reply, replySchema); }; export default createReplyModel; diff --git a/src/bot/db/models/topic.js b/src/bot/db/models/topic.js index 8465dd38..4e7cbcb7 100644 --- a/src/bot/db/models/topic.js +++ b/src/bot/db/models/topic.js @@ -12,6 +12,7 @@ import findOrCreate from 'mongoose-findorcreate'; import debuglog from 'debug-levels'; import parser from 'ss-parser'; +import modelNames from '../modelNames'; import Sort from '../sort'; import helpers from '../helpers'; @@ -53,7 +54,7 @@ const createTopicModel = function createTopicModel(db) { nostay: { type: Boolean, default: false }, filter: { type: String, default: '' }, keywords: { type: Array }, - gambits: [{ type: String, ref: 'Gambit' }], + gambits: [{ type: String, ref: modelNames.gambit }], }); topicSchema.pre('save', function (next) { @@ -72,7 +73,7 @@ const createTopicModel = function createTopicModel(db) { return callback('No data'); } - const Gambit = db.model('Gambit').byTenant(this.getTenantId()); + const Gambit = db.model(modelNames.gambit).byTenant(this.getTenantId()); const gambit = new Gambit(gambitData); gambit.save((err) => { if (err) { @@ -87,7 +88,7 @@ const createTopicModel = function createTopicModel(db) { topicSchema.methods.sortGambits = function (callback) { const expandReorder = (gambitId, cb) => { - db.model('Gambit').byTenant(this.getTenantId()).findById(gambitId, (err, gambit) => { + db.model(modelNames.gambit).byTenant(this.getTenantId()).findById(gambitId, (err, gambit) => { if (err) { console.log(err); } @@ -124,7 +125,7 @@ const createTopicModel = function createTopicModel(db) { }); }; - db.model('Topic').byTenant(this.getTenantId()).findOne({ name: this.name }, 'gambits') + db.model(modelNames.topic).byTenant(this.getTenantId()).findOne({ name: this.name }, 'gambits') .populate('gambits') .exec((err, mgambits) => { if (err) { @@ -139,13 +140,13 @@ const createTopicModel = function createTopicModel(db) { topicSchema.methods.clearGambits = function (callback) { const clearGambit = (gambitId, cb) => { this.gambits.pull({ _id: gambitId }); - db.model('Gambit').byTenant(this.getTenantId()).findById(gambitId, (err, gambit) => { + db.model(modelNames.gambit).byTenant(this.getTenantId()).findById(gambitId, (err, gambit) => { if (err) { debug.error(err); } gambit.clearReplies(() => { - db.model('Gambit').byTenant(this.getTenantId()).remove({ _id: gambitId }, (err) => { + db.model(modelNames.gambit).byTenant(this.getTenantId()).remove({ _id: gambitId }, (err) => { if (err) { debug.error(err); } @@ -167,7 +168,7 @@ const createTopicModel = function createTopicModel(db) { // This will find a gambit in any topic topicSchema.statics.findTriggerByTrigger = function (input, callback) { - db.model('Gambit').byTenant(this.getTenantId()).findOne({ input }).exec(callback); + db.model(modelNames.gambit).byTenant(this.getTenantId()).findOne({ input }).exec(callback); }; topicSchema.statics.findByName = function (name, callback) { @@ -256,7 +257,7 @@ const createTopicModel = function createTopicModel(db) { debug('Conversation RESET by clearBit'); callback(null, removeMissingTopics(pendingTopics)); } else { - db.model('Reply').byTenant(this.getTenantId()) + db.model(modelNames.reply).byTenant(this.getTenantId()) .find({ _id: { $in: lastReply.replyIds } }) .exec((err, replies) => { if (err) { @@ -297,7 +298,7 @@ const createTopicModel = function createTopicModel(db) { topicSchema.plugin(findOrCreate); topicSchema.plugin(mongoTenant); - return db.model('Topic', topicSchema); + return db.model(modelNames.topic, topicSchema); }; export default createTopicModel; diff --git a/src/bot/db/models/user.js b/src/bot/db/models/user.js index eb8474d0..e3b0aaac 100644 --- a/src/bot/db/models/user.js +++ b/src/bot/db/models/user.js @@ -6,6 +6,7 @@ import mkdirp from 'mkdirp'; import mongoose from 'mongoose'; import mongoTenant from 'mongo-tenant'; +import modelNames from '../modelNames'; import factSystem from '../../factSystem'; import logger from '../../logger'; @@ -115,7 +116,7 @@ const createUserModel = function createUserModel(db) { const pendingTopic = this.pendingTopic; this.pendingTopic = null; - db.model('Topic').byTenant(this.getTenantId()).findOne({ name: pendingTopic }, (err, topicData) => { + db.model(modelNames.topic).byTenant(this.getTenantId()).findOne({ name: pendingTopic }, (err, topicData) => { if (topicData && topicData.nostay === true) { this.currentTopic = this.history.topic[0]; } else { @@ -182,7 +183,7 @@ const createUserModel = function createUserModel(db) { return factSystem.createFactSystemForTenant(this.getTenantId()).createUserDB(this.id); }); - return db.model('User', userSchema); + return db.model(modelNames.user, userSchema); }; export default createUserModel; diff --git a/src/bot/index.js b/src/bot/index.js index e6d02076..bd6d847b 100644 --- a/src/bot/index.js +++ b/src/bot/index.js @@ -38,7 +38,7 @@ const loadPlugins = function loadPlugins(path) { }; class SuperScript { - constructor(tenantId) { + constructor(tenantId = 'master') { this.factSystem = factSystem.createFactSystemForTenant(tenantId); this.chatSystem = chatSystem.createChatSystemForTenant(tenantId); From 34a5126b92c0b663f9a6b0ff96441ba1be9d1d20 Mon Sep 17 00:00:00 2001 From: Ben James Date: Fri, 25 Nov 2016 23:06:19 +0000 Subject: [PATCH 4/7] Remove lib folder and clean up ignores --- .gitignore | 20 +- .npmignore | 20 +- lib/bin/bot-init.js | 99 ------ lib/bin/cleanup.js | 36 -- lib/bin/parse.js | 35 -- lib/bot/chatSystem.js | 70 ---- lib/bot/db/connect.js | 22 -- lib/bot/db/helpers.js | 354 ------------------- lib/bot/db/import.js | 247 ------------- lib/bot/db/modelNames.js | 13 - lib/bot/db/models/gambit.js | 191 ---------- lib/bot/db/models/reply.js | 83 ----- lib/bot/db/models/topic.js | 355 ------------------- lib/bot/db/models/user.js | 229 ------------ lib/bot/db/sort.js | 168 --------- lib/bot/dict.js | 132 ------- lib/bot/factSystem.js | 41 --- lib/bot/getReply.js | 464 ------------------------- lib/bot/history.js | 147 -------- lib/bot/index.js | 353 ------------------- lib/bot/logger.js | 47 --- lib/bot/math.js | 320 ----------------- lib/bot/message.js | 506 --------------------------- lib/bot/postParse.js | 81 ----- lib/bot/processTags.js | 516 ---------------------------- lib/bot/regexes.js | 60 ---- lib/bot/reply/capture-grammar.pegjs | 65 ---- lib/bot/reply/common.js | 156 --------- lib/bot/reply/customFunction.js | 71 ---- lib/bot/reply/inlineRedirect.js | 61 ---- lib/bot/reply/reply-grammar.pegjs | 184 ---------- lib/bot/reply/respond.js | 48 --- lib/bot/reply/topicRedirect.js | 59 ---- lib/bot/reply/wordnet.js | 121 ------- lib/bot/utils.js | 306 ----------------- lib/plugins/alpha.js | 166 --------- lib/plugins/compare.js | 270 --------------- lib/plugins/math.js | 110 ------ lib/plugins/message.js | 87 ----- lib/plugins/test.js | 119 ------- lib/plugins/time.js | 102 ------ lib/plugins/user.js | 110 ------ lib/plugins/wordnet.js | 28 -- lib/plugins/words.js | 56 --- 44 files changed, 27 insertions(+), 6701 deletions(-) delete mode 100755 lib/bin/bot-init.js delete mode 100755 lib/bin/cleanup.js delete mode 100755 lib/bin/parse.js delete mode 100644 lib/bot/chatSystem.js delete mode 100644 lib/bot/db/connect.js delete mode 100644 lib/bot/db/helpers.js delete mode 100755 lib/bot/db/import.js delete mode 100644 lib/bot/db/modelNames.js delete mode 100644 lib/bot/db/models/gambit.js delete mode 100644 lib/bot/db/models/reply.js delete mode 100644 lib/bot/db/models/topic.js delete mode 100644 lib/bot/db/models/user.js delete mode 100644 lib/bot/db/sort.js delete mode 100644 lib/bot/dict.js delete mode 100644 lib/bot/factSystem.js delete mode 100644 lib/bot/getReply.js delete mode 100644 lib/bot/history.js delete mode 100644 lib/bot/index.js delete mode 100644 lib/bot/logger.js delete mode 100644 lib/bot/math.js delete mode 100644 lib/bot/message.js delete mode 100644 lib/bot/postParse.js delete mode 100644 lib/bot/processTags.js delete mode 100644 lib/bot/regexes.js delete mode 100644 lib/bot/reply/capture-grammar.pegjs delete mode 100644 lib/bot/reply/common.js delete mode 100644 lib/bot/reply/customFunction.js delete mode 100644 lib/bot/reply/inlineRedirect.js delete mode 100644 lib/bot/reply/reply-grammar.pegjs delete mode 100644 lib/bot/reply/respond.js delete mode 100644 lib/bot/reply/topicRedirect.js delete mode 100644 lib/bot/reply/wordnet.js delete mode 100644 lib/bot/utils.js delete mode 100644 lib/plugins/alpha.js delete mode 100644 lib/plugins/compare.js delete mode 100644 lib/plugins/math.js delete mode 100644 lib/plugins/message.js delete mode 100644 lib/plugins/test.js delete mode 100644 lib/plugins/time.js delete mode 100644 lib/plugins/user.js delete mode 100644 lib/plugins/wordnet.js delete mode 100644 lib/plugins/words.js diff --git a/.gitignore b/.gitignore index 22b8c548..46b772d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,22 @@ -.DS_Store -node_modules/* +# SuperScript +lib/* logs/* -npm-debug.log test/fixtures/cache/* -dump.rdb -dump/* -coverage -*.sw* +coverage/* + +# Node +node_modules/* +npm-debug.log # Node profiler logs isolate-*-v8.log +# OS Files +.DS_Store +*.sw* +dump.rdb +dump/* + # IDEA/WebStorm Project Files .idea *.iml diff --git a/.npmignore b/.npmignore index 7e35cdfb..91bcc5d8 100644 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,11 @@ +# SuperScript src/* test/* example/* +logs/* +coverage/* .github/* + .babelrc .eslintrc .travis.yml @@ -9,16 +13,20 @@ changes.md contribute.md LICENSE.md readme.md -logs/* -test/fixtures/cache/* -dump.rdb -dump/* -coverage -*.sw* + +# Node +node_modules/* +npm-debug.log # Node profiler logs isolate-*-v8.log +# OS Files +.DS_Store +*.sw* +dump.rdb +dump/* + # IDEA/WebStorm Project Files .idea *.iml diff --git a/lib/bin/bot-init.js b/lib/bin/bot-init.js deleted file mode 100755 index 5343bc5d..00000000 --- a/lib/bin/bot-init.js +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -var _commander = require('commander'); - -var _commander2 = _interopRequireDefault(_commander); - -var _fs = require('fs'); - -var _fs2 = _interopRequireDefault(_fs); - -var _path = require('path'); - -var _path2 = _interopRequireDefault(_path); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -_commander2.default.version('1.0.0').usage('botname [options]').option('-c, --client [telnet]', 'Bot client (telnet or slack)', 'telnet').parse(process.argv); - -if (!_commander2.default.args[0]) { - _commander2.default.help(); - process.exit(1); -} - -var botName = _commander2.default.args[0]; -var botPath = _path2.default.join(process.cwd(), _path2.default.sep, botName); -var ssRoot = _path2.default.join(__dirname, '../../'); - -var write = function write(path, str) { - var mode = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 438; - - _fs2.default.writeFileSync(path, str, { mode: mode }); - console.log(' \x1B[36mcreate\x1B[0m : ' + path); -}; - -// Creating the path for your bot. -_fs2.default.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); - } - - _fs2.default.mkdirSync(_path2.default.join(botPath, _path2.default.sep, 'chat')); - _fs2.default.mkdirSync(_path2.default.join(botPath, _path2.default.sep, 'plugins')); - _fs2.default.mkdirSync(_path2.default.join(botPath, _path2.default.sep, 'src')); - - // package.json - var pkg = { - name: botName, - version: '0.0.0', - private: true, - dependencies: { - superscript: 'alpha' - }, - devDependencies: { - 'babel-cli': '^6.16.0', - 'babel-preset-es2015': '^6.16.0' - }, - scripts: { - build: 'babel src --presets babel-preset-es2015 --out-dir lib' - } - }; - - var clients = _commander2.default.client.split(','); - - clients.forEach(function (client) { - if (['telnet', 'slack'].indexOf(client) === -1) { - console.log('Cannot create bot with client type: ' + client); - return; - } - - console.log('Creating ' + _commander2.default.args[0] + ' bot with a ' + client + ' client.'); - - var clientName = client.charAt(0).toUpperCase() + client.slice(1); - - // TODO: Pull out plugins that have dialogue and move them to the new bot. - _fs2.default.createReadStream(ssRoot + 'clients' + _path2.default.sep + client + '.js').pipe(_fs2.default.createWriteStream(botPath + _path2.default.sep + 'src' + _path2.default.sep + 'server' + clientName + '.js')); - - pkg.scripts['start' + clientName] = 'npm run build && node lib/server' + clientName + '.js'; - - // TODO: Write dependencies for other clients - - if (client === 'slack') { - pkg.dependencies['slack-client'] = '~1.2.2'; - } - - if (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(_path2.default.join(botPath, _path2.default.sep, 'package.json'), JSON.stringify(pkg, null, 2)); - write(_path2.default.join(botPath, _path2.default.sep, 'chat', _path2.default.sep, 'main.ss'), firstRule); -}); \ No newline at end of file diff --git a/lib/bin/cleanup.js b/lib/bin/cleanup.js deleted file mode 100755 index af4948b2..00000000 --- a/lib/bin/cleanup.js +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -var _commander = require('commander'); - -var _commander2 = _interopRequireDefault(_commander); - -var _bot = require('../bot'); - -var _bot2 = _interopRequireDefault(_bot); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -_commander2.default.version('1.0.0').option('--host [type]', 'Mongo Host', 'localhost').option('--port [type]', 'Mongo Port', '27017').option('--mongo [type]', 'Mongo Database Name', 'superscriptDB').option('--mongoURI [type]', 'Mongo URI').option('--importFile [type]', 'Parsed JSON file path', 'data.json').parse(process.argv); - -var mongoURI = process.env.MONGO_URI || _commander2.default.mongoURI || 'mongodb://' + _commander2.default.host + ':' + _commander2.default.port + '/' + _commander2.default.mongo; - -// The use case of this file is to refresh a currently running bot. -// So the idea is to import a new file into a Mongo instance while preserving user data. -// For now, just nuke everything and import all the data into the database. - -// TODO: Prevent clearing user data -// const collectionsToRemove = ['users', 'topics', 'replies', 'gambits']; - -var options = { - mongoURI: mongoURI, - importFile: _commander2.default.importFile -}; - -(0, _bot2.default)(options, function (err) { - if (err) { - console.error(err); - } - console.log('Everything has been imported.'); - process.exit(); -}); \ No newline at end of file diff --git a/lib/bin/parse.js b/lib/bin/parse.js deleted file mode 100755 index af51329d..00000000 --- a/lib/bin/parse.js +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -var _commander = require('commander'); - -var _commander2 = _interopRequireDefault(_commander); - -var _fs = require('fs'); - -var _fs2 = _interopRequireDefault(_fs); - -var _ssParser = require('ss-parser'); - -var _ssParser2 = _interopRequireDefault(_ssParser); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -_commander2.default.version('1.0.0').option('-p, --path [type]', 'Input path', './chat').option('-o, --output [type]', 'Output options', 'data.json').option('-f, --force [type]', 'Force save if output file already exists', false).parse(process.argv); - -_fs2.default.exists(_commander2.default.output, function (exists) { - if (!exists || _commander2.default.force) { - // TODO: Allow use of own fact system in this script - _ssParser2.default.loadDirectory(_commander2.default.path, function (err, result) { - if (err) { - console.error('Error parsing bot script: ' + err); - } - _fs2.default.writeFile(_commander2.default.output, JSON.stringify(result, null, 4), function (err) { - if (err) throw err; - console.log('Saved output to ' + _commander2.default.output); - }); - }); - } else { - console.log('File', _commander2.default.output, 'already exists, remove file first or use -f to force save.'); - } -}); \ No newline at end of file diff --git a/lib/bot/chatSystem.js b/lib/bot/chatSystem.js deleted file mode 100644 index 7a4a046d..00000000 --- a/lib/bot/chatSystem.js +++ /dev/null @@ -1,70 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _gambit = require('./db/models/gambit'); - -var _gambit2 = _interopRequireDefault(_gambit); - -var _reply = require('./db/models/reply'); - -var _reply2 = _interopRequireDefault(_reply); - -var _topic = require('./db/models/topic'); - -var _topic2 = _interopRequireDefault(_topic); - -var _user = require('./db/models/user'); - -var _user2 = _interopRequireDefault(_user); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -/** - 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. - */ - -var GambitCore = null; -var ReplyCore = null; -var TopicCore = null; -var UserCore = null; - -var createChatSystem = function createChatSystem(db) { - GambitCore = (0, _gambit2.default)(db); - ReplyCore = (0, _reply2.default)(db); - TopicCore = (0, _topic2.default)(db); - UserCore = (0, _user2.default)(db); -}; - -var createChatSystemForTenant = function createChatSystemForTenant() { - var tenantId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'master'; - - var Gambit = GambitCore.byTenant(tenantId); - var Reply = ReplyCore.byTenant(tenantId); - var Topic = TopicCore.byTenant(tenantId); - var User = UserCore.byTenant(tenantId); - - return { - Gambit: Gambit, - Reply: Reply, - Topic: Topic, - User: User - }; -}; - -exports.default = { - createChatSystem: createChatSystem, - createChatSystemForTenant: createChatSystemForTenant -}; \ No newline at end of file diff --git a/lib/bot/db/connect.js b/lib/bot/db/connect.js deleted file mode 100644 index 5fdb53a9..00000000 --- a/lib/bot/db/connect.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _mongoose = require('mongoose'); - -var _mongoose2 = _interopRequireDefault(_mongoose); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -exports.default = function (mongoURI) { - var db = _mongoose2.default.createConnection('' + mongoURI); - - db.on('error', console.error); - - // If you want to debug mongoose - // mongoose.set('debug', true); - - return db; -}; \ No newline at end of file diff --git a/lib/bot/db/helpers.js b/lib/bot/db/helpers.js deleted file mode 100644 index 511b59e8..00000000 --- a/lib/bot/db/helpers.js +++ /dev/null @@ -1,354 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _async = require('async'); - -var _async2 = _interopRequireDefault(_async); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _safeEval = require('safe-eval'); - -var _safeEval2 = _interopRequireDefault(_safeEval); - -var _modelNames = require('./modelNames'); - -var _modelNames2 = _interopRequireDefault(_modelNames); - -var _utils = require('../utils'); - -var _utils2 = _interopRequireDefault(_utils); - -var _postParse = require('../postParse'); - -var _postParse2 = _interopRequireDefault(_postParse); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('SS:Common'); // These are shared helpers for the models. - -var _walkReplyParent = function _walkReplyParent(db, tenantId, replyId, replyIds, cb) { - db.model(_modelNames2.default.reply).byTenant(tenantId).findById(replyId).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(db, tenantId, reply.parent.parent, replyIds, cb); - } else { - cb(null, replyIds); - } - } else { - cb(null, replyIds); - } - }); -}; - -var _walkGambitParent = function _walkGambitParent(db, tenantId, gambitId, gambitIds, cb) { - db.model(_modelNames2.default.gambit).byTenant(tenantId).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(db, tenantId, 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 -var findMatchingGambitsForMessage = function findMatchingGambitsForMessage(db, tenantId, type, id, message, options, callback) { - // Let's query for Gambits - var execHandle = function execHandle(err, gambitsParent) { - if (err) { - console.error(err); - } - - var populateGambits = function populateGambits(gambit, next) { - debug.verbose('Populating gambit'); - db.model(_modelNames2.default.reply).byTenant(tenantId).populate(gambit, { path: 'replies' }, next); - }; - - _async2.default.each(gambitsParent.gambits, populateGambits, function (err) { - debug.verbose('Completed populating gambits'); - if (err) { - console.error(err); - } - _async2.default.map(gambitsParent.gambits, _eachGambitHandle(message, options), function (err3, matches) { - callback(null, _lodash2.default.flatten(matches)); - }); - }); - }; - - if (type === 'topic') { - debug.verbose('Looking back Topic', id); - db.model(_modelNames2.default.topic).byTenant(tenantId).findOne({ _id: id }, 'gambits').populate({ path: 'gambits' }).exec(execHandle); - } else if (type === 'reply') { - options.topic = 'reply'; - debug.verbose('Looking back at Conversation', id); - db.model(_modelNames2.default.reply).byTenant(tenantId).findOne({ _id: id }, 'gambits').populate({ path: 'gambits' }).exec(execHandle); - } else { - debug.verbose('We should never get here'); - callback(true); - } -}; - -var _afterHandle = function _afterHandle(match, gambit, topic, cb) { - debug.verbose('Match found: ' + gambit.input + ' in topic: ' + topic); - var stars = []; - if (match.length > 1) { - for (var j = 1; j < match.length; j++) { - if (match[j]) { - var starData = _utils2.default.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, gambit: gambit }; - if (topic !== 'reply') { - data.topic = topic; - } - - var matches = [data]; - cb(null, matches); -}; - -/* 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. - */ -var processConditions = function processConditions(conditions, options) { - var context = options.user.conversationState || {}; - - return _lodash2.default.every(conditions, function (condition) { - debug.verbose('Check condition - Context: ', context); - debug.verbose('Check condition - Condition: ', condition); - - try { - var result = (0, _safeEval2.default)(condition, context); - if (result) { - debug.verbose('--- Condition TRUE ---'); - return true; - } - debug.verbose('--- Condition FALSE ---'); - return false; - } catch (e) { - debug.verbose('Error in condition checking: ' + e.stack); - return false; - } - }); -}; - -/** - * Takes a gambit and a message, and returns non-null if they match. - */ -var doesMatch = function doesMatch(gambit, message, options, callback) { - if (gambit.conditions && gambit.conditions.length > 0) { - var conditionsMatch = processConditions(gambit.conditions, options); - if (!conditionsMatch) { - debug.verbose('Conditions did not match'); - callback(null, false); - return; - } - } - - var match = false; - - // Replace , etc. with the actual words in user message - (0, _postParse2.default)(gambit.trigger, message, options.user, function (regexp) { - var pattern = new RegExp('^' + regexp + '$', 'i'); - - debug.verbose('Try to match (clean)\'' + message.clean + '\' against ' + gambit.trigger + ' (' + pattern + ')'); - debug.verbose('Try to match (lemma)\'' + message.lemString + '\' against ' + gambit.trigger + ' (' + pattern + ')'); - - // Match on the question type (qtype / qsubtype) - if (gambit.isQuestion && message.isQuestion) { - debug.verbose('Gambit and message are questions, testing against question types'); - if (_lodash2.default.isEmpty(gambit.qType) && _lodash2.default.isEmpty(gambit.qSubType)) { - // Gambit does not specify what type of question it should be, so just match - match = message.clean.match(pattern); - if (!match) { - match = message.lemString.match(pattern); - } - } else if (!_lodash2.default.isEmpty(gambit.qType) && _lodash2.default.isEmpty(gambit.qSubType) && (message.questionType === gambit.qType || message.questionSubType.indexOf(gambit.qType) !== -1)) { - // Gambit specifies question type only - match = message.clean.match(pattern); - if (!match) { - match = message.lemString.match(pattern); - } - } else if (!_lodash2.default.isEmpty(gambit.qType) && !_lodash2.default.isEmpty(gambit.qSubType) && message.questionSubType.indexOf(gambit.qType) !== -1 && message.questionSubType.indexOf(gambit.qSubType) !== -1) { - // Gambit specifies both question type and question sub type - 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 -var _eachGambitHandle = function _eachGambitHandle(message, options) { - var filterRegex = /\s*\^(\w+)\(([\w<>,\|\s]*)\)\s*/i; - - // This takes a gambit that is a child of a topic or reply and checks if - // it matches the user's message or not. - return function (gambit, callback) { - var plugins = options.system.plugins; - var scope = options.system.scope; - var topic = options.topic || 'reply'; - var chatSystem = options.system.chatSystem; - - doesMatch(gambit, message, options, function (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); - - var filterFunction = gambit.filter.match(filterRegex); - debug.verbose('Filter function matched against regex gave: ' + filterFunction); - - var pluginName = _utils2.default.trim(filterFunction[1]); - var parts = _utils2.default.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) - var args = []; - for (var 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. - var filterScope = _lodash2.default.merge({}, scope); - filterScope.message = message; - // filterScope.message_props = options.localOptions.messageScope; - filterScope.user = options.user; - - args.push(function (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, function (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, function (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 - -var walkReplyParent = function walkReplyParent(db, tenantId, replyId, cb) { - _walkReplyParent(db, tenantId, replyId, [], cb); -}; - -var walkGambitParent = function walkGambitParent(db, tenantId, gambitId, cb) { - _walkGambitParent(db, tenantId, gambitId, [], cb); -}; - -exports.default = { - walkReplyParent: walkReplyParent, - walkGambitParent: walkGambitParent, - doesMatch: doesMatch, - findMatchingGambitsForMessage: findMatchingGambitsForMessage -}; \ No newline at end of file diff --git a/lib/bot/db/import.js b/lib/bot/db/import.js deleted file mode 100755 index f02fb418..00000000 --- a/lib/bot/db/import.js +++ /dev/null @@ -1,247 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _fs = require('fs'); - -var _fs2 = _interopRequireDefault(_fs); - -var _async = require('async'); - -var _async2 = _interopRequireDefault(_async); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _utils = require('../utils'); - -var _utils2 = _interopRequireDefault(_utils); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('SS:Importer'); /** - * Import a data file into MongoDB - */ - -var KEEP_REGEX = new RegExp('\{keep\}', 'i'); -var FILTER_REGEX = /\{\s*\^(\w+)\(([\w<>,\s]*)\)\s*\}/i; - -// Whenever and only when a breaking change is made to ss-parser, this needs -// to be updated. -var MIN_SUPPORTED_SCRIPT_VERSION = 1; - -var rawToGambitData = function rawToGambitData(gambitId, gambit) { - var gambitData = { - id: gambitId, - isQuestion: false, - qType: '', - qSubType: '', - conditions: gambit.conditional, - filter: gambit.trigger.filter || '', - trigger: gambit.trigger.clean, - input: gambit.trigger.raw - }; - - if (gambit.trigger.question !== null) { - gambitData.isQuestion = true; - gambitData.qType = gambit.trigger.question.questionType; - gambitData.qSubType = gambit.trigger.question.questionSubtype; - } - - if (gambit.redirect) { - gambitData.redirect = gambit.redirect; - } - - return gambitData; -}; - -var importData = function importData(chatSystem, data, callback) { - if (!data.version || data.version < MIN_SUPPORTED_SCRIPT_VERSION) { - return callback('Error: Your script has version ' + data.version + ' but the minimum supported version is ' + MIN_SUPPORTED_SCRIPT_VERSION + '.\nPlease either re-parse your file with a supported parser version, or update SuperScript.'); - } - - var Topic = chatSystem.Topic; - var Gambit = chatSystem.Gambit; - var Reply = chatSystem.Reply; - var User = chatSystem.User; - - var gambitsWithConversation = []; - - var eachReplyItor = function eachReplyItor(gambit) { - return function (replyId, nextReply) { - debug.verbose('Reply process: %s', replyId); - var properties = { - id: replyId, - reply: data.replies[replyId], - parent: gambit._id - }; - - var match = properties.reply.match(KEEP_REGEX); - if (match) { - properties.keep = true; - properties.reply = _utils2.default.trim(properties.reply.replace(match[0], '')); - } - - match = properties.reply.match(FILTER_REGEX); - if (match) { - properties.filter = '^' + match[1] + '(' + match[2] + ')'; - properties.reply = _utils2.default.trim(properties.reply.replace(match[0], '')); - } - - gambit.addReply(properties, function (err) { - if (err) { - console.error(err); - } - nextReply(); - }); - }; - }; - - var eachGambitItor = function eachGambitItor(topic) { - return function (gambitId, nextGambit) { - var gambit = data.gambits[gambitId]; - if (gambit.conversation) { - debug.verbose('Gambit has conversation (deferring process): %s', gambitId); - gambitsWithConversation.push(gambitId); - nextGambit(); - } else if (gambit.topic === topic.name) { - debug.verbose('Gambit process: %s', gambitId); - var gambitData = rawToGambitData(gambitId, gambit); - - topic.createGambit(gambitData, function (err, mongoGambit) { - if (err) { - console.error(err); - } - _async2.default.eachSeries(gambit.replies, eachReplyItor(mongoGambit), function (err) { - if (err) { - console.error(err); - } - nextGambit(); - }); - }); - } else { - nextGambit(); - } - }; - }; - - var eachTopicItor = function eachTopicItor(topicName, nextTopic) { - var topic = data.topics[topicName]; - debug.verbose('Find or create topic with name \'' + topicName + '\''); - var topicProperties = { - name: topic.name, - keep: topic.flags.indexOf('keep') !== -1, - nostay: topic.flags.indexOf('nostay') !== -1, - system: topic.flags.indexOf('system') !== -1, - keywords: topic.keywords, - filter: topic.filter || '' - }; - - Topic.findOrCreate({ name: topic.name }, topicProperties, function (err, mongoTopic) { - if (err) { - console.error(err); - } - - _async2.default.eachSeries(Object.keys(data.gambits), eachGambitItor(mongoTopic), function (err) { - if (err) { - console.error(err); - } - debug.verbose('All gambits for ' + topic.name + ' processed.'); - nextTopic(); - }); - }); - }; - - var eachConvItor = function eachConvItor(gambitId) { - return function (replyId, nextConv) { - debug.verbose('conversation/reply: %s', replyId); - Reply.findOne({ id: replyId }, function (err, reply) { - if (err) { - console.error(err); - } - if (reply) { - reply.gambits.addToSet(gambitId); - reply.save(function (err) { - if (err) { - console.error(err); - } - reply.sortGambits(function () { - debug.verbose('All conversations for %s processed.', gambitId); - nextConv(); - }); - }); - } else { - debug.warn('No reply found!'); - nextConv(); - } - }); - }; - }; - - debug.info('Cleaning database: removing all data.'); - - // Remove everything before we start importing - _async2.default.each([Gambit, Reply, Topic, User], function (model, nextModel) { - model.remove({}, function (err) { - return nextModel(); - }); - }, function (err) { - _async2.default.eachSeries(Object.keys(data.topics), eachTopicItor, function () { - _async2.default.eachSeries(_lodash2.default.uniq(gambitsWithConversation), function (gambitId, nextGambit) { - var gambitRawData = data.gambits[gambitId]; - - var conversations = gambitRawData.conversation || []; - if (conversations.length === 0) { - return nextGambit(); - } - - var gambitData = rawToGambitData(gambitId, gambitRawData); - // TODO: gambit.parent should be able to be multiple replies, not just conversations[0] - var replyId = conversations[0]; - - // TODO??: Add reply.addGambit(...) - Reply.findOne({ id: replyId }, function (err, reply) { - if (!reply) { - console.error('Gambit ' + gambitId + ' is supposed to have conversations (has %), but none were found.'); - nextGambit(); - } - var gambit = new Gambit(gambitData); - _async2.default.eachSeries(gambitRawData.replies, eachReplyItor(gambit), function (err) { - debug.verbose('All replies processed.'); - gambit.parent = reply._id; - debug.verbose('Saving new gambit: ', err, gambit); - gambit.save(function (err, gam) { - if (err) { - console.log(err); - } - _async2.default.mapSeries(conversations, eachConvItor(gam._id), function (err, results) { - debug.verbose('All conversations for %s processed.', gambitId); - nextGambit(); - }); - }); - }); - }); - }, function () { - callback(null, 'done'); - }); - }); - }); -}; - -var importFile = function importFile(chatSystem, path, callback) { - _fs2.default.readFile(path, function (err, jsonFile) { - if (err) { - console.log(err); - } - return importData(chatSystem, JSON.parse(jsonFile), callback); - }); -}; - -exports.default = { importFile: importFile, importData: importData }; \ No newline at end of file diff --git a/lib/bot/db/modelNames.js b/lib/bot/db/modelNames.js deleted file mode 100644 index 4bf18693..00000000 --- a/lib/bot/db/modelNames.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -var names = { - gambit: 'ss_gambit', - reply: 'ss_reply', - topic: 'ss_topic', - user: 'ss_user' -}; - -exports.default = names; \ No newline at end of file diff --git a/lib/bot/db/models/gambit.js b/lib/bot/db/models/gambit.js deleted file mode 100644 index 465a5dc5..00000000 --- a/lib/bot/db/models/gambit.js +++ /dev/null @@ -1,191 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _mongoose = require('mongoose'); - -var _mongoose2 = _interopRequireDefault(_mongoose); - -var _mongooseFindorcreate = require('mongoose-findorcreate'); - -var _mongooseFindorcreate2 = _interopRequireDefault(_mongooseFindorcreate); - -var _mongoTenant = require('mongo-tenant'); - -var _mongoTenant2 = _interopRequireDefault(_mongoTenant); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _async = require('async'); - -var _async2 = _interopRequireDefault(_async); - -var _ssParser = require('ss-parser'); - -var _ssParser2 = _interopRequireDefault(_ssParser); - -var _modelNames = require('../modelNames'); - -var _modelNames2 = _interopRequireDefault(_modelNames); - -var _helpers = require('../helpers'); - -var _helpers2 = _interopRequireDefault(_helpers); - -var _utils = require('../../utils'); - -var _utils2 = _interopRequireDefault(_utils); - -var _factSystem = require('../../factSystem'); - -var _factSystem2 = _interopRequireDefault(_factSystem); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -/** - A Gambit is a Trigger + Reply or Reply Set - - We define a Reply as a subDocument in Mongo. -**/ - -var debug = (0, _debugLevels2.default)('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. -**/ - -var createGambitModel = function createGambitModel(db) { - var gambitSchema = new _mongoose2.default.Schema({ - id: { type: String, index: true, default: _utils2.default.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 - conditions: [{ type: String, default: '' }], - - // 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: _modelNames2.default.reply }], - - // Save a reference to the parent Reply, so we can walk back up the tree - parent: { type: String, ref: _modelNames2.default.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 _this = this; - - // FIXME: This only works when the replies are populated which is not always the case. - // this.replies = _.uniq(this.replies, (item, key, id) => { - // return item.id; - // }); - - // If we created the trigger in an external editor, normalize the trigger before saving it. - if (this.input && !this.trigger) { - var facts = _factSystem2.default.createFactSystemForTenant(this.getTenantId()); - return _ssParser2.default.normalizeTrigger(this.input, facts, function (err, cleanTrigger) { - _this.trigger = cleanTrigger; - next(); - }); - } - next(); - }); - - gambitSchema.methods.addReply = function (replyData, callback) { - var _this2 = this; - - if (!replyData) { - return callback('No data'); - } - - var Reply = db.model(_modelNames2.default.reply).byTenant(this.getTenantId()); - var reply = new Reply(replyData); - reply.save(function (err) { - if (err) { - return callback(err); - } - _this2.replies.addToSet(reply._id); - _this2.save(function (err) { - callback(err, reply); - }); - }); - }; - - gambitSchema.methods.doesMatch = function (message, options, callback) { - _helpers2.default.doesMatch(this, message, options, callback); - }; - - gambitSchema.methods.clearReplies = function (callback) { - var self = this; - - var clearReply = function clearReply(replyId, cb) { - self.replies.pull({ _id: replyId }); - db.model(_modelNames2.default.reply).byTenant(this.getTenantId()).remove({ _id: replyId }, function (err) { - if (err) { - console.log(err); - } - - debug.verbose('removed reply %s', replyId); - - cb(null, replyId); - }); - }; - - _async2.default.map(self.replies, clearReply, function (err, clearedReplies) { - self.save(function (err2) { - callback(err2, clearedReplies); - }); - }); - }; - - gambitSchema.methods.getRootTopic = function (cb) { - var _this3 = this; - - if (!this.parent) { - db.model(_modelNames2.default.topic).byTenant(this.getTenantId()).findOne({ gambits: { $in: [this._id] } }).exec(function (err, doc) { - cb(err, doc.name); - }); - } else { - _helpers2.default.walkGambitParent(db, this.getTenantId(), this._id, function (err, gambits) { - if (gambits.length !== 0) { - db.model(_modelNames2.default.topic).byTenant(_this3.getTenantId()).findOne({ gambits: { $in: [gambits.pop()] } }).exec(function (err, topic) { - cb(null, topic.name); - }); - } else { - cb(null, 'random'); - } - }); - } - }; - - gambitSchema.plugin(_mongooseFindorcreate2.default); - gambitSchema.plugin(_mongoTenant2.default); - - return db.model('ss_gambit', gambitSchema); -}; - -exports.default = createGambitModel; \ No newline at end of file diff --git a/lib/bot/db/models/reply.js b/lib/bot/db/models/reply.js deleted file mode 100644 index 76024459..00000000 --- a/lib/bot/db/models/reply.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _mongoose = require('mongoose'); - -var _mongoose2 = _interopRequireDefault(_mongoose); - -var _mongoTenant = require('mongo-tenant'); - -var _mongoTenant2 = _interopRequireDefault(_mongoTenant); - -var _async = require('async'); - -var _async2 = _interopRequireDefault(_async); - -var _modelNames = require('../modelNames'); - -var _modelNames2 = _interopRequireDefault(_modelNames); - -var _utils = require('../../utils'); - -var _utils2 = _interopRequireDefault(_utils); - -var _sort = require('../sort'); - -var _sort2 = _interopRequireDefault(_sort); - -var _helpers = require('../helpers'); - -var _helpers2 = _interopRequireDefault(_helpers); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var createReplyModel = function createReplyModel(db) { - var replySchema = new _mongoose2.default.Schema({ - id: { type: String, index: true, default: _utils2.default.genId() }, - reply: { type: String, required: '{reply} is required.' }, - keep: { type: Boolean, default: false }, - filter: { type: String, default: '' }, - parent: { type: String, ref: _modelNames2.default.gambit }, - - // Replies could referece other gambits - // This forms the basis for the 'previous' - These are Children - gambits: [{ type: String, ref: _modelNames2.default.gambit }] - }); - - // This method is similar to the topic.findMatch - replySchema.methods.findMatch = function findMatch(message, options, callback) { - _helpers2.default.findMatchingGambitsForMessage(db, this.getTenantId(), 'reply', this._id, message, options, callback); - }; - - replySchema.methods.sortGambits = function sortGambits(callback) { - var _this = this; - - var self = this; - var expandReorder = function expandReorder(gambitId, cb) { - db.model(_modelNames2.default.gambit).byTenant(_this.getTenantId()).findById(gambitId, function (err, gambit) { - cb(err, gambit); - }); - }; - - _async2.default.map(this.gambits, expandReorder, function (err, newGambitList) { - if (err) { - console.log(err); - } - - var newList = _sort2.default.sortTriggerSet(newGambitList); - self.gambits = newList.map(function (g) { - return g._id; - }); - self.save(callback); - }); - }; - - replySchema.plugin(_mongoTenant2.default); - - return db.model(_modelNames2.default.reply, replySchema); -}; - -exports.default = createReplyModel; \ No newline at end of file diff --git a/lib/bot/db/models/topic.js b/lib/bot/db/models/topic.js deleted file mode 100644 index e9657b0c..00000000 --- a/lib/bot/db/models/topic.js +++ /dev/null @@ -1,355 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _mongoose = require('mongoose'); - -var _mongoose2 = _interopRequireDefault(_mongoose); - -var _mongoTenant = require('mongo-tenant'); - -var _mongoTenant2 = _interopRequireDefault(_mongoTenant); - -var _natural = require('natural'); - -var _natural2 = _interopRequireDefault(_natural); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _async = require('async'); - -var _async2 = _interopRequireDefault(_async); - -var _mongooseFindorcreate = require('mongoose-findorcreate'); - -var _mongooseFindorcreate2 = _interopRequireDefault(_mongooseFindorcreate); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _ssParser = require('ss-parser'); - -var _ssParser2 = _interopRequireDefault(_ssParser); - -var _modelNames = require('../modelNames'); - -var _modelNames2 = _interopRequireDefault(_modelNames); - -var _sort = require('../sort'); - -var _sort2 = _interopRequireDefault(_sort); - -var _helpers = require('../helpers'); - -var _helpers2 = _interopRequireDefault(_helpers); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('SS:Topics'); /** - Topics are a grouping of gambits. - The order of the Gambits are important, and a gambit can live in more than one topic. - **/ - -var TfIdf = _natural2.default.TfIdf; -var tfidf = new TfIdf(); - -_natural2.default.PorterStemmer.attach(); - -// Function to score the topics by TF-IDF -var scoreTopics = function scoreTopics(message) { - var topics = []; - var 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, function (index, score, name) { - // Filter out system topic pre/post - if (name !== '__pre__' && name !== '__post__') { - topics.push({ name: name, score: score, type: 'TOPIC' }); - } - }); - - // Removes duplicate entries. - topics = _lodash2.default.uniqBy(topics, 'name'); - - var topicOrder = _lodash2.default.sortBy(topics, 'score').reverse(); - debug.verbose('Scored topics: ', topicOrder); - - return topicOrder; -}; - -var createTopicModel = function createTopicModel(db) { - var topicSchema = new _mongoose2.default.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: _modelNames2.default.gambit }] - }); - - topicSchema.pre('save', function (next) { - if (!_lodash2.default.isEmpty(this.keywords)) { - var keywords = this.keywords.join(' '); - if (keywords) { - tfidf.addDocument(keywords.tokenizeAndStem(), this.name); - } - } - next(); - }); - - // This will create the Gambit and add it to the model - topicSchema.methods.createGambit = function (gambitData, callback) { - var _this = this; - - if (!gambitData) { - return callback('No data'); - } - - var Gambit = db.model(_modelNames2.default.gambit).byTenant(this.getTenantId()); - var gambit = new Gambit(gambitData); - gambit.save(function (err) { - if (err) { - return callback(err); - } - _this.gambits.addToSet(gambit._id); - _this.save(function (err) { - callback(err, gambit); - }); - }); - }; - - topicSchema.methods.sortGambits = function (callback) { - var _this2 = this; - - var expandReorder = function expandReorder(gambitId, cb) { - db.model(_modelNames2.default.gambit).byTenant(_this2.getTenantId()).findById(gambitId, function (err, gambit) { - if (err) { - console.log(err); - } - cb(null, gambit); - }); - }; - - _async2.default.map(this.gambits, expandReorder, function (err, newGambitList) { - if (err) { - console.log(err); - } - - var newList = _sort2.default.sortTriggerSet(newGambitList); - _this2.gambits = newList.map(function (gambit) { - return gambit._id; - }); - _this2.save(callback); - }); - }; - - topicSchema.methods.findMatch = function findMatch(message, options, callback) { - options.topic = this.name; - - _helpers2.default.findMatchingGambitsForMessage(db, this.getTenantId(), 'topic', this._id, message, options, callback); - }; - - // Lightweight match for one topic - // TODO: offload this to common - topicSchema.methods.doesMatch = function (message, options, cb) { - var itor = function itor(gambit, next) { - gambit.doesMatch(message, options, function (err, match2) { - if (err) { - debug.error(err); - } - next(err, match2 ? gambit._id : null); - }); - }; - - db.model(_modelNames2.default.topic).byTenant(this.getTenantId()).findOne({ name: this.name }, 'gambits').populate('gambits').exec(function (err, mgambits) { - if (err) { - debug.error(err); - } - _async2.default.filter(mgambits.gambits, itor, function (err, res) { - cb(null, res); - }); - }); - }; - - topicSchema.methods.clearGambits = function (callback) { - var _this3 = this; - - var clearGambit = function clearGambit(gambitId, cb) { - _this3.gambits.pull({ _id: gambitId }); - db.model(_modelNames2.default.gambit).byTenant(_this3.getTenantId()).findById(gambitId, function (err, gambit) { - if (err) { - debug.error(err); - } - - gambit.clearReplies(function () { - db.model(_modelNames2.default.gambit).byTenant(_this3.getTenantId()).remove({ _id: gambitId }, function (err) { - if (err) { - debug.error(err); - } - - debug.verbose('removed gambit %s', gambitId); - - cb(null, gambitId); - }); - }); - }); - }; - - _async2.default.map(this.gambits, clearGambit, function (err, clearedGambits) { - _this3.save(function (err) { - callback(err, clearedGambits); - }); - }); - }; - - // This will find a gambit in any topic - topicSchema.statics.findTriggerByTrigger = function (input, callback) { - db.model(_modelNames2.default.gambit).byTenant(this.getTenantId()).findOne({ input: input }).exec(callback); - }; - - topicSchema.statics.findByName = function (name, callback) { - this.findOne({ name: name }, {}, callback); - }; - - topicSchema.statics.findPendingTopicsForUser = function (user, message, callback) { - var _this4 = this; - - var currentTopic = user.getTopic(); - var pendingTopics = []; - - var scoredTopics = scoreTopics(message); - - var removeMissingTopics = function removeMissingTopics(topics) { - return _lodash2.default.filter(topics, function (topic) { - return topic.id; - }); - }; - - this.find({}, function (err, allTopics) { - if (err) { - debug.error(err); - } - - // Add the current topic to the front of the array. - scoredTopics.unshift({ name: currentTopic, type: 'TOPIC' }); - - var otherTopics = _lodash2.default.map(allTopics, function (topic) { - return { id: topic._id, name: topic.name, system: topic.system }; - }); - - // This gets a list if all the remaining topics. - otherTopics = _lodash2.default.filter(otherTopics, function (topic) { - return !_lodash2.default.find(scoredTopics, { name: topic.name }); - }); - - // We remove the system topics - otherTopics = _lodash2.default.filter(otherTopics, function (topic) { - return topic.system === false; - }); - - pendingTopics.push({ name: '__pre__', type: 'TOPIC' }); - - for (var i = 0; i < scoredTopics.length; i++) { - if (scoredTopics[i].name !== '__pre__' && scoredTopics[i].name !== '__post__') { - pendingTopics.push(scoredTopics[i]); - } - } - - // Search random as the highest priority after current topic and pre - if (!_lodash2.default.find(pendingTopics, { name: 'random' }) && _lodash2.default.find(otherTopics, { name: 'random' })) { - pendingTopics.push({ name: 'random', type: 'TOPIC' }); - } - - for (var _i = 0; _i < otherTopics.length; _i++) { - if (otherTopics[_i].name !== '__pre__' && otherTopics[_i].name !== '__post__') { - 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 (var _i2 = 0; _i2 < pendingTopics.length; _i2++) { - var topicName = pendingTopics[_i2].name; - for (var n = 0; n < allTopics.length; n++) { - if (allTopics[n].name === topicName) { - pendingTopics[_i2].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 (!_lodash2.default.isEmpty(lastReply)) { - // If the message is less than 5 minutes old we continue - // TODO: Make this time configurable - var delta = new Date() - lastReply.createdAt; - if (delta <= 1000 * 300) { - (function () { - var replyId = lastReply.replyId; - var clearConversation = lastReply.clearConversation; - if (clearConversation === true) { - debug('Conversation RESET by clearBit'); - callback(null, removeMissingTopics(pendingTopics)); - } else { - db.model(_modelNames2.default.reply).byTenant(_this4.getTenantId()).find({ _id: { $in: lastReply.replyIds } }).exec(function (err, replies) { - if (err) { - console.error(err); - } - if (replies === []) { - debug("We couldn't match the last reply. Continuing."); - callback(null, removeMissingTopics(pendingTopics)); - } else { - (function () { - debug('Last reply: ', lastReply.original, replyId, clearConversation); - var replyThreads = []; - _async2.default.eachSeries(replies, function (reply, next) { - _helpers2.default.walkReplyParent(db, _this4.getTenantId(), reply._id, function (err, threads) { - debug.verbose('Threads found by walkReplyParent: ' + threads); - threads.forEach(function (thread) { - return replyThreads.push(thread); - }); - next(); - }); - }, function (err) { - replyThreads = replyThreads.map(function (item) { - return { 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(_mongooseFindorcreate2.default); - topicSchema.plugin(_mongoTenant2.default); - - return db.model(_modelNames2.default.topic, topicSchema); -}; - -exports.default = createTopicModel; \ No newline at end of file diff --git a/lib/bot/db/models/user.js b/lib/bot/db/models/user.js deleted file mode 100644 index 7ae1b8dd..00000000 --- a/lib/bot/db/models/user.js +++ /dev/null @@ -1,229 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _fs = require('fs'); - -var _fs2 = _interopRequireDefault(_fs); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _mongooseFindorcreate = require('mongoose-findorcreate'); - -var _mongooseFindorcreate2 = _interopRequireDefault(_mongooseFindorcreate); - -var _mkdirp = require('mkdirp'); - -var _mkdirp2 = _interopRequireDefault(_mkdirp); - -var _mongoose = require('mongoose'); - -var _mongoose2 = _interopRequireDefault(_mongoose); - -var _mongoTenant = require('mongo-tenant'); - -var _mongoTenant2 = _interopRequireDefault(_mongoTenant); - -var _modelNames = require('../modelNames'); - -var _modelNames2 = _interopRequireDefault(_modelNames); - -var _factSystem = require('../../factSystem'); - -var _factSystem2 = _interopRequireDefault(_factSystem); - -var _logger = require('../../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('SS:User'); - -var createUserModel = function createUserModel(db) { - var userSchema = _mongoose2.default.Schema({ - id: String, - status: Number, - currentTopic: String, - pendingTopic: String, - lastMessageSentAt: Date, - volley: Number, - rally: Number, - prevAns: Number, - conversation: Number, - 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, callback) { - if (topic !== '' || topic !== 'undefined') { - debug.verbose('setTopic', topic); - this.pendingTopic = topic; - this.save(function () { - debug.verbose('setTopic Complete'); - callback(null); - }); - } else { - debug.warn('Trying to set topic to someting invalid'); - callback(null); - } - }; - - userSchema.methods.getTopic = function () { - debug.verbose('getTopic', this.currentTopic); - return this.currentTopic; - }; - - userSchema.methods.updateHistory = function (msg, reply, replyObj, cb) { - var _this = this; - - if (!_lodash2.default.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, ''); - _logger2.default.log(JSON.stringify(log) + '\r\n', cleanId + '_trans.txt'); - - // 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; - - var stars = replyObj.stars; - - // Don't serialize MongoDOWN to Mongo - msg.factSystem = null; - reply.factSystem = null; - reply.replyIds = replyObj.replyIds; - - 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 !== '') { - (function () { - var pendingTopic = _this.pendingTopic; - _this.pendingTopic = null; - - db.model(_modelNames2.default.topic).byTenant(_this.getTenantId()).findOne({ name: pendingTopic }, function (err, topicData) { - if (topicData && topicData.nostay === true) { - _this.currentTopic = _this.history.topic[0]; - } else { - _this.currentTopic = pendingTopic; - } - _this.save(function (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 }, function (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 (!_lodash2.default.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(_mongooseFindorcreate2.default); - userSchema.plugin(_mongoTenant2.default); - - userSchema.virtual('memory').get(function () { - return _factSystem2.default.createFactSystemForTenant(this.getTenantId()).createUserDB(this.id); - }); - - return db.model(_modelNames2.default.user, userSchema); -}; - -exports.default = createUserModel; \ No newline at end of file diff --git a/lib/bot/db/sort.js b/lib/bot/db/sort.js deleted file mode 100644 index 4be6c612..00000000 --- a/lib/bot/db/sort.js +++ /dev/null @@ -1,168 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _debug = require('debug'); - -var _debug2 = _interopRequireDefault(_debug); - -var _utils = require('../utils'); - -var _utils2 = _interopRequireDefault(_utils); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } - -var debug = (0, _debug2.default)('Sort'); - -var 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: [] }; -}; - -var sortTriggerSet = function sortTriggerSet(gambits) { - var gambit = void 0; - var cnt = void 0; - var inherits = void 0; - - var lengthSort = function lengthSort(a, b) { - return b.length - a.length; - }; - - // Create a priority map. - var prior = { - 0: [] }; - - // Sort triggers by their weights. - for (var i = 0; i < gambits.length; i++) { - gambit = gambits[i]; - var match = gambit.input.match(/\{weight=(\d+)\}/i); - var weight = 0; - if (match && match[1]) { - weight = match[1]; - } - - if (!prior[weight]) { - prior[weight] = []; - } - prior[weight].push(gambit); - } - - var sortFwd = function sortFwd(a, b) { - return b - a; - }; - var sortRev = function sortRev(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 (var _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 (var j = 0; j < prior[p].length; j++) { - gambit = prior[p][j]; - - inherits = -1; - if (!track[inherits]) { - track[inherits] = initSortTrack(); - } - - if (gambit.qType) { - // Qtype included - cnt = gambit.qType.length; - debug('Has a qType with ' + gambit.qType.length + ' length.'); - - if (!track[inherits].qtype[cnt]) { - track[inherits].qtype[cnt] = []; - } - track[inherits].qtype[cnt].push(gambit); - } else if (gambit.input.indexOf('*') > -1) { - // Wildcard included. - cnt = _utils2.default.wordCount(gambit.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(gambit); - } else { - track[inherits].star.push(gambit); - } - } else if (gambit.input.indexOf('[') > -1) { - // Optionals included. - cnt = _utils2.default.wordCount(gambit.input); - debug('Has optionals with ' + cnt + ' words.'); - if (!track[inherits].option[cnt]) { - track[inherits].option[cnt] = []; - } - track[inherits].option[cnt].push(gambit); - } else { - // Totally atomic. - cnt = _utils2.default.wordCount(gambit.input); - debug('Totally atomic trigger and ' + cnt + ' words.'); - if (!track[inherits].atomic[cnt]) { - track[inherits].atomic[cnt] = []; - } - track[inherits].atomic[cnt].push(gambit); - } - } - - // 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 (var _j = 0; _j < trackSorted.length; _j++) { - var ip = trackSorted[_j]; - debug('ip=' + ip); - - var kinds = ['qtype', 'atomic', 'option', 'alpha', 'number', 'wild']; - for (var k = 0; k < kinds.length; k++) { - var kind = kinds[k]; - - var kindSorted = Object.keys(track[ip][kind]).sort(sortFwd); - - for (var l = 0; l < kindSorted.length; l++) { - var item = kindSorted[l]; - running.push.apply(running, _toConsumableArray(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, _toConsumableArray(underSorted)); - running.push.apply(running, _toConsumableArray(poundSorted)); - running.push.apply(running, _toConsumableArray(starSorted)); - } - } - return running; -}; - -exports.default = { - sortTriggerSet: sortTriggerSet -}; \ No newline at end of file diff --git a/lib/bot/dict.js b/lib/bot/dict.js deleted file mode 100644 index bd3b9d0f..00000000 --- a/lib/bot/dict.js +++ /dev/null @@ -1,132 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var debug = (0, _debugLevels2.default)('SS:Dict'); - -var Dict = function () { - function Dict(wordArray) { - _classCallCheck(this, Dict); - - this.words = []; - - for (var i = 0; i < wordArray.length; i++) { - this.words.push({ word: wordArray[i], position: i }); - } - } - - _createClass(Dict, [{ - key: 'add', - value: function add(key, array) { - for (var i = 0; i < array.length; i++) { - this.words[i][key] = array[i]; - } - } - }, { - key: 'get', - value: function get(word) { - debug.verbose('Getting word from dictionary: ' + word); - for (var i = 0; i < this.words.length; i++) { - if (this.words[i].word === word || this.words[i].lemma === word) { - return this.words[i]; - } - } - return null; - } - }, { - key: 'contains', - value: function contains(word) { - for (var i = 0; i < this.words.length; i++) { - if (this.words[i].word === word || this.words[i].lemma === word) { - return true; - } - } - return false; - } - }, { - key: 'addHLC', - value: function addHLC(array) { - debug.verbose('Adding HLCs to dictionary: ' + 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.verbose('HLC extra or missing for word/phrase: ' + word); - extra.push(word); - } - } - return extra; - } - }, { - key: 'getHLC', - value: function getHLC(concept) { - for (var i = 0; i < this.words.length; i++) { - if (_lodash2.default.includes(this.words[i].hlc, concept)) { - return this.words[i]; - } - } - return null; - } - }, { - key: 'containsHLC', - value: function containsHLC(concept) { - for (var i = 0; i < this.words.length; i++) { - if (_lodash2.default.includes(this.words[i].hlc, concept)) { - return true; - } - } - return false; - } - }, { - key: 'fetch', - value: function fetch(list, thing) { - var results = []; - for (var i = 0; i < this.words.length; i++) { - if (_lodash2.default.isArray(thing)) { - if (_lodash2.default.includes(thing, this.words[i][list])) { - results.push(this.words[i].lemma); - } - } else if (_lodash2.default.isArray(this.words[i][list])) { - if (_lodash2.default.includes(this.words[i][list], thing)) { - results.push(this.words[i].lemma); - } - } - } - return results; - } - }, { - key: 'findByLem', - value: function findByLem(word) { - for (var i = 0; i < this.words.length; i++) { - if (this.words[i].lemma === word) { - return this.words[i]; - } - } - return null; - } - }]); - - return Dict; -}(); - -exports.default = Dict; \ No newline at end of file diff --git a/lib/bot/factSystem.js b/lib/bot/factSystem.js deleted file mode 100644 index 17f8adee..00000000 --- a/lib/bot/factSystem.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _sfacts = require('sfacts'); - -var _sfacts2 = _interopRequireDefault(_sfacts); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var coreFacts = null; - -var createFactSystem = function createFactSystem(mongoURI, _ref, callback) { - var clean = _ref.clean, - importData = _ref.importData; - - // TODO: On a multitenanted system, importing data should not do anything - if (importData) { - return _sfacts2.default.load(mongoURI, importData, clean, function (err, factSystem) { - coreFacts = factSystem; - callback(err, factSystem); - }); - } - return _sfacts2.default.create(mongoURI, clean, function (err, factSystem) { - coreFacts = factSystem; - callback(err, factSystem); - }); -}; - -var createFactSystemForTenant = function createFactSystemForTenant() { - var tenantId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'master'; - - return coreFacts.createUserDB('' + tenantId); -}; - -exports.default = { - createFactSystem: createFactSystem, - createFactSystemForTenant: createFactSystemForTenant -}; \ No newline at end of file diff --git a/lib/bot/getReply.js b/lib/bot/getReply.js deleted file mode 100644 index 70e6db9c..00000000 --- a/lib/bot/getReply.js +++ /dev/null @@ -1,464 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _async = require('async'); - -var _async2 = _interopRequireDefault(_async); - -var _re = require('re2'); - -var _re2 = _interopRequireDefault(_re); - -var _regexes = require('./regexes'); - -var _regexes2 = _interopRequireDefault(_regexes); - -var _utils = require('./utils'); - -var _utils2 = _interopRequireDefault(_utils); - -var _processTags = require('./processTags'); - -var _processTags2 = _interopRequireDefault(_processTags); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('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. -var topicItorHandle = function topicItorHandle(messageObject, options) { - var system = options.system; - - return function (topicData, callback) { - if (topicData.type === 'TOPIC') { - system.chatSystem.Topic.findOne({ _id: topicData.id }).populate('gambits').exec(function (err, topic) { - if (err) { - console.error(err); - } - if (topic) { - // 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(function (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); - } - }; -}; - -var 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 function (continueSearching, matchSet) { - debug.verbose('Continue searching: ' + continueSearching); - debug.verbose('Set of matches: ' + matchSet); - - // remove empties - matchSet = _lodash2.default.compact(matchSet); - - var minMatchSet = []; - var props = {}; - var clearConversation = false; - var lastTopicToMatch = null; - var lastStarSet = null; - var lastReplyId = null; - var replyString = ''; - var lastSubReplies = null; - var lastContinueMatching = null; - var lastReplyIds = 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 (!_lodash2.default.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 = _lodash2.default.assign(props, item.props); - lastTopicToMatch = item.topic; - lastStarSet = item.stars; - lastReplyId = item.reply._id; - lastSubReplies = item.subReplies; - lastContinueMatching = item.continueMatching; - lastReplyIds = item.replyIds; - - if (item.clearConversation) { - clearConversation = item.clearConversation; - } - } - - var threadsArr = []; - if (_lodash2.default.isEmpty(lastSubReplies)) { - threadsArr = _processTags2.default.processThreadTags(replyString); - } else { - threadsArr[0] = replyString; - threadsArr[1] = lastSubReplies; - } - - // only remove one trailing space (because spaces may have been added deliberately) - var replyStr = new _re2.default('(?:^[ \\t]+)|(?:[ \\t]$)').replace(threadsArr[0], ''); - - var cbdata = { - replyId: lastReplyId, - replyIds: lastReplyIds, - props: props, - clearConversation: clearConversation, - topicName: lastTopicToMatch, - minMatchSet: minMatchSet, - string: replyStr, - subReplies: threadsArr[1], - stars: lastStarSet, - continueMatching: lastContinueMatching - }; - - debug.verbose('afterHandle', cbdata); - - callback(null, cbdata); - }; -}; - -// This may be called several times, once for each topic. -var filterRepliesBySeen = function filterRepliesBySeen(filteredResults, options, callback) { - var system = options.system; - debug.verbose('filterRepliesBySeen', filteredResults); - var bucket = []; - - var eachResultItor = function eachResultItor(filteredResult, next) { - var topicName = filteredResult.topic; - system.chatSystem.Topic.findOne({ name: topicName }).exec(function (err, currentTopic) { - if (err) { - console.log(err); - } - - // var repIndex = filteredResult.id; - var replyId = filteredResult.reply._id; - var reply = filteredResult.reply; - var gambitId = 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 = options.user.history.topic[i]; - - if (topicItem !== undefined) { - // TODO: Come back to this and check names make sense - var pastGambit = options.user.history.reply[i]; - var 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(); - }); - }; - - _async2.default.each(filteredResults, eachResultItor, function () { - debug.verbose('Bucket of selected replies: ', bucket); - if (!_lodash2.default.isEmpty(bucket)) { - callback(null, _utils2.default.pickItem(bucket)); - } else { - callback(true); - } - }); -}; // end filterBySeen - -var filterRepliesByFunction = function filterRepliesByFunction(potentialReplies, options, callback) { - var filterHandle = function filterHandle(potentialReply, cb) { - var 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 !== '') { - var filterFunction = _regexes2.default.filter.match(potentialReply.reply.filter); - var pluginName = _utils2.default.trim(filterFunction[1]); - var partsStr = _utils2.default.trim(filterFunction[2]); - var args = _utils2.default.replaceCapturedText(partsStr.split(','), [''].concat(potentialReply.stars)); - - debug.verbose('Filter function found with plugin name: ' + pluginName); - - if (system.plugins[pluginName]) { - args.push(function (err, filterReply) { - if (err) { - console.log(err); - } - - if (filterReply === 'true' || filterReply === true) { - cb(err, true); - } else { - cb(err, false); - } - }); - - var filterScope = _lodash2.default.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 = _utils2.default.trim(potentialReply.reply.reply.replace(filterFunction[0], '')); - cb(null, true); - } - } else { - cb(null, true); - } - }; - - _async2.default.filter(potentialReplies, filterHandle, function (err, filteredReplies) { - debug.verbose('filterByFunction results: ', filteredReplies); - - filterRepliesBySeen(filteredReplies, options, function (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 { - _processTags2.default.processReplyTags(reply, options, function (err, replyObj) { - if (!_lodash2.default.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('Reply object after processing tags: ', replyObj); - - if (replyObj.continueMatching === false) { - debug.info('Continue matching is set to false: returning.'); - callback(true, replyObj); - } else if (replyObj.continueMatching === true || replyObj.reply.reply === '') { - debug.info('Continue matching is set to true or reply is not empty: continuing.'); - // By calling back with error set as 'true', we break out of async flow - // and return the reply to the user. - callback(null, replyObj); - } else { - debug.info('Reply is not empty: returning.'); - callback(true, replyObj); - } - } else { - debug.verbose('No reply object was received from processTags so check for more.'); - if (err) { - debug.verbose('There was an error in processTags', err); - } - callback(null, null); - } - }); - } - }); - }); -}; - -// Iterates through matched gambits -var matchItorHandle = function matchItorHandle(message, options) { - var system = options.system; - options.message = message; - - return function (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(function (err, gambitExpanded) { - if (err) { - console.log(err); - } - - match.gambit = gambitExpanded; - - match.gambit.getRootTopic(function (err, topic) { - if (err) { - console.log(err); - } - - var rootTopic = void 0; - if (match.topic) { - rootTopic = match.topic; - } else { - rootTopic = topic; - } - - var stars = match.stars; - if (!_lodash2.default.isEmpty(message.stars)) { - stars = message.stars; - } - - var potentialReplies = []; - - for (var i = 0; i < match.gambit.replies.length; i++) { - var reply = match.gambit.replies[i]; - var replyData = { - id: reply.id, - topic: rootTopic, - stars: stars, - reply: 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. - */ -var 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 (!_lodash2.default.isEmpty(options.pendingTopics)) { - debug.verbose('Using pre-set topic list via directReply, respond or topicRedirect'); - debug.info('Topics to check: ', options.pendingTopics.map(function (topic) { - return topic.name; - })); - afterFindPendingTopics(options.pendingTopics, messageObject, options, callback); - } else { - var chatSystem = options.system.chatSystem; - - // Find potential topics for the response based on the message (tfidfs) - chatSystem.Topic.findPendingTopicsForUser(options.user, messageObject, function (err, pendingTopics) { - if (err) { - console.log(err); - } - afterFindPendingTopics(pendingTopics, messageObject, options, callback); - }); - } -}; - -var 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. - _async2.default.mapSeries(pendingTopics, topicItorHandle(messageObject, options), function (err, results) { - if (err) { - console.error(err); - } - - // Remove the empty topics, and flatten the array down. - var matches = _lodash2.default.flatten(_lodash2.default.filter(results, function (n) { - return n; - })); - - // TODO - This sort should happen in the process sort logic. - // Try matching most specific question matches first - matches = matches.sort(function (a, b) { - var questionTypeA = a.gambit.qType || ''; - var questionSubTypeA = a.gambit.qSubType || ''; - var questionTypeB = b.gambit.qType || ''; - var questionSubTypeB = b.gambit.qSubType || ''; - return questionTypeA.concat(questionSubTypeA).length < questionTypeB.concat(questionSubTypeB).length; - }); - - debug.verbose('Matching gambits are: '); - matches.forEach(function (match) { - debug.verbose('Trigger: ' + match.gambit.input); - debug.verbose('Replies: ' + match.gambit.replies.map(function (reply) { - return reply.reply; - }).join('\n')); - }); - - // Was `eachSeries` - _async2.default.mapSeries(matches, matchItorHandle(messageObject, options), afterHandle(options.user, callback)); - }); -}; - -exports.default = getReply; \ No newline at end of file diff --git a/lib/bot/history.js b/lib/bot/history.js deleted file mode 100644 index bc984bc4..00000000 --- a/lib/bot/history.js +++ /dev/null @@ -1,147 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('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 historyLookup(user, options) { - debug.verbose('History Lookup with', options); - - var candidates = []; - var nn = void 0; - - var moneyWords = function moneyWords(item) { - return item[1] === '$' || item[0] === 'quid' || item[0] === 'pounds' || item[0] === 'dollars' || item[0] === 'bucks' || item[0] === 'cost'; - }; - - var _loop = function _loop(i) { - var pobj = user.history.input[i]; - - if (pobj !== undefined) { - // TODO - See why we are getting a nested array. - if (Array.isArray(pobj)) { - pobj = pobj[0]; - } - - if (options.numbers || options.number) { - if (pobj.numbers.length !== 0) { - candidates.push(pobj); - } - } - - // Special case of number - if (options.money === true && options.nouns) { - if (pobj.numbers.length !== 0) { - var t = []; - if (_lodash2.default.any(pobj.taggedWords, moneyWords)) { - t.push(pobj); - - // Now filter out the nouns - for (var n = 0; n < t.length; n++) { - nn = _lodash2.default.any(t[n].nouns, function (item) { - for (var j = 0; j < options.nouns.length; j++) { - return options.nouns[i] === item ? true : false; - } - }); - } - - if (nn) { - candidates.push(pobj); - } - } - } - } else if (options.money && pobj) { - if (pobj.numbers.length !== 0) { - if (_lodash2.default.any(pobj.taggedWords, moneyWords)) { - candidates.push(pobj); - } - } - } else if (options.nouns && pobj) { - debug.verbose('Noun Lookup'); - if (_lodash2.default.isArray(options.nouns)) { - s = 0; - c = 0; - - - nn = _lodash2.default.any(pobj.nouns, function (item) { - var x = _lodash2.default.includes(options.nouns, item); - c++; - s = x ? s + 1 : s; - return x; - }); - - if (nn) { - pobj.score = s / c; - candidates.push(pobj); - } - } else if (pobj.nouns.length !== 0) { - candidates.push(pobj); - } - } else if (options.names && pobj) { - debug.verbose('Name Lookup'); - - if (_lodash2.default.isArray(options.names)) { - nn = _lodash2.default.any(pobj.names, function (item) { - return _lodash2.default.includes(options.names, item); - }); - if (nn) { - candidates.push(pobj); - } - } else if (pobj.names.length !== 0) { - candidates.push(pobj); - } - } else if (options.adjectives && pobj) { - debug.verbose('adjectives Lookup'); - if (_lodash2.default.isArray(options.adjectives)) { - s = 0; - c = 0; - - nn = _lodash2.default.any(pobj.adjectives, function (item) { - var x = _lodash2.default.includes(options.adjectives, item); - c++; - s = x ? s + 1 : s; - return x; - }); - - if (nn) { - pobj.score = s / c; - candidates.push(pobj); - } - } else if (pobj.adjectives.length !== 0) { - candidates.push(pobj); - } - } - - if (options.date && pobj) { - if (pobj.date !== null) { - candidates.push(pobj); - } - } - } - }; - - for (var i = 0; i < user.history.input.length; i++) { - var s; - var c; - - _loop(i); - } - - return candidates; -}; - -exports.default = historyLookup; \ No newline at end of file diff --git a/lib/bot/index.js b/lib/bot/index.js deleted file mode 100644 index 578bc1a1..00000000 --- a/lib/bot/index.js +++ /dev/null @@ -1,353 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _requireDir = require('require-dir'); - -var _requireDir2 = _interopRequireDefault(_requireDir); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _common = require('./reply/common'); - -var _common2 = _interopRequireDefault(_common); - -var _connect = require('./db/connect'); - -var _connect2 = _interopRequireDefault(_connect); - -var _factSystem = require('./factSystem'); - -var _factSystem2 = _interopRequireDefault(_factSystem); - -var _chatSystem = require('./chatSystem'); - -var _chatSystem2 = _interopRequireDefault(_chatSystem); - -var _getReply = require('./getReply'); - -var _getReply2 = _interopRequireDefault(_getReply); - -var _import = require('./db/import'); - -var _import2 = _interopRequireDefault(_import); - -var _message = require('./message'); - -var _message2 = _interopRequireDefault(_message); - -var _logger = require('./logger'); - -var _logger2 = _interopRequireDefault(_logger); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var debug = (0, _debugLevels2.default)('SS:SuperScript'); - -var plugins = []; -var editMode = false; -var scope = {}; - -var loadPlugins = function loadPlugins(path) { - try { - (function () { - var pluginFiles = (0, _requireDir2.default)(path); - - Object.keys(pluginFiles).forEach(function (file) { - // For transpiled ES6 plugins with default export - if (pluginFiles[file].default) { - pluginFiles[file] = pluginFiles[file].default; - } - - Object.keys(pluginFiles[file]).forEach(function (func) { - debug.verbose('Loading plugin: ', path, func); - plugins[func] = pluginFiles[file][func]; - }); - }); - })(); - } catch (e) { - console.error('Could not load plugins from ' + path + ': ' + e); - } -}; - -var SuperScript = function () { - function SuperScript() { - var tenantId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'master'; - - _classCallCheck(this, SuperScript); - - this.factSystem = _factSystem2.default.createFactSystemForTenant(tenantId); - this.chatSystem = _chatSystem2.default.createChatSystemForTenant(tenantId); - - // We want a place to store bot related data - this.memory = this.factSystem.createUserDB('botfacts'); - - this.scope = scope; - this.scope.bot = this; - this.scope.facts = this.factSystem; - this.scope.chatSystem = this.chatSystem; - this.scope.botfacts = this.memory; - - this.plugins = plugins; - } - - _createClass(SuperScript, [{ - key: 'importFile', - value: function importFile(filePath, callback) { - _import2.default.importFile(this.chatSystem, filePath, function (err) { - console.log('Bot is ready for input!'); - debug.verbose('System loaded, waiting for replies'); - callback(err); - }); - } - }, { - key: 'getUsers', - value: function getUsers(callback) { - this.chatSystem.User.find({}, 'id', callback); - } - }, { - key: 'getUser', - value: function getUser(userId, callback) { - this.chatSystem.User.findOne({ id: userId }, callback); - } - }, { - key: 'findOrCreateUser', - value: function findOrCreateUser(userId, callback) { - var findProps = { id: userId }; - var 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 - - }, { - key: 'reply', - value: function 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); - var options = { - userId: userId, - extraScope: extraScope - }; - - this._reply(messageString, options, callback); - } - - // This is like doing a topicRedirect - - }, { - key: 'directReply', - value: function directReply(userId, topicName, messageString, callback) { - debug.log("[ New DirectReply - '%s']- %s", userId, messageString); - var options = { - userId: userId, - topicName: topicName, - extraScope: {} - }; - - this._reply(messageString, options, callback); - } - }, { - key: 'message', - value: function message(messageString, callback) { - var options = { - factSystem: this.factSystem - }; - - _message2.default.createMessage(messageString, options, function (msgObj) { - callback(null, msgObj); - }); - } - }, { - key: '_reply', - value: function _reply(messageString, options, callback) { - var _this = this; - - var 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: editMode - }; - - this.findOrCreateUser(options.userId, function (err, user) { - if (err) { - debug.error(err); - } - - var messageOptions = { - factSystem: _this.factSystem - }; - - _message2.default.createMessage(messageString, messageOptions, function (messageObject) { - _common2.default.getTopic(system.chatSystem, system.topicName, function (err, topicData) { - var options = { - user: user, - system: system, - depth: 0 - }; - - if (topicData) { - options.pendingTopics = [topicData]; - } - - (0, _getReply2.default)(messageObject, options, function (err, replyObj) { - // Convert the reply into a message object too. - var replyMessage = ''; - var messageOptions = { - factSystem: system.factSystem - }; - - if (replyObj) { - messageOptions.replyId = replyObj.replyId; - replyMessage = replyObj.string; - - if (replyObj.clearConversation) { - messageOptions.clearConversation = replyObj.clearConversation; - } - } else { - replyObj = {}; - console.log('There was no response matched.'); - } - - _message2.default.createMessage(replyMessage, messageOptions, function (replyMessageObject) { - user.updateHistory(messageObject, 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: replyMessage || '', // replyMessageObject.raw || "", - topicName: replyObj.topicName, - subReplies: replyObj.subReplies, - debug: log - }; - - var newClientObject = _lodash2.default.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); - }); - }); - }); - }); - }); - }); - } - }], [{ - key: 'getBot', - value: function getBot(tenantId) { - return new SuperScript(tenantId); - } - }]); - - return SuperScript; -}(); - -var defaultOptions = { - mongoURI: 'mongodb://localhost/superscriptDB', - importFile: null, - factSystem: { - clean: false, - importFiles: null - }, - scope: {}, - editMode: false, - pluginsPath: process.cwd() + '/plugins', - logPath: process.cwd() + '/logs' -}; - -/** - * Setup SuperScript. You may only run this a single time since it writes to global state. - * @param {Object} options - Any configuration settings you want to use. - * @param {String} options.mongoURI - The database URL you want to connect to. - * This will be used for both the chat and fact system. - * @param {String} options.importFile - Use this if you want to re-import your parsed - * '*.json' file. Otherwise SuperScript will use whatever it currently - * finds in the database. - * @param {Object} options.factSystem - Settings to use for the fact system. - * @param {Boolean} options.factSystem.clean - If you want to remove everything in the - * fact system upon launch. Otherwise SuperScript will keep facts from - * the last time it was run. - * @param {Array} options.factSystem.importFiles - Any additional data you want to - * import into the fact system. - * @param {Object} options.scope - Any extra scope you want to pass into your plugins. - * @param {Boolean} options.editMode - Used in the editor. - * @param {String} options.pluginsPath - A path to the plugins written by you. This loads - * the entire directory recursively. - * @param {String} options.logPath - If null, logging will be off. Otherwise writes - * conversation transcripts to the path. - */ -var setup = function setup() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - var callback = arguments[1]; - - options = _lodash2.default.merge(defaultOptions, options); - _logger2.default.setLogPath(options.logPath); - - // Uses schemas to create models for the db connection to use - _factSystem2.default.createFactSystem(options.mongoURI, options.factSystem, function (err) { - if (err) { - return callback(err); - } - - var db = (0, _connect2.default)(options.mongoURI); - _chatSystem2.default.createChatSystem(db); - - // Built-in plugins - loadPlugins(__dirname + '/../plugins'); - - // For user plugins - if (options.pluginsPath) { - loadPlugins(options.pluginsPath); - } - - // This is a kill switch for filterBySeen which is useless in the editor. - editMode = options.editMode || false; - scope = options.scope || {}; - - var bot = new SuperScript('master'); - - if (options.importFile) { - return bot.importFile(options.importFile, function (err) { - return callback(err, bot); - }); - } - return callback(null, bot); - }); -}; - -exports.default = { - setup: setup -}; \ No newline at end of file diff --git a/lib/bot/logger.js b/lib/bot/logger.js deleted file mode 100644 index 90cfbb91..00000000 --- a/lib/bot/logger.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _fs = require('fs'); - -var _fs2 = _interopRequireDefault(_fs); - -var _mkdirp = require('mkdirp'); - -var _mkdirp2 = _interopRequireDefault(_mkdirp); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -// The directory to write logs to -var logPath = void 0; - -var setLogPath = function setLogPath(path) { - if (path) { - try { - _mkdirp2.default.sync(path); - logPath = path; - } catch (e) { - console.error('Could not create logs folder at ' + logPath + ': ' + e); - } - } -}; - -var log = function log(message) { - var logName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'log'; - - if (logPath) { - var filePath = logPath + '/' + logName + '.log'; - try { - _fs2.default.appendFileSync(filePath, message); - } catch (e) { - console.error('Could not write log to file with path: ' + filePath); - } - } -}; - -exports.default = { - log: log, - setLogPath: setLogPath -}; \ No newline at end of file diff --git a/lib/bot/math.js b/lib/bot/math.js deleted file mode 100644 index 6439111e..00000000 --- a/lib/bot/math.js +++ /dev/null @@ -1,320 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _debug = require('debug'); - -var _debug2 = _interopRequireDefault(_debug); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -/* eslint no-eval:0 */ -// TODO - Make this into its own project - -var debug = (0, _debug2.default)('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', '%']; - -var 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 -var parse = function parse(words) { - var prev = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - - debug('In parse with ', words); - var expression = []; - var newexpression = []; - var i = void 0; - var word = void 0; - - 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 -var convertWordsToNumbers = function convertWordsToNumbers(wordArray) { - var mult = { hundred: 100, thousand: 1000 }; - var results = []; - var i = void 0; - - 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 convertWordToNumber(word) { - var number = void 0; - var multipleOfTen = void 0; - var cardinalNumber = void 0; - - if (word !== undefined) { - if (word.indexOf('-') === -1) { - if (_lodash2.default.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; - } -}; - -var numberLookup = function numberLookup(number) { - var multipleOfTen = void 0; - 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 convertNumberToWord(number) { - if (number === 0) { - return 'zero'; - } - - if (number < 0) { - return 'negative ' + numberLookup(Math.abs(number)); - } - - return numberLookup(number); -}; - -var cardPlural = function cardPlural(wordNumber) { - return cardinalNumberPlural[wordNumber]; -}; - -var arithGeo = function arithGeo(arr) { - var ap = void 0; - var gp = void 0; - - 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 (var _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; -}; - -exports.default = { - arithGeo: arithGeo, - cardPlural: cardPlural, - convertWordToNumber: convertWordToNumber, - convertWordsToNumbers: convertWordsToNumbers, - mathTerms: mathTerms, - parse: parse -}; \ No newline at end of file diff --git a/lib/bot/message.js b/lib/bot/message.js deleted file mode 100644 index 79a97f73..00000000 --- a/lib/bot/message.js +++ /dev/null @@ -1,506 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _qtypes = require('qtypes'); - -var _qtypes2 = _interopRequireDefault(_qtypes); - -var _partsOfSpeech = require('parts-of-speech'); - -var _partsOfSpeech2 = _interopRequireDefault(_partsOfSpeech); - -var _natural = require('natural'); - -var _natural2 = _interopRequireDefault(_natural); - -var _moment = require('moment'); - -var _moment2 = _interopRequireDefault(_moment); - -var _lemmer = require('lemmer'); - -var _lemmer2 = _interopRequireDefault(_lemmer); - -var _async = require('async'); - -var _async2 = _interopRequireDefault(_async); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _nodeNormalizer = require('node-normalizer'); - -var _nodeNormalizer2 = _interopRequireDefault(_nodeNormalizer); - -var _math = require('./math'); - -var _math2 = _interopRequireDefault(_math); - -var _dict = require('./dict'); - -var _dict2 = _interopRequireDefault(_dict); - -var _utils = require('./utils'); - -var _utils2 = _interopRequireDefault(_utils); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var debug = (0, _debugLevels2.default)('SS:Message'); -var ngrams = _natural2.default.NGrams; - -var patchList = function patchList(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 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 - -var Message = function () { - /** - * 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.clearConversation] - If you want to clear the conversation. - */ - function Message(message, options) { - _classCallCheck(this, Message); - - debug.verbose('Creating message from string: ' + message); - - this.id = _utils2.default.genId(); - - // If this message is based on a Reply. - if (options.replyId) { - this.replyId = options.replyId; - } - - if (options.clearConversation) { - this.clearConversation = options.clearConversation; - } - - 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 = _nodeNormalizer2.default.clean(message).trim(); - this.clean = cleanMessage(this.raw).trim(); - debug.verbose('Message before cleaning: ', message); - debug.verbose('Message after cleaning: ', this.clean); - - this.props = {}; - - var words = new _partsOfSpeech2.default.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 _dict2.default(words); - - words = _math2.default.convertWordsToNumbers(words); - this.taggedWords = new _partsOfSpeech2.default.Tagger().tag(words); - } - - _createClass(Message, [{ - key: 'finishCreating', - value: function finishCreating(callback) { - var _this = this; - - this.lemma(function (err, lemWords) { - if (err) { - console.log(err); - } - - _this.lemWords = lemWords; - _this.lemString = _this.lemWords.join(' '); - - _this.posWords = _this.taggedWords.map(function (hash) { - return hash[1]; - }); - _this.posString = _this.posWords.join(' '); - - _this.dict.add('lemma', _this.lemWords); - _this.dict.add('pos', _this.posWords); - - // Classify Question - _this.questionType = _qtypes2.default.questionType(_this.clean); - _this.questionSubType = _qtypes2.default.classify(_this.lemString); - _this.isQuestion = _qtypes2.default.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 = _lodash2.default.uniq(_this.names, function (name) { - return name.toLowerCase(); - }); - - // Nouns with Names removed. - var lowerCaseNames = _this.names.map(function (name) { - return name.toLowerCase(); - }); - - _this.cNouns = _lodash2.default.filter(_this.nouns, function (item) { - return !_lodash2.default.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(function (entities) { - var complexNouns = _this.fetchComplexNouns('nouns'); - var fullEntities = entities.map(function (item) { - return 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. - - }, { - key: 'lemma', - value: function lemma(callback) { - var itor = function itor(hash, next) { - var word = hash[0].toLowerCase(); - var tag = _utils2.default.pennToWordnet(hash[1]); - - // console.log(word, tag); - // next(null, [word]); - - if (tag) { - try { - _lemmer2.default.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]); - } - }; - - _async2.default.map(this.taggedWords, itor, function (err, lemWords) { - var result = _lodash2.default.map(_lodash2.default.flatten(lemWords), function (lemWord) { - return lemWord.split('#')[0]; - }); - callback(err, result); - }); - } - }, { - key: 'checkMath', - value: function checkMath() { - var numCount = 0; - var oppCount = 0; - - for (var i = 0; i < this.taggedWords.length; i++) { - if (this.taggedWords[i][1] === 'CD') { - numCount += 1; - } - if (this.taggedWords[i][1] === 'SYM' || _math2.default.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; - } - } - }, { - key: 'fetchCompareWords', - value: function fetchCompareWords() { - return this.dict.fetch('pos', ['JJR', 'RBR']); - } - }, { - key: 'fetchAdjectives', - value: function fetchAdjectives() { - return this.dict.fetch('pos', ['JJ', 'JJR', 'JJS']); - } - }, { - key: 'fetchAdverbs', - value: function fetchAdverbs() { - return this.dict.fetch('pos', ['RB', 'RBR', 'RBS']); - } - }, { - key: 'fetchNumbers', - value: function fetchNumbers() { - return this.dict.fetch('pos', ['CD']); - } - }, { - key: 'fetchVerbs', - value: function fetchVerbs() { - return this.dict.fetch('pos', ['VB', 'VBN', 'VBD', 'VBZ', 'VBP', 'VBG']); - } - }, { - key: 'fetchPronouns', - value: function fetchPronouns() { - return this.dict.fetch('pos', ['PRP', 'PRP$']); - } - }, { - key: 'fetchNouns', - value: function 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 - - }, { - key: 'fetchList', - value: function fetchList() { - debug.verbose('Fetch list'); - var 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)) { - var sn = false; - for (var 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 && _utils2.default.isTag(this.taggedWords[i][1], 'nouns')) { - list.push(this.taggedWords[i][0]); - sn = false; - } - } - } - return list; - } - }, { - key: 'fetchDate', - value: function fetchDate() { - var date = null; - var months = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december']; - - // http://rubular.com/r/SAw0nUqHJh - var regex = /([a-z]{3,10}\s+[\d]{1,2}\s?,?\s+[\d]{2,4}|[\d]{2}\/[\d]{2}\/[\d]{2,4})/i; - var match = this.clean.match(regex); - - if (match) { - debug.verbose('Date: ', match); - date = (0, _moment2.default)(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 (_lodash2.default.includes(this.nouns, 'month')) { - if (this.dict.includes('next')) { - date = (0, _moment2.default)().add('M', 1); - } - if (this.dict.includes('last')) { - date = (0, _moment2.default)().subtract('M', 1); - } - } else if (_utils2.default.inArray(this.nouns, months)) { - // IN month vs ON month - var p = _utils2.default.inArray(this.nouns, months); - date = (0, _moment2.default)(this.nouns[p] + ' 1', 'MMM D'); - } - } - - return date; - } - - // Pulls concepts from the bigram DB. - - }, { - key: 'fetchNamedEntities', - value: function fetchNamedEntities(callback) { - var _this2 = this; - - var bigrams = ngrams.bigrams(this.taggedWords); - - var sentenceBigrams = _lodash2.default.map(bigrams, function (bigram) { - return _lodash2.default.map(bigram, function (item) { - return item[0]; - }); - }); - - var itor = function itor(item, cb) { - var bigramLookup = { subject: item.join(' '), predicate: 'isa', object: 'bigram' }; - _this2.factSystem.db.get(bigramLookup, function (err, res) { - if (err) { - debug.error(err); - } - - if (!_lodash2.default.isEmpty(res)) { - cb(err, true); - } else { - cb(err, false); - } - }); - }; - - _async2.default.filter(sentenceBigrams, itor, function (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" - - }, { - key: 'fetchComplexNouns', - value: function fetchComplexNouns(lookupType) { - var tags = this.taggedWords; - var bigrams = ngrams.bigrams(tags); - var tester = void 0; - - // TODO: Might be able to get rid of this and use this.dict to get nouns/proper names - if (lookupType === 'names') { - tester = function tester(item) { - return item[1] === 'NNP' || item[1] === 'NNPS'; - }; - } else { - tester = function tester(item) { - return item[1] === 'NN' || item[1] === 'NNS' || item[1] === 'NNP' || item[1] === 'NNPS'; - }; - } - - var nouns = _lodash2.default.filter(_lodash2.default.map(tags, function (item) { - return tester(item) ? item[0] : null; - }), Boolean); - - var nounBigrams = ngrams.bigrams(nouns); - - // Get a list of term - var neTest = _lodash2.default.map(bigrams, function (bigram) { - return _lodash2.default.map(bigram, function (item) { - return tester(item); - }); - }); - - // TODO: Work out what this is - var thing = _lodash2.default.map(neTest, function (item, key) { - return _lodash2.default.every(item, _lodash2.default.identity) ? bigrams[key] : null; - }); - - // Return full names from the list - var fullnames = _lodash2.default.map(_lodash2.default.filter(thing, Boolean), function (item) { - return _lodash2.default.map(item, function (item2) { - return item2[0]; - }).join(' '); - }); - - debug.verbose('Full names found from lookupType ' + lookupType + ': ' + fullnames); - - var x = _lodash2.default.map(nounBigrams, function (item) { - return _lodash2.default.includes(fullnames, item.join(' ')); - }); - - // FIXME: This doesn't do anything (result not used) - // Filter X out of the bigrams or names? - _lodash2.default.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); - } - }], [{ - key: 'createMessage', - value: function createMessage(message, options, callback) { - if (!message) { - debug.verbose('Message received was empty, callback immediately'); - return callback({}); - } - - var messageObj = new Message(message, options); - messageObj.finishCreating(callback); - } - }]); - - return Message; -}(); - -exports.default = Message; \ No newline at end of file diff --git a/lib/bot/postParse.js b/lib/bot/postParse.js deleted file mode 100644 index 3190361b..00000000 --- a/lib/bot/postParse.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _re = require('re2'); - -var _re2 = _interopRequireDefault(_re); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -/** - * Insert replacements into `source` string - * - * - `` gets replaced by `(replacements[0])` - * - `` gets replaced by `(replacements[0]|replacements[1]|...)` - * - `` gets replaced by `(replacements[N])` - * - * @param {string} basename - * @param {string} source - * @param {Array} replacements - * @returns {string} - */ -var replaceOneOrMore = function replaceOneOrMore(basename, source, replacements) { - var pronounsRE = new _re2.default('<(' + basename + ')([s0-' + replacements.length + '])?>', 'g'); - if (pronounsRE.search(source) !== -1 && replacements.length !== 0) { - return pronounsRE.replace(source, function (c, p1, p2) { - if (p1 === 's') { - return '(' + replacements.join('|') + ')'; - } else { - var index = Number.parseInt(p2); - index = index ? index - 1 : 0; - return '(' + replacements[index] + ')'; - } - }); - } else { - return source; - } -}; - -/** - * 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. - */ -var postParse = function postParse(regexp, message, user, callback) { - if (_lodash2.default.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); - - var inputOrReplyRE = new _re2.default('<(input|reply)([1-9])?>', 'g'); - if (inputOrReplyRE.search(regexp) !== -1) { - (function () { - var history = user.history; - regexp = inputOrReplyRE.replace(regexp, function (c, p1, p2) { - var index = p2 ? Number.parseInt(p2) : 0; - return history[p1][index] ? history[p1][index].raw : c; - }); - })(); - } - } - - callback(regexp); -}; - -exports.default = postParse; \ No newline at end of file diff --git a/lib/bot/processTags.js b/lib/bot/processTags.js deleted file mode 100644 index 10e61417..00000000 --- a/lib/bot/processTags.js +++ /dev/null @@ -1,516 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _async = require('async'); - -var _async2 = _interopRequireDefault(_async); - -var _re = require('re2'); - -var _re2 = _interopRequireDefault(_re); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _pegjs = require('pegjs'); - -var _pegjs2 = _interopRequireDefault(_pegjs); - -var _fs = require('fs'); - -var _fs2 = _interopRequireDefault(_fs); - -var _utils = require('./utils'); - -var _utils2 = _interopRequireDefault(_utils); - -var _common = require('./reply/common'); - -var _common2 = _interopRequireDefault(_common); - -var _regexes = require('./regexes'); - -var _regexes2 = _interopRequireDefault(_regexes); - -var _wordnet = require('./reply/wordnet'); - -var _wordnet2 = _interopRequireDefault(_wordnet); - -var _inlineRedirect = require('./reply/inlineRedirect'); - -var _inlineRedirect2 = _interopRequireDefault(_inlineRedirect); - -var _topicRedirect = require('./reply/topicRedirect'); - -var _topicRedirect2 = _interopRequireDefault(_topicRedirect); - -var _respond = require('./reply/respond'); - -var _respond2 = _interopRequireDefault(_respond); - -var _customFunction = require('./reply/customFunction'); - -var _customFunction2 = _interopRequireDefault(_customFunction); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -// 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}` - */ - -var debug = (0, _debugLevels2.default)('SS:ProcessTags'); - -var grammar = _fs2.default.readFileSync(__dirname + '/reply/reply-grammar.pegjs', 'utf-8'); -// Change trace to true to debug peg -var parser = _pegjs2.default.generate(grammar, { trace: false }); - -var captureGrammar = _fs2.default.readFileSync(__dirname + '/reply/capture-grammar.pegjs', 'utf-8'); -// Change trace to true to debug peg -var captureParser = _pegjs2.default.generate(captureGrammar, { trace: false }); - -/* topicRedirect -/ respond -/ redirect -/ customFunction -/ newTopic -/ capture -/ previousCapture -/ clearConversation -/ continueSearching -/ endSearching -/ previousInput -/ previousReply -/ wordnetLookup -/ alternates -/ delay -/ setState -/ string*/ - -var processCapture = function processCapture(tag, replyObj, options) { - var starID = (tag.starID || 1) - 1; - debug.verbose('Processing capture: '); - var replacedCapture = starID < replyObj.stars.length ? replyObj.stars[starID] : ''; - debug.verbose('Replacing with "' + replacedCapture + '"'); - return replacedCapture; -}; - -var processPreviousCapture = function processPreviousCapture(tag, replyObj, options) { - // 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. - // TODO: handle captures within captures, but only 1 level deep - var starID = (tag.starID || 1) - 1; - var conversationID = (tag.conversationID || 1) - 1; - debug.verbose('Processing previous capture: '); - var replacedCapture = ''; - - if (options.user.history.stars[conversationID] && options.user.history.stars[conversationID][starID]) { - replacedCapture = options.user.history.stars[conversationID][starID]; - debug.verbose('Replacing with "' + replacedCapture + '"'); - } else { - debug.verbose('Attempted to use previous capture data, but none was found in user history.'); - } - return replacedCapture; -}; - -var processPreviousInput = function processPreviousInput(tag, replyObj, options) { - if (tag.inputID === null) { - debug.verbose('Processing previous input '); - // This means instead of , etc. so give the current input back - var _replacedInput = options.message.clean; - return _replacedInput; - } - - var inputID = (tag.inputID || 1) - 1; - debug.verbose('Processing previous input '); - var replacedInput = ''; - if (!options.user.history.input) { - // Nothing yet in the history - replacedInput = ''; - } else { - replacedInput = options.user.history.input[inputID].clean; - } - debug.verbose('Replacing with "' + replacedInput + '"'); - return replacedInput; -}; - -var processPreviousReply = function processPreviousReply(tag, replyObj, options) { - var replyID = (tag.replyID || 1) - 1; - debug.verbose('Processing previous reply '); - var replacedReply = ''; - if (!options.user.history.reply) { - // Nothing yet in the history - replacedReply = ''; - } else { - replacedReply = options.user.history.reply[replyID]; - } - debug.verbose('Replacing with "' + replacedReply + '"'); - return replacedReply; -}; - -var processCaptures = function processCaptures(tag, replyObj, options) { - switch (tag.type) { - case 'capture': - { - return processCapture(tag, replyObj, options); - } - case 'previousCapture': - { - return processPreviousCapture(tag, replyObj, options); - } - case 'previousInput': - { - return processPreviousInput(tag, replyObj, options); - } - case 'previousReply': - { - return processPreviousReply(tag, replyObj, options); - } - default: - { - console.error('Capture tag type does not exist: ' + tag.type); - return ''; - } - } -}; - -var preprocess = function preprocess(reply, replyObj, options) { - var captureTags = captureParser.parse(reply); - var cleanedReply = captureTags.map(function (tag) { - // Don't do anything to non-captures - if (typeof tag === 'string') { - return tag; - } - // It's a capture e.g. , so replace it with the captured star in replyObj.stars - return processCaptures(tag, replyObj, options); - }); - cleanedReply = cleanedReply.join(''); - return cleanedReply; -}; - -var postAugment = function postAugment(replyObject, tag, callback) { - return function (err, augmentedReplyObject) { - 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, ''); - } - - replyObject.continueMatching = augmentedReplyObject.continueMatching; - replyObject.clearConversation = augmentedReplyObject.clearConversation; - replyObject.topic = augmentedReplyObject.topicName; - replyObject.props = _lodash2.default.merge(replyObject.props, augmentedReplyObject.props); - - // Keep track of all the ids of all the triggers we go through via redirects - if (augmentedReplyObject.replyIds) { - augmentedReplyObject.replyIds.forEach(function (replyId) { - replyObject.replyIds.push(replyId); - }); - } - - if (augmentedReplyObject.subReplies) { - if (replyObject.subReplies) { - replyObject.subReplies = replyObject.subReplies.concat(augmentedReplyObject.subReplies); - } else { - replyObject.subReplies = augmentedReplyObject.subReplies; - } - } - - replyObject.minMatchSet = augmentedReplyObject.minMatchSet; - return callback(null, augmentedReplyObject.string); - }; -}; - -var processTopicRedirect = function processTopicRedirect(tag, replyObj, options, callback) { - debug.verbose('Processing topic redirect ^topicRedirect(' + tag.topicName + ',' + tag.topicTrigger + ')'); - options.depth = options.depth + 1; - (0, _topicRedirect2.default)(tag.topicName, tag.topicTrigger, options, postAugment(replyObj, tag, callback)); -}; - -var processRespond = function processRespond(tag, replyObj, options, callback) { - debug.verbose('Processing respond: ^respond(' + tag.topicName + ')'); - options.depth = options.depth + 1; - (0, _respond2.default)(tag.topicName, options, postAugment(replyObj, tag, callback)); -}; - -var processRedirect = function processRedirect(tag, replyObj, options, callback) { - debug.verbose('Processing inline redirect: {@' + tag.trigger + '}'); - options.depth = options.depth + 1; - (0, _inlineRedirect2.default)(tag.trigger, options, postAugment(replyObj, tag, callback)); -}; - -var processCustomFunction = function processCustomFunction(tag, replyObj, options, callback) { - if (tag.args === null) { - debug.verbose('Processing custom function: ^' + tag.functionName + '()'); - return (0, _customFunction2.default)(tag.functionName, [], replyObj, options, callback); - } - - // If there's a wordnet lookup as a parameter, expand it first - return _async2.default.map(tag.functionArgs, function (arg, next) { - if (typeof arg === 'string') { - return next(null, arg); - } - return processWordnetLookup(arg, replyObj, options, next); - }, function (err, args) { - if (err) { - console.error(err); - } - debug.verbose('Processing custom function: ^' + tag.functionName + '(' + args.join(', ') + ')'); - return (0, _customFunction2.default)(tag.functionName, args, replyObj, options, callback); - }); -}; - -var processNewTopic = function processNewTopic(tag, replyObj, options, callback) { - debug.verbose('Processing new topic: ' + tag.topicName); - var newTopic = tag.topicName; - options.user.setTopic(newTopic, function () { - return callback(null, ''); - }); -}; - -var processClearConversation = function processClearConversation(tag, replyObj, options, callback) { - debug.verbose('Processing clear conversation: setting clear conversation to true'); - replyObj.clearConversation = true; - callback(null, ''); -}; - -var processContinueSearching = function processContinueSearching(tag, replyObj, options, callback) { - debug.verbose('Processing continue searching: setting continueMatching to true'); - replyObj.continueMatching = true; - callback(null, ''); -}; - -var processEndSearching = function processEndSearching(tag, replyObj, options, callback) { - debug.verbose('Processing end searching: setting continueMatching to false'); - replyObj.continueMatching = false; - callback(null, ''); -}; - -var processWordnetLookup = function processWordnetLookup(tag, replyObj, options, callback) { - debug.verbose('Processing wordnet lookup for word: ~' + tag.term); - _wordnet2.default.lookup(tag.term, '~', function (err, words) { - if (err) { - console.error(err); - } - - words = words.map(function (item) { - return item.replace(/_/g, ' '); - }); - debug.verbose('Terms found in wordnet: ' + words); - - var replacedWordnet = _utils2.default.pickItem(words); - debug.verbose('Wordnet replaced term: ' + replacedWordnet); - callback(null, replacedWordnet); - }); -}; - -var processAlternates = function processAlternates(tag, replyObj, options, callback) { - debug.verbose('Processing alternates: ' + tag.alternates); - var alternates = tag.alternates; - var random = _utils2.default.getRandomInt(0, alternates.length - 1); - var result = alternates[random]; - callback(null, result); -}; - -var processDelay = function processDelay(tag, replyObj, options, callback) { - callback(null, '{delay=' + tag.delayLength + '}'); -}; - -var processSetState = function processSetState(tag, replyObj, options, callback) { - debug.verbose('Processing setState: ' + JSON.stringify(tag.stateToSet)); - var stateToSet = tag.stateToSet; - var newState = {}; - stateToSet.forEach(function (keyValuePair) { - var key = keyValuePair.key; - var value = keyValuePair.value; - - // Value is a string - value = value.replace(/["']/g, ''); - - // Value is an integer - if (/^[\d]+$/.test(value)) { - value = +value; - } - - // Value is a boolean - if (value === 'true') { - value = true; - } else if (value === 'false') { - value = false; - } - - newState[key] = value; - }); - debug.verbose('New state: ' + JSON.stringify(newState)); - options.user.conversationState = _lodash2.default.merge(options.user.conversationState, newState); - options.user.markModified('conversationState'); - callback(null, ''); -}; - -var processTag = function processTag(tag, replyObj, options, next) { - if (typeof tag === 'string') { - next(null, tag); - } else { - var tagType = tag.type; - switch (tagType) { - case 'topicRedirect': - { - processTopicRedirect(tag, replyObj, options, next); - break; - } - case 'respond': - { - processRespond(tag, replyObj, options, next); - break; - } - case 'customFunction': - { - processCustomFunction(tag, replyObj, options, next); - break; - } - case 'newTopic': - { - processNewTopic(tag, replyObj, options, next); - break; - } - case 'clearConversation': - { - processClearConversation(tag, replyObj, options, next); - break; - } - case 'continueSearching': - { - processContinueSearching(tag, replyObj, options, next); - break; - } - case 'endSearching': - { - processEndSearching(tag, replyObj, options, next); - break; - } - case 'wordnetLookup': - { - processWordnetLookup(tag, replyObj, options, next); - break; - } - case 'redirect': - { - processRedirect(tag, replyObj, options, next); - break; - } - case 'alternates': - { - processAlternates(tag, replyObj, options, next); - break; - } - case 'delay': - { - processDelay(tag, replyObj, options, next); - break; - } - case 'setState': - { - processSetState(tag, replyObj, options, next); - break; - } - default: - { - next('No such tag type: ' + tagType); - break; - } - } - } -}; - -var processReplyTags = function processReplyTags(replyObj, options, callback) { - debug.verbose('Depth: ', options.depth); - - var replyString = replyObj.reply.reply; - debug.info('Reply before processing reply tags: "' + replyString + '"'); - - options.topic = replyObj.topic; - - // Deals with captures as a preprocessing step (avoids tricksy logic having captures - // as function parameters) - var preprocessed = preprocess(replyString, replyObj, options); - var replyTags = parser.parse(preprocessed); - - replyObj.replyIds = [replyObj.reply._id]; - - _async2.default.mapSeries(replyTags, function (tag, next) { - if (typeof tag === 'string') { - next(null, tag); - } else { - processTag(tag, replyObj, options, next); - } - }, function (err, processedReplyParts) { - if (err) { - console.error('There was an error processing reply tags: ' + err); - } - - replyString = processedReplyParts.join('').trim(); - - replyObj.reply.reply = new _re2.default('\\\\s', 'g').replace(replyString, ' '); - - debug.verbose('Final reply object from processTags: ', replyObj); - - if (_lodash2.default.isEmpty(options.user.pendingTopic)) { - return options.user.setTopic(replyObj.topic, function () { - return callback(err, replyObj); - }); - } - - return callback(err, replyObj); - }); -}; - -var processThreadTags = function processThreadTags(string) { - var threads = []; - var strings = []; - string.split('\n').forEach(function (line) { - var match = _regexes2.default.delay.match(line); - if (match) { - threads.push({ delay: match[1], string: _utils2.default.trim(line.replace(match[0], '')) }); - } else { - strings.push(line); - } - }); - return [strings.join('\n'), threads]; -}; - -exports.default = { processThreadTags: processThreadTags, processReplyTags: processReplyTags }; \ No newline at end of file diff --git a/lib/bot/regexes.js b/lib/bot/regexes.js deleted file mode 100644 index 27023786..00000000 --- a/lib/bot/regexes.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _re = require('re2'); - -var _re2 = _interopRequireDefault(_re); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -// 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 - -exports.default = { - redirect: new _re2.default('\\{@(.+?)\\}'), - topic: new _re2.default('\\^topicRedirect\\(\\s*([~\\w<>\\s]*),([~\\w<>\\s]*)\\s*\\)'), - respond: new _re2.default('\\^respond\\(\\s*([\\w~]*)\\s*\\)'), - - customFn: new _re2.default('\\^(\\w+)\\(([\\w<>%,\\s\\-&()"\';:$]*)\\)'), - wordnet: new _re2.default('(~)(\\w[\\w]+)', 'g'), - state: new _re2.default('{([^}]*)}', 'g'), - - filter: new _re2.default('\\^(\\w+)\\(([\\w<>,\\s]*)\\)', 'i'), - - delay: new _re2.default('{\\s*delay\\s*=\\s*(\\d+)\\s*}'), - - clear: new _re2.default('{\\s*clear\\s*}', 'i'), - continue: new _re2.default('{\\s*continue\\s*}', 'i'), - end: new _re2.default('{\\s*end\\s*}', 'i'), - - capture: new _re2.default('', 'i'), - captures: new _re2.default('', 'ig'), - pcapture: new _re2.default('', 'i'), - pcaptures: new _re2.default('', 'ig'), - - comma: new _re2.default(','), - commas: new _re2.default(',', 'g'), - - space: { - inner: new _re2.default('[ \\t]+', 'g'), - leading: new _re2.default('^[ \\t]+'), - trailing: new _re2.default('[ \\t]+$'), - oneInner: new _re2.default('[ \\t]', 'g'), - oneLeading: new _re2.default('^[ \\t]'), - oneTrailing: new _re2.default('[ \\t]$') - }, - - whitespace: { - both: new _re2.default('(?:^\\s+)|(?:\\s+$)', 'g'), - leading: new _re2.default('^\s+'), - trailing: new _re2.default('\s+$'), - oneLeading: new _re2.default('^\s'), - oneTrailing: new _re2.default('\s$') - } -}; \ No newline at end of file diff --git a/lib/bot/reply/capture-grammar.pegjs b/lib/bot/reply/capture-grammar.pegjs deleted file mode 100644 index abd06f42..00000000 --- a/lib/bot/reply/capture-grammar.pegjs +++ /dev/null @@ -1,65 +0,0 @@ -start = captures - -capture - = "" - { - return { - type: "capture", - starID: starID - }; - } - -previousCapture - = "" - { - return { - type: "previousCapture", - starID, - conversationID - }; - } - -previousInput - = "" - { - return { - type: "previousInput", - inputID: inputID - } - } - -previousReply - = "" - { - return { - type: "previousReply", - replyID: replyID - } - } - -stringCharacter - = "\\" character:[<>] { return character; } - / character:[^<>] { return character; } - -string - = string:stringCharacter+ { return string.join(""); } - -captureType - = capture - / previousCapture - / previousInput - / previousReply - / string - -captures - = captureType* - -integer - = numbers:[0-9]+ - { return Number.parseInt(numbers.join("")); } - -ws "whitespace" - = [ \t] - -nl "newline" - = [\n\r] diff --git a/lib/bot/reply/common.js b/lib/bot/reply/common.js deleted file mode 100644 index 2660b749..00000000 --- a/lib/bot/reply/common.js +++ /dev/null @@ -1,156 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _utils = require('../utils'); - -var _utils2 = _interopRequireDefault(_utils); - -var _wordnet = require('./wordnet'); - -var _wordnet2 = _interopRequireDefault(_wordnet); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('SS:ProcessHelpers'); - -var getTopic = function getTopic(chatSystem, name, cb) { - if (name) { - chatSystem.Topic.findOne({ name: name }, function (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: 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. - */ -var topicSetter = function topicSetter(replyString) { - var TOPIC_REGEX = /\{topic=(.+?)\}/i; - var match = replyString.match(TOPIC_REGEX); - var depth = 0; - var 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=' + _utils2.default.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: replyString, newTopic: newTopic }; -}; - -var processAlternates = function processAlternates(reply) { - // Reply Alternates. - var match = reply.match(/\(\((.+?)\)\)/); - var 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 ''; - } - - var parts = match[1].split('|'); - var opts = []; - for (var i = 0; i < parts.length; i++) { - opts.push(parts[i].trim()); - } - - var resp = _utils2.default.getRandomInt(0, opts.length - 1); - reply = reply.replace(new RegExp('\\(\\(\\s*' + _utils2.default.quotemeta(match[1]) + '\\s*\\)\\)'), opts[resp]); - match = reply.match(/\(\((.+?)\)\)/); - } - - return reply; -}; - -// Handle WordNet in Replies -var wordnetReplace = function wordnetReplace(match, sym, word, p3, offset, done) { - _wordnet2.default.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 = _utils2.default.pickItem(words); - done(null, resp); - }); -}; - -var addStateData = function addStateData(data) { - 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; - - // 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; -}; - -exports.default = { - addStateData: addStateData, - getTopic: getTopic, - processAlternates: processAlternates, - topicSetter: topicSetter, - wordnetReplace: wordnetReplace -}; \ No newline at end of file diff --git a/lib/bot/reply/customFunction.js b/lib/bot/reply/customFunction.js deleted file mode 100644 index 1d6c3eaf..00000000 --- a/lib/bot/reply/customFunction.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('SS:Reply:customFunction'); - -var customFunction = function customFunction(functionName, functionArgs, replyObj, options, callback) { - var plugins = options.system.plugins; - // Important to create a new scope object otherwise we could leak data - var scope = _lodash2.default.merge({}, options.system.scope); - scope.extraScope = options.system.extraScope; - scope.message = options.message; - scope.user = options.user; - - if (plugins[functionName]) { - functionArgs.push(function (err, functionResponse, stopMatching) { - var reply = ''; - var props = {}; - if (err) { - console.error('Error in plugin function (' + functionName + '): ' + err); - return callback(err); - } - - if (_lodash2.default.isPlainObject(functionResponse)) { - if (functionResponse.text) { - reply = functionResponse.text; - delete functionResponse.text; - } - - if (functionResponse.reply) { - reply = functionResponse.reply; - delete functionResponse.reply; - } - - // There may be data, so merge it with the reply object - replyObj.props = _lodash2.default.merge(replyObj.props, functionResponse); - if (stopMatching !== undefined) { - replyObj.continueMatching = !stopMatching; - } - } else { - reply = functionResponse || ''; - if (stopMatching !== undefined) { - replyObj.continueMatching = !stopMatching; - } - } - - return callback(err, reply); - }); - - debug.verbose('Calling plugin function: ' + functionName); - plugins[functionName].apply(scope, functionArgs); - } else { - // If a function is missing, we kill the line and return empty handed - console.error('WARNING: Custom function (' + functionName + ') was not found. Your script may not behave as expected.'); - callback(true, ''); - } -}; - -exports.default = customFunction; \ No newline at end of file diff --git a/lib/bot/reply/inlineRedirect.js b/lib/bot/reply/inlineRedirect.js deleted file mode 100644 index 69b29ede..00000000 --- a/lib/bot/reply/inlineRedirect.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _message = require('../message'); - -var _message2 = _interopRequireDefault(_message); - -var _common = require('./common'); - -var _common2 = _interopRequireDefault(_common); - -var _getReply = require('../getReply'); - -var _getReply2 = _interopRequireDefault(_getReply); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('SS:Reply:inline'); - -var inlineRedirect = function inlineRedirect(triggerTarget, options, callback) { - debug.verbose('Inline redirection to: \'' + triggerTarget + '\''); - - // 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]; - } - } - - _common2.default.getTopic(options.system.chatSystem, options.topic, function (err, topicData) { - var messageOptions = { - factSystem: options.system.factSystem - }; - - _message2.default.createMessage(triggerTarget, messageOptions, function (redirectMessage) { - options.pendingTopics = [topicData]; - - (0, _getReply2.default)(redirectMessage, options, function (err, redirectReply) { - if (err) { - console.error(err); - } - - debug.verbose('Response from inlineRedirect: ', redirectReply); - if (redirectReply) { - return callback(null, redirectReply); - } - return callback(null, {}); - }); - }); - }); -}; - -exports.default = inlineRedirect; \ No newline at end of file diff --git a/lib/bot/reply/reply-grammar.pegjs b/lib/bot/reply/reply-grammar.pegjs deleted file mode 100644 index 63052cc3..00000000 --- a/lib/bot/reply/reply-grammar.pegjs +++ /dev/null @@ -1,184 +0,0 @@ -start = reply - -functionArg - = arg:[^),]+ { return arg.join(""); } - -topicRedirect - = "^topicRedirect(" ws* topicName:functionArg ws* "," ws* topicTrigger:functionArg ")" - { - return { - type: "topicRedirect", - topicName, - topicTrigger - } - } - -respond - = "^respond(" ws* topicName:functionArg ws* ")" - { - return { - type: "respond", - topicName: topicName - } - } - -redirect - = "{@" ws* trigger:[^}]+ ws* "}" - { - return { - type: "redirect", - trigger: trigger.join("") - } - } - -customFunctionArg - = ws* "[" arrayContents:[^\]]* "]" ws* - { return `[${arrayContents.join("") || ''}]`; } - / ws* "{" objectContents:[^}]* "}" ws* - { return `{${objectContents.join("") || ''}}`; } - / ws* wordnetLookup:wordnetLookup ws* - { return wordnetLookup; } - / ws* string:[^,)]+ ws* - { return string.join(""); } - -customFunctionArgs - = argFirst:customFunctionArg args:("," arg:customFunctionArg { return arg; })* - { return [argFirst].concat(args); } - -customFunction - = "^" !"topicRedirect" !"respond" name:[A-Za-z0-9_]+ "(" args:customFunctionArgs? ")" - { - return { - type: "customFunction", - functionName: name.join(""), - functionArgs: args - }; - } - -newTopic - = "{" ws* "topic" ws* "=" ws* topicName:[A-Za-z0-9~_]* ws* "}" - { - return { - type: "newTopic", - topicName: topicName.join("") - }; - } - -clearString - = "clear" - / "CLEAR" - -clearConversation - = "{" ws* clearString ws* "}" - { - return { - type: "clearConversation" - } - } - -continueString - = "continue" - / "CONTINUE" - -continueSearching - = "{" ws* continueString ws* "}" - { - return { - type: "continueSearching" - } - } - -endString - = "end" - / "END" - -endSearching - = "{" ws* endString ws* "}" - { - return { - type: "endSearching" - } - } - -wordnetLookup - = "~" term:[A-Za-z0-9_]+ - { - return { - type: "wordnetLookup", - term: term.join("") - } - } - -alternates - = "((" alternateFirst:[^|]+ alternates:("|" alternate:[^|)]+ { return alternate.join(""); })+ "))" - { - return { - type: "alternates", - alternates: [alternateFirst.join("")].concat(alternates) - } - } - -delay - = "{" ws* "delay" ws* "=" ws* delayLength:integer "}" - { - return { - type: "delay", - delayLength - } - } - -keyValuePair - = ws* key:[A-Za-z0-9_]+ ws* "=" ws* value:[A-Za-z0-9_'"]+ ws* - { - return { - key: key.join(""), - value: value.join("") - } - } - -setState - = "{" keyValuePairFirst:keyValuePair keyValuePairs:("," keyValuePair:keyValuePair { return keyValuePair; })* "}" - { - return { - type: "setState", - stateToSet: [keyValuePairFirst].concat(keyValuePairs) - } - } - -stringCharacter - = !"((" "\\" character:[n] { return `\n`; } - / !"((" "\\" character:[s] { return `\\s`; } - / !"((" "\\" character:. { return character; } - / !"((" character:[^^{<~] { return character; } - -string - = string:stringCharacter+ { return string.join(""); } - -replyToken - = topicRedirect - / respond - / redirect - / customFunction - / newTopic - / clearConversation - / continueSearching - / endSearching - / wordnetLookup - / alternates - / delay - / setState - / string - -reply - = tokens:replyToken* - { return tokens; } - -integer - = numbers:[0-9]+ - { return Number.parseInt(numbers.join("")); } - -ws "whitespace" - = [ \t] - -nl "newline" - = [\n\r] diff --git a/lib/bot/reply/respond.js b/lib/bot/reply/respond.js deleted file mode 100644 index 91feac95..00000000 --- a/lib/bot/reply/respond.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _common = require('./common'); - -var _common2 = _interopRequireDefault(_common); - -var _getReply = require('../getReply'); - -var _getReply2 = _interopRequireDefault(_getReply); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('SS:Reply:Respond'); - -var respond = function respond(topicName, options, callback) { - debug.verbose('Responding to topic: ' + topicName); - - _common2.default.getTopic(options.system.chatSystem, topicName, function (err, topicData) { - if (err) { - console.error(err); - } - - options.pendingTopics = [topicData]; - - (0, _getReply2.default)(options.message, options, function (err, respondReply) { - if (err) { - console.error(err); - } - - debug.verbose('Callback from respond getReply: ', respondReply); - - if (respondReply) { - return callback(err, respondReply); - } - return callback(err, {}); - }); - }); -}; - -exports.default = respond; \ No newline at end of file diff --git a/lib/bot/reply/topicRedirect.js b/lib/bot/reply/topicRedirect.js deleted file mode 100644 index d5b1a9ca..00000000 --- a/lib/bot/reply/topicRedirect.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _common = require('./common'); - -var _common2 = _interopRequireDefault(_common); - -var _message = require('../message'); - -var _message2 = _interopRequireDefault(_message); - -var _getReply = require('../getReply'); - -var _getReply2 = _interopRequireDefault(_getReply); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('SS:Reply:topicRedirect'); - -var topicRedirect = function topicRedirect(topicName, topicTrigger, options, callback) { - debug.verbose('Topic redirection to topic: ' + topicName + ', trigger: ' + topicTrigger); - - // Here we are looking for gambits in the NEW topic. - _common2.default.getTopic(options.system.chatSystem, topicName, function (err, topicData) { - if (err) { - console.error(err); - return callback(null, {}); - } - - var messageOptions = { - facts: options.system.factSystem - }; - - _message2.default.createMessage(topicTrigger, messageOptions, function (redirectMessage) { - options.pendingTopics = [topicData]; - - (0, _getReply2.default)(redirectMessage, options, function (err, redirectReply) { - if (err) { - console.error(err); - } - - debug.verbose('redirectReply', redirectReply); - if (redirectReply) { - return callback(null, redirectReply); - } - return callback(null, {}); - }); - }); - }); -}; - -exports.default = topicRedirect; \ No newline at end of file diff --git a/lib/bot/reply/wordnet.js b/lib/bot/reply/wordnet.js deleted file mode 100644 index 6c690c17..00000000 --- a/lib/bot/reply/wordnet.js +++ /dev/null @@ -1,121 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _natural = require('natural'); - -var _natural2 = _interopRequireDefault(_natural); - -var _async = require('async'); - -var _async2 = _interopRequireDefault(_async); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var wordnet = new _natural2.default.WordNet(); // This is a shim for wordnet lookup. -// http://wordnet.princeton.edu/wordnet/man/wninput.5WN.html - -var define = function define(word, cb) { - wordnet.lookup(word, function (results) { - if (!_lodash2.default.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 -var lookup = function lookup(word) { - var pointerSymbol = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '~'; - var cb = arguments[2]; - - var pos = null; - - var match = word.match(/~(\w)$/); - if (match) { - pos = match[1]; - word = word.replace(match[0], ''); - } - - 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); - } - }); - }); - - var itor = function itor(word, next) { - wordnet.get(word.synsetOffset, word.pos, function (sub) { - next(null, sub.lemma); - }); - }; - - _async2.default.map(synets, itor, function (err, items) { - items = _lodash2.default.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 -var explore = function explore(word, cb) { - var ptrs = []; - - wordnet.lookup(word, function (results) { - for (var i = 0; i < results.length; i++) { - ptrs.push(results[i].ptrs); - } - - ptrs = _lodash2.default.uniq(_lodash2.default.flatten(ptrs)); - ptrs = _lodash2.default.map(ptrs, function (item) { - return { pos: item.pos, sym: item.pointerSymbol }; - }); - - ptrs = _lodash2.default.chain(ptrs).groupBy('pos').map(function (value, key) { - return { - pos: key, - ptr: _lodash2.default.uniq(_lodash2.default.map(value, 'sym')) - }; - }).value(); - - var itor = function itor(item, next) { - var itor2 = function itor2(ptr, next2) { - lookup(word + '~' + item.pos, ptr, function (err, res) { - if (err) { - console.error(err); - } - console.log(word, item.pos, ':', ptr, res.join(', ')); - next2(); - }); - }; - _async2.default.map(item.ptr, itor2, next); - }; - _async2.default.each(ptrs, itor, function () { - return cb(); - }); - }); -}; - -exports.default = { - define: define, - explore: explore, - lookup: lookup -}; \ No newline at end of file diff --git a/lib/bot/utils.js b/lib/bot/utils.js deleted file mode 100644 index 955a9dc7..00000000 --- a/lib/bot/utils.js +++ /dev/null @@ -1,306 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _fs = require('fs'); - -var _fs2 = _interopRequireDefault(_fs); - -var _string = require('string'); - -var _string2 = _interopRequireDefault(_string); - -var _debugLevels = require('debug-levels'); - -var _debugLevels2 = _interopRequireDefault(_debugLevels); - -var _partsOfSpeech = require('parts-of-speech'); - -var _partsOfSpeech2 = _interopRequireDefault(_partsOfSpeech); - -var _re = require('re2'); - -var _re2 = _interopRequireDefault(_re); - -var _regexes = require('./regexes'); - -var _regexes2 = _interopRequireDefault(_regexes); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debugLevels2.default)('SS:Utils'); -var Lex = _partsOfSpeech2.default.Lexer; - -//-------------------------- - -var encodeCommas = function encodeCommas(s) { - return s ? _regexes2.default.commas.replace(s, '') : s; -}; - -var encodedCommasRE = new _re2.default('', 'g'); -var decodeCommas = function decodeCommas(s) { - return 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 - */ -var trim = function trim() { - var text = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - return _regexes2.default.space.inner.replace(_regexes2.default.whitespace.both.replace(text, ''), ' '); -}; - -var wordSepRE = new _re2.default('[\\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` - */ -var wordCount = function wordCount(text) { - return wordSepRE.split(text).filter(function (w) { - return 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 -var inArray = function inArray(list, value) { - if (_lodash2.default.isArray(value)) { - var match = false; - for (var i = 0; i < value.length; i++) { - if (_lodash2.default.includes(list, value[i]) > 0) { - match = _lodash2.default.indexOf(list, value[i]); - } - } - return match; - } else { - return _lodash2.default.indexOf(list, value); - } -}; - -var sentenceSplit = function sentenceSplit(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 && _lodash2.default.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; -}; - -var commandsRE = new _re2.default('[\\\\.+?${}=!:]', 'g'); -var nonCommandsRE = new _re2.default('[\\\\.+*?\\[^\\]$(){}=!<>|:]', 'g'); -/** - * Escape a string sp that it can be used in a regular expression. - * @param {string} string - the string to escape - * @param {boolean} commands - - */ -var quotemeta = function quotemeta(string) { - var commands = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - return (commands ? commandsRE : nonCommandsRE).replace(string, function (c) { - return '\\' + c; - }); -}; - -var cleanArray = function cleanArray(actual) { - var newArray = []; - for (var i = 0; i < actual.length; i++) { - if (actual[i]) { - newArray.push(actual[i]); - } - } - return newArray; -}; - -var aRE = new _re2.default('^(([bcdgjkpqtuvwyz]|onc?e|onetime)$|e[uw]|uk|ur[aeiou]|use|ut([^t])|uni(l[^l]|[a-ko-z]))', 'i'); -var anRE = new _re2.default('^([aefhilmnorsx]$|hono|honest|hour|heir|[aeiou])', 'i'); -var upcaseARE = new _re2.default('^(UN$)'); -var upcaseANRE = new _re2.default('^$'); -var dashSpaceRE = new _re2.default('[- ]'); -var indefiniteArticlerize = function indefiniteArticlerize(word) { - var first = dashSpaceRE.split(word, 2)[0]; - var prefix = (anRE.test(first) || upcaseARE.test(first)) && !(aRE.test(first) || upcaseANRE.test(first)) ? 'an' : 'a'; - return prefix + ' ' + word; -}; - -var indefiniteList = function indefiniteList(list) { - var n = list.map(indefiniteArticlerize); - if (n.length > 1) { - var last = n.pop(); - return n.join(', ') + ' and ' + last; - } else { - return n.join(', '); - } -}; - -var getRandomInt = function getRandomInt(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; -}; - -var underscoresRE = new _re2.default('_', 'g'); -var pickItem = function pickItem(arr) { - // TODO - Item may have a wornet suffix meal~2 or meal~n - var ind = getRandomInt(0, arr.length - 1); - return _lodash2.default.isString(arr[ind]) ? underscoresRE.replace(arr[ind], ' ') : arr[ind]; -}; - -// Capital first letter, and add period. -var makeSentense = function makeSentense(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'] -}; - -var isTag = function isTag(posTag, wordClass) { - return !!(tags[wordClass].indexOf(posTag) > -1); -}; - -var genId = function genId() { - 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 - */ -var replaceCapturedText = function replaceCapturedText(strings, caps) { - var encoded = caps.map(function (s) { - return encodeCommas(s); - }); - return strings.filter(function (s) { - return !_lodash2.default.isEmpty(s); - }).map(function (s) { - return _regexes2.default.captures.replace(s, function (m, p1) { - return encoded[Number.parseInt(p1 || 1)]; - }); - }); -}; - -var walk = function walk(dir, done) { - if (_fs2.default.statSync(dir).isFile()) { - debug.verbose('Expected directory, found file, simulating directory with only one file: %s', dir); - return done(null, [dir]); - } - - var results = []; - _fs2.default.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; - _fs2.default.stat(file, function (err2, stat) { - if (err2) { - console.log(err2); - } - - if (stat && stat.isDirectory()) { - var 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); - } - } - }); - }); - }); -}; - -var pennToWordnet = function pennToWordnet(pennTag) { - if ((0, _string2.default)(pennTag).startsWith('J')) { - return 'a'; - } else if ((0, _string2.default)(pennTag).startsWith('V')) { - return 'v'; - } else if ((0, _string2.default)(pennTag).startsWith('N')) { - return 'n'; - } else if ((0, _string2.default)(pennTag).startsWith('R')) { - return 'r'; - } else { - return null; - } -}; - -exports.default = { - cleanArray: cleanArray, - encodeCommas: encodeCommas, - decodeCommas: decodeCommas, - genId: genId, - getRandomInt: getRandomInt, - inArray: inArray, - indefiniteArticlerize: indefiniteArticlerize, - indefiniteList: indefiniteList, - isTag: isTag, - makeSentense: makeSentense, - pennToWordnet: pennToWordnet, - pickItem: pickItem, - quotemeta: quotemeta, - replaceCapturedText: replaceCapturedText, - sentenceSplit: sentenceSplit, - trim: trim, - walk: walk, - wordCount: wordCount -}; \ No newline at end of file diff --git a/lib/plugins/alpha.js b/lib/plugins/alpha.js deleted file mode 100644 index 08733993..00000000 --- a/lib/plugins/alpha.js +++ /dev/null @@ -1,166 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _rhymes = require('rhymes'); - -var _rhymes2 = _interopRequireDefault(_rhymes); - -var _syllablistic = require('syllablistic'); - -var _syllablistic2 = _interopRequireDefault(_syllablistic); - -var _debug = require('debug'); - -var _debug2 = _interopRequireDefault(_debug); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debug2.default)('AlphaPlugins'); - -var getRandomInt = function getRandomInt(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; -}; - -// TODO: deprecate oppisite and replace with opposite -var oppisite = function oppisite(word, cb) { - debug('oppisite', word); - - this.facts.db.get({ subject: word, predicate: 'opposite' }, function (err, opp) { - if (!_lodash2.default.isEmpty(opp)) { - var oppositeWord = opp[0].object; - oppositeWord = oppositeWord.replace(/_/g, ' '); - cb(null, oppositeWord); - } else { - cb(null, ''); - } - }); -}; - -var rhymes = function rhymes(word, cb) { - debug('rhyming', word); - - var rhymedWords = (0, _rhymes2.default)(word); - var i = getRandomInt(0, rhymedWords.length - 1); - - if (rhymedWords.length !== 0) { - cb(null, rhymedWords[i].word.toLowerCase()); - } else { - cb(null, null); - } -}; - -var syllable = function syllable(word, cb) { - return cb(null, _syllablistic2.default.text(word)); -}; - -var letterLookup = function letterLookup(cb) { - 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); -}; - -var wordLength = function wordLength(cap, cb) { - var _this = this; - - 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) { - (function () { - // 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, ''); - } -}; - -var nextNumber = function nextNumber(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); -}; - -exports.default = { - letterLookup: letterLookup, - nextNumber: nextNumber, - oppisite: oppisite, - rhymes: rhymes, - syllable: syllable, - wordLength: wordLength -}; \ No newline at end of file diff --git a/lib/plugins/compare.js b/lib/plugins/compare.js deleted file mode 100644 index 7e454d96..00000000 --- a/lib/plugins/compare.js +++ /dev/null @@ -1,270 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _debug = require('debug'); - -var _debug2 = _interopRequireDefault(_debug); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _async = require('async'); - -var _async2 = _interopRequireDefault(_async); - -var _history = require('../bot/history'); - -var _history2 = _interopRequireDefault(_history); - -var _utils = require('../bot/utils'); - -var _utils2 = _interopRequireDefault(_utils); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debug2.default)('Compare Plugin'); - -var createFact = function createFact(s, v, o, cb) { - var _this = this; - - this.user.memory.create(s, v, o, false, function () { - _this.facts.db.get({ subject: v, predicate: 'opposite' }, function (e, r) { - if (r.length !== 0) { - _this.user.memory.create(o, r[0].object, s, false, function () { - cb(null, ''); - }); - } else { - cb(null, ''); - } - }); - }); -}; - -var findOne = function findOne(haystack, arr) { - return arr.some(function (v) { - return haystack.indexOf(v) >= 0; - }); -}; - -var resolveAdjective = function resolveAdjective(cb) { - var candidates = (0, _history2.default)(this.user, { names: true }); - var message = this.message; - var userFacts = this.user.memory.db; - var botFacts = this.facts.db; - - var getOpp = function getOpp(term, callback) { - botFacts.search({ - subject: term, - predicate: 'opposite', - object: botFacts.v('opp') - }, function (e, oppResult) { - if (!_lodash2.default.isEmpty(oppResult)) { - callback(null, oppResult[0].opp); - } else { - callback(null, null); - } - }); - }; - - var negatedTerm = function negatedTerm(msg, names, cb) { - // Are we confused about what we are looking for??! - // Could be "least tall" negated terms - if (_lodash2.default.contains(msg.adjectives, 'least') && msg.adjectives.length === 2) { - // We need to flip the adjective to the oppisite and do a lookup. - var cmpWord = _lodash2.default.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) { - (function () { - var pn1 = names[0].toLowerCase(); - var pn2 = names[1].toLowerCase(); - - userFacts.get({ subject: pn1, predicate: oppWord, object: pn2 }, function (e, r) { - // r is treated as 'truthy' - if (!_lodash2.default.isEmpty(r)) { - cb(null, _lodash2.default.capitalize(pn1) + ' is ' + oppWord + 'er.'); - } else { - cb(null, _lodash2.default.capitalize(pn2) + ' is ' + oppWord + 'er.'); - } - }); - })(); - } else { - cb(null, _lodash2.default.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 - var baseWord = null; - var getAdjective = function getAdjective(m, cb) { - var cmpWord = void 0; - - if (findOne(m.adjectives, ['least', 'less'])) { - cmpWord = _lodash2.default.first(_lodash2.default.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) { - (function () { - 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 handle(e, cmpTerms) { - var compareWord = cmpTerms[0]; - var compareWord2 = cmpTerms[1]; - - debug('CMP ', compareWord, compareWord2); - - botFacts.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 (!_lodash2.default.isEmpty(message.names)) { - (function () { - debug('We have names', message.names); - // Make sure we say a name they are looking for. - var nameOne = message.names[0].toLowerCase(); - - userFacts.get({ subject: nameOne, predicate: compareWord }, function (e, result) { - if (_lodash2.default.isEmpty(result)) { - // So the fact is wrong, lets try the other way round - - userFacts.get({ object: nameOne, predicate: compareWord }, function (e, result) { - debug('RES', result); - - if (!_lodash2.default.isEmpty(result)) { - if (message.names.length === 2 && result[0].subject === message.names[1]) { - cb(null, _lodash2.default.capitalize(result[0].subject) + ' is ' + compareWord + 'er than ' + _lodash2.default.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, _lodash2.default.capitalize(message.names[1]) + ' is ' + compareWord + 'er than ' + _lodash2.default.capitalize(result[0].object) + '.'); - } else { - cb(null, _utils2.default.pickItem(message.names) + ' is ' + compareWord + 'er?'); - } - } else { - // Lets do it again if we have another name - cb(null, _utils2.default.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') }], function (err, results) { - if (!_lodash2.default.isEmpty(results)) { - if (results[0].v === message.names[1].toLowerCase()) { - cb(null, _lodash2.default.capitalize(message.names[0]) + ' is ' + compareWord + 'er than ' + _lodash2.default.capitalize(message.names[1]) + '.'); - } else { - // Test this - cb(null, _lodash2.default.capitalize(message.names[1]) + ' is ' + compareWord + 'er than ' + _lodash2.default.capitalize(message.names[0]) + '.'); - } - } else { - // Test this block - cb(null, _utils2.default.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) { - userFacts.search([{ subject: userFacts.v('oldest'), - predicate: compareWord, - object: userFacts.v('rand1') }, { subject: userFacts.v('oldest'), - predicate: compareWord, - object: userFacts.v('rand2') }], function (err, results) { - if (!_lodash2.default.isEmpty(results)) { - cb(null, _lodash2.default.capitalize(results[0].oldest) + ' is the ' + compareWord + 'est.'); - } else { - // Pick one. - cb(null, _lodash2.default.capitalize(_utils2.default.pickItem(prevMessage.names)) + ' is the ' + compareWord + 'est.'); - } - }); - } else { - if (!_lodash2.default.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 }, function (e, result) { - if (!_lodash2.default.isEmpty(result)) { - if (message.qSubType === 'YN') { - cb(null, 'Yes, ' + _lodash2.default.capitalize(result[0].object) + ' is ' + compareWord + 'er.'); - } else { - cb(null, _lodash2.default.capitalize(result[0].object) + ' is ' + compareWord + 'er than ' + prevMessage.names[0] + '.'); - } - } else { - if (message.qSubType === 'YN') { - cb(null, 'Yes, ' + _lodash2.default.capitalize(prevMessage.names[1]) + ' is ' + compareWord + 'er.'); - } else { - cb(null, _lodash2.default.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, ' + _lodash2.default.capitalize(prevMessage.names[0]) + ' is ' + compareWord + 'er.'); - } else { - cb(null, _lodash2.default.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."); - } - } - } - }); - }; - - _async2.default.map([message, prevMessage], getAdjective, handle); - } else { - negatedTerm(message, prevMessage.names, cb); - } - } - })(); - } else { - cb(null, '??'); - } -}; - -exports.default = { - createFact: createFact, - resolveAdjective: resolveAdjective -}; \ No newline at end of file diff --git a/lib/plugins/math.js b/lib/plugins/math.js deleted file mode 100644 index fbf01902..00000000 --- a/lib/plugins/math.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -/* - Math functions for - - evaluating expressions - - converting functions - - sequence functions -*/ - -var math = require('../bot/math'); -var roman = require('roman-numerals'); -var debug = require('debug')('mathPlugin'); - -var evaluateExpression = function evaluateExpression(cb) { - if (this.message.numericExp || this.message.halfNumericExp && this.user.prevAns) { - var answer = math.parse(this.message.cwords, this.user.prevAns); - var suggestedReply = void 0; - 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, ''); - } -}; - -var numToRoman = function numToRoman(cb) { - var suggest = 'I think it is ' + roman.toRoman(this.message.numbers[0]); - cb(null, suggest); -}; - -var numToHex = function numToHex(cb) { - var suggest = 'I think it is ' + parseInt(this.message.numbers[0], 10).toString(16); - cb(null, suggest); -}; - -var numToBinary = function numToBinary(cb) { - var suggest = 'I think it is ' + parseInt(this.message.numbers[0], 10).toString(2); - cb(null, suggest); -}; - -var numMissing = function numMissing(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 += 1; - } - } - } - var s = mia.sort(function (a, b) { - return a - b; - }); - cb(null, 'I think it is ' + s.join(' ')); - } else { - cb(true, ''); - } -}; - -// Sequence -var numSequence = function numSequence(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; - }); - - var suggest = void 0; - if (math.arithGeo(numArray) === 'Arithmetic') { - var x = void 0; - for (var 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') { - var a = numArray[1]; - var r = a / numArray[0]; - suggest = 'I think it is ' + numArray.pop() * r; - } - - cb(null, suggest); - } else { - cb(true, ''); - } -}; - -exports.default = { - evaluateExpression: evaluateExpression, - numMissing: numMissing, - numSequence: numSequence, - numToBinary: numToBinary, - numToHex: numToHex, - numToRoman: numToRoman -}; \ No newline at end of file diff --git a/lib/plugins/message.js b/lib/plugins/message.js deleted file mode 100644 index 5f701c11..00000000 --- a/lib/plugins/message.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -// import debuglog from 'debug'; -// import _ from 'lodash'; - -// import history from '../bot/history'; - -// const debug = debuglog('Message Plugin'); - -var addMessageProp = function addMessageProp(key, value, callback) { - if (key !== '' && value !== '') { - return callback(null, _defineProperty({}, key, value)); - } - - return callback(null, ''); -}; - -/* - - ## First Person (Single, Plural) - I, we - me, us - my/mine, our/ours - - ## Second Person (Single, Plural) - you, yous - - ## Third Person Single - he (masculine) - she (feminine) - it (neuter) - him (masculine) - her (feminine) - it (neuter) - his/his (masculine) - her/hers (feminine) - its/its (neuter) - - ## Third Person plural - they - them - their/theirs - -*/ -// exports.resolvePronouns = function(cb) { -// var message = this.message; -// var user = this.user; -// message.pronounMap = {}; - -// if (user['history']['input'].length !== 0) { -// console.log(message.pronouns); -// for (var i = 0; i < message.pronouns.length;i++) { -// var pn = message.pronouns[i]; -// var value = findPronoun(pn, user); -// message.pronounMap[pn] = value; -// } -// console.log(message.pronounMap) -// cb(null, ""); -// } else { -// for (var i = 0; i < message.pronouns.length;i++) { -// var pn = message.pronouns[i]; -// message.pronounMap[pn] = null; -// } -// console.log(message.pronounMap) -// cb(null, ""); -// } -// } - -// var findPronoun = function(pnoun, user) { -// console.log("Looking in history for", pnoun); - -// var candidates = history(user, { names: true }); -// if (!_.isEmpty(candidates)) { -// debug("history candidates", candidates); -// return candidates[0].names; -// } else { -// return null; -// } -// } - -exports.default = { addMessageProp: addMessageProp }; \ No newline at end of file diff --git a/lib/plugins/test.js b/lib/plugins/test.js deleted file mode 100644 index f84275a0..00000000 --- a/lib/plugins/test.js +++ /dev/null @@ -1,119 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -// This is used in a test to verify fall though works -// TODO: Move this into a fixture. -var bail = function bail(cb) { - cb(true, null); -}; - -var one = function one(cb) { - cb(null, 'one'); -}; - -var num = function num(n, cb) { - cb(null, n); -}; - -var changetopic = function changetopic(n, cb) { - this.user.setTopic(n, function () { - return cb(null, ''); - }); -}; - -var changefunctionreply = function changefunctionreply(newtopic, cb) { - cb(null, '{topic=' + newtopic + '}'); -}; - -var doSomething = function doSomething(cb) { - console.log('this.message.raw', this.message.raw); - cb(null, 'function'); -}; - -var breakFunc = function breakFunc(cb) { - cb(null, '', true); -}; - -var nobreak = function nobreak(cb) { - cb(null, '', false); -}; - -var objparam1 = function objparam1(cb) { - var data = { - text: 'world', - attachments: [{ - text: 'Optional text that appears *within* the attachment' - }] - }; - cb(null, data); -}; - -var objparam2 = function objparam2(cb) { - cb(null, { test: 'hello', text: 'world' }); -}; - -var showScope = function showScope(cb) { - cb(null, this.extraScope.key + ' ' + this.user.id + ' ' + this.message.clean); -}; - -var word = function word(word1, word2, cb) { - cb(null, word1 === word2); -}; - -var hasFirstName = function hasFirstName(bool, cb) { - this.user.getVar('firstName', function (e, name) { - if (name !== null) { - cb(null, bool === 'true'); - } else { - cb(null, bool === 'false'); - } - }); -}; - -var getUserId = function getUserId(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', _lodash2.default.isEqual(userID, that.user.id)); - cb(null, that.user.id); - }); -}; - -var hasName = function hasName(bool, cb) { - this.user.getVar('name', function (e, name) { - if (name !== null) { - cb(null, bool === 'true'); - } else { - // We have no name - cb(null, bool === 'false'); - } - }); -}; - -exports.default = { - bail: bail, - breakFunc: breakFunc, - doSomething: doSomething, - changefunctionreply: changefunctionreply, - changetopic: changetopic, - getUserId: getUserId, - hasFirstName: hasFirstName, - hasName: hasName, - nobreak: nobreak, - num: num, - objparam1: objparam1, - objparam2: objparam2, - one: one, - showScope: showScope, - word: word -}; \ No newline at end of file diff --git a/lib/plugins/time.js b/lib/plugins/time.js deleted file mode 100644 index dec6fd55..00000000 --- a/lib/plugins/time.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; - -var _moment = require('moment'); - -var _moment2 = _interopRequireDefault(_moment); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var COEFF = 1000 * 60 * 5; - -var getSeason = function getSeason() { - var now = (0, _moment2.default)(); - 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'; - } - return 'unknown'; -}; - -exports.getDOW = function getDOW(cb) { - cb(null, (0, _moment2.default)().format('dddd')); -}; - -exports.getDate = function getDate(cb) { - cb(null, (0, _moment2.default)().format('ddd, MMMM Do')); -}; - -exports.getDateTomorrow = function getDateTomorrow(cb) { - var date = (0, _moment2.default)().add('d', 1).format('ddd, MMMM Do'); - cb(null, date); -}; - -exports.getSeason = function getSeason(cb) { - cb(null, getSeason()); -}; - -exports.getTime = function getTime(cb) { - var date = new Date(); - var rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); - var time = (0, _moment2.default)(rounded).format('h:mm'); - cb(null, 'The time is ' + time); -}; - -exports.getGreetingTimeOfDay = function getGreetingTimeOfDay(cb) { - var date = new Date(); - var rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); - var time = (0, _moment2.default)(rounded).format('H'); - var tod = void 0; - if (time < 12) { - tod = 'morning'; - } else if (time < 17) { - tod = 'afternoon'; - } else { - tod = 'evening'; - } - - cb(null, tod); -}; - -exports.getTimeOfDay = function getTimeOfDay(cb) { - var date = new Date(); - var rounded = new Date(Math.round(date.getTime() / COEFF) * COEFF); - var time = (0, _moment2.default)(rounded).format('H'); - var tod = void 0; - if (time < 12) { - tod = 'morning'; - } else if (time < 17) { - tod = 'afternoon'; - } else { - tod = 'night'; - } - - cb(null, tod); -}; - -exports.getDayOfWeek = function getDayOfWeek(cb) { - cb(null, (0, _moment2.default)().format('dddd')); -}; - -exports.getMonth = function getMonth(cb) { - var reply = ''; - if (this.message.words.indexOf('next') !== -1) { - reply = (0, _moment2.default)().add('M', 1).format('MMMM'); - } else if (this.message.words.indexOf('previous') !== -1) { - reply = (0, _moment2.default)().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 = (0, _moment2.default)().format('MMMM'); - } - cb(null, reply); -}; \ No newline at end of file diff --git a/lib/plugins/user.js b/lib/plugins/user.js deleted file mode 100644 index 1891f584..00000000 --- a/lib/plugins/user.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _debug = require('debug'); - -var _debug2 = _interopRequireDefault(_debug); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debug2.default)('SS:UserFacts'); - -var save = function save(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 (_lodash2.default.isFunction(value)) { - cb = value; - value = ''; - } - } - - memory.db.get({ subject: key, predicate: userId }, function (err, results) { - if (!_lodash2.default.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, ''); - }); - } - }); -}; - -var hasItem = function hasItem(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 (err, res) { - if (!_lodash2.default.isEmpty(res)) { - cb(null, bool === 'true'); - } else { - cb(null, bool === 'false'); - } - }); -}; - -var get = function get(key, cb) { - var memory = this.user.memory; - var userId = this.user.id; - - debug('getVar', key, userId); - - memory.db.get({ subject: key, predicate: userId }, function (err, res) { - if (res && res.length !== 0) { - cb(err, res[0].object); - } else { - cb(err, ''); - } - }); -}; - -var createUserFact = function createUserFact(s, v, o, cb) { - this.user.memory.create(s, v, o, false, function () { - cb(null, ''); - }); -}; - -var known = function known(bool, cb) { - var memory = this.user.memory; - var name = this.message.names && !_lodash2.default.isEmpty(this.message.names) ? this.message.names[0] : ''; - memory.db.get({ subject: name.toLowerCase() }, function (err, res1) { - memory.db.get({ object: name.toLowerCase() }, function (err, res2) { - if (_lodash2.default.isEmpty(res1) && _lodash2.default.isEmpty(res2)) { - cb(null, bool === 'false'); - } else { - cb(null, bool === 'true'); - } - }); - }); -}; - -var inTopic = function inTopic(topic, cb) { - if (topic === this.user.currentTopic) { - cb(null, 'true'); - } else { - cb(null, 'false'); - } -}; - -exports.default = { - createUserFact: createUserFact, - get: get, - hasItem: hasItem, - inTopic: inTopic, - known: known, - save: save -}; \ No newline at end of file diff --git a/lib/plugins/wordnet.js b/lib/plugins/wordnet.js deleted file mode 100644 index fbcb09c7..00000000 --- a/lib/plugins/wordnet.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _wordnet = require('../bot/reply/wordnet'); - -var _wordnet2 = _interopRequireDefault(_wordnet); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var wordnetDefine = function wordnetDefine(cb) { - var args = Array.prototype.slice.call(arguments); - var word = void 0; - - if (args.length === 2) { - word = args[0]; - } else { - word = this.message.words.pop(); - } - - _wordnet2.default.define(word, function (err, result) { - cb(null, 'The Definition of ' + word + ' is ' + result); - }); -}; - -exports.default = { wordnetDefine: wordnetDefine }; \ No newline at end of file diff --git a/lib/plugins/words.js b/lib/plugins/words.js deleted file mode 100644 index cf9663af..00000000 --- a/lib/plugins/words.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _pluralize = require('pluralize'); - -var _pluralize2 = _interopRequireDefault(_pluralize); - -var _debug = require('debug'); - -var _debug2 = _interopRequireDefault(_debug); - -var _utils = require('../bot/utils'); - -var _utils2 = _interopRequireDefault(_utils); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var debug = (0, _debug2.default)('Word Plugin'); - -var plural = function plural(word, cb) { - // Sometimes WordNet will give us more then one word - var reply = void 0; - var parts = word.split(' '); - - if (parts.length === 2) { - reply = _pluralize2.default.plural(parts[0]) + ' ' + parts[1]; - } else { - reply = _pluralize2.default.plural(word); - } - - cb(null, reply); -}; - -var not = function not(word, cb) { - var words = word.split('|'); - var results = _utils2.default.inArray(this.message.words, words); - debug('RES', results); - cb(null, results === false); -}; - -var lowercase = function lowercase(word, cb) { - if (word) { - cb(null, word.toLowerCase()); - } else { - cb(null, ''); - } -}; - -exports.default = { - lowercase: lowercase, - not: not, - plural: plural -}; \ No newline at end of file From 2fd4ba444f2aeb5f69ebec800a9806f82202b248 Mon Sep 17 00:00:00 2001 From: Ben James Date: Sat, 26 Nov 2016 00:14:15 +0000 Subject: [PATCH 5/7] Allow multiple bot instances per server again --- src/bot/chatSystem.js | 46 ++++++------- src/bot/db/models/gambit.js | 5 +- src/bot/db/models/user.js | 6 +- src/bot/factSystem.js | 23 ++++--- src/bot/index.js | 127 ++++++++++++++++++++++-------------- src/bot/logger.js | 42 ++++++------ 6 files changed, 134 insertions(+), 115 deletions(-) diff --git a/src/bot/chatSystem.js b/src/bot/chatSystem.js index 481470f9..6900d808 100644 --- a/src/bot/chatSystem.js +++ b/src/bot/chatSystem.js @@ -17,33 +17,29 @@ import createReplyModel from './db/models/reply'; import createTopicModel from './db/models/topic'; import createUserModel from './db/models/user'; -let GambitCore = null; -let ReplyCore = null; -let TopicCore = null; -let UserCore = null; - -const createChatSystem = function createChatSystem(db) { - GambitCore = createGambitModel(db); - ReplyCore = createReplyModel(db); - TopicCore = createTopicModel(db); - UserCore = createUserModel(db); -}; - -const createChatSystemForTenant = function createChatSystemForTenant(tenantId = 'master') { - const Gambit = GambitCore.byTenant(tenantId); - const Reply = ReplyCore.byTenant(tenantId); - const Topic = TopicCore.byTenant(tenantId); - const User = UserCore.byTenant(tenantId); - - return { - Gambit, - Reply, - Topic, - User, +const setupChatSystem = function setupChatSystem(db, coreFactSystem, logger) { + const GambitCore = createGambitModel(db, coreFactSystem); + const ReplyCore = createReplyModel(db); + const TopicCore = createTopicModel(db); + const UserCore = createUserModel(db, coreFactSystem, logger); + + const getChatSystem = function getChatSystem(tenantId = 'master') { + const Gambit = GambitCore.byTenant(tenantId); + const Reply = ReplyCore.byTenant(tenantId); + const Topic = TopicCore.byTenant(tenantId); + const User = UserCore.byTenant(tenantId); + + return { + Gambit, + Reply, + Topic, + User, + }; }; + + return { getChatSystem }; }; export default { - createChatSystem, - createChatSystemForTenant, + setupChatSystem, }; diff --git a/src/bot/db/models/gambit.js b/src/bot/db/models/gambit.js index 3d69dcda..9357ea0d 100644 --- a/src/bot/db/models/gambit.js +++ b/src/bot/db/models/gambit.js @@ -13,7 +13,6 @@ import parser from 'ss-parser'; import modelNames from '../modelNames'; import helpers from '../helpers'; import Utils from '../../utils'; -import factSystem from '../../factSystem'; const debug = debuglog('SS:Gambit'); @@ -22,7 +21,7 @@ const debug = debuglog('SS:Gambit'); A trigger also contains one or more replies. **/ -const createGambitModel = function createGambitModel(db) { +const createGambitModel = function createGambitModel(db, factSystem) { const gambitSchema = new mongoose.Schema({ id: { type: String, index: true, default: Utils.genId() }, @@ -68,7 +67,7 @@ const createGambitModel = function createGambitModel(db) { // If we created the trigger in an external editor, normalize the trigger before saving it. if (this.input && !this.trigger) { - const facts = factSystem.createFactSystemForTenant(this.getTenantId()); + const facts = factSystem.getFactSystem(this.getTenantId()); return parser.normalizeTrigger(this.input, facts, (err, cleanTrigger) => { this.trigger = cleanTrigger; next(); diff --git a/src/bot/db/models/user.js b/src/bot/db/models/user.js index e3b0aaac..294d68be 100644 --- a/src/bot/db/models/user.js +++ b/src/bot/db/models/user.js @@ -7,12 +7,10 @@ import mongoose from 'mongoose'; import mongoTenant from 'mongo-tenant'; import modelNames from '../modelNames'; -import factSystem from '../../factSystem'; -import logger from '../../logger'; const debug = debuglog('SS:User'); -const createUserModel = function createUserModel(db) { +const createUserModel = function createUserModel(db, factSystem, logger) { const userSchema = mongoose.Schema({ id: String, status: Number, @@ -180,7 +178,7 @@ const createUserModel = function createUserModel(db) { userSchema.plugin(mongoTenant); userSchema.virtual('memory').get(function () { - return factSystem.createFactSystemForTenant(this.getTenantId()).createUserDB(this.id); + return factSystem.getFactSystem(this.getTenantId()).createUserDB(this.id); }); return db.model(modelNames.user, userSchema); diff --git a/src/bot/factSystem.js b/src/bot/factSystem.js index e9ed6ae9..615cb7bb 100644 --- a/src/bot/factSystem.js +++ b/src/bot/factSystem.js @@ -1,26 +1,25 @@ import facts from 'sfacts'; -let coreFacts = null; +const decorateFactSystem = function decorateFactSystem(factSystem) { + const getFactSystem = function getFactSystem(tenantId = 'master') { + return factSystem.createUserDB(`${tenantId}`); + }; -const createFactSystem = function createFactSystem(mongoURI, { clean, importData }, callback) { + return { getFactSystem }; +}; + +const setupFactSystem = function setupFactSystem(mongoURI, { clean, importData }, callback) { // TODO: On a multitenanted system, importing data should not do anything if (importData) { return facts.load(mongoURI, importData, clean, (err, factSystem) => { - coreFacts = factSystem; - callback(err, factSystem); + callback(err, decorateFactSystem(factSystem)); }); } return facts.create(mongoURI, clean, (err, factSystem) => { - coreFacts = factSystem; - callback(err, factSystem); + callback(err, decorateFactSystem(factSystem)); }); }; -const createFactSystemForTenant = function createFactSystemForTenant(tenantId = 'master') { - return coreFacts.createUserDB(`${tenantId}`); -}; - export default { - createFactSystem, - createFactSystemForTenant, + setupFactSystem, }; diff --git a/src/bot/index.js b/src/bot/index.js index bd6d847b..639a95d6 100644 --- a/src/bot/index.js +++ b/src/bot/index.js @@ -9,38 +9,14 @@ import chatSystem from './chatSystem'; import getReply from './getReply'; import Importer from './db/import'; import Message from './message'; -import logger from './logger'; +import Logger from './logger'; const debug = debuglog('SS:SuperScript'); -const plugins = []; -let editMode = false; -let scope = {}; - -const loadPlugins = function loadPlugins(path) { - try { - const pluginFiles = requireDir(path); - - Object.keys(pluginFiles).forEach((file) => { - // For transpiled ES6 plugins with default export - if (pluginFiles[file].default) { - pluginFiles[file] = pluginFiles[file].default; - } - - Object.keys(pluginFiles[file]).forEach((func) => { - debug.verbose('Loading plugin: ', path, func); - plugins[func] = pluginFiles[file][func]; - }); - }); - } catch (e) { - console.error(`Could not load plugins from ${path}: ${e}`); - } -}; - class SuperScript { - constructor(tenantId = 'master') { - this.factSystem = factSystem.createFactSystemForTenant(tenantId); - this.chatSystem = chatSystem.createChatSystemForTenant(tenantId); + constructor(coreChatSystem, coreFactSystem, plugins, scope, editMode, tenantId = 'master') { + this.chatSystem = coreChatSystem.getChatSystem(tenantId); + this.factSystem = coreFactSystem.getFactSystem(tenantId); // We want a place to store bot related data this.memory = this.factSystem.createUserDB('botfacts'); @@ -133,7 +109,7 @@ class SuperScript { extraScope: options.extraScope, chatSystem: this.chatSystem, factSystem: this.factSystem, - editMode, + editMode: this.editMode, }; this.findOrCreateUser(options.userId, (err, user) => { @@ -182,7 +158,7 @@ class SuperScript { const clientObject = { replyId: replyObj.replyId, createdAt: replyMessageObject.createdAt || new Date(), - string: replyMessage || '', // replyMessageObject.raw || "", + string: replyMessage || '', topicName: replyObj.topicName, subReplies: replyObj.subReplies, debug: log, @@ -201,9 +177,61 @@ class SuperScript { }); }); } +} - static getBot(tenantId) { - return new SuperScript(tenantId); +/** + * This a class which has global settings for all bots on a certain database server, + * so we can reuse parts of the chat and fact systems and share plugins, whilst still + * being able to have multiple bots on different databases per server. + */ +class SuperScriptInstance { + constructor(coreChatSystem, coreFactSystem, options) { + this.coreChatSystem = coreChatSystem; + this.coreFactSystem = coreFactSystem; + this.editMode = options.editMode || false; + this.plugins = []; + + // This is a kill switch for filterBySeen which is useless in the editor. + this.editMode = options.editMode || false; + this.scope = options.scope || {}; + + // Built-in plugins + this.loadPlugins(`${__dirname}/../plugins`); + + // For user plugins + if (options.pluginsPath) { + this.loadPlugins(options.pluginsPath); + } + } + + loadPlugins(path) { + try { + const pluginFiles = requireDir(path); + + Object.keys(pluginFiles).forEach((file) => { + // For transpiled ES6 plugins with default export + if (pluginFiles[file].default) { + pluginFiles[file] = pluginFiles[file].default; + } + + Object.keys(pluginFiles[file]).forEach((func) => { + debug.verbose('Loading plugin: ', path, func); + this.plugins[func] = pluginFiles[file][func]; + }); + }); + } catch (e) { + console.error(`Could not load plugins from ${path}: ${e}`); + } + } + + getBot(tenantId) { + return new SuperScript(this.coreChatSystem, + this.coreFactSystem, + this.plugins, + this.scope, + this.editMode, + tenantId, + ); } } @@ -218,6 +246,7 @@ const defaultOptions = { editMode: false, pluginsPath: `${process.cwd()}/plugins`, logPath: `${process.cwd()}/logs`, + useMultitenancy: false, }; /** @@ -240,34 +269,36 @@ const defaultOptions = { * the entire directory recursively. * @param {String} options.logPath - If null, logging will be off. Otherwise writes * conversation transcripts to the path. + * @param {Boolean} options.useMultitenancy - If true, will return a bot instance instead + * of a bot, so you can get different tenancies of a single server. Otherwise, + * returns a default bot in the 'master' tenancy. */ const setup = function setup(options = {}, callback) { options = _.merge(defaultOptions, options); - logger.setLogPath(options.logPath); // Uses schemas to create models for the db connection to use - factSystem.createFactSystem(options.mongoURI, options.factSystem, (err) => { + factSystem.setupFactSystem(options.mongoURI, options.factSystem, (err, coreFactSystem) => { if (err) { return callback(err); } const db = connect(options.mongoURI); - chatSystem.createChatSystem(db); - - // Built-in plugins - loadPlugins(`${__dirname}/../plugins`); - - // For user plugins - if (options.pluginsPath) { - loadPlugins(options.pluginsPath); + const logger = new Logger(options.logPath); + const coreChatSystem = chatSystem.setupChatSystem(db, coreFactSystem, logger); + + const instance = new SuperScriptInstance(coreChatSystem, coreFactSystem, options); + + /** + * When you want to use multitenancy, don't return a bot, but instead an instance that can + * get bots in different tenancies. Then you can just do: + * + * instance.getBot('myBot'); + */ + if (options.useMultitenancy) { + return callback(null, instance); } - // This is a kill switch for filterBySeen which is useless in the editor. - editMode = options.editMode || false; - scope = options.scope || {}; - - const bot = new SuperScript('master'); - + const bot = instance.getBot('master'); if (options.importFile) { return bot.importFile(options.importFile, err => callback(err, bot)); } diff --git a/src/bot/logger.js b/src/bot/logger.js index d0137488..bf5f5768 100644 --- a/src/bot/logger.js +++ b/src/bot/logger.js @@ -1,32 +1,28 @@ import fs from 'fs'; import mkdirp from 'mkdirp'; -// The directory to write logs to -let logPath; - -const setLogPath = function setLogPath(path) { - if (path) { - try { - mkdirp.sync(path); - logPath = path; - } catch (e) { - console.error(`Could not create logs folder at ${logPath}: ${e}`); +class Logger { + constructor(logPath) { + if (logPath) { + try { + mkdirp.sync(logPath); + this.logPath = logPath; + } catch (e) { + console.error(`Could not create logs folder at ${logPath}: ${e}`); + } } } -}; -const log = function log(message, logName = 'log') { - if (logPath) { - const filePath = `${logPath}/${logName}.log`; - try { - fs.appendFileSync(filePath, message); - } catch (e) { - console.error(`Could not write log to file with path: ${filePath}`); + log(message, logName = 'default') { + if (this.logPath) { + const filePath = `${this.logPath}/${logName}.log`; + try { + fs.appendFileSync(filePath, message); + } catch (e) { + console.error(`Could not write log to file with path: ${filePath}`); + } } } -}; +} -export default { - log, - setLogPath, -}; +export default Logger; From 95390b2f1e178b6449fecb37c977ce0b9da965c5 Mon Sep 17 00:00:00 2001 From: Ben James Date: Sat, 26 Nov 2016 00:23:40 +0000 Subject: [PATCH 6/7] Update clients to use setup method --- clients/hangout.js | 2 +- clients/slack.js | 2 +- clients/telegram.js | 2 +- clients/telnet.js | 2 +- clients/twilio.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/clients/hangout.js b/clients/hangout.js index bc73168d..e69961ed 100644 --- a/clients/hangout.js +++ b/clients/hangout.js @@ -44,6 +44,6 @@ const options = { importFile: './data.json', }; -SuperScript(options, (err, bot) => { +SuperScript.setup(options, (err, bot) => { botHandle(null, bot); }); diff --git a/clients/slack.js b/clients/slack.js index 60708fe7..c9262d21 100644 --- a/clients/slack.js +++ b/clients/slack.js @@ -95,6 +95,6 @@ const options = { importFile: './data.json', }; -SuperScript(options, (err, bot) => { +SuperScript.setup(options, (err, bot) => { botHandle(null, bot); }); diff --git a/clients/telegram.js b/clients/telegram.js index 0eeab72b..dbbbb409 100644 --- a/clients/telegram.js +++ b/clients/telegram.js @@ -8,7 +8,7 @@ const options = { importFile: './data.json', }; -SuperScript(options, (err, bot) => { +SuperScript.setup(options, (err, bot) => { if (err) { console.error(err); } diff --git a/clients/telnet.js b/clients/telnet.js index b30e1e44..06ac1dd9 100644 --- a/clients/telnet.js +++ b/clients/telnet.js @@ -80,6 +80,6 @@ const options = { importFile: './data.json', }; -SuperScript(options, (err, bot) => { +SuperScript.setup(options, (err, bot) => { botHandle(null, bot); }); diff --git a/clients/twilio.js b/clients/twilio.js index 16d2dad6..6f9dc27f 100644 --- a/clients/twilio.js +++ b/clients/twilio.js @@ -77,7 +77,7 @@ const options = { importFile: './data.json', }; -SuperScript(options, (err, bot) => { +SuperScript.setup(options, (err, bot) => { // Middleware app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); From c69fd01cf008d2dbf07af9e6b775678d48806ab7 Mon Sep 17 00:00:00 2001 From: Ben James Date: Sun, 27 Nov 2016 23:48:49 +0000 Subject: [PATCH 7/7] Add multitenant test --- test/fixtures/multitenant1/main.ss | 5 ++ test/fixtures/multitenant2/main.ss | 5 ++ test/helpers.js | 126 ++++++++++++++++++----------- test/multitenant.js | 46 +++++++++++ 4 files changed, 134 insertions(+), 48 deletions(-) create mode 100644 test/fixtures/multitenant1/main.ss create mode 100644 test/fixtures/multitenant2/main.ss create mode 100644 test/multitenant.js diff --git a/test/fixtures/multitenant1/main.ss b/test/fixtures/multitenant1/main.ss new file mode 100644 index 00000000..4d5266b2 --- /dev/null +++ b/test/fixtures/multitenant1/main.ss @@ -0,0 +1,5 @@ ++ should reply to this +- in tenancy one + ++ * +- catch all diff --git a/test/fixtures/multitenant2/main.ss b/test/fixtures/multitenant2/main.ss new file mode 100644 index 00000000..36d33f18 --- /dev/null +++ b/test/fixtures/multitenant2/main.ss @@ -0,0 +1,5 @@ ++ should not reply to this +- in tenancy two + ++ * +- catch all diff --git a/test/helpers.js b/test/helpers.js index c42d616d..dc3d947b 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -50,68 +50,96 @@ const after = function after(end) { } }; -const before = function before(file) { - const options = { - mongoURI: 'mongodb://localhost/superscripttest', - factSystem: { - clean: false, - }, - logPath: null, - pluginsPath: null, - }; +const parse = function parse(file, callback) { + 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) { + return callback(err); + } + return callback(null, fileCache, result); + }); + }); + } else { + console.log(`Loading cached script from ${fileCache}`); + let contents = fs.readFileSync(fileCache, 'utf-8'); + contents = JSON.parse(contents); - const afterParse = (fileCache, result, callback) => { - fs.exists(`${__dirname}/fixtures/cache`, (exists) => { - if (!exists) { - fs.mkdirSync(`${__dirname}/fixtures/cache`); - } - return fs.writeFile(fileCache, JSON.stringify(result), (err) => { + bootstrap((err, factSystem) => { if (err) { return callback(err); } - options.importFile = fileCache; - return SuperScript.setup(options, (err, botInstance) => { + const checksums = contents.checksums; + return parser.loadDirectory(`${__dirname}/fixtures/${file}`, { factSystem, cache: checksums }, (err, result) => { if (err) { return callback(err); } - bot = botInstance; - return callback(); + const results = _.merge(contents, result); + return callback(null, fileCache, results); }); }); + } + }); +}; + +const saveToCache = function saveToCache(fileCache, result, callback) { + fs.exists(`${__dirname}/fixtures/cache`, (exists) => { + if (!exists) { + fs.mkdirSync(`${__dirname}/fixtures/cache`); + } + return fs.writeFile(fileCache, JSON.stringify(result), (err) => { + if (err) { + return callback(err); + } + return callback(); }); + }); +}; + +const parseAndSaveToCache = function parseAndSaveToCache(file, callback) { + parse(file, (err, fileCache, result) => { + if (err) { + return callback(err); + } + return saveToCache(fileCache, result, (err) => { + if (err) { + return callback(err); + } + return callback(null, fileCache); + }); + }); +}; + +const setupBot = function setupBot(fileCache, multitenant, callback) { + const options = { + mongoURI: 'mongodb://localhost/superscripttest', + factSystem: { + clean: false, + }, + logPath: null, + pluginsPath: null, + importFile: fileCache, + useMultitenancy: multitenant, }; - 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 from ${fileCache}`); - let contents = fs.readFileSync(fileCache, 'utf-8'); - contents = JSON.parse(contents); + return SuperScript.setup(options, (err, botInstance) => { + if (err) { + return callback(err); + } + bot = botInstance; + return callback(); + }); +}; - 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); - }); - }); +const before = function before(file, multitenant = false) { + return (done) => { + parseAndSaveToCache(file, (err, fileCache) => { + if (err) { + return done(err); } + return setupBot(fileCache, multitenant, done); }); }; }; @@ -120,4 +148,6 @@ export default { after, before, getBot, + parseAndSaveToCache, + setupBot, }; diff --git a/test/multitenant.js b/test/multitenant.js new file mode 100644 index 00000000..34e6ead1 --- /dev/null +++ b/test/multitenant.js @@ -0,0 +1,46 @@ +/* global describe, it, before, after */ + +import mocha from 'mocha'; +import should from 'should'; +import helpers from './helpers'; + +describe('SuperScript Multitenant', () => { + before((done) => { + helpers.parseAndSaveToCache('multitenant1', (err, fileCache) => { + if (err) { + return done(err); + } + return helpers.parseAndSaveToCache('multitenant2', (err2, fileCache2) => { + if (err2) { + return done(err2); + } + return helpers.setupBot(null, true, (err3) => { + if (err3) { + return done(err3); + } + return helpers.getBot().getBot('multitenant1').importFile(fileCache, (err) => { + helpers.getBot().getBot('multitenant2').importFile(fileCache2, (err) => { + done(); + }); + }); + }); + }); + }); + }); + + describe('Different tenancy', () => { + it('should reply to trigger in tenancy', (done) => { + helpers.getBot().getBot('multitenant1').reply('user1', 'should reply to this', (err, reply) => { + reply.string.should.eql('in tenancy one'); + done(); + }); + }); + + it('should not reply to trigger not in tenancy', (done) => { + helpers.getBot().getBot('multitenant1').reply('user1', 'should not reply to this', (err, reply) => { + reply.string.should.eql('catch all'); + done(); + }); + }); + }); +});