Emoticons erroneous quote
[KiwiIRC.git] / server / client.js
index 4276043c77eb1e5110b25d6650cde673fb4a4ba2..609c906713e30e15d794d13aafc0e6a1ab745178 100755 (executable)
@@ -1,34 +1,73 @@
-var util            = require('util'),
-    events          = require('events'),
-    IRCConnection   = require('./irc-connection.js').IRCConnection;
-    IRCCommands     = require('./irc-commands.js');
-
-var Client = function (websocket) {
-    var c = this;
-    
+var util             = require('util'),
+    events           = require('events'),
+    crypto           = require('crypto'),
+    _                = require('lodash'),
+    State            = require('./irc/state.js'),
+    IrcConnection    = require('./irc/connection.js').IrcConnection,
+    ClientCommands   = require('./clientcommands.js'),
+    WebsocketRpc     = require('./websocketrpc.js'),
+    Stats            = require('./stats.js');
+
+
+var Client = function (websocket, opts) {
+    var that = this;
+
+    Stats.incr('client.created');
+
     events.EventEmitter.call(this);
     this.websocket = websocket;
-    
-    this.IRC_connections = [];
-    this.next_connection = 0;
-    
+
+    // Keep a record of how this client connected
+    this.server_config = opts.server_config;
+
+    this.rpc = new WebsocketRpc(this.websocket);
+    this.rpc.on('all', function(func_name, return_fn) {
+        if (typeof func_name === 'string' && typeof return_fn === 'function') {
+            Stats.incr('client.command');
+            Stats.incr('client.command.' + func_name);
+        }
+    });
+
+    // Clients address
+    this.real_address = this.websocket.meta.real_address;
+
+    // A hash to identify this client instance
+    this.hash = crypto.createHash('sha256')
+        .update(this.real_address)
+        .update('' + Date.now())
+        .update(Math.floor(Math.random() * 100000).toString())
+        .digest('hex');
+
+    this.state = new State(this);
+
     this.buffer = {
         list: [],
         motd: ''
     };
-    
-    websocket.on('irc', function () {
-        IRC_command.apply(c, arguments);
-    });
-    websocket.on('kiwi', function () {
-        kiwi_command.apply(c, arguments);
+
+    // Handler for any commands sent from the client
+    this.client_commands = new ClientCommands(this);
+    this.client_commands.addRpcEvents(this, this.rpc);
+
+    // Handles the kiwi.* RPC functions
+    this.attachKiwiCommands();
+
+    websocket.on('message', function() {
+        // A message from the client is a sure sign the client is still alive, so consider it a heartbeat
+        that.heartbeat();
     });
-    websocket.on('disconnect', function () {
-        disconnect.apply(c, arguments);
+
+    websocket.on('close', function () {
+        websocketDisconnect.apply(that, arguments);
     });
     websocket.on('error', function () {
-        error.apply(c, arguments);
+        websocketError.apply(that, arguments);
     });
+
+    this.disposed = false;
+
+    // Let the client know it's finished connecting
+    this.sendKiwiCommand('connected');
 };
 util.inherits(Client, events.EventEmitter);
 
@@ -40,111 +79,107 @@ module.exports.Client = Client;
 // error MUST otherwise be a truthy value and SHOULD be a string where the cause of the error is known.
 // response MAY be given even if error is truthy
 
-Client.prototype.sendIRCCommand = function (command, data, callback) {
+Client.prototype.sendIrcCommand = function (command, data, callback) {
     var c = {command: command, data: data};
-    console.log('C<--', c);
-    this.websocket.emit('irc', c, callback);
+    this.rpc('irc', c, callback);
 };
 
-Client.prototype.sendKiwiCommand = function (command, callback) {
-    this.websocket.emit('kiwi', command, callback);
+Client.prototype.sendKiwiCommand = function (command, data, callback) {
+    var c = {command: command, data: data};
+    this.rpc('kiwi', c, callback);
 };
 
-var IRC_command = function (command, callback) {
-    console.log('C-->', command);
-    var method, str = '';
-    if (typeof callback !== 'function') {
-        callback = function () {};
-    }
-    if ((command.server === null) || (typeof command.server !== 'number')) {
-        return callback('server not specified');
-    } else if (!this.IRC_connections[command.server]) {
-        return callback('not connected to server');
-    }
-    
-    command.data = JSON.parse(command.data);
-    
-    if (!_.isArray(command.data.args.params)){
-        command.data.args.params = [command.data.args.params];
-    }
-    
-    if (command.data.method === 'ctcp') {
-        if (command.data.args.request) {
-            str += 'PRIVMSG ';
-        } else {
-            str += 'NOTICE ';
-        }
-        str += command.data.args.target + ' :'
-        str += String.fromCharCode(1) + command.data.args.type + ' ';
-        str += command.data.args.params + String.fromCharCode(1);
-        this.IRC_connections[command.server].write(str);
-    } else if (command.data.method === 'raw') {
-        this.IRC_connections[command.server].write(command.data.args.data);
-    } else if (command.data.method === 'kiwi') {
-        // do special Kiwi stuff here
-    } else {
-        method = command.data.method;
-        command.data = command.data.args;
-        this.IRC_connections[command.server].write(method + ((command.data.params) ? ' ' + command.data.params.join(' ') : '') + ((command.data.trailing) ? ' :' + command.data.trailing : ''), callback);
+Client.prototype.dispose = function () {
+    Stats.incr('client.disposed');
+
+    if (this._heartbeat_tmr) {
+        clearTimeout(this._heartbeat_tmr);
     }
+
+    this.rpc.dispose();
+    this.websocket.removeAllListeners();
+
+    this.disposed = true;
+    this.emit('dispose');
+
+    this.removeAllListeners();
 };
 
-var kiwi_command = function (command, callback) {
-    var that = this;
-    console.log(typeof callback);
-    if (typeof callback !== 'function') {
-        callback = function () {};
-    }
-    switch (command.command) {
-               case 'connect':
-                       if ((command.hostname) && (command.port) && (command.nick)) {
-                               var con = new IRCConnection(command.hostname, command.port, command.ssl,
-                                       command.nick, {hostname: this.websocket.handshake.revdns, address: this.websocket.handshake.address.address},
-                                       command.password, null);
-
-                               var con_num = this.next_connection++;
-                               this.IRC_connections[con_num] = con;
-
-                               var binder = new IRCCommands.Binder(con, con_num, this);
-                               binder.bind_irc_commands();
-                               
-                               con.on('connected', function () {
-                    console.log("con.on('connected')");
-                                       return callback(null, con_num);
-                               });
-                               
-                               con.on('error', function (err) {
-                                       this.websocket.sendKiwiCommand('error', {server: con_num, error: err});
-                               });
-                
-                con.on('close', function () {
-                    that.IRC_connections[con_num] = null;
-                });
-                       } else {
-                               return callback('Hostname, port and nickname must be specified');
-                       }
-               break;
-               default:
-                       callback();
+
+
+Client.prototype.heartbeat = function() {
+    if (this._heartbeat_tmr) {
+        clearTimeout(this._heartbeat_tmr);
     }
+
+    // After 2 minutes of this heartbeat not being called again, assume the client has disconnected
+    console.log('resetting heartbeat');
+    this._heartbeat_tmr = setTimeout(_.bind(this._heartbeat_timeout, this), 120000);
 };
 
-var extension_command = function (command, callback) {
-    if (typeof callback === 'function') {
-        callback('not implemented');
-    }
+
+Client.prototype._heartbeat_timeout = function() {
+    console.log('heartbeat stopped');
+    Stats.incr('client.timeout');
+    this.dispose();
 };
 
-var disconnect = function () {
-    _.each(this.IRC_connections, function (irc_connection, i, cons) {
-        if (irc_connection) {
-            irc_connection.end('QUIT :Kiwi IRC');
-            cons[i] = null;
+
+
+Client.prototype.attachKiwiCommands = function() {
+    var that = this;
+
+    this.rpc.on('kiwi.connect_irc', function(callback, command) {
+        if (command.hostname && command.port && command.nick) {
+            var options = {};
+
+            // Get any optional parameters that may have been passed
+            if (command.encoding)
+                options.encoding = command.encoding;
+
+            options.password = global.config.restrict_server_password || command.password;
+
+            that.state.connect(
+                (global.config.restrict_server || command.hostname),
+                (global.config.restrict_server_port || command.port),
+                (typeof global.config.restrict_server_ssl !== 'undefined' ?
+                    global.config.restrict_server_ssl :
+                    command.ssl),
+                command.nick,
+                {hostname: that.websocket.meta.revdns, address: that.websocket.meta.real_address},
+                options,
+                callback);
+        } else {
+            return callback('Hostname, port and nickname must be specified');
         }
     });
-    this.emit('destroy');
-};
 
-var error = function () {
-    this.emit('destroy');
+
+    this.rpc.on('kiwi.client_info', function(callback, args) {
+        // keep hold of selected parts of the client_info
+        that.client_info = {
+            build_version: args.build_version.toString() || undefined
+        };
+    });
+
+
+    // Just to let us know the client is still there
+    this.rpc.on('kiwi.heartbeat', function(callback, args) {
+        that.heartbeat();
+    });
 };
+
+
+
+// Websocket has disconnected, so quit all the IRC connections
+function websocketDisconnect() {
+    this.emit('disconnect');
+
+    this.dispose();
+}
+
+
+// TODO: Should this close all the websocket connections too?
+function websocketError() {
+    this.dispose();
+}