dbot/jsbot/jsbot.js

573 lines
17 KiB
JavaScript
Raw Permalink Normal View History

var _ = require('underscore')._,
net = require('net'),
async = require('async'),
tls = require('tls'),
Tokenizer = require('./tokenizer');
var JSBot = function(nick) {
this.nick = nick;
this.connections = {};
this.ignores = {};
this.preEmitHooks = [];
this.events = {
'JOIN': [],
'PART': [],
'QUIT': [],
'NICK': [],
'PRIVMSG': [],
'MODE': [],
'KICK': []
};
this.addDefaultListeners();
};
// connections
var Connection = function(name, instance, host, port, owner, onReady, nickserv, password, tlsOptions) {
this.name = name;
this.instance = instance;
this.host = host;
this.port = port;
this.owner = owner;
this.onReady = onReady;
this.nickserv = nickserv;
this.password = password;
this.tlsOptions = tlsOptions;
this.channels = {};
this.commands = {};
this.encoding = 'utf8';
this.netBuffer = '';
this.conn = null;
this.lastSent = Date.now();
};
Connection.prototype.connect = function() {
if((typeof this.port == 'string' || this.port instanceof String) && this.port.substring(0, 1) == '+') {
this.conn = tls.connect(parseInt(this.port.substring(1)), this.host, this.tlsOptions);
} else {
this.conn = net.createConnection(this.port, this.host);
}
console.log('trying to connect' + this.port + ' ' + this.host);
this.conn.setTimeout(60 * 60 * 1000);
this.conn.setEncoding(this.encoding);
this.conn.setKeepAlive(enable=true, 10000);
connectListener = function() {
this.send('NICK', this.instance.nick);
//this.send('USER', this.instance.nick, '0', '*', this.instance.nick);
this.send('PASS ' + this.instance.nick + ':' + this.password);
this.send('USER', this.instance.nick + ' ' + this.instance.nick + ' ' + this.instance.nick, ' ', this.instance.nick);
}.bind(this);
this.conn.addListener('connect', connectListener.bind(this));
this.conn.addListener('secureConnect', connectListener.bind(this));
this.conn.addListener('error', function(err) {
console.log('hi')
console.log(err);
console.error(err);
});
this.conn.addListener('data', function(chunk) {
this.netBuffer += chunk;
console.log(chunk)
var t = new Tokenizer(this.netBuffer);
while(true) {
var line = t.tokenize('\r\n');
if(line == null) {
this.netBuffer = t.tokenize(null);
break;
}
this.instance.parse(this, line);
}
}.bind(this));
};
Connection.prototype.send = function() {
var message = [].splice.call(arguments, 0).join(' ');
//if(Date.now() > this.lastSent + 500) {
message += '\r\n';
this.conn.write(message, this.encoding);
this.lastSent = Date.now();
//} else {
/* setImmediate(function() {
this.send(message);
}.bind(this));
}*/
};
Connection.prototype.pong = function(message) {
this.send('PONG', ':' ); //+ message.split(':')[1]);
};
Connection.prototype.join = function(channel) {
this.send('JOIN', channel);
};
Connection.prototype.part = function(channel) {
this.send('PART', channel);
};
JSBot.prototype.addConnection = function(name, host, port, owner, onReady, nickserv, password, tlsOptions) {
tlsOptions = tlsOptions || {};
tlsOptions = _.defaults(tlsOptions, {rejectUnauthorized: false});
this.connections[name] = new Connection(name, this, host, port, owner, onReady,
nickserv, password, tlsOptions);
};
JSBot.prototype.connect = function(name) {
var conn = this.connections[name];
this.addListener('001', 'onReady', function(event) {
conn.instance.say(conn.name, conn.nickserv, 'IDENTIFY ' + this.nick + " " + conn.password);
setTimeout(function() {
if(conn.onReady != null)
conn.onReady(event);
}, 5000);
});
conn.connect();
};
JSBot.prototype.connectAll = function() {
_.each(this.connections, function(connection, name) {
this.connect(name);
}, this);
};
// event parsing and processing
JSBot.prototype.parse = function(connection, input) {
var event = new Event(this),
t = new Tokenizer(input);
event.server = connection.name;
event.allChannels = this.connections[event.server].channels;
if(input[0] == ':') {
// consume to next whitespace, strip leading ':'
var prefix = t.tokenize(' '),
maskMatch = prefix.match(/:(.+)!(.+)@(.+)/);
if(maskMatch && maskMatch.length == 4) {
event.user = maskMatch[1];
event.ident = maskMatch[2];
event.host = maskMatch[3];
}
else {
event.host = prefix.substring(1);
}
}
/* parameter string extraction */
// try consuming to beginning of a message
var paramsStr = t.tokenize(' :');
if(!paramsStr) {
// if that fails (no message), fall back to line ending
paramsStr = t.tokenize(null);
} else {
// first attempt succeeded, extract message
event.message = t.tokenize(null);
}
// split the parameter string
event.args = paramsStr.split(' ');
// use first item as action, remove from list
event.action = event.args.shift();
// -- Common Event Variables --
// All channel/nick/target parameters in server-to-client events are accounted for here.
// Others need to be handled manually via event.params.
if (/^\d+$/.test(event.action)) {
var rsp = parseInt(event.action),
nickRsps = [ 301, 311, 312, 313, 317, 318, 319, 314,
369, 322, 324, 338, 401, 406, 432, 433, 436 ],
channelRsps = [ 322, 324, 331, 332, 346, 347, 348, 349,
366, 367, 368, 403, 404, 405, 467, 471,
473, 474, 475, 476, 477, 478, 482 ],
channelNickRsps = [ 325, 341 ],
targetRsps = [ 407, 437 ];
if(nickRsps.indexOf(rsp) != -1) {
event.user = event.args[0];
}
else if(channelRsps.indexOf(rsp) != -1) {
event.channel = event.args[0];
}
else if(channelNickRsps.indexOf(rsp) != -1) {
event.channel = event.args[0];
event.user = event.args[1];
}
else if(targetRsps.indexOf(rsp) != -1) {
if ('&#!+.~'.indexOf(event.args[0][0]) != -1) {
event.channel = event.args[0];
} else {
event.user = event.args[0];
}
}
else if(rsp == 352) {
event.channel = event.args[0];
event.user = event.args[4];
}
else if(rsp == 353) {
event.channel = event.args[2];
}
else if(rsp == 441) {
event.user = event.args[0];
event.channel = event.args[1];
}
}
else {
if(event.action == 'PRIVMSG') {
if('&#!+.~'.indexOf(event.args[0][0]) != -1) {
event.channel = event.args[0];
}
}
else if(event.action == 'JOIN' ||
event.action == 'PART' ||
event.action == 'TOPIC')
{
event.channel = event.args[0];
}
else if(event.action == 'KICK') {
event.channel = event.args[0];
event.targetUser = event.args[1];
}
else if(event.action == 'NICK') {
event.newNick = event.args[1];
event.multiChannel = true;
}
else if(event.action == 'MODE') {
event.channel = event.args[0];
event.modeChanges = event.args[1];
if(event.args.length > 2) {
event.targetUsers = event.args.slice(2);
}
}
else if(event.action == 'QUIT') {
event.multiChannel = true;
}
if(event.multiChannel) {
// populate a list of channels this event applies to
event.channels = [];
for(var ch in event.allChannels) {
for(var nick in event.allChannels[ch].nicks) {
if(nick == event.user) {
event.channels.push(event.allChannels[ch]);
}
}
}
}
else if(event.channel && event.channel in event.allChannels) {
// replace the channel name with it's coresponding object
event.channel = event.allChannels[event.channel];
} else {
event.channel = {
'name': event.user,
'nicks': {},
'toString': function() {
return this.name;
}
}
}
}
// Run any pre-emit hooks
async.eachSeries(this.preEmitHooks, function(hook, callback) {
hook(event, callback);
}, function(err) {
this.emit(event);
}.bind(this));
// for handlers
if(event.message) {
event.params = event.message.split(' ');
} else {
event.params = [];
}
};
JSBot.prototype.addPreEmitHook = function(func) {
this.preEmitHooks.push(func);
};
JSBot.prototype.clearHooks = function() {
this.preEmitHooks = [];
};
JSBot.prototype.emit = function(event) {
if(event.action in this.events) {
_.each(this.events[event.action], function(listener) {
var eventFunc = listener.listener;
var channel = false;
if(event.channel) {
channel = event.channel.name;
}
if(_.isFunction(eventFunc) && this.ignores &&
(_.has(this.ignores, event.user) && _.include(this.ignores[event.user], listener.tag)) == false &&
(_.has(this.ignores, channel) && _.include(this.ignores[channel], listener.tag)) == false) {
try {
eventFunc.call(this, event);
} catch(err) {
console.log('ERROR: ' + eventFunc + '\n' + err);
console.log(err.stack.split('\n')[1].trim());
}
}
}.bind(this));
}
};
// client functionality
JSBot.prototype.say = function(server, channel, msg) {
var event = new Event(this);
event.server = server;
event.channel = channel;
event.msg = msg;
event.reply(msg);
};
JSBot.prototype.reply = function(event, msg) {
this.connections[event.server].send('PRIVMSG ' + event.channel + ' ' + ':' + msg);
};
JSBot.prototype.act = function(event, msg) {
this.connections[event.server].send('PRIVMSG', event.channel, '\001ACTION ' + msg + '\001');
}
JSBot.prototype.replyNotice = function(event, msg) {
this.connections[event.server].send('NOTICE', event.user , ':' + msg);
}
JSBot.prototype.join = function(event, channel) {
this.connections[event.server].join(channel);
};
JSBot.prototype.part = function(event, channel) {
this.connections[event.server].part(channel);
};
JSBot.prototype.mode = function(event, channel, msg) {
this.connections[event.server].send('MODE', channel, msg);
}
JSBot.prototype.nick = function(event, nick) {
this.connections[event.server].send('NICK', nick);
}
// listeners
JSBot.prototype.addListener = function(index, tag, func) {
if(!(index instanceof Array)) {
index = [index];
}
var listener = {
'listener': func,
'tag': tag
};
_.each(index, function(type) {
if(!_.has(this.events, type)) {
this.events[type] = [];
}
this.events[type].push(listener);
}, this);
};
JSBot.prototype.removeListeners = function() {
this.events = {
'JOIN': [],
'PART': [],
'QUIT': [],
'NICK': [],
'PRIVMSG': [],
'MODE': [],
'KICK': []
};
this.addDefaultListeners();
};
JSBot.prototype.addDefaultListeners = function() {
// PING
this.addListener('PING', 'pong', function(event) {
this.connections[event.server].pong(event.message);
}.bind(this));
// JOIN
this.addListener('JOIN', 'joinname', function(event) {
if(event.user != this.nick) {
if(!_.has(this.connections[event.server].channels[event.channel].nicks, event.user)) {
this.connections[event.server].channels[event.channel].nicks[event.user] = {
'name': event.user,
'op': false,
'voice': false,
'toString': function() {
return this.name;
}
};
}
event.user = this.connections[event.server].channels[event.channel].nicks[event.user];
}
}.bind(this));
// PART
this.addListener('PART', 'partname', function(event) {
if(event.user == this.nick)
delete this.connections[event.server].channels[event.channel];
else
delete event.channel.nicks[event.user];
}.bind(this));
// KICK
this.addListener('KICK', 'kickname', function(event) {
if(event.targetUser == this.nick)
delete this.connections[event.server].channels[event.channel];
else
delete event.channel.nicks[event.user];
}.bind(this));
// QUIT
this.addListener('QUIT', 'quitname', function(event) {
_.each(event.allChannels, function(channel) {
delete event.allChannels[channel].nicks[event.user];
});
});
// NICK
this.addListener('NICK', 'nickchan', function(event) {
if(event.user == this.nick) {
this.nick = event.message;
} else {
_.each(event.allChannels, function(channel) {
if(_.has(channel, 'nicks')) {
if(event.user in channel.nicks) {
channel.nicks[event.message] = channel.nicks[event.user];
channel.nicks[event.message].name = event.message;
delete channel.nicks[event.user];
}
}
});
}
}.bind(this));
// MODE
this.addListener('MODE', 'modop', function(event) {
if(!event.modeChanges || !event.targetUsers)
return;
var changeSets = event.modeChanges.match(/[+-][ov]+/);
if(!changeSets)
return;
for(var i=0; i < changeSets.length && i < event.targetUsers.length; ++i) {
var chanUser = event.channel.nicks[event.targetUsers[i]],
prefix = changeSets[i].match(/[+-]/)[0],
flags = changeSets[i].match(/[ov]+/)[0],
value = prefix == '+';
if(!chanUser) {
event.channel.nicks[event.targetUsers[i]] = {
'name': event.targetUsers[i],
'op': false,
'voice': false,
'toString': function() {
return this.name;
}
};
chanUser = event.channel.nicks[event.targetUsers[i]];
}
for(var f=0; f < flags.length; ++f) {
if(flags[f] == 'o') {
chanUser.op = value;
}
else if(flags[f] == 'v')
chanUser.voice = value;
}
}
});
// 353 replies
this.addListener('353', 'names', function(event) {
if(!_.has(this.connections[event.server].channels, event.channel)) {
this.connections[event.server].channels[event.channel] = {
'name': event.channel,
'nicks': {},
'toString': function() {
return this.name;
}
};
}
event.channel = this.connections[event.server].channels[event.channel];
for(var i=0; i < event.params.length; ++i) {
var hasFlag = '~&@%+'.indexOf(event.params[i][0]) != -1,
name = hasFlag ? event.params[i].slice(1) : event.params[i];
event.channel.nicks[name] = {
'name': name,
'op': hasFlag && event.params[i][0] == '@',
'voice': hasFlag && event.params[i][0] == '+',
'toString': function() {
return this.name;
}
};
}
});
this.addListener('PRIVMSG', 'ping', function(event) {
//if(event.message.match(/\x01PING .+\x01/) != null)
event.replyNotice(event.message);
});
};
// ignore functionality
JSBot.prototype.ignoreTag = function(item, tag) {
if(_.has(this.ignores, item) == false)
this.ignores[item] = [];
this.ignores[item].push(tag);
}
JSBot.prototype.clearIgnores = function() {
this.ignores = {};
}
JSBot.prototype.removeIgnore = function(item, tag) {
if(_.has(this.ignores, item) && _.include(this.ignores[item], tag))
this.ignores[item].slice(this.ignores[item].indexOf(tag), 1);
}
// events
var Event = function(instance) {
this.instance = instance;
};
Event.prototype.reply = function(msg) {
this.instance.reply(this, msg);
};
Event.prototype.replyNotice = function(msg) {
this.instance.replyNotice(this, msg);
}
// export that shit
exports.createJSBot = function(name) {
return new JSBot(name);
};