ncurses
Version:
An ncurses binding for node.js
1,026 lines (990 loc) • 26.3 kB
JavaScript
/*
irc.js - Node JS IRC client library
(C) Copyright Martyn Smith 2010
This library is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
exports.Client = Client;
var util = require('util');
var net = require('net');
const replyFor = { // {{{
"200" : {
"name" : "rpl_tracelink",
"type" : "reply"
},
"201" : {
"name" : "rpl_traceconnecting",
"type" : "reply"
},
"202" : {
"name" : "rpl_tracehandshake",
"type" : "reply"
},
"203" : {
"name" : "rpl_traceunknown",
"type" : "reply"
},
"204" : {
"name" : "rpl_traceoperator",
"type" : "reply"
},
"205" : {
"name" : "rpl_traceuser",
"type" : "reply"
},
"206" : {
"name" : "rpl_traceserver",
"type" : "reply"
},
"208" : {
"name" : "rpl_tracenewtype",
"type" : "reply"
},
"211" : {
"name" : "rpl_statslinkinfo",
"type" : "reply"
},
"212" : {
"name" : "rpl_statscommands",
"type" : "reply"
},
"213" : {
"name" : "rpl_statscline",
"type" : "reply"
},
"214" : {
"name" : "rpl_statsnline",
"type" : "reply"
},
"215" : {
"name" : "rpl_statsiline",
"type" : "reply"
},
"216" : {
"name" : "rpl_statskline",
"type" : "reply"
},
"218" : {
"name" : "rpl_statsyline",
"type" : "reply"
},
"219" : {
"name" : "rpl_endofstats",
"type" : "reply"
},
"221" : {
"name" : "rpl_umodeis",
"type" : "reply"
},
"241" : {
"name" : "rpl_statslline",
"type" : "reply"
},
"242" : {
"name" : "rpl_statsuptime",
"type" : "reply"
},
"243" : {
"name" : "rpl_statsoline",
"type" : "reply"
},
"244" : {
"name" : "rpl_statshline",
"type" : "reply"
},
"251" : {
"name" : "rpl_luserclient",
"type" : "reply"
},
"252" : {
"name" : "rpl_luserop",
"type" : "reply"
},
"253" : {
"name" : "rpl_luserunknown",
"type" : "reply"
},
"254" : {
"name" : "rpl_luserchannels",
"type" : "reply"
},
"255" : {
"name" : "rpl_luserme",
"type" : "reply"
},
"256" : {
"name" : "rpl_adminme",
"type" : "reply"
},
"257" : {
"name" : "rpl_adminloc1",
"type" : "reply"
},
"258" : {
"name" : "rpl_adminloc2",
"type" : "reply"
},
"259" : {
"name" : "rpl_adminemail",
"type" : "reply"
},
"261" : {
"name" : "rpl_tracelog",
"type" : "reply"
},
"300" : {
"name" : "rpl_none",
"type" : "reply"
},
"301" : {
"name" : "rpl_away",
"type" : "reply"
},
"302" : {
"name" : "rpl_userhost",
"type" : "reply"
},
"303" : {
"name" : "rpl_ison",
"type" : "reply"
},
"305" : {
"name" : "rpl_unaway",
"type" : "reply"
},
"306" : {
"name" : "rpl_nowaway",
"type" : "reply"
},
"311" : {
"name" : "rpl_whoisuser",
"type" : "reply"
},
"312" : {
"name" : "rpl_whoisserver",
"type" : "reply"
},
"313" : {
"name" : "rpl_whoisoperator",
"type" : "reply"
},
"314" : {
"name" : "rpl_whowasuser",
"type" : "reply"
},
"315" : {
"name" : "rpl_endofwho",
"type" : "reply"
},
"317" : {
"name" : "rpl_whoisidle",
"type" : "reply"
},
"318" : {
"name" : "rpl_endofwhois",
"type" : "reply"
},
"319" : {
"name" : "rpl_whoischannels",
"type" : "reply"
},
"320" : { // freenode
"name" : "rpl_whoisidentified",
"type" : "reply"
},
"321" : {
"name" : "rpl_liststart",
"type" : "reply"
},
"322" : {
"name" : "rpl_list",
"type" : "reply"
},
"323" : {
"name" : "rpl_listend",
"type" : "reply"
},
"324" : {
"name" : "rpl_channelmodeis",
"type" : "reply"
},
"330" : { // ircu
"name" : "rpl_whoisaccount",
"type" : "reply"
},
"331" : {
"name" : "rpl_notopic",
"type" : "reply"
},
"332" : {
"name" : "rpl_topic",
"type" : "reply"
},
"338" : { // hybrid/ratbox/others
"name" : "rpl_whoisactually",
"type" : "reply"
},
"341" : {
"name" : "rpl_inviting",
"type" : "reply"
},
"342" : {
"name" : "rpl_summoning",
"type" : "reply"
},
"351" : {
"name" : "rpl_version",
"type" : "reply"
},
"352" : {
"name" : "rpl_whoreply",
"type" : "reply"
},
"353" : {
"name" : "rpl_namreply",
"type" : "reply"
},
"364" : {
"name" : "rpl_links",
"type" : "reply"
},
"365" : {
"name" : "rpl_endoflinks",
"type" : "reply"
},
"366" : {
"name" : "rpl_endofnames",
"type" : "reply"
},
"367" : {
"name" : "rpl_banlist",
"type" : "reply"
},
"368" : {
"name" : "rpl_endofbanlist",
"type" : "reply"
},
"369" : {
"name" : "rpl_endofwhowas",
"type" : "reply"
},
"371" : {
"name" : "rpl_info",
"type" : "reply"
},
"372" : {
"name" : "rpl_motd",
"type" : "reply"
},
"374" : {
"name" : "rpl_endofinfo",
"type" : "reply"
},
"375" : {
"name" : "rpl_motdstart",
"type" : "reply"
},
"376" : {
"name" : "rpl_endofmotd",
"type" : "reply"
},
"381" : {
"name" : "rpl_youreoper",
"type" : "reply"
},
"382" : {
"name" : "rpl_rehashing",
"type" : "reply"
},
"391" : {
"name" : "rpl_time",
"type" : "reply"
},
"392" : {
"name" : "rpl_usersstart",
"type" : "reply"
},
"393" : {
"name" : "rpl_users",
"type" : "reply"
},
"394" : {
"name" : "rpl_endofusers",
"type" : "reply"
},
"395" : {
"name" : "rpl_nousers",
"type" : "reply"
},
"401" : {
"name" : "err_nosuchnick",
"type" : "error"
},
"402" : {
"name" : "err_nosuchserver",
"type" : "error"
},
"403" : {
"name" : "err_nosuchchannel",
"type" : "error"
},
"404" : {
"name" : "err_cannotsendtochan",
"type" : "error"
},
"405" : {
"name" : "err_toomanychannels",
"type" : "error"
},
"406" : {
"name" : "err_wasnosuchnick",
"type" : "error"
},
"407" : {
"name" : "err_toomanytargets",
"type" : "error"
},
"409" : {
"name" : "err_noorigin",
"type" : "error"
},
"411" : {
"name" : "err_norecipient",
"type" : "error"
},
"412" : {
"name" : "err_notexttosend",
"type" : "error"
},
"413" : {
"name" : "err_notoplevel",
"type" : "error"
},
"414" : {
"name" : "err_wildtoplevel",
"type" : "error"
},
"421" : {
"name" : "err_unknowncommand",
"type" : "error"
},
"422" : {
"name" : "err_nomotd",
"type" : "error"
},
"423" : {
"name" : "err_noadmininfo",
"type" : "error"
},
"424" : {
"name" : "err_fileerror",
"type" : "error"
},
"431" : {
"name" : "err_nonicknamegiven",
"type" : "error"
},
"432" : {
"name" : "err_erroneusnickname",
"type" : "error"
},
"433" : {
"name" : "err_nicknameinuse",
"type" : "error"
},
"436" : {
"name" : "err_nickcollision",
"type" : "error"
},
"441" : {
"name" : "err_usernotinchannel",
"type" : "error"
},
"442" : {
"name" : "err_notonchannel",
"type" : "error"
},
"443" : {
"name" : "err_useronchannel",
"type" : "error"
},
"444" : {
"name" : "err_nologin",
"type" : "error"
},
"445" : {
"name" : "err_summondisabled",
"type" : "error"
},
"446" : {
"name" : "err_usersdisabled",
"type" : "error"
},
"451" : {
"name" : "err_notregistered",
"type" : "error"
},
"461" : {
"name" : "err_needmoreparams",
"type" : "error"
},
"462" : {
"name" : "err_alreadyregistred",
"type" : "error"
},
"463" : {
"name" : "err_nopermforhost",
"type" : "error"
},
"464" : {
"name" : "err_passwdmismatch",
"type" : "error"
},
"465" : {
"name" : "err_yourebannedcreep",
"type" : "error"
},
"467" : {
"name" : "err_keyset",
"type" : "error"
},
"471" : {
"name" : "err_channelisfull",
"type" : "error"
},
"472" : {
"name" : "err_unknownmode",
"type" : "error"
},
"473" : {
"name" : "err_inviteonlychan",
"type" : "error"
},
"474" : {
"name" : "err_bannedfromchan",
"type" : "error"
},
"475" : {
"name" : "err_badchannelkey",
"type" : "error"
},
"481" : {
"name" : "err_noprivileges",
"type" : "error"
},
"482" : {
"name" : "err_chanoprivsneeded",
"type" : "error"
},
"483" : {
"name" : "err_cantkillserver",
"type" : "error"
},
"491" : {
"name" : "err_nooperhost",
"type" : "error"
},
"501" : {
"name" : "err_umodeunknownflag",
"type" : "error"
},
"502" : {
"name" : "err_usersdontmatch",
"type" : "error"
},
"671" : {
"name" : "rpl_whoissecure",
"type" : "reply"
}
}; // }}}
function Client(server, nick, opt) {
var self = this, whois = {};
self.opt = {
server: server,
nick: nick,
password: null,
userName: 'nodebot',
realName: 'nodeJS IRC client',
port: 6667,
debug: false,
showErrors: false,
autoConnect: true,
autoRejoin: true,
channels: [],
retryCount: null,
retryDelay: 2000,
secure: false,
};
if (typeof arguments[2] == 'object') {
var keys = Object.keys(self.opt);
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
if (arguments[2][k] !== undefined)
self.opt[k] = arguments[2][k];
}
}
// TODO - fail if nick or server missing
// TODO - fail if username has a space in it
if (self.opt.autoConnect)
self.connect();
self.on("raw", function (message) { // {{{
switch (message.command) {
case "001":
case "002":
case "003":
case "004":
case "005":
case "250":
case "265":
case "266":
case "rpl_luserclient":
case "rpl_luserop":
case "rpl_luserchannels":
case "rpl_luserme":
case "rpl_luserunknown":
if (message.command === "004")
self.emit('ready');
// Welcome stuff, ignoring for now
break;
case "err_nicknameinuse":
if (!self.prevnick) {
if (typeof(self.opt.nickMod) == 'undefined')
self.opt.nickMod = 0;
self.opt.nickMod++;
self.send("NICK", self.opt.nick + self.opt.nickMod);
self.nick = self.opt.nick + self.opt.nickMod;
} else
self.nick = self.prevnick;
break;
case "rpl_whoisuser":
var who = message.args[1];
if (!whois[who])
whois[who] = {};
whois[who].username = message.args[2];
whois[who].host = message.args[3];
whois[who].realname = message.args.slice(5).join(' ');
break;
case "rpl_whoisidle":
var who = message.args[1];
whois[who].idle = parseInt(message.args[2]);
try {
whois[who].signon = parseInt(message.args[3]);
} catch (err) {}
break;
case "rpl_whoisoperator":
whois[message.args[1]].isServerOp = true;
break;
case "rpl_whoischannels":
var who = message.args[1], chans = message.args.slice(2);
if (!whois[who].channels)
whois[who].channels = [];
for (var i=0; i<chans.length; i++) {
if (chans[i][0] !== '#' && chans[i][0] !== '&')
whois[who].channels.push({ name: chans[i].substring(1), mode: chans[i][0] });
else
whois[who].channels.push({ name: chans[i] });
}
break;
case "rpl_whoisserver":
var who = message.args[1];
whois[who].server = { host: message.args[2], info: message.args[3] };
break;
case "378":
if (message.args[2] && message.args[2].substr(0, 3) === 'is ') {
// hostinfo
whois[message.args[1]].hostinfo = message.args[2];
}
break;
/*case "rpl_whoisidentified":
whois[message.args[1]].identifiedAs = message.args[2];
break;*/
/*case "rpl_whoisactually":
var host = messages.args[2].split('@'), username;
if (host.length > 1) {
username = host[0];
host = host.slice(1).join('@');
} else
host = host[0];
whois[message.args[1]].actually = { username: username, host: host, ip: message.args[3] };
break;*/
case "rpl_whoissecure":
whois[message.args[1]].secureConn = true;
break;
case "rpl_whoisaccount":
whois[message.args[1]].account = message.args[2];
break;
case "rpl_endofwhois":
var who = message.args[1];
if (whois[who]) {
self.emit('whois', who, whois[who]);
delete whois[who];
}
break;
case "PING":
self.send("PONG", message.args[0]);
break;
case "QUIT":
for (var i=0,keys=Object.keys(self.chans); i < keys.length; i++) {
if (Object.keys(self.chans[keys[i]].users).indexOf(message.nick) > -1)
self.emit('quit', keys[i], message.nick, message.args.join(" "));
}
if (self.opt.debug)
util.log("QUIT: " + message.prefix + " " + message.args.join(" "));
break;
case "NOTICE":
var from = message.nick, to = message.args[0], text = message.args[1];
self.emit('notice', from, to, text);
if (self.opt.debug && to == self.nick)
util.log('NOTICE from ' + (from ? from : '(server)') + ': ' + text);
break;
case "MODE":
if (self.opt.debug)
util.log("MODE:" + message.args[0] + " sets mode: " + message.args[1]);
if (message.args.length >= 3) {
var channel = self.chans[message.args[0]],
nicklist_offset = 2,
mode = message.args[1].split(''),
adding = (mode.shift() === "+");
for (var i = 0; i < mode.length; i++) {
self.emit('mode', message.args[0], message.nick, message.args[nicklist_offset+i], (adding ? '+' : '-') + mode[i]);
if (mode[i] == 'o') {
if (adding)
channel.users[message.args[nicklist_offset+i]] = '@';
else
channel.users[message.args[nicklist_offset+i]] = '';
} else if (mode[i] == 'v') {
if (adding)
channel.users[message.args[nicklist_offset+i]] = '+';
else
channel.users[message.args[nicklist_offset+i]] = '';
}
}
}
break;
case "rpl_away":
self.emit('away', message.args[1], message.args[2]);
break;
case "rpl_nowaway":
self.away = true;
break;
case "rpl_unaway":
self.away = false;
break;
case "NICK":
var oldnick = message.nick, newnick = message.args[0];
for (var i=0,keys=Object.keys(self.chans); i<keys.length; i++) {
if (Object.keys(self.chans[keys[i]].users).indexOf(oldnick) > -1) {
self.chans[keys[i]].users[newnick] = self.chans[keys[i]].users[oldnick];
delete self.chans[keys[i]].users[oldnick];
self.emit('nick', keys[i], oldnick, newnick);
}
}
self.emit('nick', undefined, oldnick, newnick);
break;
case "rpl_motdstart":
self.motd = message.args[1] + "\n";
break;
case "rpl_motd":
self.motd += message.args[1] + "\n";
break;
case "rpl_endofmotd":
case "err_nomotd":
self.motd += message.args[1] + "\n";
self.emit('motd', self.motd);
break;
case "rpl_namreply":
var channel = self.chans[message.args[2]];
var users = message.args[3].split(/ +/);
users.forEach(function (user) {
var match = user.match(/^([@+])?(.*)$/);
if (!match[1])
match[1] = "";
channel.users[match[2]] = match[1];
});
break;
case "rpl_endofnames":
var channel = self.chans[message.args[1]];
self.emit('names', message.args[1], channel.users);
self.send('MODE', message.args[1]);
break;
case "rpl_topic":
var channel = self.chans[message.args[1]];
if (channel)
channel.topic = message.args[2];
break;
case "333":
var channel = self.chans[message.args[1]];
if (channel) {
channel.topicBy = message.args[2];
// channel, topic, nick
self.emit('topic', message.args[1], channel.topic, channel.topicBy, true);
}
break;
case "TOPIC":
// channel, topic, nick
self.emit('topic', message.args[0], message.args[1], message.nick);
var channel = self.chans[message.args[0]];
if (channel) {
channel.topic = message.args[1];
channel.topicBy = message.nick;
}
break;
case "rpl_channelmodeis":
var channel = self.chans[message.args[1]];
if (channel)
channel.mode = message.args[2];
break;
case "329":
var channel = self.chans[message.args[1]];
if (channel)
channel.created = message.args[2];
break;
case "JOIN":
// channel, who
self.emit('join', message.args[0], message.nick);
self.emit('join' + message.args[0], message.nick);
if (self.nick == message.nick) {
self.chans[message.args[0]] = {
users: {},
};
}
else {
var channel = self.chans[message.args[0]];
channel.users[message.nick] = '';
}
break;
case "PART":
// channel, who, reason
self.emit('part', message.args[0], message.nick, message.args[1]);
self.emit('part' + message.args[0], message.nick, message.args[1]);
if (self.nick == message.nick)
delete self.chans[message.args[0]];
else {
var channel = self.chans[message.args[0]];
delete channel.users[message.nick];
}
break;
case "KICK":
// channel, who, by, reason
self.emit('kick', message.args[0], message.args[1], message.nick, message.args[2]);
self.emit('kick' + message.args[0], message.args[1], message.nick, message.args[2]);
if (self.nick == message.args[1])
delete self.chans[message.args[0]];
else {
var channel = self.chans[message.args[0]];
delete channel.users[message.args[1]];
}
break;
case "KILL":
var nick = message.args[0];
for (var channel in self.chans)
delete self.chans[channel].users[nick];
break;
case "PRIVMSG":
var from = message.nick;
var to = message.args[0];
var text = message.args[1];
self.emit('message', from, to, text);
if (to.match(/^[&#+!]/))
self.emit('message' + to, from, text);
if (to == self.nick)
self.emit('pm', from, text);
if (self.opt.debug && to == self.nick)
util.log('GOT MESSAGE from ' + from + ': ' + text);
break;
default:
if (message.commandType == 'error') {
self.emit('error', message);
if (self.opt.showErrors)
util.log("\033[01;31mERROR: " + util.inspect(message) + "\033[0m");
} else {
self.emit('unhandledMessage', message);
if (self.opt.debug)
util.log("\033[01;31mUnhandled message: " + util.inspect(message) + "\033[0m");
}
}
}); // }}}
self.on('kick', function(channel, who, by, reason) {
if (self.opt.autoRejoin)
self.send('JOIN', channel);
});
self.on('motd', function (motd) {
self.opt.channels.forEach(function(channel) {
self.send('JOIN', channel);
});
});
process.EventEmitter.call(this);
}
util.inherits(Client, process.EventEmitter);
Client.prototype.conn = null;
Client.prototype.chans = {};
Client.prototype.connect = function (retryCount) { // {{{
retryCount = retryCount || 0;
var self = this;
self.chans = {};
self.prevnick = undefined;
self.conn = net.createConnection(self.opt.port, self.opt.server);
self.conn.requestedDisconnect = false;
if (self.opt.secure) {
// Set up a secure (SSL) connection
if (typeof self.opt.secure !== 'object') {
// Do not verify connection
self.opt.secure = require('crypto').createCredentials({});
} else {
// Verify connection
self.conn.on("secure", function(){
if(!self.conn.verifyPeer()) {
if (self.opt.debug) {
util.log('Warning: failed to verify SSL peer certificate'+
' -- aborting connection');
}
this.end();
}
});
}
self.conn.setSecure(self.opt.secure);
}
self.conn.setTimeout(0);
self.conn.setEncoding('utf8');
self.conn.on("connect", function () {
if (self.opt.password !== null)
self.send("PASS", self.opt.password);
self.send("NICK", self.opt.nick);
self.nick = self.opt.nick;
self.send("USER", self.opt.userName, 8, "*", self.opt.realName);
self.emit("connect");
});
var buffer = '';
self.conn.on("data", function (chunk) {
buffer += chunk;
var lines = buffer.split("\r\n");
buffer = lines.pop();
lines.forEach(function (line) {
var message = parseMessage(line);
self.emit('raw', message);
});
});
self.conn.on("end", function() {
if (self.conn.requestedDisconnect)
return;
if (self.opt.debug)
util.log('Disconnected: reconnecting');
if (self.opt.retryCount !== null && retryCount >= self.opt.retryCount) {
if (self.opt.debug)
util.log('Maximum retry count (' + self.opt.retryCount + ') reached. Aborting');
self.emit('abort', self.opt.retryCount);
return;
}
if (self.opt.debug)
util.log('Waiting ' + self.opt.retryDelay + 'ms before retrying');
setTimeout(function() {
self.connect(retryCount + 1);
}, self.opt.retryDelay);
});
}; // }}}
Client.prototype.disconnect = function (message) { // {{{
message = message || "Goodbye";
var self = this;
if (self.conn.readyState == 'open')
self.send("QUIT", message);
self.conn.requestedDisconnect = true;
self.conn.end();
}; // }}}
Client.prototype.send = function(command) { // {{{
var args = Array.prototype.slice.call(arguments);
if (args.length > 1) {
if (args[args.length-1] !== false)
args[args.length-1] = ":" + args[args.length-1];
else
args.splice(args.length-1, 1);
}
// Remove the command
args.shift();
if (this.opt.debug)
util.log('SEND: ' + command + " " + args.join(" "));
this.conn.write(command + (args.length ? " " + args.join(" ") : "") + "\r\n");
}; // }}}
Client.prototype.join = function(channel, callback) { // {{{
if (typeof(callback) == 'function') {
var callbackWrapper = function () {
this.removeListener('join' + channel, callbackWrapper);
return callback.apply(this, arguments);
};
this.on('join' + channel, callbackWrapper);
}
this.send('JOIN', channel);
} // }}}
Client.prototype.part = function(channel, reason, callback) { // {{{
if (typeof(reason) == 'function') {
callback = reason;
reason = undefined;
}
if (typeof(callback) == 'function') {
var callbackWrapper = function () {
this.removeListener('part' + channel, callbackWrapper);
return callback.apply(this, arguments);
};
this.on('part' + channel, callbackWrapper);
}
if (reason)
this.send('PART', channel, ''+reason);
else
this.send('PART', channel);
} // }}}
Client.prototype.say = function(target, text) { // {{{
this.send('PRIVMSG', target, text);
} // }}}
/*
* parseMessage(line)
*
* takes a raw "line" from the IRC server and turns it into an object with
* useful keys
*/
function parseMessage(line) { // {{{
var message = {};
var match;
// Parse prefix
if (match = line.match(/^:([^ ]+) +/)) {
message.prefix = match[1];
line = line.replace(/^:[^ ]+ +/, '');
if (match = message.prefix.match(/^([_a-zA-Z0-9\[\]\\`^{}|-]*)(!([^@]+)@(.*))?$/)) {
message.nick = match[1];
message.user = match[3];
message.host = match[4];
}
else
message.server = message.prefix;
}
// Parse command
match = line.match(/^([^ ]+) +/);
message.command = match[1];
message.rawCommand = match[1];
message.commandType = 'normal';
line = line.replace(/^[^ ]+ +/, '');
if (replyFor[message.rawCommand]) {
message.command = replyFor[message.rawCommand].name;
message.commandType = replyFor[message.rawCommand].type;
}
message.args = [];
var middle, trailing;
// Parse parameters
if (line.indexOf(':') != -1) {
var index = line.indexOf(':');
middle = line.substr(0, index).replace(/ +$/, "");
trailing = line.substr(index+1);
} else
middle = line;
if (middle.length)
message.args = middle.split(/ +/);
if (typeof(trailing) != 'undefined' && trailing.length)
message.args.push(trailing);
return message;
} // }}}