Skip to content
26 changes: 26 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"env": {
"jasmine": true
},
"extends": "airbnb-base",
"parserOptions": {
"sourceType": "script"
},
"rules": {
"brace-style": [2, "stroustrup"],
"consistent-return": 0,
"curly": [2, "all"],
"function-paren-newline": 0,
"import/no-dynamic-require": 0,
"import/no-extraneous-dependencies": [2, {"devDependencies": ["**/*spec.js"]}],
"indent": [2, 4],
"max-len": [2, 100],
"newline-per-chained-call": 0,
"no-multi-assign": 0,
"no-multi-spaces": [2, {"ignoreEOLComments": true}],
"no-shadow": [2, {"allow": ["err"]}],
"no-underscore-dangle": [2, {"allowAfterThis": true}],
"object-curly-spacing": [2, "never"],
"radix": [2, "as-needed"]
}
}
6 changes: 4 additions & 2 deletions borgil.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
var Bot = require('./bot/bot');
'use strict';

const Bot = require('./bot/bot');

var borgil = new Bot();

module.exports = new Bot();
157 changes: 133 additions & 24 deletions bot/bot.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,146 @@
var EventEmitter = require('eventemitter2').EventEmitter2;
var util = require('util');
'use strict';

var Config = require('./config');
var Plugin = require('./plugin');
const EventEmitter = require('eventemitter2').EventEmitter2;
const fs = require('fs');
const handlebars = require('handlebars');
const moment = require('moment-timezone');
const path = require('path');
const winston = require('winston');

const Config = require('./config');
const Plugin = require('./plugin');

// The bot object.
var Bot = module.exports = function (configfile) {
// Run the event emitter constructor.
EventEmitter.call(this);

this.config = new Config(configfile);
const logDefaults = {
dir: 'logs',
filename_template: 'bot--{{date}}.log',
date_format: 'YYYY-MM-DD--HH-mm-ss',
console: false,
debug: false,
};
const defaultBuffer = 100;

this.plugins = {};
this.memory = {};
module.exports = class Bot extends EventEmitter {
constructor(configfile) {
super();

// Include extra functionality.
require('./log')(this);
require('./transports')(this);
require('./buffers')(this);
this.config = new Config(configfile);

// Activate all plugins mentioned in the plugins section of the config.
for (pluginName in this.config.get('plugins', {})) {
this.log.info('Activating plugin:', pluginName);
// A shared key/value store that all plugins can read and write to.
this.memory = new Map();

this.initLog();
this.initBuffers();
this.initTransports();
this.initPlugins();
}

initLog() {
const level = this.config.get('log.debug') ? 'debug' : 'info';
const renderFilename = handlebars.compile(
this.config.get('log.filename_template', logDefaults.filename_template));

const logdir = this.config.get('log.dir', logDefaults.dir);
try {
var plugin = new Plugin(this, pluginName);
fs.mkdirSync(logdir);
}
catch (err) {
this.log.warn('Error activating plugin %s:', pluginName, err.message);
continue;
if (err.code !== 'EEXIST') {
throw err;
}
}
this.plugins[pluginName] = plugin;

const dateFormat = this.config.get('log.date_format', logDefaults.dateFormat);
const timezone = this.config.get('log.timezone');
const logfile = path.join(logdir, renderFilename({
date: (timezone ? moment.tz(timezone) : moment()).format(dateFormat),
}));

const transports = [];
if (logfile) {
transports.push(new winston.transports.File({
filename: logfile,
json: false,
level,
timestamp: true,
}));
}
if (this.config.get('log.console')) {
transports.push(new winston.transports.Console({
colorize: true,
level,
timestamp: true,
}));
}

this.log = new winston.Logger({
transports,
});
}

initBuffers() {
// Create buffer objects for each client.
this.buffers = {};

// Log each message to a buffer.
this.on('message', (transport, msg) => {
// Initialize buffer for this transport and source if necessary.
if (!(transport.name in this.buffers)) {
this.buffers[transport.name] = {};
}
if (!(msg.replyto in this.buffers[transport.name])) {
this.buffers[transport.name][msg.replyto] = [];
}
const buffer = this.buffers[transport.name][msg.replyto];

// Trim buffer to maximum length, then add this message.
if (buffer.length >= this.config.get('buffer', defaultBuffer)) {
buffer.pop();
}
buffer.unshift(msg);
});
}

// Activate all transports mentioned in the transports section of the config.
initTransports() {
const tpConfigs = this.config.get('transports', {});

this.transports = {};
Object.keys(tpConfigs).forEach((tpName) => {
this.log.info('Activating transport:', tpName);
const tpConfig = tpConfigs[tpName];
let transport;
try {
// eslint-disable-next-line global-require
const TpType = require(`./transports/${tpConfig.type}`);
transport = new TpType(this, tpName, tpConfig);
}
catch (err) {
this.log.warn('Error activating transport %s:', tpName, err.message);
return;
}
this.transports[tpName] = transport;
});

this.log.info(Object.keys(this.transports).length, 'transports(s) activated.');
}

// Activate all plugins mentioned in the plugins section of the config.
initPlugins() {
this.plugins = {};
Object.keys(this.config.get('plugins', {})).forEach((pluginName) => {
this.log.info('Activating plugin:', pluginName);
let plugin;
try {
plugin = new Plugin(this, pluginName);
}
catch (err) {
this.log.warn('Error activating plugin %s:', pluginName, err.message);
return;
}
this.plugins[pluginName] = plugin;
});

this.log.info(Object.keys(this.plugins).length, 'plugin(s) activated.');
}
};
// Extend the event emitter class.
util.inherits(Bot, EventEmitter);
24 changes: 0 additions & 24 deletions bot/buffers.js

This file was deleted.

125 changes: 66 additions & 59 deletions bot/config.js
Original file line number Diff line number Diff line change
@@ -1,87 +1,94 @@
var extend = require('extend');
var fs = require('fs');
var yaml = require('js-yaml');
'use strict';

const extend = require('extend');
const fs = require('fs');
const yaml = require('js-yaml');


function getValue(obj, elems) {
if (elems.length == 1) {
const value = obj[elems[0]];

if (elems.length === 1) {
// Leaf node.
return obj[elems[0]];
return value;
}

if (typeof obj[elems[0]] != 'object') {
if (!value || typeof value !== 'object') {
// Branch doesn't exist.
return undefined;
}

// Move down the tree.
return getValue(obj[elems[0]], elems.slice(1));
return getValue(value, elems.slice(1));
}

function splitPath(key, value) {
value = splitValue(value);
module.exports = class Config {
constructor(configInit) {
this.config = {};

// If the key is a dotted path, split off the first element and parse the rest.
var tree = {};
var kelem = Array.isArray(key) ? key : key.split('.');
tree[kelem[0]] = kelem.length == 1 ? value : splitPath(kelem.slice(1), value);
return tree;
}
if (typeof configInit === 'object') {
// If an object is passed, use it as the config.
this.config = Config.splitValue(configInit);
}
else {
// Otherwise treat it as a filename and read that file as YAML or JSON.
// Fall back to default filenames if no existing filename was passed.
const configPath = [configInit, 'config.json', 'config.yml']
.find(fs.existsSync.bind(fs));
if (!configPath) {
return console.error('Config file not found.');
}

function splitValue(value) {
if (Array.isArray(value)) {
// Parse each value individually.
return value.map(splitValue);
}
if (typeof value === 'object') {
// Parse each key and value individually.
var tree = {};
for (key in value) {
extend(true, tree, splitPath(key, value[key]));
const configFile = fs.readFileSync(configPath, 'utf-8');

try {
if (configPath.match(/\.ya?ml$/i)) {
this.config = Config.splitValue(yaml.safeLoad(configFile));
}
else {
this.config = Config.splitValue(JSON.parse(configFile));
}
}
catch (err) {
console.error(`Error reading config file at ${configPath}:`, err.message);
}
}
return tree;
}
return value;
}

var Config = module.exports = function (config_init) {
this.config = {};
static splitPath(key, value) {
const val = Config.splitValue(value);

if (typeof config_init == 'object') {
// If an object is passed, use it as the config.
this.config = splitValue(config_init);
// If the key is a dotted path, split off the first element and parse the rest.
const tree = {};
const kelem = Array.isArray(key) ? key : key.split('.');
tree[kelem[0]] = kelem.length === 1 ? val : Config.splitPath(kelem.slice(1), val);
return tree;
}
else {
// Otherwise treat it as a filename and read that file as YAML or JSON.
// Fall back to default filenames if no existing filename was passed.
var configpath = [config_init, 'config.json', 'config.yml'].find(fs.existsSync.bind(fs));
if (!configpath) {
return console.error('Config file not found.');
}

var configfile = fs.readFileSync(configpath, 'utf-8');
static splitValue(value) {
if (Array.isArray(value)) {
// Parse each value individually.
return value.map(Config.splitValue);
}

try {
if (configpath.match(/\.ya?ml$/i)) this.config = splitValue(yaml.safeLoad(configfile));
else this.config = splitValue(JSON.parse(configfile));
} catch (e) {
console.error('Error reading config file at ' + configpath + ':', e.message);
if (value && typeof value === 'object') {
// Parse each key and value individually.
const tree = {};
Object.keys(value).forEach((key) => {
extend(true, tree, Config.splitPath(key, value[key]));
});
return tree;
}
}
};

Config.prototype.get = function (path, defval) {
var value = getValue(this.config, Array.isArray(path) ? path : path.split('.'));
return value !== undefined ? value : defval;
};
return value;
}

Config.prototype.set = function (path, value) {
//setValue(this.config, path, value);
extend(true, this.config, splitPath(path, value));
};
get(path, defval) {
const value = getValue(this.config, Array.isArray(path) ? path : path.split('.'));
return (value === undefined || value === null) ? defval : value;
}

Config.prototype.save = function () {
if (typeof config_init == 'string') {
fs.writeFile(config_init, JSON.stringify(this.config, null, 4), {encoding: 'utf-8'});
set(path, value) {
extend(true, this.config, Config.splitPath(path, value));
}
};
Loading