parrot-bot
Version:
A parrot-like bot you can talk with.
640 lines (521 loc) • 21.7 kB
JavaScript
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;
;