index.js
/*jslint node: true*/
"use strict";
var EventEmitter = require('eventemitter3');
var Parser = require('./lib/parser');
var net = require('net');
var tls = require('tls');
var fs = require('fs');
var replies = require('irc-replies');
var StreamReadable = require('stream').Readable;
var StreamWritable = require('stream').Writable;
var utils = require('./lib/utils');
var RateLimiter = require('limiter').RateLimiter;
/**
* Client constructor
*
* Initializes the client object, sets max listeners,
* initializes stream buffer and other variables.
* Then loads plugins and parses the network info passed.
* Check Client.add for more information about the network info.
*
* @class
* @param {object} info network configuration object
* @param {bool} throttling enable/disable throttling
* @property {string} version coffea version
* @property {object} me client irc user
*/
function Client(info, throttling) {
if (!(this instanceof Client)) { return new Client(info, throttling); }
try {
var pkg = require('./package.json');
this.version = pkg.version;
} catch (err) { }
this.streams = {};
this.settings = {};
this.stinfo = {};
this.networked_me = {};
this.capabilities = [];
this._loadPlugins();
if (typeof info === 'boolean') {
throttling = info;
info = null;
}
if (throttling !== undefined) {
this.throttling = throttling;
}
if (info) {
// compatibility
this.add(info);
}
}
// expose client
module.exports = Client;
// inherit from Emitter.prototype to make Client and EventEmitter
utils.inherit(Client, EventEmitter);
Client.prototype.getInfo = function (network) {
return this.stinfo[network];
};
Client.prototype.splitString = function (text) {
var message = text.match(/[^\s"']+|"([^"]*)"|'([^']*)'/g);
message = message && message.map(function (m) {
if (((m.charAt(0) === '"') && (m.charAt(m.length - 1) === '"')) ||
((m.charAt(0) === '\'') && (m.charAt(m.length - 1) === '\''))) {
return m.substring(1, m.length - 1).split('\\').join("");
// .split.join is actually faster than .replace on v8: http://stackoverflow.com/a/1145525/702288
}
return m;
});
return message;
};
/**
* Internal function that loads all plugins
* Later this should be replaced with a plugin manager
*
* @api private
*/
Client.prototype._loadPlugins = function() {
var _this = this;
var files = fs.readdirSync(__dirname + '/lib/plugins/');
files.forEach(function (file) {
if (file.substr(-3, 3) === '.js') {
// console.log('Loading plugin', file);
_this.use(require(__dirname + '/lib/plugins/' + file)());
}
});
};
/**
* Internal function that does a sanity check
* on the network information, adding defaults
*
* @params {Object} network
* @return {Object} network
* @api private
*/
Client.prototype._check = function (network) {
var ret = {};
var randnick = "coffea"+Math.floor(Math.random() * 100000);
if (typeof network === 'string') {
ret.host = network; // super lazy config - a host was passed as a string
} else {
ret.host = network.host === undefined ? null : network.host; // Required.
}
ret.name = network.name;
ret.nick = network.nick === undefined ? randnick : network.nick;
var port = network.ssl === true ? 6697 : 6667;
ret.port = parseInt(network.port === undefined ? port : network.port);
ret.ssl = network.ssl === undefined ? (ret.port === 6697) : network.ssl;
ret.ssl_allow_invalid = network.ssl_allow_invalid === undefined ? false : network.ssl_allow_invalid;
ret.username = network.username === undefined ? ret.nick : network.username;
ret.realname = network.realname === undefined ? ret.nick : network.realname;
ret.pass = network.pass;
ret.prefix = network.prefix === undefined ? '!' : network.prefix;
ret.channels = network.channels === undefined ? ret.channels : network.channels;
ret.throttling = network.throttling;
ret.sasl = network.sasl === undefined? null : network.sasl;
ret.nickserv = network.nickserv === undefined? null : network.nickserv;
return ret;
};
/**
* Internal function that loads a stream into the client
* Returns specified network name or generated stream id
*
* @params {Object} stream Must be an instanceof StreamReadable and StreamWritable
* @params {string} network Specify a network name/stream id
* @return {string} stream_id
* @api private
*/
Client.prototype._useStream = function (stream, network, throttling, info) {
if (network) { stream.coffea_id = network; } // user-defined stream id
else { stream.coffea_id = Object.keys(this.streams).length.toString(); } // assign unique id to stream
stream.setEncoding('utf8'); // set stream encoding
throttling = ((throttling === undefined) ? this.throttling : throttling);
// rate limiting/throttling
stream.limiter = new RateLimiter(1, (typeof throttling === 'number') ? throttling : 250, (throttling === false));
// set up parser
var parser = new Parser();
var _this = this;
parser.on('message', function (msg) {
_this.onmessage(msg, stream.coffea_id);
});
parser.on('end', function() {
utils.emit(_this, stream.coffea_id, 'disconnect', {});
});
stream.pipe(parser);
// add stream to client
this.streams[stream.coffea_id] = stream;
this.settings[stream.coffea_id] = {};
this.stinfo[stream.coffea_id] = info;
// return stream id
return stream.coffea_id;
};
/* Depreciated. This is here for compatibility. */
Client.prototype.useStream = function (stream, network) {
this._useStream(stream, network, network.throttling, network);
};
/**
* Reconnects the socket that is assigned to the current stream_id.
*
* @params {string} stream_id
*/
Client.prototype.reconnect = function (stream_id) {
var info = this.stinfo[stream_id];
var stream = info.ssl ? tls.connect({host: info.host, port: info.port}) : net.connect({host: info.host, port: info.port});
this._useStream(stream, stream_id, info.throttling, info);
this._connect(stream_id, info);
};
/**
* Internal function to handle incoming messages from the streams
*
* @params {string} msg
* @api private
*/
Client.prototype.onmessage = function (msg, network) {
msg.command = replies[msg.command] || msg.command;
utils.emit(this, network, 'data', msg);
};
Client.prototype._emitConnect = function (network) {
utils.emit(this, network, 'connect', null); // We don't need no data
};
Client.prototype._setupSASL = function (stream_id, info) {
this.on('cap_ack', function (err, event) {
if (event.capability === 'sasl') {
this.sasl.mechanism('PLAIN', stream_id);
if (info.sasl && info.sasl.account && info.sasl.password) {
this.sasl.login(info.sasl.account, info.sasl.password, stream_id);
} else if (info.sasl && info.sasl.password) {
this.sasl.login(info.username, info.sasl.password, stream_id);
} else {
this.sasl.login(null, null, stream_id);
}
}
});
};
Client.prototype._connect = function (stream_id, info) {
this._setupSASL(stream_id, info);
if (info.pass) { this.pass(info.pass); }
this.capReq(['account-notify', 'away-notify', 'extended-join', 'sasl'], stream_id);
this.capEnd(stream_id);
this.nick(info.nick, stream_id);
this.user(info.username, info.realname, stream_id);
if (info.nickserv && info.nickserv.username && info.nickserv.password) {
this.identify(info.nickserv.username, info.nickserv.password, stream_id);
} else if (info.nickserv && info.nickserv.password) {
this.identify(info.nickserv.password, stream_id);
}
if (info.channels) {
this.on('motd', function defaultOnMotd(err, event) {
this.join(info.channels, stream_id);
});
}
};
/**
* Add a network to the client, the argument can be a stream, network config object
* or an array of network config objects (see README.md and wiki for more information)
* Returns specified network name or generated stream id
*
* @params {Object} info
* @return {string} stream_id
* @api public
*/
Client.prototype.add = function (info) {
var stream, stream_id, streams = [];
var _this = this;
if (info instanceof Array) {
// We've been passed multiple server information
info.forEach(function(network) {
var stream;
network = _this._check(network);
if (network.ssl) {
stream = tls.connect({
host: network.host,
port: network.port,
rejectUnauthorized: !network.ssl_allow_invalid
}, function() {
stream_id = _this._useStream(stream, network.name, network.throttling, network);
utils.emit(_this, stream_id, 'ssl-error', new utils.SSLError(stream.authorizationError));
_this._connect(stream_id, network);
streams.push(stream_id);
_this._emitConnect(stream_id);
});
} else {
stream = net.connect({host: network.host, port: network.port}, function () {
stream_id = _this._useStream(stream, network.name, network.throttling, network);
_this._connect(stream_id, network);
streams.push(stream_id);
_this._emitConnect(stream_id);
});
}
});
} else if ((typeof info === 'string') || (info instanceof Object && !(info instanceof StreamReadable) && !(info instanceof StreamWritable))) {
// We've been passed single server information
info = _this._check(info);
if (info.ssl) {
stream = tls.connect({
host: info.host,
port: info.port,
rejectUnauthorized: !info.ssl_allow_invalid
}, function() {
stream_id = _this._useStream(stream, info.name, info.throttling, info);
utils.emit(_this, stream_id, 'ssl-error', new utils.SSLError(stream.authorizationError));
_this._connect(stream_id, info);
_this._emitConnect(stream_id)
});
} else {
stream = net.connect({host: info.host, port: info.port}, function () {
stream_id = _this._useStream(stream, info.name, info.throttling, info);
_this._connect(stream_id, info);
_this._emitConnect(stream_id);
});
}
} else {
// Assume we've been passed the legacy stream.
stream_id = this._useStream(info, null, info.throttling, info);
}
if(streams.length === 0) {
return stream_id;
} else {
return streams;
}
};
/**
* Write data to a specific network (stream)
*
* @params {string} str
* @params {string} network
* @params {Function} fn
* @api public
*/
Client.prototype.write = function (str, network, fn) {
// if network is the callback, then it wasn't defined either
if (typeof(network) === 'function') {
fn = network;
network = undefined;
}
// somebody passed the stream, not the id, get id from stream
if (network !== null && typeof network === 'object') {
network = network.coffea_id;
}
var _this = this;
if (network && this.streams.hasOwnProperty(network)) {
// send to specified network
// this.streams[network].limiter.removeTokens(1, function() {
_this.streams[network].write(str + '\r\n', fn);
// });
} else {
// send to all networks
for (var id in this.streams) {
if (this.streams.hasOwnProperty(id)) {
this.write(str, id);
}
}
if (fn) {
fn();
}
}
};
/**
* Load a plugin into the client
*
* @params {Function} fn
* @return {Object} this
* @api public
*/
Client.prototype.use = function (fn) {
fn(this);
return this;
};
/**
* fallback for `function(event)` callbacks
*
* @api private
*/
Client.prototype.fallbackCallback = function fallbackCallback(extend, event, fn, context) {
var params = utils.getParamNames(fn);
var func = fn;
if (params.length === 1) {
func = function(err, event) {
fn(event, err);
};
}
extend.call(this, event, func, context);
};
/**
* apply fallback to `client.on()` events
*
* @api private
*/
Client.prototype.on = function on(event, fn, context) {
this.fallbackCallback(this.parent.on, event, fn, context);
};
/**
* apply fallback to `client.once()` events
*
* @api private
*/
Client.prototype.once = function once(event, fn, context) {
this.fallbackCallback(this.parent.once, event, fn, context);
};