Highlights/activity alerts
[KiwiIRC.git] / server / app.js
index cbaff4da2c8fcebb508974018bb97c011f27ecc1..7f544a503f62aaf2e68c25dffa3eeeda38adadc7 100644 (file)
@@ -1,20 +1,22 @@
 /*jslint sloppy: true, continue: true, forin: true, regexp: true, undef: false, node: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
 /*globals kiwi_root */
 /* Fuck you, git. */
-var tls = null;
-var net = null;
-var http = null;
-var https = null;
-var fs = null;
-var url = null;
-var dns = null;
-var crypto = null;
-var ws = null;
-var jsp = null;
-var pro = null;
-var _ = null;
-var starttls = null;
-var kiwi = null;
+var tls = null,
+    net = null,
+    http = null,
+    https = null,
+    fs = null,
+    url = null,
+    dns = null,
+    crypto = null,
+    events = null,
+    util = null,
+    ws = null,
+    jsp = null,
+    pro = null,
+    _ = null,
+    starttls = null,
+    kiwi = null;
 
 this.init = function (objs) {
     tls = objs.tls;
@@ -25,12 +27,16 @@ this.init = function (objs) {
     url = objs.url;
     dns = objs.dns;
     crypto = objs.crypto;
+    events = objs.events;
+    util = objs.util;
     ws = objs.ws;
     jsp = objs.jsp;
     pro = objs.pro;
     _ = objs._;
     starttls = objs.starttls;
     kiwi = require('./kiwi.js');
+
+    util.inherits(this.IRCConnection, events.EventEmitter);
 };
 
 
@@ -50,7 +56,7 @@ this.changeUser = function () {
         try {
             process.setgid(kiwi.config.group);
         } catch (err) {
-            console.log('Failed to set gid: ' + err);
+            kiwi.log('Failed to set gid: ' + err);
             process.exit();
         }
     }
@@ -59,7 +65,7 @@ this.changeUser = function () {
         try {
             process.setuid(kiwi.config.user);
         } catch (e) {
-            console.log('Failed to set uid: ' + e);
+            kiwi.log('Failed to set uid: ' + e);
             process.exit();
         }
     }
@@ -88,11 +94,14 @@ var ircNumerics = {
     RPL_LISTEND:            '323',
     RPL_NOTOPIC:            '331',
     RPL_TOPIC:              '332',
+    RPL_TOPICWHOTIME:       '333',
     RPL_NAMEREPLY:          '353',
     RPL_ENDOFNAMES:         '366',
     RPL_BANLIST:            '367',
     RPL_ENDOFBANLIST:       '368',
     RPL_MOTD:               '372',
+    RPL_MOTDSTART:          '375',
+    RPL_ENDOFMOTD:          '376',
     RPL_WHOISMODES:         '379',
     ERR_NOSUCHNICK:         '401',
     ERR_CANNOTSENDTOCHAN:   '404',
@@ -110,317 +119,355 @@ var ircNumerics = {
     RPL_STARTTLS:           '670'
 };
 
-
-
-this.parseIRCMessage = function (websocket, ircSocket, data) {
-    /*global ircSocketDataHandler */
-    var msg, regex, opts, options, opt, i, j, matches, nick, users, chan, channel,
-        params, nicklist, caps, rtn, obj, tmp, namespace, whois_end = false;
-    //regex = /^(?::(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)?) )?([a-z0-9]+)(?:(?: ([^:]+))?(?: :(.+))?)$/i;
-    //regex = /^(?::(\S+) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
-    regex = /^(?::(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
-
-    msg = regex.exec(data);
-    if (msg) {
-        msg = {
-            prefix:     msg[1],
-            nick:       msg[2],
-            ident:      msg[3],
-            hostname:   msg[4] || '',
-            command:    msg[5],
-            params:     msg[6] || '',
-            trailing:   (msg[7]) ? msg[7].trim() : ''
+this.bindIRCCommands = function (irc_connection, websocket) {
+    var bound_events = [],
+        bindCommand = function (command, listener) {
+            command = 'irc_' + command;
+            irc_connection.on(command, listener);
+            bound_events.push({"command": command, "listener": listener});
         };
 
-        switch (msg.command.toUpperCase()) {
-        case 'PING':
-            websocket.sendServerLine('PONG ' + msg.trailing);
-            break;
-        case ircNumerics.RPL_WELCOME:
-            if (ircSocket.IRC.CAP.negotiating) {
-                ircSocket.IRC.CAP.negotiating = false;
-                ircSocket.IRC.CAP.enabled = [];
-                ircSocket.IRC.CAP.requested = [];
-                ircSocket.IRC.registered = true;
-            }
-            //regex = /([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)/i;
-            //matches = regex.exec(msg.trailing);
-            nick =  msg.params.split(' ')[0];
-            websocket.sendClientEvent('connect', {connected: true, host: null, nick: nick});
-            break;
-        case ircNumerics.RPL_ISUPPORT:
-            opts = msg.params.split(" ");
-            options = [];
-            for (i = 0; i < opts.length; i++) {
-                opt = opts[i].split("=", 2);
-                opt[0] = opt[0].toUpperCase();
-                ircSocket.IRC.options[opt[0]] = (typeof opt[1] !== 'undefined') ? opt[1] : true;
-                if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES', 'NAMESX'], opt[0])) {
-                    if (opt[0] === 'PREFIX') {
-                        regex = /\(([^)]*)\)(.*)/;
-                        matches = regex.exec(opt[1]);
-                        if ((matches) && (matches.length === 3)) {
-                            ircSocket.IRC.options[opt[0]] = [];
-                            for (j = 0; j < matches[2].length; j++) {
-                                //ircSocket.IRC.options[opt[0]][matches[2].charAt(j)] = matches[1].charAt(j);
-                                ircSocket.IRC.options[opt[0]].push({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)});
-                            }
+    bindCommand('PING', function (msg) {
+        websocket.sendServerLine('PONG ' + msg.trailing);
+    });
 
+    bindCommand(ircNumerics.RPL_WELCOME, function (msg) {
+        if (irc_connection.IRC.CAP.negotiating) {
+            irc_connection.IRC.CAP.negotiating = false;
+            irc_connection.IRC.CAP.enabled = [];
+            irc_connection.IRC.CAP.requested = [];
+            irc_connection.IRC.registered = true;
+        }
+        var nick =  msg.params.split(' ')[0];
+        websocket.sendClientEvent('connect', {connected: true, host: null, nick: nick});
+    });
+
+    bindCommand(ircNumerics.RPL_ISUPPORT, function (msg) {
+        var opts = msg.params.split(" "),
+            opt,
+            i,
+            j,
+            regex,
+            matches;
+        for (i = 0; i < opts.length; i++) {
+            opt = opts[i].split("=", 2);
+            opt[0] = opt[0].toUpperCase();
+            irc_connection.IRC.options[opt[0]] = (typeof opt[1] !== 'undefined') ? opt[1] : true;
+            if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES', 'NAMESX'], opt[0])) {
+                if (opt[0] === 'PREFIX') {
+                    regex = /\(([^)]*)\)(.*)/;
+                    matches = regex.exec(opt[1]);
+                    if ((matches) && (matches.length === 3)) {
+                        irc_connection.IRC.options[opt[0]] = [];
+                        for (j = 0; j < matches[2].length; j++) {
+                            irc_connection.IRC.options[opt[0]].push({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)});
                         }
-                    }
-                    if (opt[0] === 'NAMESX') {
-                        websocket.sendServerLine('PROTOCTL NAMESX');
+
                     }
                 }
+                if (opt[0] === 'NAMESX') {
+                    websocket.sendServerLine('PROTOCTL NAMESX');
+                }
             }
+        }
 
-            websocket.sendClientEvent('options', {server: '', "options": ircSocket.IRC.options});
-            break;
+        websocket.sendClientEvent('options', {server: '', "options": irc_connection.IRC.options});
+    });
 
-        case ircNumerics.RPL_ENDOFWHOIS:
-            whois_end = true;
-        case ircNumerics.RPL_WHOISUSER:
-        case ircNumerics.RPL_WHOISSERVER:
-        case ircNumerics.RPL_WHOISOPERATOR:
-        case ircNumerics.RPL_WHOISCHANNELS:
-        case ircNumerics.RPL_WHOISMODES:
-            websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: whois_end});
-            break;
+    bindCommand(ircNumerics.RPL_ENDOFWHOIS, function (msg) {
+        websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: true});
+    });
 
-        case ircNumerics.RPL_LISTSTART:
-            (function () {
-                websocket.sendClientEvent('list_start', {server: ''});
-                websocket.kiwi.buffer.list = [];
-            }());
-            break;
-        case ircNumerics.RPL_LISTEND:
-            (function () {
-                if (websocket.kiwi.buffer.list.length > 0) {
-                    websocket.kiwi.buffer.list = _.sortBy(websocket.kiwi.buffer.list, function (channel) {
-                        return channel.num_users;
-                    });
-                    websocket.sendClientEvent('list_channel', {chans: websocket.kiwi.buffer.list});
-                    websocket.kiwi.buffer.list = [];
-                }
-                websocket.sendClientEvent('list_end', {server: ''});
-            }());
-            break;
+    bindCommand(ircNumerics.RPL_WHOISUSER, function (msg) {
+        websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
+    });
 
-        case ircNumerics.RPL_LIST:
-            (function () {
-                var parts, channel, num_users, modes, topic;
-
-                parts = msg.params.split(' ');
-                channel = parts[1];
-                num_users = parts[2];
-                topic = msg.trailing;
-
-                //websocket.sendClientEvent('list_channel', {
-                websocket.kiwi.buffer.list.push({
-                    server: '',
-                    channel: channel,
-                    topic: topic,
-                    //modes: modes,
-                    num_users: parseInt(num_users, 10)
-                });
+    bindCommand(ircNumerics.RPL_WHOISSERVER, function (msg) {
+        websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
+    });
 
-                if (websocket.kiwi.buffer.list.length > 200) {
-                    websocket.kiwi.buffer.list = _.sortBy(websocket.kiwi.buffer.list, function (channel) {
-                        return channel.num_users;
-                    });
-                    websocket.sendClientEvent('list_channel', {chans: websocket.kiwi.buffer.list});
-                    websocket.kiwi.buffer.list = [];
-                }
+    bindCommand(ircNumerics.RPL_WHOISOPERATOR, function (msg) {
+        websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
+    });
 
-            }());
-            break;
+    bindCommand(ircNumerics.RPL_WHOISCHANNELS, function (msg) {
+        websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
+    });
 
-        case ircNumerics.RPL_WHOISIDLE:
-            params = msg.params.split(" ", 4);
+    bindCommand(ircNumerics.RPL_WHOISMODES, function (msg) {
+        websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
+    });
+
+    bindCommand(ircNumerics.RPL_LISTSTART, function (msg) {
+        websocket.sendClientEvent('list_start', {server: ''});
+        websocket.kiwi.buffer.list = [];
+    });
+
+    bindCommand(ircNumerics.RPL_LISTEND, function (msg) {
+        if (websocket.kiwi.buffer.list.length > 0) {
+            websocket.kiwi.buffer.list = _.sortBy(websocket.kiwi.buffer.list, function (channel) {
+                return channel.num_users;
+            });
+            websocket.sendClientEvent('list_channel', {chans: websocket.kiwi.buffer.list});
+            websocket.kiwi.buffer.list = [];
+        }
+        websocket.sendClientEvent('list_end', {server: ''});
+    });
+
+    bindCommand(ircNumerics.RPL_LIST, function (msg) {
+        var parts, channel, num_users, topic;
+
+        parts = msg.params.split(' ');
+        channel = parts[1];
+        num_users = parts[2];
+        topic = msg.trailing;
+
+        //websocket.sendClientEvent('list_channel', {
+        websocket.kiwi.buffer.list.push({
+            server: '',
+            channel: channel,
+            topic: topic,
+            //modes: modes,
+            num_users: parseInt(num_users, 10)
+        });
+
+        if (websocket.kiwi.buffer.list.length > 200) {
+            websocket.kiwi.buffer.list = _.sortBy(websocket.kiwi.buffer.list, function (channel) {
+                return channel.num_users;
+            });
+            websocket.sendClientEvent('list_channel', {chans: websocket.kiwi.buffer.list});
+            websocket.kiwi.buffer.list = [];
+        }
+    });
+
+    bindCommand(ircNumerics.RPL_WHOISIDLE, function (msg) {
+        var params = msg.params.split(" ", 4),
             rtn = {server: '', nick: params[1], idle: params[2]};
-            if (params[3]) {
-                rtn.logon = params[3];
-            }
-            websocket.sendClientEvent('whois', rtn);
-            break;
-        case ircNumerics.RPL_MOTD:
-            websocket.sendClientEvent('motd', {server: '', "msg": msg.trailing});
-            break;
-        case ircNumerics.RPL_NAMEREPLY:
-            params = msg.params.split(" ");
-            nick = params[0];
-            chan = params[2];
-            users = msg.trailing.split(" ");
-            nicklist = [];
+        if (params[3]) {
+            rtn.logon = params[3];
+        }
+        websocket.sendClientEvent('whois', rtn);
+    });
+
+    bindCommand(ircNumerics.RPL_MOTD, function (msg) {
+        websocket.kiwi.buffer.motd += msg.trailing + '\n';
+    });
+
+    bindCommand(ircNumerics.RPL_MOTDSTART, function (msg) {
+        websocket.kiwi.buffer.motd = '';
+    });
+
+    bindCommand(ircNumerics.RPL_ENDOFMOTD, function (msg) {
+        websocket.sendClientEvent('motd', {server: '', 'msg': websocket.kiwi.buffer.motd});
+    });
+
+    bindCommand(ircNumerics.RPL_NAMEREPLY, function (msg) {
+        var params = msg.params.split(" "),
+            chan = params[2],
+            users = msg.trailing.split(" "),
+            nicklist = [],
             i = 0;
-            _.each(users, function (user) {
-                var j, k, modes = [];
-                for (j = 0; j < user.length; j++) {
-                    for (k = 0; k < ircSocket.IRC.options.PREFIX.length; k++) {
-                        if (user.charAt(j) === ircSocket.IRC.options.PREFIX[k].symbol) {
-                            modes.push(ircSocket.IRC.options.PREFIX[k].mode);
-                        }
+
+        _.each(users, function (user) {
+            var j, k, modes = [];
+            for (j = 0; j < user.length; j++) {
+                for (k = 0; k < irc_connection.IRC.options.PREFIX.length; k++) {
+                    if (user.charAt(j) === irc_connection.IRC.options.PREFIX[k].symbol) {
+                        modes.push(irc_connection.IRC.options.PREFIX[k].mode);
                     }
                 }
-                nicklist.push({nick: user, modes: modes});
-                if (i++ >= 50) {
-                    websocket.sendClientEvent('userlist', {server: '', 'users': nicklist, channel: chan});
-                    nicklist = [];
-                    i = 0;
-                }
-            });
-            if (i > 0) {
-                websocket.sendClientEvent('userlist', {server: '', "users": nicklist, channel: chan});
-            } else {
-                console.log("oops");
             }
-            break;
-        case ircNumerics.RPL_ENDOFNAMES:
-            websocket.sendClientEvent('userlist_end', {server: '', channel: msg.params.split(" ")[1]});
-            break;
-        case ircNumerics.ERR_LINKCHANNEL:
-            params = msg.params.split(" ");
-            websocket.sendClientEvent('channel_redirect', {from: params[1], to: params[2]});
-            break;
-        case ircNumerics.ERR_NOSUCHNICK:
-            websocket.sendClientEvent('irc_error', {error: 'no_such_nick', nick: msg.params.split(" ")[1], reason: msg.trailing});
-            break;
-        case ircNumerics.RPL_BANLIST:
-            params = msg.params.split(" ");
-            console.log(params);
-            websocket.sendClientEvent('banlist', {server: '', channel: params[1], banned: params[2], banned_by: params[3], banned_at: params[4]});
-            break;
-        case ircNumerics.RPL_ENDOFBANLIST:
-            websocket.sendClientEvent('banlist_end', {server: '', channel: msg.params.split(" ")[1]});
-            break;
-        case 'JOIN':
-            // Some BNC's send malformed JOIN causing the channel to be as a
-            // parameter instead of trailing.
-            if (typeof msg.trailing === 'string' && msg.trailing !== '') {
-                channel = msg.trailing;
-            } else if (typeof msg.params === 'string' && msg.params !== '') {
-                channel = msg.params;
+            nicklist.push({nick: user, modes: modes});
+            if (i++ >= 50) {
+                websocket.sendClientEvent('userlist', {server: '', 'users': nicklist, channel: chan});
+                nicklist = [];
+                i = 0;
             }
+        });
+        if (i > 0) {
+            websocket.sendClientEvent('userlist', {server: '', "users": nicklist, channel: chan});
+        } else {
+            kiwi.log("oops");
+        }
+    });
 
-            websocket.sendClientEvent('join', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: channel});
-            if (msg.nick === ircSocket.IRC.nick) {
-                websocket.sendServerLine('NAMES ' + msg.trailing);
-            }
-            break;
-        case 'PART':
-            websocket.sendClientEvent('part', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), message: msg.trailing});
+    bindCommand(ircNumerics.RPL_ENDOFNAMES, function (msg) {
+        websocket.sendClientEvent('userlist_end', {server: '', channel: msg.params.split(" ")[1]});
+    });
+
+    bindCommand(ircNumerics.ERR_LINKCHANNEL, function (msg) {
+        var params = msg.params.split(" ");
+        websocket.sendClientEvent('channel_redirect', {from: params[1], to: params[2]});
+    });
+
+    bindCommand(ircNumerics.ERR_NOSUCHNICK, function (msg) {
+        websocket.sendClientEvent('irc_error', {error: 'no_such_nick', nick: msg.params.split(" ")[1], reason: msg.trailing});
+    });
+
+    bindCommand(ircNumerics.RPL_BANLIST, function (msg) {
+        var params = msg.params.split(" ");
+        kiwi.log(params);
+        websocket.sendClientEvent('banlist', {server: '', channel: params[1], banned: params[2], banned_by: params[3], banned_at: params[4]});
+    });
+
+    bindCommand(ircNumerics.RPL_ENDOFBANLIST, function (msg) {
+        websocket.sendClientEvent('banlist_end', {server: '', channel: msg.params.split(" ")[1]});
+    });
+
+    bindCommand('JOIN', function (msg) {
+        var channel;
+
+        // Some BNC's send malformed JOIN causing the channel to be as a
+        // parameter instead of trailing.
+        if (typeof msg.trailing === 'string' && msg.trailing !== '') {
+            channel = msg.trailing;
+        } else if (typeof msg.params === 'string' && msg.params !== '') {
+            channel = msg.params;
+        }
+
+        websocket.sendClientEvent('join', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: channel});
+        if (msg.nick === irc_connection.IRC.nick) {
+            websocket.sendServerLine('NAMES ' + msg.trailing);
+        }
+    });
+
+    bindCommand('PART', function (msg) {
+        websocket.sendClientEvent('part', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), message: msg.trailing});
+    });
+
+    bindCommand('KICK', function (msg) {
+        var params = msg.params.split(" ");
+        websocket.sendClientEvent('kick', {kicked: params[1], nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: params[0].trim(), message: msg.trailing});
+    });
+
+    bindCommand('QUIT', function (msg) {
+        websocket.sendClientEvent('quit', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, message: msg.trailing});
+    });
+
+    bindCommand('NOTICE', function (msg) {
+        if ((msg.trailing.charAt(0) === String.fromCharCode(1)) && (msg.trailing.charAt(msg.trailing.length - 1) === String.fromCharCode(1))) {
+            // It's a CTCP response
+            websocket.sendClientEvent('ctcp_response', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(1, msg.trailing.length - 2)});
+        } else {
+            websocket.sendClientEvent('notice', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, target: msg.params.trim(), msg: msg.trailing});
+        }
+    });
+
+    bindCommand('NICK', function (msg) {
+        websocket.sendClientEvent('nick', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, newnick: msg.trailing});
+    });
+
+    bindCommand('TOPIC', function (msg) {
+        var obj = {nick: msg.nick, channel: msg.params, topic: msg.trailing};
+        websocket.sendClientEvent('topic', obj);
+    });
+
+    bindCommand(ircNumerics.RPL_TOPIC, function (msg) {
+        var obj = {nick: '', channel: msg.params.split(" ")[1], topic: msg.trailing};
+        websocket.sendClientEvent('topic', obj);
+    });
+
+    bindCommand(ircNumerics.RPL_NOTOPIC, function (msg) {
+        var obj = {nick: '', channel: msg.params.split(" ")[1], topic: ''};
+        websocket.sendClientEvent('topic', obj);
+    });
+
+    bindCommand(ircNumerics.RPL_TOPICWHOTIME, function (msg) {
+        var parts = msg.params.split(' '),
+            nick = parts[2],
+            channel = parts[1],
+            when = parts[3],
+            obj = {nick: nick, channel: channel, when: when};
+        websocket.sendClientEvent('topicsetby', obj);
+    });
+
+    bindCommand('MODE', function (msg) {
+        var opts = msg.params.split(" "),
+            params = {nick: msg.nick};
+
+        switch (opts.length) {
+        case 1:
+            params.effected_nick = opts[0];
+            params.mode = msg.trailing;
             break;
-        case 'KICK':
-            params = msg.params.split(" ");
-            websocket.sendClientEvent('kick', {kicked: params[1], nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: params[0].trim(), message: msg.trailing});
+        case 2:
+            params.channel = opts[0];
+            params.mode = opts[1];
             break;
-        case 'QUIT':
-            websocket.sendClientEvent('quit', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, message: msg.trailing});
+        default:
+            params.channel = opts[0];
+            params.mode = opts[1];
+            params.effected_nick = opts[2];
             break;
-        case 'NOTICE':
-            if ((msg.trailing.charAt(0) === String.fromCharCode(1)) && (msg.trailing.charAt(msg.trailing.length - 1) === String.fromCharCode(1))) {
-                // It's a CTCP response
-                websocket.sendClientEvent('ctcp_response', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(1, msg.trailing.length - 2)});
+        }
+        websocket.sendClientEvent('mode', params);
+    });
+
+    bindCommand('PRIVMSG', function (msg) {
+        var tmp, namespace, obj;
+        if ((msg.trailing.charAt(0) === String.fromCharCode(1)) && (msg.trailing.charAt(msg.trailing.length - 1) === String.fromCharCode(1))) {
+            // It's a CTCP request
+            if (msg.trailing.substr(1, 6) === 'ACTION') {
+                websocket.sendClientEvent('action', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(7, msg.trailing.length - 2)});
+            } else if (msg.trailing.substr(1, 4) === 'KIWI') {
+                tmp = msg.trailing.substr(6, msg.trailing.length - 2);
+                namespace = tmp.split(' ', 1)[0];
+                websocket.sendClientEvent('kiwi', {namespace: namespace, data: tmp.substr(namespace.length + 1)});
+
+            } else if (msg.trailing.substr(1, 7) === 'VERSION') {
+                irc_connection.write('NOTICE ' + msg.nick + ' :' + String.fromCharCode(1) + 'VERSION KiwiIRC' + String.fromCharCode(1) + '\r\n');
             } else {
-                websocket.sendClientEvent('notice', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, target: msg.params.trim(), msg: msg.trailing});
+                websocket.sendClientEvent('ctcp_request', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(1, msg.trailing.length - 2)});
             }
-            break;
-        case 'NICK':
-            websocket.sendClientEvent('nick', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, newnick: msg.trailing});
-            break;
-        case 'TOPIC':
-            obj = {nick: msg.nick, channel: msg.params, topic: msg.trailing};
-            websocket.sendClientEvent('topic', obj);
-            break;
-        case ircNumerics.RPL_TOPIC:
-            obj = {nick: '', channel: msg.params.split(" ")[1], topic: msg.trailing};
-            websocket.sendClientEvent('topic', obj);
-            break;
-        case ircNumerics.RPL_NOTOPIC:
-            obj = {nick: '', channel: msg.params.split(" ")[1], topic: ''};
-            websocket.sendClientEvent('topic', obj);
-            break;
-        case 'MODE':
-            opts = msg.params.split(" ");
-            params = {nick: msg.nick};
-            switch (opts.length) {
-            case 1:
-                params.effected_nick = opts[0];
-                params.mode = msg.trailing;
-                break;
-            case 2:
-                params.channel = opts[0];
-                params.mode = opts[1];
-                break;
-            default:
-                params.channel = opts[0];
-                params.mode = opts[1];
-                params.effected_nick = opts[2];
-                break;
-            }
-            websocket.sendClientEvent('mode', params);
-            break;
-        case 'PRIVMSG':
-            if ((msg.trailing.charAt(0) === String.fromCharCode(1)) && (msg.trailing.charAt(msg.trailing.length - 1) === String.fromCharCode(1))) {
-                // It's a CTCP request
-                if (msg.trailing.substr(1, 6) === 'ACTION') {
-                    websocket.sendClientEvent('action', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(7, msg.trailing.length - 2)});
-                } else if (msg.trailing.substr(1, 4) === 'KIWI') {
-                    tmp = msg.trailing.substr(6, msg.trailing.length - 2);
-                    namespace = tmp.split(' ', 1)[0];
-                    websocket.sendClientEvent('kiwi', {namespace: namespace, data: tmp.substr(namespace.length + 1)});
-
-                } else if (msg.trailing.substr(1, 7) === 'VERSION') {
-                    ircSocket.write('NOTICE ' + msg.nick + ' :' + String.fromCharCode(1) + 'VERSION KiwiIRC' + String.fromCharCode(1) + '\r\n');
-                } else {
-                    websocket.sendClientEvent('ctcp_request', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(1, msg.trailing.length - 2)});
+        } else {
+            obj = {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing};
+            websocket.sendClientEvent('msg', obj);
+        }
+    });
+
+    bindCommand('CAP', function (msg) {
+        var caps = kiwi.config.cap_options,
+            options = msg.trailing.split(" "),
+            opts;
+
+        switch (_.last(msg.params.split(" "))) {
+        case 'LS':
+            opts = '';
+            _.each(_.intersect(caps, options), function (cap) {
+                if (opts !== '') {
+                    opts += " ";
                 }
+                opts += cap;
+                irc_connection.IRC.CAP.requested.push(cap);
+            });
+            if (opts.length > 0) {
+                websocket.sendServerLine('CAP REQ :' + opts);
             } else {
-                obj = {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing};
-                websocket.sendClientEvent('msg', obj);
+                websocket.sendServerLine('CAP END');
             }
+            // TLS is special
+            /*if (_.include(options, 'tls')) {
+                websocket.sendServerLine('STARTTLS');
+                ircSocket.IRC.CAP.requested.push('tls');
+            }*/
             break;
-        case 'CAP':
-            caps = kiwi.config.cap_options;
-            options = msg.trailing.split(" ");
-            switch (_.last(msg.params.split(" "))) {
-            case 'LS':
-                opts = '';
-                _.each(_.intersect(caps, options), function (cap) {
-                    if (opts !== '') {
-                        opts += " ";
-                    }
-                    opts += cap;
-                    ircSocket.IRC.CAP.requested.push(cap);
-                });
-                if (opts.length > 0) {
-                    websocket.sendServerLine('CAP REQ :' + opts);
-                } else {
-                    websocket.sendServerLine('CAP END');
-                }
-                // TLS is special
-                /*if (_.include(options, 'tls')) {
-                    websocket.sendServerLine('STARTTLS');
-                    ircSocket.IRC.CAP.requested.push('tls');
-                }*/
-                break;
-            case 'ACK':
-                _.each(options, function (cap) {
-                    ircSocket.IRC.CAP.enabled.push(cap);
-                });
-                if (_.last(msg.params.split(" ")) !== '*') {
-                    ircSocket.IRC.CAP.requested = [];
-                    ircSocket.IRC.CAP.negotiating = false;
-                    websocket.sendServerLine('CAP END');
-                }
-                break;
-            case 'NAK':
-                ircSocket.IRC.CAP.requested = [];
-                ircSocket.IRC.CAP.negotiating = false;
+        case 'ACK':
+            _.each(options, function (cap) {
+                irc_connection.IRC.CAP.enabled.push(cap);
+            });
+            if (_.last(msg.params.split(" ")) !== '*') {
+                irc_connection.IRC.CAP.requested = [];
+                irc_connection.IRC.CAP.negotiating = false;
                 websocket.sendServerLine('CAP END');
-                break;
             }
             break;
+        case 'NAK':
+            irc_connection.IRC.CAP.requested = [];
+            irc_connection.IRC.CAP.negotiating = false;
+            websocket.sendServerLine('CAP END');
+            break;
+        }
+    });
         /*case ircNumerics.RPL_STARTTLS:
             try {
                 IRC = ircSocket.IRC;
@@ -436,106 +483,85 @@ this.parseIRCMessage = function (websocket, ircSocket, data) {
                         ircSocket.addListener('data', listener);
                     });
                 });
-                //console.log(ircSocket);
+                //log(ircSocket);
             } catch (e) {
-                console.log(e);
+                kiwi.log(e);
             }
             break;*/
-        case ircNumerics.ERR_CANNOTSENDTOCHAN:
-            websocket.sendClientEvent('irc_error', {error: 'cannot_send_to_chan', channel: msg.params.split(" ")[1], reason: msg.trailing});
-            break;
-        case ircNumerics.ERR_TOOMANYCHANNELS:
-            websocket.sendClientEvent('irc_error', {error: 'too_many_channels', channel: msg.params.split(" ")[1], reason: msg.trailing});
-            break;
-        case ircNumerics.ERR_USERNOTINCHANNEL:
-            params = msg.params.split(" ");
-            websocket.sendClientEvent('irc_error', {error: 'user_not_in_channel', nick: params[0], channel: params[1], reason: msg.trainling});
-            break;
-        case ircNumerics.ERR_NOTONCHANNEL:
-            websocket.sendClientEvent('irc_error', {error: 'not_on_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
-            break;
-        case ircNumerics.ERR_CHANNELISFULL:
-            websocket.sendClientEvent('irc_error', {error: 'channel_is_full', channel: msg.params.split(" ")[1], reason: msg.trailing});
-            break;
-        case ircNumerics.ERR_INVITEONLYCHAN:
-            websocket.sendClientEvent('irc_error', {error: 'invite_only_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
-            break;
-        case ircNumerics.ERR_BANNEDFROMCHAN:
-            websocket.sendClientEvent('irc_error', {error: 'banned_from_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
-            break;
-        case ircNumerics.ERR_BADCHANNELKEY:
-            websocket.sendClientEvent('irc_error', {error: 'bad_channel_key', channel: msg.params.split(" ")[1], reason: msg.trailing});
-            break;
-        case ircNumerics.ERR_CHANOPRIVSNEEDED:
-            websocket.sendClientEvent('irc_error', {error: 'chanop_privs_needed', channel: msg.params.split(" ")[1], reason: msg.trailing});
-            break;
-        case ircNumerics.ERR_NICKNAMEINUSE:
-            websocket.sendClientEvent('irc_error', {error: 'nickname_in_use', nick: _.last(msg.params.split(" ")), reason: msg.trailing});
-            break;
-        case 'ERROR':
-            ircSocket.end();
-            websocket.sendClientEvent('irc_error', {error: 'error', reason: msg.trailing});
-            websocket.disconnect();
-            break;
-        case ircNumerics.ERR_NOTREGISTERED:
-            if (ircSocket.IRC.registered) {
-                console.log('Kiwi thinks user is registered, but the IRC server thinks differently');
-            }
-            break;
-        default:
-            console.log("Unknown command (" + String(msg.command).toUpperCase() + ")");
-        }
-    } else {
-        console.log("Malformed IRC line: " + data);
-    }
-};
+    bindCommand(ircNumerics.ERR_CANNOTSENDTOCHAN, function (msg) {
+        websocket.sendClientEvent('irc_error', {error: 'cannot_send_to_chan', channel: msg.params.split(" ")[1], reason: msg.trailing});
+    });
 
+    bindCommand(ircNumerics.ERR_TOOMANYCHANNELS, function (msg) {
+        websocket.sendClientEvent('irc_error', {error: 'too_many_channels', channel: msg.params.split(" ")[1], reason: msg.trailing});
+    });
 
+    bindCommand(ircNumerics.ERR_USERNOTINCHANNEL, function (msg) {
+        var params = msg.params.split(" ");
+        websocket.sendClientEvent('irc_error', {error: 'user_not_in_channel', nick: params[0], channel: params[1], reason: msg.trainling});
+    });
 
+    bindCommand(ircNumerics.ERR_NOTONCHANNEL, function (msg) {
+        websocket.sendClientEvent('irc_error', {error: 'not_on_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
+    });
 
+    bindCommand(ircNumerics.ERR_CHANNELISFULL, function (msg) {
+        websocket.sendClientEvent('irc_error', {error: 'channel_is_full', channel: msg.params.split(" ")[1], reason: msg.trailing});
+    });
 
+    bindCommand(ircNumerics.ERR_INVITEONLYCHAN, function (msg) {
+        websocket.sendClientEvent('irc_error', {error: 'invite_only_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
+    });
 
-/*
- * NOTE: Some IRC servers or BNC's out there incorrectly use
- * only \n as a line splitter.
- */
-this.ircSocketDataHandler = function (data, websocket, ircSocket) {
-    var i;
-    if ((ircSocket.holdLast) && (ircSocket.held !== '')) {
-        data = ircSocket.held + data;
-        ircSocket.holdLast = false;
-        ircSocket.held = '';
-    }
-    if (data.substr(-1) !== '\n') {
-        ircSocket.holdLast = true;
-    }
-    data = data.split("\n");
-    for (i = 0; i < data.length; i++) {
-        if (data[i]) {
-            if ((ircSocket.holdLast) && (i === data.length - 1)) {
-                ircSocket.held = data[i];
-                break;
-            }
+    bindCommand(ircNumerics.ERR_BANNEDFROMCHAN, function (msg) {
+        websocket.sendClientEvent('irc_error', {error: 'banned_from_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
+    });
 
-            // We have a complete line of data, parse it!
-            kiwi.parseIRCMessage(websocket, ircSocket, data[i].replace(/^\r+|\r+$/, ''));
-        }
-    }
-};
+    bindCommand(ircNumerics.ERR_BADCHANNELKEY, function (msg) {
+        websocket.sendClientEvent('irc_error', {error: 'bad_channel_key', channel: msg.params.split(" ")[1], reason: msg.trailing});
+    });
+
+    bindCommand(ircNumerics.ERR_CHANOPRIVSNEEDED, function (msg) {
+        websocket.sendClientEvent('irc_error', {error: 'chanop_privs_needed', channel: msg.params.split(" ")[1], reason: msg.trailing});
+    });
 
+    bindCommand(ircNumerics.ERR_NICKNAMEINUSE, function (msg) {
+        websocket.sendClientEvent('irc_error', {error: 'nickname_in_use', nick: _.last(msg.params.split(" ")), reason: msg.trailing});
+    });
 
+    bindCommand('ERROR', function (msg) {
+        irc_connection.end();
+        websocket.sendClientEvent('irc_error', {error: 'error', reason: msg.trailing});
+        websocket.disconnect();
+    });
 
+    bindCommand(ircNumerics.ERR_NOTREGISTERED, function (msg) {
+        if (irc_connection.IRC.registered) {
+            kiwi.log('Kiwi thinks user is registered, but the IRC server thinks differently');
+        }
+    });
+
+    return bound_events;
+};
+
+this.rebindIRCCommands = function () {
+    _.each(kiwi.connections, function (con) {
+        _.each(con.sockets, function (sock) {
+            sock.ircConnection.rebindIRCCommands();
+        });
+    });
+};
 
 
 this.httpHandler = function (request, response) {
     var uri, uri_parts, subs, useragent, agent, server_set, server, nick, debug, touchscreen, hash,
-        min = {}, public_http_path, port, ssl, host, obj, args, ircuri, pass, target, modifiers, query,
+        min = {}, public_http_path, port, ssl, obj, args, ircuri, target, modifiers, query,
         secure = (typeof request.client.encrypted === 'object');
 
-    //try {
+    try {
         if (kiwi.config.handle_http) {
             // Run through any plugins..
-            args = {request: request, response: response};
+            args = {request: request, response: response, ssl: secure};
             obj = kiwi.kiwi_mod.run('http', args);
             if (obj === null) {
                 return;
@@ -547,7 +573,7 @@ this.httpHandler = function (request, response) {
 
             subs = uri.pathname.substr(0, 4);
             public_http_path = kiwi.kiwi_root + '/' + kiwi.config.public_http;
-            
+
             if (typeof uri.query.ircuri !== 'undefined') {
                 ircuri = url.parse(uri.query.ircuri, true);
                 if (ircuri.protocol === 'irc:') {
@@ -559,7 +585,7 @@ this.httpHandler = function (request, response) {
                     nick = _.detect(modifiers, function (mod) {
                         return mod === ',isnick';
                     });
-                    console.log(request.headers);
+                    kiwi.log(request.headers);
                     response.statusCode = 303;
                     response.setHeader('Location', 'http' + ((secure) ? 's' : '') + '://' + request.headers.host + '/client/' + ircuri.host + '/' + ((!nick) ? target : ''));
                     response.end();
@@ -617,25 +643,25 @@ this.httpHandler = function (request, response) {
 
                 debug = (typeof uri.query.debug !== 'undefined');
 
-                ssl = (typeof request.socket.pair !== 'undefined');
-                port = ssl ? 6697 : 6667;
+                ssl = secure;   // ssl is passed to the client
+                port = ssl ? kiwi.config.client_defaults.port_ssl : kiwi.config.client_defaults.port;
                 if (uri_parts[1] !== 'client') {
                     if (uri.query) {
                         server_set = ((typeof uri.query.server !== 'undefined') && (uri.query.server !== ''));
-                        server = uri.query.server || 'irc.anonnet.org';
+                        server = uri.query.server || kiwi.config.client_defaults.server;
                         nick = uri.query.nick || '';
                     } else {
                         server_set = false;
-                        server = 'irc.anonnet.org';
+                        server = kiwi.config.client_defaults.server;
                         nick = '';
                     }
                 } else {
                     server_set = ((typeof uri_parts[2] !== 'undefined') && (uri_parts[2] !== ''));
-                    server = server_set ? uri_parts[2] : 'irc.anonnet.org';
+                    server = server_set ? uri_parts[2] : kiwi.config.client_defaults.server;
                     if (server.search(/:/) > 0) {
                         port = server.substring(server.search(/:/) + 1);
                         server = server.substring(0, server.search(/:/));
-                        if (port[0] == '+') {
+                        if (port[0] === '+') {
                             port = port.substring(1);
                             ssl = true;
                         } else {
@@ -645,6 +671,14 @@ this.httpHandler = function (request, response) {
                     nick = uri.query.nick || '';
                 }
 
+                // Set the default nick if one isn't provided
+                if (nick === '') {
+                    nick = 'kiwi_?';
+                }
+
+                // Set any random numbers if needed
+                nick = nick.replace('?', Math.floor(Math.random() * 100000).toString());
+
                 response.setHeader('X-Generated-By', 'KiwiIRC');
                 hash = crypto.createHash('md5').update(touchscreen ? 't' : 'f')
                     .update(debug ? 't' : 'f')
@@ -683,10 +717,10 @@ this.httpHandler = function (request, response) {
                                 }
                             } catch (e) {
                                 response.statusCode = 500;
-                                console.log(e);
+                                kiwi.log(e);
                             }
                         } else {
-                            console.log(err);
+                            kiwi.log(err);
                             response.statusCode = 500;
                         }
                         response.end();
@@ -700,16 +734,16 @@ this.httpHandler = function (request, response) {
             }
         }
 
-    //} catch (e) {
-    //    console.log('ERROR app.httpHandler()');
-    //    console.log(e);
-    //}
+    } catch (e) {
+        kiwi.log('ERROR app.httpHandler()');
+        kiwi.log(e);
+    }
 };
 
 
 
 
-this.websocketListen = function (ports, host, handler, key, cert) {
+this.websocketListen = function (servers, handler) {
     if (kiwi.httpServers.length > 0) {
         _.each(kiwi.httpServers, function (hs) {
             hs.close();
@@ -717,18 +751,30 @@ this.websocketListen = function (ports, host, handler, key, cert) {
         kiwi.httpsServers = [];
     }
 
-    _.each(ports, function (port) {
-        var hs;
-        if (port.secure === true) {
-            hs = https.createServer({key: fs.readFileSync(__dirname + '/' + key), cert: fs.readFileSync(__dirname + '/' + cert)}, handler);
+    _.each(servers, function (server) {
+        var hs, opts;
+        if (server.secure === true) {
+            // Start some SSL server up
+            opts = {
+                key: fs.readFileSync(__dirname + '/' + server.ssl_key),
+                cert: fs.readFileSync(__dirname + '/' + server.ssl_cert)
+            };
+
+            // Do we have an intermediate certificate?
+            if (typeof server.ssl_ca !== 'undefined') {
+                opts.ca = fs.readFileSync(__dirname + '/' + server.ssl_ca);
+            }
+
+            hs = https.createServer(opts, handler);
             kiwi.io.push(ws.listen(hs, {secure: true}));
-            hs.listen(port.number, host);
-            console.log("Listening on %s:%d with SSL", host, port.number);
+            hs.listen(server.port, server.address);
+            kiwi.log('Listening on ' + server.address + ':' + server.port.toString() + ' with SSL');
         } else {
+            // Start some plain-text server up
             hs = http.createServer(handler);
             kiwi.io.push(ws.listen(hs, {secure: false}));
-            hs.listen(port.number, host);
-            console.log("Listening on %s:%d without SSL", host, port.number);
+            hs.listen(server.port, server.address);
+            kiwi.log('Listening on ' + server.address + ':' + server.port.toString() + ' without SSL');
         }
 
         kiwi.httpServers.push(hs);
@@ -757,7 +803,7 @@ this.websocketListen = function (ports, host, handler, key, cert) {
 
 this.websocketConnection = function (websocket) {
     var con;
-    console.log("New connection!");
+    kiwi.log("New connection!");
     websocket.kiwi = {address: websocket.handshake.address.address, buffer: {list: []}};
     con = kiwi.connections[websocket.kiwi.address];
 
@@ -778,132 +824,281 @@ this.websocketConnection = function (websocket) {
             websocket.emit('message', data);
         };
 
-        websocket.sendServerLine = function (data, eol) {
-            eol = (typeof eol === 'undefined') ? '\r\n' : eol;
+        websocket.sendServerLine = function (data, eol, callback) {
+            if ((arguments.length < 3) && (typeof eol === 'function')) {
+                callback = eol;
+            }
+            eol = (typeof eol !== 'string') ? '\r\n' : eol;
 
             try {
-                websocket.ircSocket.write(data + eol);
+                websocket.ircConnection.write(data + eol, 'utf-8', callback);
             } catch (e) { }
         };
 
-        websocket.on('irc connect', kiwi.websocketIRCConnect);
+        websocket.on('irc connect', function (nick, host, port, ssl, password, callback) {
+            websocket.ircConnection = new kiwi.IRCConnection(this, nick, host, port, ssl, password, callback);
+        });
         websocket.on('message', kiwi.websocketMessage);
         websocket.on('disconnect', kiwi.websocketDisconnect);
     }
 };
 
 
+this.IRCConnection = function (websocket, nick, host, port, ssl, password, callback) {
+    var ircSocket,
+        that = this,
+        regex,
+        onConnectHandler,
+        bound_events;
 
+    events.EventEmitter.call(this);
+
+    onConnectHandler = function () {
+        that.IRC.nick = nick;
+        // Send the login data
+        dns.reverse(websocket.kiwi.address, function (err, domains) {
+            websocket.kiwi.hostname = (err) ? websocket.kiwi.address : _.first(domains);
+            if ((kiwi.config.webirc) && (kiwi.config.webirc_pass[host])) {
+                websocket.sendServerLine('WEBIRC ' + kiwi.config.webirc_pass[host] + ' KiwiIRC ' + websocket.kiwi.hostname + ' ' + websocket.kiwi.address);
+            }
+            if (password) {
+                websocket.sendServerLine('PASS ' + password);
+            }
+            websocket.sendServerLine('CAP LS');
+            websocket.sendServerLine('NICK ' + nick);
+            websocket.sendServerLine('USER kiwi_' + nick.replace(/[^0-9a-zA-Z\-_.]/, '') + ' 0 0 :' + nick);
 
+            that.connected = true;
+            that.emit('connect');
+        });
+    };
 
-this.websocketIRCConnect = function (websocket, nick, host, port, ssl, password, callback) {
-    var ircSocket;
-    //setup IRC connection
     if (!ssl) {
         ircSocket = net.createConnection(port, host);
+        ircSocket.on('connect', onConnectHandler);
     } else {
-        ircSocket = tls.connect(port, host);
+        ircSocket = tls.connect(port, host, {}, onConnectHandler);
     }
-    ircSocket.setEncoding('ascii');
-    ircSocket.IRC = {options: {}, CAP: {negotiating: true, requested: [], enabled: []}, registered: false};
-    ircSocket.on('error', function (e) {
-        if (ircSocket.IRC.registered) {
+
+    ircSocket.setEncoding('utf-8');
+    this.IRC = {options: {}, CAP: {negotiating: true, requested: [], enabled: []}, registered: false};
+
+    this.on('error', function (e) {
+        if (that.IRC.registered) {
             websocket.emit('disconnect');
         } else {
             websocket.emit('error', e.message);
         }
     });
-    websocket.ircSocket = ircSocket;
-    ircSocket.holdLast = false;
-    ircSocket.held = '';
 
-    ircSocket.on('data', function (data) {
-        kiwi.ircSocketDataHandler(data, websocket, ircSocket);
+    ircSocket.on('error', function (e) {
+        that.connected = false;
+        that.emit('error', e);
+        that.destroySoon();
     });
 
-    ircSocket.IRC.nick = nick;
-    // Send the login data
-    dns.reverse(websocket.kiwi.address, function (err, domains) {
-        //console.log(domains);
-        websocket.kiwi.hostname = (err) ? websocket.kiwi.address : _.first(domains);
-        if ((kiwi.config.webirc) && (kiwi.config.webirc_pass[host])) {
-            websocket.sendServerLine('WEBIRC ' + kiwi.config.webirc_pass[host] + ' KiwiIRC ' + websocket.kiwi.hostname + ' ' + websocket.kiwi.address);
+    if (typeof callback === 'function') {
+        this.on('connect', callback);
+    }
+
+    regex = /^(?::(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
+    ircSocket.holdLast = false;
+    ircSocket.held = '';
+    ircSocket.on('data', function (data) {
+        var i, msg;
+        if ((ircSocket.holdLast) && (ircSocket.held !== '')) {
+            data = ircSocket.held + data;
+            ircSocket.holdLast = false;
+            ircSocket.held = '';
         }
-        if (password) {
-            websocket.sendServerLine('PASS ' + password);
+        if (data.substr(-1) !== '\n') {
+            ircSocket.holdLast = true;
         }
-        websocket.sendServerLine('CAP LS');
-        websocket.sendServerLine('NICK ' + nick);
-        websocket.sendServerLine('USER kiwi_' + nick.replace(/[^0-9a-zA-Z\-_.]/, '') + ' 0 0 :' + nick);
+        data = data.split("\n");
+        for (i = 0; i < data.length; i++) {
+            if (data[i]) {
+                if ((ircSocket.holdLast) && (i === data.length - 1)) {
+                    ircSocket.held = data[i];
+                    break;
+                }
 
-        if ((callback) && (typeof (callback) === 'function')) {
-            callback();
+                // We have a complete line of data, parse it!
+                msg = regex.exec(data[i].replace(/^\r+|\r+$/, ''));
+                if (msg) {
+                    msg = {
+                        prefix:     msg[1],
+                        nick:       msg[2],
+                        ident:      msg[3],
+                        hostname:   msg[4] || '',
+                        command:    msg[5],
+                        params:     msg[6] || '',
+                        trailing:   (msg[7]) ? msg[7].trim() : ''
+                    };
+                    that.emit('irc_' + msg.command.toUpperCase(), msg);
+                    if (that.listeners('irc_' + msg.command.toUpperCase()).length < 1) {
+                        kiwi.log("Unknown command (" + String(msg.command).toUpperCase() + ")");
+                    }
+                } else {
+                    kiwi.log("Malformed IRC line: " + data[i].replace(/^\r+|\r+$/, ''));
+                }
+            }
         }
     });
+
+    if (callback) {
+        ircSocket.on('connect', callback);
+    }
+
+    ircSocket.on('end', function () {
+        that.connected = false;
+        that.emit('disconnect', false);
+    });
+
+    ircSocket.on('close', function (had_error) {
+        that.connected = false;
+        that.emit('disconnect', had_error);
+    });
+
+    ircSocket.on('timeout', function () {
+        ircSocket.destroy();
+        that.connected = false;
+        that.emit('error', {message: 'Connection timed out'});
+    });
+
+    ircSocket.on('drain', function () {
+        that.emit('drain');
+    });
+
+    this.write = function (data, encoding, callback) {
+        ircSocket.write(data, encoding, callback);
+    };
+
+    this.end = function (data, encoding, callback) {
+        that.connected = false;
+        ircSocket.end(data, encoding, callback);
+    };
+
+    this.destroySoon = function () {
+        ircSocket.destroySoon();
+    };
+
+    bound_events = kiwi.bindIRCCommands(this, websocket);
+
+    this.rebindIRCCommands = function () {
+        _.each(bound_events, function (event) {
+            that.removeListener(event.command, event.listener);
+        });
+        bound_events = kiwi.bindIRCCommands(that, websocket);
+    };
 };
 
 
 
 this.websocketMessage = function (websocket, msg, callback) {
-    var args, obj;
-    try {
-        msg.data = JSON.parse(msg.data);
+    var args, obj, channels, keys;
+    //try {
+        if ((callback) && (typeof (callback) !== 'function')) {
+            callback = null;
+        }
+        try {
+            msg.data = JSON.parse(msg.data);
+        } catch (e) {
+            kiwi.log('[app.websocketMessage] JSON parsing error ' + msg.data);
+            return;
+        }
         args = msg.data.args;
         switch (msg.data.method) {
-        case 'msg':
+        case 'privmsg':
             if ((args.target) && (args.msg)) {
                 obj = kiwi.kiwi_mod.run('msgsend', args, {websocket: websocket});
                 if (obj !== null) {
-                    websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + args.msg);
+                    websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + args.msg, callback);
                 }
             }
             break;
-        case 'action':
-            if ((args.target) && (args.msg)) {
-                websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + String.fromCharCode(1) + 'ACTION ' + args.msg + String.fromCharCode(1));
-            }
-            break;
-
-        case 'kiwi':
-            if ((args.target) && (args.data)) {
-                websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + String.fromCharCode(1) + 'KIWI ' + args.data + String.fromCharCode(1));
+        case 'ctcp':
+            if ((args.target) && (args.type)) {
+                if (args.request) {
+                    websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + String.fromCharCode(1) + args.type.toUpperCase() + ' ' + args.params + String.fromCharCode(1), callback);
+                } else {
+                    websocket.sendServerLine('NOTICE ' + args.target + ' :' + String.fromCharCode(1) + args.type.toUpperCase() + ' ' + args.params + String.fromCharCode(1), callback);
+                }
             }
             break;
 
         case 'raw':
-            websocket.sendServerLine(args.data);
+            websocket.sendServerLine(args.data, callback);
             break;
+
         case 'join':
+            if (args.channel) {
+                channels = args.channel.split(",");
+                keys = (args.key) ? args.key.split(",") : [];
+                _.each(channels, function (chan, index) {
+                    websocket.sendServerLine('JOIN ' + chan + ' ' + (keys[index] || ''), callback);
+                });
+            }
+            break;
+
+        case 'part':
             if (args.channel) {
                 _.each(args.channel.split(","), function (chan) {
-                    websocket.sendServerLine('JOIN ' + chan);
+                    websocket.sendServerLine('PART ' + chan, callback);
                 });
             }
             break;
+
         case 'topic':
             if (args.channel) {
-                websocket.sendServerLine('TOPIC ' + args.channel + ' :' + args.topic);
+                if (args.topic) {
+                    websocket.sendServerLine('TOPIC ' + args.channel + ' :' + args.topic, callback);
+                } else {
+                    websocket.sendServerLine('TOPIC ' + args.channel, callback);
+                }
             }
             break;
+
+        case 'kick':
+            if ((args.channel) && (args.nick)) {
+                websocket.sendServerLine('KICK ' + args.channel + ' ' + args.nick + ':' + args.reason, callback);
+            }
+            break;
+
         case 'quit':
-            websocket.ircSocket.end('QUIT :' + args.message + '\r\n');
+            websocket.ircConnection.end('QUIT :' + args.message + '\r\n');
             websocket.sentQUIT = true;
-            websocket.ircSocket.destroySoon();
+            websocket.ircConnection.destroySoon();
             websocket.disconnect();
             break;
+
         case 'notice':
             if ((args.target) && (args.msg)) {
-                websocket.sendServerLine('NOTICE ' + args.target + ' :' + args.msg);
+                websocket.sendServerLine('NOTICE ' + args.target + ' :' + args.msg, callback);
+            }
+            break;
+
+        case 'mode':
+            if ((args.target) && (args.mode)) {
+                websocket.sendServerLine('MODE ' + args.target + ' ' + args.mode + ' ' + args.params, callback);
+            }
+            break;
+
+        case 'nick':
+            if (args.nick) {
+                websocket.sendServerLine('NICK ' + args.nick, callback);
+            }
+            break;
+
+        case 'kiwi':
+            if ((args.target) && (args.data)) {
+                websocket.sendServerLine('PRIVMSG ' + args.target + ': ' + String.fromCharCode(1) + 'KIWI ' + args.data + String.fromCharCode(1), callback);
             }
             break;
         default:
         }
-        if ((callback) && (typeof (callback) === 'function')) {
-            callback();
-        }
-    } catch (e) {
-        console.log("Caught error: " + e);
-    }
+    //} catch (e) {
+    //    kiwi.log("Caught error (app.websocketMessage): " + e);
+    //}
 };
 
 
@@ -911,11 +1106,11 @@ this.websocketMessage = function (websocket, msg, callback) {
 this.websocketDisconnect = function (websocket) {
     var con;
 
-    if ((!websocket.sentQUIT) && (websocket.ircSocket)) {
+    if ((!websocket.sentQUIT) && (websocket.ircConnection.connected)) {
         try {
-            websocket.ircSocket.end('QUIT :' + kiwi.config.quit_message + '\r\n');
+            websocket.ircConnection.end('QUIT :' + kiwi.config.quit_message + '\r\n');
             websocket.sentQUIT = true;
-            websocket.ircSocket.destroySoon();
+            websocket.ircConnection.destroySoon();
         } catch (e) {
         }
     }
@@ -944,14 +1139,11 @@ this.rehash = function () {
     changes = reload_config[1];
 
     if (Object.keys(changes).length !== 0) {
-        console.log('%s config changes: \n', Object.keys(changes).length, changes);
+        kiwi.log('%s config changes: \n', Object.keys(changes).length, changes);
         for (i in changes) {
             switch (i) {
-            case 'ports':
-            case 'bind_address':
-            case 'ssl_key':
-            case 'ssl_cert':
-                kiwi.websocketListen(kiwi.config.ports, kiwi.config.bind_address, kiwi.httpHandler, kiwi.config.ssl_key, kiwi.config.ssl_cert);
+            case 'servers':
+                kiwi.websocketListen(kiwi.config.servers, kiwi.httpHandler);
                 delete changes.ports;
                 delete changes.bind_address;
                 delete changes.ssl_key;
@@ -995,19 +1187,26 @@ this.manageControll = function (data) {
         i;
     switch (parts[0]) {
     case 'rehash':
-        console.log('Rehashing...');
-        console.log(kiwi.rehash() ? 'Rehash complete' : 'Rehash failed');
+        kiwi.log('Rehashing...');
+        kiwi.log(kiwi.rehash() ? 'Rehash complete' : 'Rehash failed');
         break;
 
     case 'recode':
-        console.log('Recoding...');
-        console.log(kiwi.recode() ? 'Recode complete' : 'Recode failed');
+        kiwi.log('Recoding...');
+        kiwi.log(kiwi.recode() ? 'Recode complete' : 'Recode failed');
         break;
 
     case 'mod':
         if (parts[1] === 'reload') {
-            console.log('Reloading module (' + parts[2] + ')..');
+            if (!parts[2]) {
+                kiwi.log('Usage: mod reload module_name');
+                return;
+            }
+
+            kiwi.log('Reloading module (' + parts[2] + ')..');
             kiwi.kiwi_mod.reloadModule(parts[2]);
+        } else if (parts[1] === 'list') {
+            kiwi.kiwi_mod.printMods();
         }
         break;
 
@@ -1015,7 +1214,7 @@ this.manageControll = function (data) {
         if (parts[1] === 'clear') {
             kiwi.cache.html = {};
             kiwi.cache.alljs = '';
-            console.log('HTML cache cleared');
+            kiwi.log('HTML cache cleared');
         }
         break;
 
@@ -1023,10 +1222,10 @@ this.manageControll = function (data) {
         for (i in kiwi.connections) {
             connections_cnt = connections_cnt + parseInt(kiwi.connections[i].count, 10);
         }
-        console.log(connections_cnt.toString() + ' connected clients');
+        kiwi.log(connections_cnt.toString() + ' connected clients');
         break;
 
     default:
-        console.log('Unknown command \'' + parts[0] + '\'');
+        kiwi.log('Unknown command \'' + parts[0] + '\'');
     }
 };