val-bot
Version:
A bot that does things.
867 lines (734 loc) • 20.1 kB
JavaScript
const modulesConfig = require('./config/_val.modules.js');
const _Val = function(commandModuleName, userConfig) {
const commandModule = userConfig.command[commandModuleName];
const coreConfig = commandModule.coreConfig || {};
const _botConfig = Object.assign({}, userConfig, coreConfig);
const { trigger } = _botConfig;
const commandType = commandModule.botName;
const req = userConfig.req;
const http = req.http;
const https = req.https;
const fs = req.fs;
const chalk = req.chalk;
let channel;
let _bot = {};
let channels = [];
const modules = {};
const debugChalkBox = {
PING: 'blue',
MODE: 'magenta',
rpl_channelmodeis: 'cyan',
rpl_myinfo: 'cyan',
rpl_creationtime: 'cyan',
rpl_namreply: 'cyan',
rpl_endofnames: 'cyan',
rpl_topic: 'gray',
rpl_isupport: 'magenta',
rpl_welcome: 'magenta',
rpl_luserclient: 'magenta',
rpl_motdstart: 'bgMagenta',
rpl_motd: 'bgMagenta',
rpl_endofmotd: 'bgMagenta',
JOIN: 'green',
KILL: 'green',
NOTICE: 'yellow',
TOPIC: 'yellow',
};
/**
* ## addLanguageParsers
*
* resolves the language parsers from the config and adds them to _bot
*
* @return {Void}
*/
function addLanguageParsers() {
_bot.languageParsers = [];
const languageParsers = userConfig.language;
for (const parserName in languageParsers) {
const parser = languageParsers[parserName];
if (parser.enabled) {
_bot.languageParsers.push(require(parser.url));
if (parser.options) {
for (let option in parser.options) {
_botConfig[option] = parser.options[option];
}
}
}
}
}
/**
* ## apiGet
*
* gets and parses JSON from api sources
*
* @param {String} url target url
* @param {Function} cb callback
* @param {Boolean} secure https?
* @param {String} from channel
* @param {String} to user
*
* @return {Void}
*/
function apiGet(options, cb, secure, from, to) {
secure = !!secure;
const error = e => {
if (_bot.say && from && to) {
_bot.say(
from,
`sorry, ${to} bad query or url. (depends on what you were trying to do)`
);
} else {
console.warn(
`${options.url || options.host || options} appears to be down`,
e
);
}
};
const callback = res => {
let body = '';
res.on('data', chunk => {
body += chunk;
});
res.on('end', () => {
let data;
try {
try {
data = JSON.parse(body);
} catch (e) {
data = body;
}
cb(data);
} catch (e) {
error(e);
}
});
};
try {
if (options.method === 'POST') {
if (secure) {
https.request(options, callback).on('error', function(e) {
error(e);
});
} else {
http.request(options, callback).on('error', function(e) {
error(e);
});
}
} else {
if (secure) {
https.get(options, callback).on('error', function(e) {
error(e);
});
} else {
http.get(options, callback).on('error', function(e) {
error(e);
});
}
}
} catch (e) {
console.log(e);
}
}
/**
* ## baseResponses
*
* val's base responses that both require no modules and are non-optional
* @type {Object}
*/
const baseResponses = {
commands: {
active: {
module: 'base',
f: checkActive,
desc: 'checks how many people are active in the channel',
syntax: [`${trigger}active`],
},
help: {
module: 'base',
f: helpText,
desc: 'returns help text',
syntax: [`${trigger}help`, `${trigger}help <command>`],
},
isup: {
module: 'base',
f: () => "Yes, but c'mon! At least use a full sentence!",
desc: "returns _val's current status",
syntax: [`${trigger}isup`],
},
'moon?': {
module: 'base',
f: () =>
'In 500 million years, the moon will be 14,600 miles farther away than it is right now. When it is that far, total eclipses will not take place',
desc: 'learn more about the moon',
syntax: [`${trigger}moon`],
},
},
dynamic: {},
regex: {},
};
/**
* ## buildClient
*
* assembles the _val modules. like Voltron but node
*
* @return {Void}
*/
function buildClient() {
/*
* adds core components to an obj to be passed modules
*/
modules.core = {
checkActive: checkActive,
userData: userData,
apiGet: apiGet,
};
modules.constructors = {};
/**
* load _val modules
*/
for (const moduleName in modulesConfig) {
const module = modulesConfig[moduleName];
if (module.enabled) {
modules.constructors[moduleName] = require(module.url);
if (module.options) {
for (let option in module.options) {
_botConfig[option] = module.options[option];
}
}
}
}
}
/**
* ## buildCore
*
* dynamic core loader
*
* @return {Void}
*/
function buildCore() {
const Commander = require(commandModule.url);
_bot = new Commander(
_botConfig,
channels,
listenToMessages,
displayDebugInfo,
this,
commandModule
);
_bot.name = commandModule.botName;
addLanguageParsers();
}
/**
* ## checkActive
*
* returns a list of users that have posted within the defined amount of time
*
* @param {String} from originating channel
* @param {String} to originating user
* @param {String} text full message text
* @param {Boolean} talk true to say, otherwise active only returns
*
* @return {Array} active users
*/
function checkActive(from, to, text, talk) {
let name;
let i = 0;
let now = Date.now();
const activeUsers = [];
if (!_bot.active[from]) {
_bot.active[from] = {};
}
const activeChannel = _bot.active[from];
if (
!activeChannel[to] &&
to !== _bot.name &&
_botConfig.bots.indexOf(to) === -1
) {
activeChannel[to] = now;
now++;
}
for (name in activeChannel) {
if (now - _botConfig.activeTime < activeChannel[name]) {
i++;
activeUsers.push(name);
} else {
delete activeChannel[name];
}
}
if (talk !== false) {
botText = `I see ${i} active user`;
if (i > 1 || i === 0) {
botText += 's';
}
botText += ` in ${from}`;
return botText;
}
return activeUsers;
}
/**
* ## combineResponses
*
* combines two response structures while checking for duplicate keys
*
* @param {Object} res responses
* @param {Object} newRes responses to add
*
* @return {Object} combined object
*/
function combineResponses(res, newRes, regex) {
if (newRes) {
Object.keys(newRes).forEach(c => {
let command = c;
if (regex) {
command = c.slice(0, c.length - 1).slice(1);
}
if (res[c]) {
console.warn(`duplicate property ${c}`);
} else {
res[command] = newRes[c];
}
});
}
return res;
}
/**
* ## displayDebugInfo
*
* formats and displays debug information
*
* @return {Void}
*/
function displayDebugInfo(e) {
const command = e.command;
if (command !== 'PRIVMSG') {
const color = debugChalkBox[command];
let text = ` * ${command} : `;
e.args.forEach(arg => (text += `${arg} `));
if (color) {
if (command === 'PING') {
const now = Date.now();
let minUp = `${Math.round(((now - up) / 1000 / 60) * 100) / 100}`;
if (minUp.indexOf('.') === -1) {
minUp += '.00';
} else if (minUp.split('.')[1].length !== 2) {
minUp += '0';
}
console.log(
chalk[color](text),
`${now - lastPing}ms`,
chalk.grey(`(${minUp}min up)`, new Date().toLocaleString())
);
lastPing = now;
if (connectionTimer) {
clearTimeout(connectionTimer);
}
connectionTimer = setTimeout(
reConnection,
_botConfig.reconnectionTimeout
);
} else {
console.log(chalk[color](text));
}
} else {
console.log(e);
}
}
}
/**
* ## generateChannelList
*
* generates a channel list based on settings and environment.
*
* @return {Void}
*/
function generateChannelList() {
/**
* adds private channels from _botConfig.channelsPrivateJoin to the list of
* channels to join.
*/
function addPrivateChannels() {
const privateChannels = commandModule.channelsPrivateJoin;
if (privateChannels) {
const privateChannelsLength = privateChannels.length;
for (let i = 0; i < privateChannelsLength; i++) {
const channel = privateChannels[i];
if (channels.indexOf(channel) === -1) {
channels.push(channel);
}
}
}
}
/**
* assembles the channel list and starts the client
*
* @return {Void}
*/
function finishChannels() {
_botConfig.publicChannels = [].concat(channels);
if (commandModule.slackTeam) {
addPrivateChannels();
}
removeBlacklistChannels();
_botConfig.channels = channels;
ini();
}
/**
* if any channels are blacklisted from entering from _botConfig.channelsPublicIgnore,
* this removes them from the channels array
*/
function removeBlacklistChannels() {
const blockedChannels = _botConfig.channelsPublicIgnore || [];
const blockedChannelsLength = blockedChannels.length;
if (blockedChannelsLength) {
for (let i = 0; i < blockedChannelsLength; i++) {
const b = blockedChannels[i];
const index = channels.indexOf(b);
if (index !== -1) {
channels.splice(index, 1);
}
}
}
}
if (commandModule.slackTeam && commandModule.autojoin) {
const url = `https://${
commandModule.slackTeam
}.slack.com/api/channels.list?token=${userConfig.slackAPIKey}`;
apiGet(
url,
function(res) {
const channels = res.channels;
for (let channel in channels) {
channel = channels[channel].name;
channel = channel[0] !== '#' ? `#${channel}` : channel;
channels.push(channel);
}
finishChannels();
},
true
);
} else if (_botConfig.channels) {
channels = _botConfig.channels;
finishChannels();
} else {
console.log('no channels found');
}
}
/**
* ## helpText
*
* displays help text to the channel
*
* @param {String} from originating channel
* @param {String} to originating user
* @param {String} query search parameter
*
* @return {String} help text
*/
function helpText(from, to, text) {
const responses = Object.assign(
_bot.responses.commands || {},
_bot.responses.regex,
_bot.responses.dynamic
);
const responseText = responses[text];
if (text.length === 0 || !responseText) {
let str = 'available commands: ';
Object.keys(responses).forEach(key => {
str += ` ${key}, `;
});
return str.slice(0, str.length - 2);
} else {
let helpText = responseText.desc;
const syntax = responseText.syntax;
if (syntax) {
try {
syntax.forEach(s => (helpText += `\n${s}`));
} catch (e) {
throw `broken help : is ${text} syntax an array?`;
}
}
return helpText;
}
}
/**
* ## ini
*
* sets listeners and module list up
*
* @return {Void}
*/
function ini() {
buildCore();
_bot.active = {};
_bot.responses = baseResponses;
for (const moduleName in modules.constructors) {
if (_botConfig.disabledModules.indexOf(moduleName) === -1) {
const ModulesConstructor = modules.constructors[moduleName];
const module = (modules[moduleName] = new ModulesConstructor(
_bot,
modules,
_botConfig,
commandModule
));
function formatResponses(module, name) {
module.responses = module.responses();
['commands', 'regex'].forEach(category => {
const commands = module.responses[category];
if (commands) {
Object.keys(commands).forEach(r => {
const res = commands[r];
res.f = res.f.bind(module);
res.moduleName = name;
res.module = module;
});
}
});
}
formatResponses(module, moduleName);
_bot.responses.regex = combineResponses(
_bot.responses.regex || {},
module.responses.regex,
'regex'
);
_bot.responses.commands = combineResponses(
_bot.responses.commands || {},
module.responses.commands
);
}
}
_bot.modules = modules;
console.log(`${commandType} built`);
}
/**
* ## listenToMessages
*
* .... what do you think?
*
* @param {String} to user
* @param {String} from originating channel
* @param {String} text full message text
* @param {Object} confObj pass through variables from the core
*/
function listenToMessages(to, from, text, confObj) {
if (text) {
if (_botConfig.verbose === true) {
console.log(commandType, chalk.green(from), chalk.red(to), text);
}
text = trimUsernames(text);
watchActive(from, to);
if (_botConfig.bots.indexOf(to) === -1) {
const trigger = _botConfig.trigger;
const triggerLength = trigger.length;
let botText = '';
_bot.languageParsers.forEach(func => {
if (text && botText === '') {
let res = func(to, from, text, botText, _botConfig, confObj, _bot);
to = res.to;
text = res.text;
botText = res.botText;
}
});
if (
text &&
text.slice(0, triggerLength) === trigger &&
text !== trigger &&
botText === ''
) {
text = text.slice(triggerLength);
let textArr = text.split(' ');
const command = textArr[0];
textArr = textArr.slice(1);
text = textArr.join(' ');
if (_bot.responses.commands[command]) {
return _bot.responses.commands[command].f(
from,
to,
text,
textArr,
command,
confObj
);
} else if (_bot.responses.dynamic[command]) {
return _bot.responses.dynamic[command].f(
from,
to,
text,
textArr,
command,
confObj
);
} else {
const regexKeys = Object.keys(_bot.responses.regex);
regexKeys.every(r => {
const regex = new RegExp(r);
const match = command.match(regex);
if (match && match.length > 0) {
botText = _bot.responses.regex[r].f(
from,
to,
text,
textArr,
command,
confObj
);
return false;
}
return true;
});
}
}
return botText;
} else if (
_botConfig.bots.indexOf(to) !== -1 &&
(text[0] === _botConfig.trigger && text !== _botConfig.trigger)
) {
// automated response to automated people
}
}
}
/**
* ## reConnection
*
* disconnects and reconnects _val
*
* @return {Void}
*/
function reConnection() {
_bot.disconnect('Fine... I was on my way out anyways.', function(e) {
console.log('disconnected? ', e);
});
ini();
console.log('re-initializing client...');
}
/**
* ## start
*
* start the thing!
*
* @return {Void}
*/
function start() {
buildClient();
generateChannelList();
}
/**
* ## trimUsernames
*
* removes the set usernamePrefix from the front of usernames
*
* @param {String} text original text
*
* @return {String}
*/
function trimUsernames(text) {
if (_botConfig.usernamePrefix && _botConfig.usernamePrefix.length > 0) {
text = text.split(' ');
for (let i = 0, lenI = text.length; i < lenI; i++) {
if (_botConfig.usernamePrefix.indexOf(text[i][0]) !== -1) {
text[i] = text[i].slice(1);
}
}
return text.join(' ');
}
return text;
}
/**
* ## userData
*
* gets userdata from the nickserv authentication bot
*
* @param {String} to user
* @param {String} from originating channel
* @param {Function} cb callback
* @param {String} origText original message text
*
* @return {Void}
*/
function userData(to, from, cb, origText) {
if (_botConfig.autoAuth) {
const textSplit = origText.split(' ');
cb(to, 'true', textSplit, origText);
} else {
const response = function(_from, text) {
_bot.removeListener('pm', response);
const textSplit = text.split(' ');
const apiReturn = textSplit[0];
const returnMessage = textSplit[1];
const user = textSplit[2];
const result = textSplit[3];
if (
apiReturn === _botConfig.nickservAPI &&
returnMessage === 'identified' &&
user === to &&
result === 'true'
) {
cb(to, result, textSplit, origText);
} else if (
apiReturn === _botConfig.nickservAPI &&
returnMessage === 'identified' &&
user === to &&
result === 'false'
) {
_bot.say(to, 'You are not identified. (/msg NickServ help)');
} else if (
apiReturn === _botConfig.NickservAPI &&
returnMessage === 'notRegistered' &&
user === to
) {
_bot.say(to, 'You are not a registered user. (/msg NickServ help)');
}
};
_bot.addListener('pm', response);
_bot.say(
_botConfig.nickservBot,
`${_botConfig.nickservAPI} identify ${to}`
);
}
}
/**
* ## watchActive
*
* sets the latest active time for a user in a channel
*
* @param {String} from originating channel
* @param {String} to originating user
*
* @return {Void}
*/
function watchActive(from, to) {
const ignoreTheBots = _botConfig.bots || [];
if (ignoreTheBots.indexOf(to) === -1) {
if (!_bot.active[from]) {
_bot.active[from] = {};
}
_bot.active[from][to] = Date.now();
}
}
start();
return this;
};
function _val(commander, commanderConfig) {
return new _Val(commander, commanderConfig);
}
const valConfig = require('./config/_val.config.js');
const packageJSON = require('./package.json');
let connectionTimer = null;
let up = Date.now();
let lastPing = Date.now();
valConfig.version = packageJSON.version;
const req = (valConfig.req = {});
(req.http = require('http')),
(req.https = require('https')),
(req.fs = require('fs')),
(req.chalk = require('chalk'));
req.request = require('request');
valConfig.commandModules = [];
const commanders = valConfig.command;
const cores = [];
for (let commander in commanders) {
const commandObj = commanders[commander];
if (commandObj.enabled !== false) {
cores.push(_val(commander, valConfig));
}
}
module.exports = cores;