forked from GitHub/dbot
573 lines
17 KiB
JavaScript
573 lines
17 KiB
JavaScript
|
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);
|
||
|
};
|