UNPKG

parrot-bot

Version:
640 lines (521 loc) 21.7 kB
"use strict"; 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; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } var mongoose = require("mongoose"), Logger = require("bug-killer"), ul = require("ul"), diacritics = require("diacritics").remove, EventEmitter = require("events").EventEmitter, deffy = require("deffy"), deasync = require("deasync"), noop = require("noop6"), uniqueRandArr = require("unique-random-array"), mapO = require("map-o"); mongoose.Promise = Promise; var DEFAULT_CACHE = { received: { A: [], Q: [] }, sent: { A: [], Q: [] } }; var ParrotBot = function (_EventEmitter) { _inherits(ParrotBot, _EventEmitter); /** * ParrotBot * * @name ParrotBot * @function * @param {String|Object} config The bot language or the bot config itself. * @param {Object} options An object containing the following fields: * * - `database` (Object): The database configuration: * - `name` (String): The database name (default: `parrotbot`) * - `uri` (String): The MongoDB uri. * - `collection` (String): The collection name. * - `lang` (String): The bot language. Supported: `ro` (Romanian), `en` (English). **Feel free to extend this by adding new files in the `lib/languages` directory.** * * @returns {ParrotBot} The `ParrotBot` instance. */ function ParrotBot(config, options) { _classCallCheck(this, ParrotBot); var _this = _possibleConstructorReturn(this, (ParrotBot.__proto__ || Object.getPrototypeOf(ParrotBot)).call(this)); if (typeof config === "string") { config = { lang: config }; } config = config || {}; var lang = config.lang, conf = ParrotBot.languages[lang]; if (conf) { config = conf; } _this.config = ul.deepMerge(config, { messages: { fail: [], duplicate: { bot: [], human: [] } }, meta: { ignore: [] } }); _this.options = ul.deepMerge(options, { database: {}, name: "Alice" }); var db = _this.options.database; db.name = db.name || "parrotbot"; db.uri = db.uri || "mongodb://localhost/" + db.name; db.collection = db.collection || "messages_" + _this.options.name.toLowerCase() + (lang ? "_" + lang : ""); _this._db = mongoose.createConnection(db.uri, function (err, data) { if (err) { _this.emit("error", err); } else { _this.emit("connected"); } }); _this.Message = _this._db.model("Message", { type: String, message: { type: String, unique: true, dropDups: true }, meta: [String] }, db.collection); _this._col = _this._db.collection(db.collection); _this.duplicateMessages = mapO(_this.config.messages.duplicate, uniqueRandArr, true); _this.failMessageRandom = uniqueRandArr(_this.config.messages.fail); _this.debug = _this.options.debug; _this.useCache = deffy(_this.options.cache, true); _this.clearCache(); if (!_this.debug) { Logger.config.level = 0; } _this.tellSync = deasync(_this.tell); return _this; } /** * getWordsFromMessage * Gets the words from the message, ignoring the words that should be ignored (configuredin the bot config). * * @name getWordsFromMessage * @function * @param {String} message The message to get the words from. * @returns {Array} The message words. */ _createClass(ParrotBot, [{ key: "getWordsFromMessage", value: function getWordsFromMessage(message) { // +=========================+ // | NATIVE FUNCTIONS: | // | MESSAGE -> WORDS | // | GET FAIL MESSAGE | // | QUESTION OR ANSWER? | // | REMOVE DIACRITICS | // +=========================+ if (!message) return []; var words = message.split(" "); var wordsToSend = []; for (var i in words) { words[i] = this.removeDiacritics(words[i]); words[i] = words[i].replace(new RegExp(/[^a-zA-Z0-9 -]/g), "").toLowerCase().trim(); if (words[i].length > 3 && this.config.meta.ignore.indexOf(words[i]) === -1) { wordsToSend.push(words[i]); } } return wordsToSend; } /** * getFailMessage * Returns a fail message (such as *I don't know how to answer*). * * @name getFailMessage * @function * @returns {String} The message. */ }, { key: "getFailMessage", value: function getFailMessage() { return this.failMessageRandom(); } /** * getDuplicateMessage * Returns a message such as *Hey, you have already asked me this!*. * * @name getDuplicateMessage * @function * @param {String} type The message type. * @returns {String} The message. */ }, { key: "getDuplicateMessage", value: function getDuplicateMessage(type) { return this.duplicateMessages[type](); } /** * getMessageType * Returns the message type (question or answer). * * @name getMessageType * @function * @param {String} message The message. * @returns {String} `A` for answer, `Q` for question. */ }, { key: "getMessageType", value: function getMessageType(message) { if (!message) { return "A"; } // The message contains "?" if (message.indexOf("?") !== -1) { // The message ends with "?", so it's a question. if (message.indexOf("?") === message.length - 1) { return "Q"; } return "Q"; } return "A"; } /** * removeDiacritics * Removes the diacritics from the message. * * @name removeDiacritics * @function * @param {String} message The message containing special characters. * @returns {String} The message without diacrtics. */ }, { key: "removeDiacritics", value: function removeDiacritics(message) { return diacritics(message); } /** * getConfig * Returns the config object. * * @name getConfig * @function * @returns {Object} The config object. */ }, { key: "getConfig", value: function getConfig() { return this.config; } /** * tell * Tell something to the bot. * * @name tell * @function * @param {String} message The message to send to the bot. * @param {Function} cb The callback function. */ }, { key: "tell", value: function tell(message, cb) { var _this2 = this; Logger.log("Messege received: " + message, "exit"); // if (!message) { return cb(); } // Don't insert fail messages in database. // if (config.fail.messages.indexOf(message) !== -1) { return cb(null, null) } /////////////////////////////// // IS THIS A DUPLICATE MESSAGE? /////////////////////////////// var duplicate = true; var regexArray = []; var words = this.getWordsFromMessage(message); Logger.log("Found " + words.length + " words: " + JSON.stringify(words), "exit"); for (var i in words) { Logger.log("Adding " + words[i], "exit"); var item = new RegExp("^" + words[i]); Logger.log("> Item: " + item); regexArray.push(item); } Logger.log("--------------------------", "warning"); Logger.log("A new message to filter...", "warning"); Logger.log("Message: " + message, "warning"); Logger.log("Words: " + JSON.stringify(words)); Logger.log("RegexArray:", "warning"); Logger.log(regexArray, "error"); // Find docs with these words this.Message.find({ "meta": { $all: regexArray } }, function (err, docs) { Logger.log("Found " + docs.length + " docs"); if (err) { return cb(err); } if (!docs || !docs.length) { duplicate = false; } // Search in each document the words. for (var _i in docs) { for (var word in docs[_i].meta) { // If ONE word is NOT duplicated, then // the message isn't duplicated var keyword = docs[_i].meta[word]; if (words.indexOf(keyword) === -1 && _this2.config.meta.ignore.indexOf(keyword) === -1) { duplicate = false; break; } } } Logger.log("> Duplicate: " + duplicate, "warning"); // Prepare object to insert var objectToInsert = _this2.processMessageToInsert(message); // If the message is NOT duplicated, insert it. if (!duplicate && objectToInsert.message) { var newMsg = new _this2.Message(objectToInsert); newMsg.save(function (err, insertedDoc) { if (err) { return Logger.log(err, "error"); } Logger.log("Inserted successfully a new message in database.", "exit"); }); } // Message to insert data var messageData = _this2.processMessageToInsert(message); var messageToSend = ""; var config = _this2.config; if (messageData.message) { if (_this2.useCache && _this2.cache.received[messageData.type].indexOf(messageData.message) !== -1) { messageToSend = _this2.getDuplicateMessage("human"); return cb(null, messageToSend); } else if (_this2.useCache) { _this2.cache.sent["A"].push(message); } } if (_this2.useCache) { _this2.cache.received[messageData.type].push(messageData.message); } var messageReceived = _this2.processMessageToInsert(message); // Process messages to send _this2.processMessageToSend(message, function (err, message) { if (err) { return cb(err); } Logger.log("Message: " + message); if (message === "<!>") { Logger.log("Returning an empty string.", "warning"); cb(null, ""); return; } if (message === "<A>") { cb(null, _this2.getFailMessage()); return; } if (!message) { _this2.Message.find({ "type": "Q" }, function (err, docs) { if (err) { return cb(err); } // No questions found. if (!docs || !docs.length) { Logger.log("No docs found"); cb(null, _this2.getFailMessage()); } messageToSend = docs[Math.floor(Math.random() * docs.length)].message; if (_this2.useCache && _this2.cache.sent["Q"].indexOf(messageToSend) !== -1) { messageToSend = _this2.getDuplicateMessage("bot"); } else if (_this2.useCache) { _this2.cache.sent["Q"].push(messageToSend); } cb(null, (messageReceived.type === "Q" ? "<%>" : "") + messageToSend); }); return; } if (messageReceived.message) if (_this2.useCache && _this2.cache.received[messageReceived.type].indexOf(messageReceived.message) !== -1) { messageToSend = _this2.getDuplicateMessage("human"); } else if (_this2.useCache && _this2.cache.sent["A"].indexOf(message) !== -1) { message = _this2.getDuplicateMessage("human"); } else if (_this2.useCache) { _this2.cache.sent["A"].push(message); } cb(null, message); }); }); } // +==============================================+ // |. . * . * . | // | R 0 B 0 T . + . . | // | . . M € M 0 R Y . | // | . + . 0 P € R @ T I 0 N S + | // +==============================================+ /** * processMessageToInsert * Parse the message and prepare the database record. * * @name processMessageToInsert * @function * @param {String} message The message to insert. * @returns {Object} An object containing: * * - `type` (String): The message type (`A`/`Q`). * - `message` (String): The raw message. * - `meta` (Array): The message words. */ }, { key: "processMessageToInsert", value: function processMessageToInsert(message) { var dataToInsert = { "type": "", "message": message, "meta": [] }; dataToInsert.type = this.getMessageType(message); dataToInsert.meta = this.getWordsFromMessage(message); return dataToInsert; } /** * processMessageToSend * Answers a message, without remembering it. * * @name processMessageToSend * @function * @param {String} message The message to answer to. * @param {Function} cb The callback function. */ }, { key: "processMessageToSend", value: function processMessageToSend(message, cb) { var _this3 = this; Logger.log("Message: " + message, "info"); // First try to five an answer {type:"A"} var words = this.getWordsFromMessage(message); if (!message || !words || !words.length) { this.Message.find({ "type": "Q" }, function (err, docs) { if (err) { return cb(err); } if (!docs || !docs.length) { return cb(null, _this3.getFailMessage()); } var l = docs.length; var randQuestion = docs[Math.floor(Math.random() * l)].message; if (!randQuestion) { return cb(null, _this3.getFailMessage()); } if (_this3.useCache && _this3.cache.sent["Q"].indexOf(randQuestion) !== -1) { randQuestion = _this3.getDuplicateMessage("bot"); } else if (_this3.useCache) { _this3.cache.sent["Q"].push(randQuestion); } cb(null, randQuestion); }); return; } // If the message is an answer, return a question if (this.getMessageType(message) === "A") { Logger.log("The message is an answer or an affirmation.", "info"); return cb(null, "<!>"); } Logger.log("The message is a question.", "info"); // The message is a question for robot, find answers. this.Message.find({ "type": "A" }, function (err, docs) { if (err) { return cb(err); } var filter = {}; var max = 0; // Answer found if (docs || docs.length) { // Scan every doc for (var doc in docs) { var why = []; // Scanning current doc var power = 0; for (var word in words) { if (docs[doc].meta.indexOf(words[word]) !== -1) { ++power; why.push(words[word]); } } if (power !== 0) { if (!filter[power.toString()]) { filter[power.toString()] = []; } docs[doc].why = why; filter[power.toString()].push(docs[doc]); if (power > max) { max = power; } } } var bestAnswers = filter[max] || []; var answer = (bestAnswers[Math.floor(Math.random() * bestAnswers.length)] || {}).message || "<A>"; cb(null, answer); return; } // Answer not found, return a question calback(null, ""); }); } /** * clearCache * Clears the internal cache. * * @name clearCache * @function * @param {Function} cb The callback function. */ }, { key: "clearCache", value: function clearCache(cb) { cb = cb || noop; if (!this.useCache) { return cb("Cache has to be enabled."); } this.cache = ul.clone(DEFAULT_CACHE); cb(null, "Successfully cleared cache."); } /** * remove * Removes messages. * * @name remove * @function * @param {Object} filters The query filters. * @param {Object} options The query options. * @param {Function} cb The callback function. */ }, { key: "remove", value: function remove(filters, options, cb) { if (!filters) { return cb("Missing filters object."); } if (typeof options === "function") { cb = options; options = {}; } this.Message.remove(filters, options, cb); } /** * die * Ends the connection to the database, but doesn't clear the memory (the database documents). * * @name die * @function */ }, { key: "die", value: function die() { this._db.close(); } }]); return ParrotBot; }(EventEmitter); ; ParrotBot.languages = require("./languages"); module.exports = ParrotBot;