merging extensive changes from prawnsalad/KiwiIRC
authorJack Allnutt <m2ys4u@Gmail.com>
Tue, 18 Sep 2012 22:12:42 +0000 (23:12 +0100)
committerJack Allnutt <m2ys4u@Gmail.com>
Tue, 18 Sep 2012 22:12:42 +0000 (23:12 +0100)
39 files changed:
.gitignore
client/index.html.jade [changed mode: 0644->0755]
client/js/backbone-0.5.3-min.js [new file with mode: 0755]
client/js/backbone-git.js [new file with mode: 0755]
client/js/front.events.js [changed mode: 0644->0755]
client/js/front.js [changed mode: 0644->0755]
client/js/front.ui.js [changed mode: 0644->0755]
client/js/gateway.js [changed mode: 0644->0755]
client/js/iscroll.js [changed mode: 0644->0755]
client/js/jquery-ui.1.8.16.min.js [changed mode: 0644->0755]
client/js/jquery.1.6.4.min.js [changed mode: 0644->0755]
client/js/jquery.json-2.2.min.js [changed mode: 0644->0755]
client/js/mobile.js [changed mode: 0644->0755]
client/js/model.js [new file with mode: 0755]
client/js/touchscreen_tweaks.js [changed mode: 0644->0755]
client/js/underscore.min.js [changed mode: 0644->0755]
client/js/util.js [changed mode: 0644->0755]
client/js/view.js [new file with mode: 0755]
client_backbone/css/style.css [moved from client_backbone/style.css with 100% similarity]
client_backbone/dev/model_gateway.js [changed mode: 0644->0755]
client_backbone/index.jade [new file with mode: 0644]
client_backbone/js/backbone-git.js [moved from client_backbone/backbone-git.js with 100% similarity, mode: 0755]
client_backbone/js/jquery-1.7.1.min.js [moved from client_backbone/jquery-1.7.1.min.js with 100% similarity, mode: 0755]
client_backbone/js/underscore-min.js [moved from client_backbone/underscore-min.js with 100% similarity, mode: 0755]
client_backbone/manifest.json [new file with mode: 0644]
server/app.js [deleted file]
server/client.js [new file with mode: 0755]
server/config.json [changed mode: 0644->0755]
server/http-handler.js [new file with mode: 0755]
server/irc-commands.js [new file with mode: 0755]
server/irc-connection.js [new file with mode: 0755]
server/kiwi.js [changed mode: 0644->0755]
server/kiwi_modules/forcessl.js [deleted file]
server/kiwi_modules/spamfilter.js [deleted file]
server/kiwi_modules/statistics.js [deleted file]
server/lib/kiwi_mod.js [deleted file]
server/lib/starttls.js [deleted file]
server/lib/underscore.min.js [deleted file]
server/web.js [new file with mode: 0755]

index 6d95f8767ba786fcab56719bc9f0957d7b61dfc2..0880872fd603687832b6ceb156f02c0505b8b6ee 100644 (file)
@@ -1,3 +1,4 @@
 *.DS_*
 node/node_modules/
 node_modules/
+doc/
\ No newline at end of file
old mode 100644 (file)
new mode 100755 (executable)
index d1ce2c3..eb96042
@@ -110,7 +110,10 @@ html(lang="en-gb")
         - if (debug)
             script(type="text/javascript", src="/js/underscore.min.js")
             script(type="text/javascript", src="/js/util.js")
+            script(type="text/javascript", src="/js/backbone-git.js");
             script(type="text/javascript", src="/js/gateway.js")
+            script(type="text/javascript", src="/js/model.js")
+            script(type="text/javascript", src="/js/view.js")
             script(type="text/javascript", src="/js/front.js")
             script(type="text/javascript", src="/js/front.events.js")
             script(type="text/javascript", src="/js/front.ui.js")
diff --git a/client/js/backbone-0.5.3-min.js b/client/js/backbone-0.5.3-min.js
new file mode 100755 (executable)
index 0000000..08623c0
--- /dev/null
@@ -0,0 +1,34 @@
+// Backbone.js 0.5.3
+// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
+// Backbone may be freely distributed under the MIT license.
+// For all details and documentation:
+// http://documentcloud.github.com/backbone
+(function(){var h=this,p=h.Backbone,e;e=typeof exports!=="undefined"?exports:h.Backbone={};e.VERSION="0.5.3";var f=h._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var g=h.jQuery||h.Zepto;e.noConflict=function(){h.Backbone=p;return this};e.emulateHTTP=!1;e.emulateJSON=!1;e.Events={bind:function(a,b,c){var d=this._callbacks||(this._callbacks={});(d[a]||(d[a]=[])).push([b,c]);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=
+0,e=c.length;d<e;d++)if(c[d]&&b===c[d][0]){c[d]=null;break}}else c[a]=[]}else this._callbacks={};return this},trigger:function(a){var b,c,d,e,f=2;if(!(c=this._callbacks))return this;for(;f--;)if(b=f?a:"all",b=c[b])for(var g=0,h=b.length;g<h;g++)(d=b[g])?(e=f?Array.prototype.slice.call(arguments,1):arguments,d[0].apply(d[1]||this,e)):(b.splice(g,1),g--,h--);return this}};e.Model=function(a,b){var c;a||(a={});if(c=this.defaults)f.isFunction(c)&&(c=c.call(this)),a=f.extend({},c,a);this.attributes={};
+this._escapedAttributes={};this.cid=f.uniqueId("c");this.set(a,{silent:!0});this._changed=!1;this._previousAttributes=f.clone(this.attributes);if(b&&b.collection)this.collection=b.collection;this.initialize(a,b)};f.extend(e.Model.prototype,e.Events,{_previousAttributes:null,_changed:!1,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.attributes[a];
+return this._escapedAttributes[a]=(b==null?"":""+b).replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")},has:function(a){return this.attributes[a]!=null},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,d=this._escapedAttributes;if(!b.silent&&this.validate&&!this._performValidation(a,b))return!1;if(this.idAttribute in a)this.id=a[this.idAttribute];
+var e=this._changing;this._changing=!0;for(var g in a){var h=a[g];if(!f.isEqual(c[g],h))c[g]=h,delete d[g],this._changed=!0,b.silent||this.trigger("change:"+g,this,h,b)}!e&&!b.silent&&this._changed&&this.change(b);this._changing=!1;return this},unset:function(a,b){if(!(a in this.attributes))return this;b||(b={});var c={};c[a]=void 0;if(!b.silent&&this.validate&&!this._performValidation(c,b))return!1;delete this.attributes[a];delete this._escapedAttributes[a];a==this.idAttribute&&delete this.id;this._changed=
+!0;b.silent||(this.trigger("change:"+a,this,void 0,b),this.change(b));return this},clear:function(a){a||(a={});var b,c=this.attributes,d={};for(b in c)d[b]=void 0;if(!a.silent&&this.validate&&!this._performValidation(d,a))return!1;this.attributes={};this._escapedAttributes={};this._changed=!0;if(!a.silent){for(b in c)this.trigger("change:"+b,this,void 0,a);this.change(a)}return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&
+c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"read",this,a)},save:function(a,b){b||(b={});if(a&&!this.set(a,b))return!1;var c=this,d=b.success;b.success=function(a,e,f){if(!c.set(c.parse(a,f),b))return!1;d&&d(c,a,f)};b.error=i(b.error,c,b);var f=this.isNew()?"create":"update";return(this.sync||e.sync).call(this,f,this,b)},destroy:function(a){a||(a={});if(this.isNew())return this.trigger("destroy",this,this.collection,a);var b=this,c=a.success;a.success=function(d){b.trigger("destroy",
+b,b.collection,a);c&&c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"delete",this,a)},url:function(){var a=k(this.collection)||this.urlRoot||l();if(this.isNew())return a;return a+(a.charAt(a.length-1)=="/"?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return this.id==null},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=!1},hasChanged:function(a){if(a)return this._previousAttributes[a]!=
+this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=!1,d;for(d in a)f.isEqual(b[d],a[d])||(c=c||{},c[d]=a[d]);return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);if(c)return b.error?b.error(this,c,b):this.trigger("error",this,c,b),!1;return!0}});
+e.Collection=function(a,b){b||(b={});if(b.comparator)this.comparator=b.comparator;f.bindAll(this,"_onModelEvent","_removeReference");this._reset();a&&this.reset(a,{silent:!0});this.initialize.apply(this,arguments)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c<d;c++)this._add(a[c],b);else this._add(a,b);return this},remove:function(a,b){if(f.isArray(a))for(var c=
+0,d=a.length;c<d;c++)this._remove(a[c],b);else this._remove(a,b);return this},get:function(a){if(a==null)return null;return this._byId[a.id!=null?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");this.models=this.sortBy(this.comparator);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},
+reset:function(a,b){a||(a=[]);b||(b={});this.each(this._removeReference);this._reset();this.add(a,{silent:!0});b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(d,f,e){b[a.add?"add":"reset"](b.parse(d,e),a);c&&c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"read",this,a)},create:function(a,b){var c=this;b||(b={});a=this._prepareModel(a,b);if(!a)return!1;var d=b.success;b.success=function(a,e,f){c.add(a,b);
+d&&d(a,e,f)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_prepareModel:function(a,b){if(a instanceof e.Model){if(!a.collection)a.collection=this}else{var c=a;a=new this.model(c,{collection:this});a.validate&&!a._performValidation(c,b)&&(a=!1)}return a},_add:function(a,b){b||(b={});a=this._prepareModel(a,b);if(!a)return!1;var c=this.getByCid(a);if(c)throw Error(["Can't add the same model to a set twice",
+c.id]);this._byId[a.id]=a;this._byCid[a.cid]=a;this.models.splice(b.at!=null?b.at:this.comparator?this.sortedIndex(a,this.comparator):this.length,0,a);a.bind("all",this._onModelEvent);this.length++;b.silent||a.trigger("add",a,this,b);return a},_remove:function(a,b){b||(b={});a=this.getByCid(a)||this.get(a);if(!a)return null;delete this._byId[a.id];delete this._byCid[a.cid];this.models.splice(this.indexOf(a),1);this.length--;b.silent||a.trigger("remove",a,this,b);this._removeReference(a);return a},
+_removeReference:function(a){this==a.collection&&delete a.collection;a.unbind("all",this._onModelEvent)},_onModelEvent:function(a,b,c,d){(a=="add"||a=="remove")&&c!=this||(a=="destroy"&&this._remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,arguments))}});f.each(["forEach","each","map","reduce","reduceRight","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","max",
+"min","sortBy","sortedIndex","toArray","size","first","rest","last","without","indexOf","lastIndexOf","isEmpty","groupBy"],function(a){e.Collection.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});e.Router=function(a){a||(a={});if(a.routes)this.routes=a.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var q=/:([\w\d]+)/g,r=/\*([\w\d]+)/g,s=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(e.Router.prototype,e.Events,{initialize:function(){},route:function(a,
+b,c){e.history||(e.history=new e.History);f.isRegExp(a)||(a=this._routeToRegExp(a));e.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d))},this))},navigate:function(a,b){e.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(s,"\\$&").replace(q,
+"([^/]*)").replace(r,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});e.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")};var j=/^#*/,t=/msie [\w.]+/,m=!1;f.extend(e.History.prototype,{interval:50,getFragment:function(a,b){if(a==null)if(this._hasPushState||b){a=window.location.pathname;var c=window.location.search;c&&(a+=c);a.indexOf(this.options.root)==0&&(a=a.substr(this.options.root.length))}else a=window.location.hash;return decodeURIComponent(a.replace(j,
+""))},start:function(a){if(m)throw Error("Backbone.history has already been started");this.options=f.extend({},{root:"/"},this.options,a);this._wantsPushState=!!this.options.pushState;this._hasPushState=!(!this.options.pushState||!window.history||!window.history.pushState);a=this.getFragment();var b=document.documentMode;if(b=t.exec(navigator.userAgent.toLowerCase())&&(!b||b<=7))this.iframe=g('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);
+this._hasPushState?g(window).bind("popstate",this.checkUrl):"onhashchange"in window&&!b?g(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);this.fragment=a;m=!0;a=window.location;b=a.pathname==this.options.root;if(this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;else if(this._wantsPushState&&this._hasPushState&&b&&a.hash)this.fragment=a.hash.replace(j,""),window.history.replaceState({},
+document.title,a.protocol+"//"+a.host+this.options.root+this.fragment);if(!this.options.silent)return this.loadUrl()},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.iframe.location.hash));if(a==this.fragment||a==decodeURIComponent(this.fragment))return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(window.location.hash)},loadUrl:function(a){var b=this.fragment=this.getFragment(a);
+return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){var c=(a||"").replace(j,"");if(!(this.fragment==c||this.fragment==decodeURIComponent(c))){if(this._hasPushState){var d=window.location;c.indexOf(this.options.root)!=0&&(c=this.options.root+c);this.fragment=c;window.history.pushState({},document.title,d.protocol+"//"+d.host+c)}else if(window.location.hash=this.fragment=c,this.iframe&&c!=this.getFragment(this.iframe.location.hash))this.iframe.document.open().close(),
+this.iframe.location.hash=c;b&&this.loadUrl(a)}}});e.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize.apply(this,arguments)};var u=/^(\S+)\s*(.*)$/,n=["model","collection","el","id","attributes","className","tagName"];f.extend(e.View.prototype,e.Events,{tagName:"div",$:function(a){return g(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){g(this.el).remove();return this},make:function(a,
+b,c){a=document.createElement(a);b&&g(a).attr(b);c&&g(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events))for(var b in f.isFunction(a)&&(a=a.call(this)),g(this.el).unbind(".delegateEvents"+this.cid),a){var c=this[a[b]];if(!c)throw Error('Event "'+a[b]+'" does not exist');var d=b.match(u),e=d[1];d=d[2];c=f.bind(c,this);e+=".delegateEvents"+this.cid;d===""?g(this.el).bind(e,c):g(this.el).delegate(d,e,c)}},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=
+0,c=n.length;b<c;b++){var d=n[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el){if(f.isString(this.el))this.el=g(this.el).get(0)}else{var a=this.attributes||{};if(this.id)a.id=this.id;if(this.className)a["class"]=this.className;this.el=this.make(this.tagName,a)}}});e.Model.extend=e.Collection.extend=e.Router.extend=e.View.extend=function(a,b){var c=v(this,a,b);c.extend=this.extend;return c};var w={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};e.sync=function(a,
+b,c){var d=w[a];c=f.extend({type:d,dataType:"json"},c);if(!c.url)c.url=k(b)||l();if(!c.data&&b&&(a=="create"||a=="update"))c.contentType="application/json",c.data=JSON.stringify(b.toJSON());if(e.emulateJSON)c.contentType="application/x-www-form-urlencoded",c.data=c.data?{model:c.data}:{};if(e.emulateHTTP&&(d==="PUT"||d==="DELETE")){if(e.emulateJSON)c.data._method=d;c.type="POST";c.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)}}if(c.type!=="GET"&&!e.emulateJSON)c.processData=
+!1;return g.ajax(c)};var o=function(){},v=function(a,b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};f.extend(d,a);o.prototype=a.prototype;d.prototype=new o;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},k=function(a){if(!a||!a.url)return null;return f.isFunction(a.url)?a.url():a.url},l=function(){throw Error('A "url" property or function must be specified');},i=function(a,b,c){return function(d){a?
+a(b,d,c):b.trigger("error",b,d,c)}}}).call(this);
+
diff --git a/client/js/backbone-git.js b/client/js/backbone-git.js
new file mode 100755 (executable)
index 0000000..7c32137
--- /dev/null
@@ -0,0 +1,1260 @@
+//     Backbone.js 0.5.3\r
+//     (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.\r
+//     Backbone may be freely distributed under the MIT license.\r
+//     For all details and documentation:\r
+//     http://backbonejs.org\r
+\r
+(function(){\r
+\r
+  // Initial Setup\r
+  // -------------\r
+\r
+  // Save a reference to the global object (`window` in the browser, `global`\r
+  // on the server).\r
+  var root = this;\r
+\r
+  // Save the previous value of the `Backbone` variable, so that it can be\r
+  // restored later on, if `noConflict` is used.\r
+  var previousBackbone = root.Backbone;\r
+\r
+  // Create a local reference to slice/splice.\r
+  var slice = Array.prototype.slice;\r
+  var splice = Array.prototype.splice;\r
+\r
+  // The top-level namespace. All public Backbone classes and modules will\r
+  // be attached to this. Exported for both CommonJS and the browser.\r
+  var Backbone;\r
+  if (typeof exports !== 'undefined') {\r
+    Backbone = exports;\r
+  } else {\r
+    Backbone = root.Backbone = {};\r
+  }\r
+\r
+  // Current version of the library. Keep in sync with `package.json`.\r
+  Backbone.VERSION = '0.5.3';\r
+\r
+  // Require Underscore, if we're on the server, and it's not already present.\r
+  var _ = root._;\r
+  if (!_ && (typeof require !== 'undefined')) _ = require('underscore');\r
+\r
+  // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.\r
+  var $ = root.jQuery || root.Zepto || root.ender;\r
+\r
+  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable\r
+  // to its previous owner. Returns a reference to this Backbone object.\r
+  Backbone.noConflict = function() {\r
+    root.Backbone = previousBackbone;\r
+    return this;\r
+  };\r
+\r
+  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option\r
+  // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and\r
+  // set a `X-Http-Method-Override` header.\r
+  Backbone.emulateHTTP = false;\r
+\r
+  // Turn on `emulateJSON` to support legacy servers that can't deal with direct\r
+  // `application/json` requests ... will encode the body as\r
+  // `application/x-www-form-urlencoded` instead and will send the model in a\r
+  // form param named `model`.\r
+  Backbone.emulateJSON = false;\r
+\r
+  // Backbone.Events\r
+  // -----------------\r
+\r
+  // A module that can be mixed in to *any object* in order to provide it with\r
+  // custom events. You may bind with `on` or remove with `off` callback functions\r
+  // to an event; trigger`-ing an event fires all callbacks in succession.\r
+  //\r
+  //     var object = {};\r
+  //     _.extend(object, Backbone.Events);\r
+  //     object.on('expand', function(){ alert('expanded'); });\r
+  //     object.trigger('expand');\r
+  //\r
+  Backbone.Events = {\r
+\r
+    // Bind an event, specified by a string name, `ev`, to a `callback`\r
+    // function. Passing `"all"` will bind the callback to all events fired.\r
+    on: function(events, callback, context) {\r
+      var ev;\r
+      events = events.split(/\s+/);\r
+      var calls = this._callbacks || (this._callbacks = {});\r
+      while (ev = events.shift()) {\r
+        // Create an immutable callback list, allowing traversal during\r
+        // modification.  The tail is an empty object that will always be used\r
+        // as the next node.\r
+        var list  = calls[ev] || (calls[ev] = {});\r
+        var tail = list.tail || (list.tail = list.next = {});\r
+        tail.callback = callback;\r
+        tail.context = context;\r
+        list.tail = tail.next = {};\r
+      }\r
+      return this;\r
+    },\r
+\r
+    // Remove one or many callbacks. If `context` is null, removes all callbacks\r
+    // with that function. If `callback` is null, removes all callbacks for the\r
+    // event. If `ev` is null, removes all bound callbacks for all events.\r
+    off: function(events, callback, context) {\r
+      var ev, calls, node;\r
+      if (!events) {\r
+        delete this._callbacks;\r
+      } else if (calls = this._callbacks) {\r
+        events = events.split(/\s+/);\r
+        while (ev = events.shift()) {\r
+          node = calls[ev];\r
+          delete calls[ev];\r
+          if (!callback || !node) continue;\r
+          // Create a new list, omitting the indicated event/context pairs.\r
+          while ((node = node.next) && node.next) {\r
+            if (node.callback === callback &&\r
+              (!context || node.context === context)) continue;\r
+            this.on(ev, node.callback, node.context);\r
+          }\r
+        }\r
+      }\r
+      return this;\r
+    },\r
+\r
+    // Trigger an event, firing all bound callbacks. Callbacks are passed the\r
+    // same arguments as `trigger` is, apart from the event name.\r
+    // Listening for `"all"` passes the true event name as the first argument.\r
+    trigger: function(events) {\r
+      var event, node, calls, tail, args, all, rest;\r
+      if (!(calls = this._callbacks)) return this;\r
+      all = calls['all'];\r
+      (events = events.split(/\s+/)).push(null);\r
+      // Save references to the current heads & tails.\r
+      while (event = events.shift()) {\r
+        if (all) events.push({next: all.next, tail: all.tail, event: event});\r
+        if (!(node = calls[event])) continue;\r
+        events.push({next: node.next, tail: node.tail});\r
+      }\r
+      // Traverse each list, stopping when the saved tail is reached.\r
+      rest = slice.call(arguments, 1);\r
+      while (node = events.pop()) {\r
+        tail = node.tail;\r
+        args = node.event ? [node.event].concat(rest) : rest;\r
+        while ((node = node.next) !== tail) {\r
+          node.callback.apply(node.context || this, args);\r
+        }\r
+      }\r
+      return this;\r
+    }\r
+\r
+  };\r
+\r
+  // Aliases for backwards compatibility.\r
+  Backbone.Events.bind   = Backbone.Events.on;\r
+  Backbone.Events.unbind = Backbone.Events.off;\r
+\r
+  // Backbone.Model\r
+  // --------------\r
+\r
+  // Create a new model, with defined attributes. A client id (`cid`)\r
+  // is automatically generated and assigned for you.\r
+  Backbone.Model = function(attributes, options) {\r
+    var defaults;\r
+    attributes || (attributes = {});\r
+    if (options && options.parse) attributes = this.parse(attributes);\r
+    if (defaults = getValue(this, 'defaults')) {\r
+      attributes = _.extend({}, defaults, attributes);\r
+    }\r
+    if (options && options.collection) this.collection = options.collection;\r
+    this.attributes = {};\r
+    this._escapedAttributes = {};\r
+    this.cid = _.uniqueId('c');\r
+    if (!this.set(attributes, {silent: true})) {\r
+      throw new Error("Can't create an invalid model");\r
+    }\r
+    this._changed = false;\r
+    this._previousAttributes = _.clone(this.attributes);\r
+    this.initialize.apply(this, arguments);\r
+  };\r
+\r
+  // Attach all inheritable methods to the Model prototype.\r
+  _.extend(Backbone.Model.prototype, Backbone.Events, {\r
+\r
+    // Has the item been changed since the last `"change"` event?\r
+    _changed: false,\r
+\r
+    // The default name for the JSON `id` attribute is `"id"`. MongoDB and\r
+    // CouchDB users may want to set this to `"_id"`.\r
+    idAttribute: 'id',\r
+\r
+    // Initialize is an empty function by default. Override it with your own\r
+    // initialization logic.\r
+    initialize: function(){},\r
+\r
+    // Return a copy of the model's `attributes` object.\r
+    toJSON: function() {\r
+      return _.clone(this.attributes);\r
+    },\r
+\r
+    // Get the value of an attribute.\r
+    get: function(attr) {\r
+      return this.attributes[attr];\r
+    },\r
+\r
+    // Get the HTML-escaped value of an attribute.\r
+    escape: function(attr) {\r
+      var html;\r
+      if (html = this._escapedAttributes[attr]) return html;\r
+      var val = this.attributes[attr];\r
+      return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);\r
+    },\r
+\r
+    // Returns `true` if the attribute contains a value that is not null\r
+    // or undefined.\r
+    has: function(attr) {\r
+      return this.attributes[attr] != null;\r
+    },\r
+\r
+    // Set a hash of model attributes on the object, firing `"change"` unless\r
+    // you choose to silence it.\r
+    set: function(key, value, options) {\r
+      var attrs, attr, val;\r
+      if (_.isObject(key) || key == null) {\r
+        attrs = key;\r
+        options = value;\r
+      } else {\r
+        attrs = {};\r
+        attrs[key] = value;\r
+      }\r
+\r
+      // Extract attributes and options.\r
+      options || (options = {});\r
+      if (!attrs) return this;\r
+      if (attrs instanceof Backbone.Model) attrs = attrs.attributes;\r
+      if (options.unset) for (var attr in attrs) attrs[attr] = void 0;\r
+      var now = this.attributes, escaped = this._escapedAttributes;\r
+\r
+      // Run validation.\r
+      if (this.validate && !this._performValidation(attrs, options)) return false;\r
+\r
+      // Check for changes of `id`.\r
+      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];\r
+\r
+      // We're about to start triggering change events.\r
+      var alreadyChanging = this._changing;\r
+      this._changing = true;\r
+\r
+      // Update attributes.\r
+      var changes = {};\r
+      for (attr in attrs) {\r
+        val = attrs[attr];\r
+        if (!_.isEqual(now[attr], val) || (options.unset && (attr in now))) {\r
+          delete escaped[attr];\r
+          this._changed = true;\r
+          changes[attr] = val;\r
+        }\r
+        options.unset ? delete now[attr] : now[attr] = val;\r
+      }\r
+\r
+      // Fire `change:attribute` events.\r
+      for (var attr in changes) {\r
+        if (!options.silent) this.trigger('change:' + attr, this, changes[attr], options);\r
+      }\r
+\r
+      // Fire the `"change"` event, if the model has been changed.\r
+      if (!alreadyChanging) {\r
+        if (!options.silent && this._changed) this.change(options);\r
+        this._changing = false;\r
+      }\r
+      return this;\r
+    },\r
+\r
+    // Remove an attribute from the model, firing `"change"` unless you choose\r
+    // to silence it. `unset` is a noop if the attribute doesn't exist.\r
+    unset: function(attr, options) {\r
+      (options || (options = {})).unset = true;\r
+      return this.set(attr, null, options);\r
+    },\r
+\r
+    // Clear all attributes on the model, firing `"change"` unless you choose\r
+    // to silence it.\r
+    clear: function(options) {\r
+      (options || (options = {})).unset = true;\r
+      return this.set(_.clone(this.attributes), options);\r
+    },\r
+\r
+    // Fetch the model from the server. If the server's representation of the\r
+    // model differs from its current attributes, they will be overriden,\r
+    // triggering a `"change"` event.\r
+    fetch: function(options) {\r
+      options = options ? _.clone(options) : {};\r
+      var model = this;\r
+      var success = options.success;\r
+      options.success = function(resp, status, xhr) {\r
+        if (!model.set(model.parse(resp, xhr), options)) return false;\r
+        if (success) success(model, resp);\r
+      };\r
+      options.error = Backbone.wrapError(options.error, model, options);\r
+      return (this.sync || Backbone.sync).call(this, 'read', this, options);\r
+    },\r
+\r
+    // Set a hash of model attributes, and sync the model to the server.\r
+    // If the server returns an attributes hash that differs, the model's\r
+    // state will be `set` again.\r
+    save: function(key, value, options) {\r
+      var attrs;\r
+      if (_.isObject(key) || key == null) {\r
+        attrs = key;\r
+        options = value;\r
+      } else {\r
+        attrs = {};\r
+        attrs[key] = value;\r
+      }\r
+\r
+      options = options ? _.clone(options) : {};\r
+      if (attrs && !this[options.wait ? '_performValidation' : 'set'](attrs, options)) return false;\r
+      var model = this;\r
+      var success = options.success;\r
+      options.success = function(resp, status, xhr) {\r
+        var serverAttrs = model.parse(resp, xhr);\r
+        if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);\r
+        if (!model.set(serverAttrs, options)) return false;\r
+        if (success) {\r
+          success(model, resp);\r
+        } else {\r
+          model.trigger('sync', model, resp, options);\r
+        }\r
+      };\r
+      options.error = Backbone.wrapError(options.error, model, options);\r
+      var method = this.isNew() ? 'create' : 'update';\r
+      return (this.sync || Backbone.sync).call(this, method, this, options);\r
+    },\r
+\r
+    // Destroy this model on the server if it was already persisted.\r
+    // Optimistically removes the model from its collection, if it has one.\r
+    // If `wait: true` is passed, waits for the server to respond before removal.\r
+    destroy: function(options) {\r
+      options = options ? _.clone(options) : {};\r
+      var model = this;\r
+      var success = options.success;\r
+\r
+      var triggerDestroy = function() {\r
+        model.trigger('destroy', model, model.collection, options);\r
+      };\r
+\r
+      if (this.isNew()) return triggerDestroy();\r
+      options.success = function(resp) {\r
+        if (options.wait) triggerDestroy();\r
+        if (success) {\r
+          success(model, resp);\r
+        } else {\r
+          model.trigger('sync', model, resp, options);\r
+        }\r
+      };\r
+      options.error = Backbone.wrapError(options.error, model, options);\r
+      var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);\r
+      if (!options.wait) triggerDestroy();\r
+      return xhr;\r
+    },\r
+\r
+    // Default URL for the model's representation on the server -- if you're\r
+    // using Backbone's restful methods, override this to change the endpoint\r
+    // that will be called.\r
+    url: function() {\r
+      var base = getValue(this.collection, 'url') || getValue(this, 'urlRoot') || urlError();\r
+      if (this.isNew()) return base;\r
+      return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);\r
+    },\r
+\r
+    // **parse** converts a response into the hash of attributes to be `set` on\r
+    // the model. The default implementation is just to pass the response along.\r
+    parse: function(resp, xhr) {\r
+      return resp;\r
+    },\r
+\r
+    // Create a new model with identical attributes to this one.\r
+    clone: function() {\r
+      return new this.constructor(this.attributes);\r
+    },\r
+\r
+    // A model is new if it has never been saved to the server, and lacks an id.\r
+    isNew: function() {\r
+      return this.id == null;\r
+    },\r
+\r
+    // Call this method to manually fire a `change` event for this model.\r
+    // Calling this will cause all objects observing the model to update.\r
+    change: function(options) {\r
+      this.trigger('change', this, options);\r
+      this._previousAttributes = _.clone(this.attributes);\r
+      this._changed = false;\r
+    },\r
+\r
+    // Determine if the model has changed since the last `"change"` event.\r
+    // If you specify an attribute name, determine if that attribute has changed.\r
+    hasChanged: function(attr) {\r
+      if (attr) return !_.isEqual(this._previousAttributes[attr], this.attributes[attr]);\r
+      return this._changed;\r
+    },\r
+\r
+    // Return an object containing all the attributes that have changed, or\r
+    // false if there are no changed attributes. Useful for determining what\r
+    // parts of a view need to be updated and/or what attributes need to be\r
+    // persisted to the server. Unset attributes will be set to undefined.\r
+    changedAttributes: function(now) {\r
+      if (!this._changed) return false;\r
+      now || (now = this.attributes);\r
+      var changed = false, old = this._previousAttributes;\r
+      for (var attr in now) {\r
+        if (_.isEqual(old[attr], now[attr])) continue;\r
+        (changed || (changed = {}))[attr] = now[attr];\r
+      }\r
+      for (var attr in old) {\r
+        if (!(attr in now)) (changed || (changed = {}))[attr] = void 0;\r
+      }\r
+      return changed;\r
+    },\r
+\r
+    // Get the previous value of an attribute, recorded at the time the last\r
+    // `"change"` event was fired.\r
+    previous: function(attr) {\r
+      if (!attr || !this._previousAttributes) return null;\r
+      return this._previousAttributes[attr];\r
+    },\r
+\r
+    // Get all of the attributes of the model at the time of the previous\r
+    // `"change"` event.\r
+    previousAttributes: function() {\r
+      return _.clone(this._previousAttributes);\r
+    },\r
+\r
+    // Run validation against a set of incoming attributes, returning `true`\r
+    // if all is well. If a specific `error` callback has been passed,\r
+    // call that instead of firing the general `"error"` event.\r
+    _performValidation: function(attrs, options) {\r
+      var newAttrs = _.extend({}, this.attributes, attrs);\r
+      var error = this.validate(newAttrs, options);\r
+      if (error) {\r
+        if (options.error) {\r
+          options.error(this, error, options);\r
+        } else {\r
+          this.trigger('error', this, error, options);\r
+        }\r
+        return false;\r
+      }\r
+      return true;\r
+    }\r
+\r
+  });\r
+\r
+  // Backbone.Collection\r
+  // -------------------\r
+\r
+  // Provides a standard collection class for our sets of models, ordered\r
+  // or unordered. If a `comparator` is specified, the Collection will maintain\r
+  // its models in sort order, as they're added and removed.\r
+  Backbone.Collection = function(models, options) {\r
+    options || (options = {});\r
+    if (options.comparator) this.comparator = options.comparator;\r
+    this._reset();\r
+    this.initialize.apply(this, arguments);\r
+    if (models) this.reset(models, {silent: true, parse: options.parse});\r
+  };\r
+\r
+  // Define the Collection's inheritable methods.\r
+  _.extend(Backbone.Collection.prototype, Backbone.Events, {\r
+\r
+    // The default model for a collection is just a **Backbone.Model**.\r
+    // This should be overridden in most cases.\r
+    model: Backbone.Model,\r
+\r
+    // Initialize is an empty function by default. Override it with your own\r
+    // initialization logic.\r
+    initialize: function(){},\r
+\r
+    // The JSON representation of a Collection is an array of the\r
+    // models' attributes.\r
+    toJSON: function() {\r
+      return this.map(function(model){ return model.toJSON(); });\r
+    },\r
+\r
+    // Add a model, or list of models to the set. Pass **silent** to avoid\r
+    // firing the `add` event for every new model.\r
+    add: function(models, options) {\r
+      var i, index, length, model, cids = {};\r
+      options || (options = {});\r
+      models = _.isArray(models) ? models.slice() : [models];\r
+\r
+      // Begin by turning bare objects into model references, and preventing\r
+      // invalid models or duplicate models from being added.\r
+      for (i = 0, length = models.length; i < length; i++) {\r
+        if (!(model = models[i] = this._prepareModel(models[i], options))) {\r
+          throw new Error("Can't add an invalid model to a collection");\r
+        }\r
+        var hasId = model.id != null;\r
+        if (this._byCid[model.cid] || (hasId && this._byId[model.id])) {\r
+          throw new Error("Can't add the same model to a collection twice");\r
+        }\r
+      }\r
+\r
+      // Listen to added models' events, and index models for lookup by\r
+      // `id` and by `cid`.\r
+      for (i = 0; i < length; i++) {\r
+        (model = models[i]).on('all', this._onModelEvent, this);\r
+        this._byCid[model.cid] = model;\r
+        if (model.id != null) this._byId[model.id] = model;\r
+        cids[model.cid] = true;\r
+      }\r
+\r
+      // Insert models into the collection, re-sorting if needed, and triggering\r
+      // `add` events unless silenced.\r
+      this.length += length;\r
+      index = options.at != null ? options.at : this.models.length;\r
+      splice.apply(this.models, [index, 0].concat(models));\r
+      if (this.comparator) this.sort({silent: true});\r
+      if (options.silent) return this;\r
+      for (i = 0, length = this.models.length; i < length; i++) {\r
+        if (!cids[(model = this.models[i]).cid]) continue;\r
+        options.index = i;\r
+        model.trigger('add', model, this, options);\r
+      }\r
+      return this;\r
+    },\r
+\r
+    // Remove a model, or a list of models from the set. Pass silent to avoid\r
+    // firing the `remove` event for every model removed.\r
+    remove: function(models, options) {\r
+      var i, l, index, model;\r
+      options || (options = {});\r
+      models = _.isArray(models) ? models.slice() : [models];\r
+      for (i = 0, l = models.length; i < l; i++) {\r
+        model = this.getByCid(models[i]) || this.get(models[i]);\r
+        if (!model) continue;\r
+        delete this._byId[model.id];\r
+        delete this._byCid[model.cid];\r
+        index = this.indexOf(model);\r
+        this.models.splice(index, 1);\r
+        this.length--;\r
+        if (!options.silent) {\r
+          options.index = index;\r
+          model.trigger('remove', model, this, options);\r
+        }\r
+        this._removeReference(model);\r
+      }\r
+      return this;\r
+    },\r
+\r
+    // Get a model from the set by id.\r
+    get: function(id) {\r
+      if (id == null) return null;\r
+      return this._byId[id.id != null ? id.id : id];\r
+    },\r
+\r
+    // Get a model from the set by client id.\r
+    getByCid: function(cid) {\r
+      return cid && this._byCid[cid.cid || cid];\r
+    },\r
+\r
+    // Get the model at the given index.\r
+    at: function(index) {\r
+      return this.models[index];\r
+    },\r
+\r
+    // Force the collection to re-sort itself. You don't need to call this under\r
+    // normal circumstances, as the set will maintain sort order as each item\r
+    // is added.\r
+    sort: function(options) {\r
+      options || (options = {});\r
+      if (!this.comparator) throw new Error('Cannot sort a set without a comparator');\r
+      var boundComparator = _.bind(this.comparator, this);\r
+      if (this.comparator.length == 1) {\r
+        this.models = this.sortBy(boundComparator);\r
+      } else {\r
+        this.models.sort(boundComparator);\r
+      }\r
+      if (!options.silent) this.trigger('reset', this, options);\r
+      return this;\r
+    },\r
+\r
+    // Pluck an attribute from each model in the collection.\r
+    pluck: function(attr) {\r
+      return _.map(this.models, function(model){ return model.get(attr); });\r
+    },\r
+\r
+    // When you have more items than you want to add or remove individually,\r
+    // you can reset the entire set with a new list of models, without firing\r
+    // any `add` or `remove` events. Fires `reset` when finished.\r
+    reset: function(models, options) {\r
+      models  || (models = []);\r
+      options || (options = {});\r
+      for (var i = 0, l = this.models.length; i < l; i++) {\r
+        this._removeReference(this.models[i]);\r
+      }\r
+      this._reset();\r
+      this.add(models, {silent: true, parse: options.parse});\r
+      if (!options.silent) this.trigger('reset', this, options);\r
+      return this;\r
+    },\r
+\r
+    // Fetch the default set of models for this collection, resetting the\r
+    // collection when they arrive. If `add: true` is passed, appends the\r
+    // models to the collection instead of resetting.\r
+    fetch: function(options) {\r
+      options = options ? _.clone(options) : {};\r
+      if (options.parse === undefined) options.parse = true;\r
+      var collection = this;\r
+      var success = options.success;\r
+      options.success = function(resp, status, xhr) {\r
+        collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);\r
+        if (success) success(collection, resp);\r
+      };\r
+      options.error = Backbone.wrapError(options.error, collection, options);\r
+      return (this.sync || Backbone.sync).call(this, 'read', this, options);\r
+    },\r
+\r
+    // Create a new instance of a model in this collection. Add the model to the\r
+    // collection immediately, unless `wait: true` is passed, in which case we\r
+    // wait for the server to agree.\r
+    create: function(model, options) {\r
+      var coll = this;\r
+      options = options ? _.clone(options) : {};\r
+      model = this._prepareModel(model, options);\r
+      if (!model) return false;\r
+      if (!options.wait) coll.add(model, options);\r
+      var success = options.success;\r
+      options.success = function(nextModel, resp, xhr) {\r
+        if (options.wait) coll.add(nextModel, options);\r
+        if (success) {\r
+          success(nextModel, resp);\r
+        } else {\r
+          nextModel.trigger('sync', model, resp, options);\r
+        }\r
+      };\r
+      model.save(null, options);\r
+      return model;\r
+    },\r
+\r
+    // **parse** converts a response into a list of models to be added to the\r
+    // collection. The default implementation is just to pass it through.\r
+    parse: function(resp, xhr) {\r
+      return resp;\r
+    },\r
+\r
+    // Proxy to _'s chain. Can't be proxied the same way the rest of the\r
+    // underscore methods are proxied because it relies on the underscore\r
+    // constructor.\r
+    chain: function () {\r
+      return _(this.models).chain();\r
+    },\r
+\r
+    // Reset all internal state. Called when the collection is reset.\r
+    _reset: function(options) {\r
+      this.length = 0;\r
+      this.models = [];\r
+      this._byId  = {};\r
+      this._byCid = {};\r
+    },\r
+\r
+    // Prepare a model or hash of attributes to be added to this collection.\r
+    _prepareModel: function(model, options) {\r
+      if (!(model instanceof Backbone.Model)) {\r
+        var attrs = model;\r
+        options.collection = this;\r
+        model = new this.model(attrs, options);\r
+        if (model.validate && !model._performValidation(model.attributes, options)) model = false;\r
+      } else if (!model.collection) {\r
+        model.collection = this;\r
+      }\r
+      return model;\r
+    },\r
+\r
+    // Internal method to remove a model's ties to a collection.\r
+    _removeReference: function(model) {\r
+      if (this == model.collection) {\r
+        delete model.collection;\r
+      }\r
+      model.off('all', this._onModelEvent, this);\r
+    },\r
+\r
+    // Internal method called every time a model in the set fires an event.\r
+    // Sets need to update their indexes when models change ids. All other\r
+    // events simply proxy through. "add" and "remove" events that originate\r
+    // in other collections are ignored.\r
+    _onModelEvent: function(ev, model, collection, options) {\r
+      if ((ev == 'add' || ev == 'remove') && collection != this) return;\r
+      if (ev == 'destroy') {\r
+        this.remove(model, options);\r
+      }\r
+      if (model && ev === 'change:' + model.idAttribute) {\r
+        delete this._byId[model.previous(model.idAttribute)];\r
+        this._byId[model.id] = model;\r
+      }\r
+      this.trigger.apply(this, arguments);\r
+    }\r
+\r
+  });\r
+\r
+  // Underscore methods that we want to implement on the Collection.\r
+  var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',\r
+    'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',\r
+    'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',\r
+    'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',\r
+    'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];\r
+\r
+  // Mix in each Underscore method as a proxy to `Collection#models`.\r
+  _.each(methods, function(method) {\r
+    Backbone.Collection.prototype[method] = function() {\r
+      return _[method].apply(_, [this.models].concat(_.toArray(arguments)));\r
+    };\r
+  });\r
+\r
+  // Backbone.Router\r
+  // -------------------\r
+\r
+  // Routers map faux-URLs to actions, and fire events when routes are\r
+  // matched. Creating a new one sets its `routes` hash, if not set statically.\r
+  Backbone.Router = function(options) {\r
+    options || (options = {});\r
+    if (options.routes) this.routes = options.routes;\r
+    this._bindRoutes();\r
+    this.initialize.apply(this, arguments);\r
+  };\r
+\r
+  // Cached regular expressions for matching named param parts and splatted\r
+  // parts of route strings.\r
+  var namedParam    = /:\w+/g;\r
+  var splatParam    = /\*\w+/g;\r
+  var escapeRegExp  = /[-[\]{}()+?.,\\^$|#\s]/g;\r
+\r
+  // Set up all inheritable **Backbone.Router** properties and methods.\r
+  _.extend(Backbone.Router.prototype, Backbone.Events, {\r
+\r
+    // Initialize is an empty function by default. Override it with your own\r
+    // initialization logic.\r
+    initialize: function(){},\r
+\r
+    // Manually bind a single named route to a callback. For example:\r
+    //\r
+    //     this.route('search/:query/p:num', 'search', function(query, num) {\r
+    //       ...\r
+    //     });\r
+    //\r
+    route: function(route, name, callback) {\r
+      Backbone.history || (Backbone.history = new Backbone.History);\r
+      if (!_.isRegExp(route)) route = this._routeToRegExp(route);\r
+      if (!callback) callback = this[name];\r
+      Backbone.history.route(route, _.bind(function(fragment) {\r
+        var args = this._extractParameters(route, fragment);\r
+        callback && callback.apply(this, args);\r
+        this.trigger.apply(this, ['route:' + name].concat(args));\r
+        Backbone.history.trigger('route', this, name, args);\r
+      }, this));\r
+    },\r
+\r
+    // Simple proxy to `Backbone.history` to save a fragment into the history.\r
+    navigate: function(fragment, options) {\r
+      Backbone.history.navigate(fragment, options);\r
+    },\r
+\r
+    // Bind all defined routes to `Backbone.history`. We have to reverse the\r
+    // order of the routes here to support behavior where the most general\r
+    // routes can be defined at the bottom of the route map.\r
+    _bindRoutes: function() {\r
+      if (!this.routes) return;\r
+      var routes = [];\r
+      for (var route in this.routes) {\r
+        routes.unshift([route, this.routes[route]]);\r
+      }\r
+      for (var i = 0, l = routes.length; i < l; i++) {\r
+        this.route(routes[i][0], routes[i][1], this[routes[i][1]]);\r
+      }\r
+    },\r
+\r
+    // Convert a route string into a regular expression, suitable for matching\r
+    // against the current location hash.\r
+    _routeToRegExp: function(route) {\r
+      route = route.replace(escapeRegExp, '\\$&')\r
+                   .replace(namedParam, '([^\/]+)')\r
+                   .replace(splatParam, '(.*?)');\r
+      return new RegExp('^' + route + '$');\r
+    },\r
+\r
+    // Given a route, and a URL fragment that it matches, return the array of\r
+    // extracted parameters.\r
+    _extractParameters: function(route, fragment) {\r
+      return route.exec(fragment).slice(1);\r
+    }\r
+\r
+  });\r
+\r
+  // Backbone.History\r
+  // ----------------\r
+\r
+  // Handles cross-browser history management, based on URL fragments. If the\r
+  // browser does not support `onhashchange`, falls back to polling.\r
+  Backbone.History = function() {\r
+    this.handlers = [];\r
+    _.bindAll(this, 'checkUrl');\r
+  };\r
+\r
+  // Cached regex for cleaning leading hashes and slashes .\r
+  var routeStripper = /^[#\/]/;\r
+\r
+  // Cached regex for detecting MSIE.\r
+  var isExplorer = /msie [\w.]+/;\r
+\r
+  // Has the history handling already been started?\r
+  var historyStarted = false;\r
+\r
+  // Set up all inheritable **Backbone.History** properties and methods.\r
+  _.extend(Backbone.History.prototype, Backbone.Events, {\r
+\r
+    // The default interval to poll for hash changes, if necessary, is\r
+    // twenty times a second.\r
+    interval: 50,\r
+\r
+    // Get the cross-browser normalized URL fragment, either from the URL,\r
+    // the hash, or the override.\r
+    getFragment: function(fragment, forcePushState) {\r
+      if (fragment == null) {\r
+        if (this._hasPushState || forcePushState) {\r
+          fragment = window.location.pathname;\r
+          var search = window.location.search;\r
+          if (search) fragment += search;\r
+        } else {\r
+          fragment = window.location.hash;\r
+        }\r
+      }\r
+      fragment = decodeURIComponent(fragment.replace(routeStripper, ''));\r
+      if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);\r
+      return fragment;\r
+    },\r
+\r
+    // Start the hash change handling, returning `true` if the current URL matches\r
+    // an existing route, and `false` otherwise.\r
+    start: function(options) {\r
+\r
+      // Figure out the initial configuration. Do we need an iframe?\r
+      // Is pushState desired ... is it available?\r
+      if (historyStarted) throw new Error("Backbone.history has already been started");\r
+      this.options          = _.extend({}, {root: '/'}, this.options, options);\r
+      this._wantsHashChange = this.options.hashChange !== false;\r
+      this._wantsPushState  = !!this.options.pushState;\r
+      this._hasPushState    = !!(this.options.pushState && window.history && window.history.pushState);\r
+      var fragment          = this.getFragment();\r
+      var docMode           = document.documentMode;\r
+      var oldIE             = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));\r
+      if (oldIE) {\r
+        this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;\r
+        this.navigate(fragment);\r
+      }\r
+\r
+      // Depending on whether we're using pushState or hashes, and whether\r
+      // 'onhashchange' is supported, determine how we check the URL state.\r
+      if (this._hasPushState) {\r
+        $(window).bind('popstate', this.checkUrl);\r
+      } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {\r
+        $(window).bind('hashchange', this.checkUrl);\r
+      } else if (this._wantsHashChange) {\r
+        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);\r
+      }\r
+\r
+      // Determine if we need to change the base url, for a pushState link\r
+      // opened by a non-pushState browser.\r
+      this.fragment = fragment;\r
+      historyStarted = true;\r
+      var loc = window.location;\r
+      var atRoot  = loc.pathname == this.options.root;\r
+\r
+      // If we've started off with a route from a `pushState`-enabled browser,\r
+      // but we're currently in a browser that doesn't support it...\r
+      if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {\r
+        this.fragment = this.getFragment(null, true);\r
+        window.location.replace(this.options.root + '#' + this.fragment);\r
+        // Return immediately as browser will do redirect to new url\r
+        return true;\r
+\r
+      // Or if we've started out with a hash-based route, but we're currently\r
+      // in a browser where it could be `pushState`-based instead...\r
+      } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {\r
+        this.fragment = loc.hash.replace(routeStripper, '');\r
+        window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);\r
+      }\r
+\r
+      if (!this.options.silent) {\r
+        return this.loadUrl();\r
+      }\r
+    },\r
+\r
+    // Disable Backbone.history, perhaps temporarily. Not useful in a real app,\r
+    // but possibly useful for unit testing Routers.\r
+    stop: function() {\r
+      $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);\r
+      clearInterval(this._checkUrlInterval);\r
+      historyStarted = false;\r
+    },\r
+\r
+    // Add a route to be tested when the fragment changes. Routes added later\r
+    // may override previous routes.\r
+    route: function(route, callback) {\r
+      this.handlers.unshift({route: route, callback: callback});\r
+    },\r
+\r
+    // Checks the current URL to see if it has changed, and if it has,\r
+    // calls `loadUrl`, normalizing across the hidden iframe.\r
+    checkUrl: function(e) {\r
+      var current = this.getFragment();\r
+      if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);\r
+      if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;\r
+      if (this.iframe) this.navigate(current);\r
+      this.loadUrl() || this.loadUrl(window.location.hash);\r
+    },\r
+\r
+    // Attempt to load the current URL fragment. If a route succeeds with a\r
+    // match, returns `true`. If no defined routes matches the fragment,\r
+    // returns `false`.\r
+    loadUrl: function(fragmentOverride) {\r
+      var fragment = this.fragment = this.getFragment(fragmentOverride);\r
+      var matched = _.any(this.handlers, function(handler) {\r
+        if (handler.route.test(fragment)) {\r
+          handler.callback(fragment);\r
+          return true;\r
+        }\r
+      });\r
+      return matched;\r
+    },\r
+\r
+    // Save a fragment into the hash history, or replace the URL state if the\r
+    // 'replace' option is passed. You are responsible for properly URL-encoding\r
+    // the fragment in advance.\r
+    //\r
+    // The options object can contain `trigger: true` if you wish to have the\r
+    // route callback be fired (not usually desirable), or `replace: true`, if\r
+    // you which to modify the current URL without adding an entry to the history.\r
+    navigate: function(fragment, options) {\r
+      if (!historyStarted) return false;\r
+      if (!options || options === true) options = {trigger: options};\r
+      var frag = (fragment || '').replace(routeStripper, '');\r
+      if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;\r
+\r
+      // If pushState is available, we use it to set the fragment as a real URL.\r
+      if (this._hasPushState) {\r
+        if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;\r
+        this.fragment = frag;\r
+        window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);\r
+\r
+      // If hash changes haven't been explicitly disabled, update the hash\r
+      // fragment to store history.\r
+      } else if (this._wantsHashChange) {\r
+        this.fragment = frag;\r
+        this._updateHash(window.location, frag, options.replace);\r
+        if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {\r
+          // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.\r
+          // When replace is true, we don't want this.\r
+          if(!options.replace) this.iframe.document.open().close();\r
+          this._updateHash(this.iframe.location, frag, options.replace);\r
+        }\r
+\r
+      // If you've told us that you explicitly don't want fallback hashchange-\r
+      // based history, then `navigate` becomes a page refresh.\r
+      } else {\r
+        window.location.assign(this.options.root + fragment);\r
+      }\r
+      if (options.trigger) this.loadUrl(fragment);\r
+    },\r
+\r
+    // Update the hash location, either replacing the current entry, or adding\r
+    // a new one to the browser history.\r
+    _updateHash: function(location, fragment, replace) {\r
+      if (replace) {\r
+        location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);\r
+      } else {\r
+        location.hash = fragment;\r
+      }\r
+    }\r
+  });\r
+\r
+  // Backbone.View\r
+  // -------------\r
+\r
+  // Creating a Backbone.View creates its initial element outside of the DOM,\r
+  // if an existing element is not provided...\r
+  Backbone.View = function(options) {\r
+    this.cid = _.uniqueId('view');\r
+    this._configure(options || {});\r
+    this._ensureElement();\r
+    this.initialize.apply(this, arguments);\r
+    this.delegateEvents();\r
+  };\r
+\r
+  // Cached regex to split keys for `delegate`.\r
+  var eventSplitter = /^(\S+)\s*(.*)$/;\r
+\r
+  // List of view options to be merged as properties.\r
+  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];\r
+\r
+  // Set up all inheritable **Backbone.View** properties and methods.\r
+  _.extend(Backbone.View.prototype, Backbone.Events, {\r
+\r
+    // The default `tagName` of a View's element is `"div"`.\r
+    tagName: 'div',\r
+\r
+    // jQuery delegate for element lookup, scoped to DOM elements within the\r
+    // current view. This should be prefered to global lookups where possible.\r
+    $: function(selector) {\r
+      return $(selector, this.el);\r
+    },\r
+\r
+    // Initialize is an empty function by default. Override it with your own\r
+    // initialization logic.\r
+    initialize: function(){},\r
+\r
+    // **render** is the core function that your view should override, in order\r
+    // to populate its element (`this.el`), with the appropriate HTML. The\r
+    // convention is for **render** to always return `this`.\r
+    render: function() {\r
+      return this;\r
+    },\r
+\r
+    // Remove this view from the DOM. Note that the view isn't present in the\r
+    // DOM by default, so calling this method may be a no-op.\r
+    remove: function() {\r
+      this.$el.remove();\r
+      return this;\r
+    },\r
+\r
+    // For small amounts of DOM Elements, where a full-blown template isn't\r
+    // needed, use **make** to manufacture elements, one at a time.\r
+    //\r
+    //     var el = this.make('li', {'class': 'row'}, this.model.escape('title'));\r
+    //\r
+    make: function(tagName, attributes, content) {\r
+      var el = document.createElement(tagName);\r
+      if (attributes) $(el).attr(attributes);\r
+      if (content) $(el).html(content);\r
+      return el;\r
+    },\r
+\r
+    // Change the view's element (`this.el` property), including event\r
+    // re-delegation.\r
+    setElement: function(element, delegate) {\r
+      this.$el = $(element);\r
+      this.el = this.$el[0];\r
+      if (delegate !== false) this.delegateEvents();\r
+    },\r
+\r
+    // Set callbacks, where `this.events` is a hash of\r
+    //\r
+    // *{"event selector": "callback"}*\r
+    //\r
+    //     {\r
+    //       'mousedown .title':  'edit',\r
+    //       'click .button':     'save'\r
+    //       'click .open':       function(e) { ... }\r
+    //     }\r
+    //\r
+    // pairs. Callbacks will be bound to the view, with `this` set properly.\r
+    // Uses event delegation for efficiency.\r
+    // Omitting the selector binds the event to `this.el`.\r
+    // This only works for delegate-able events: not `focus`, `blur`, and\r
+    // not `change`, `submit`, and `reset` in Internet Explorer.\r
+    delegateEvents: function(events) {\r
+      if (!(events || (events = getValue(this, 'events')))) return;\r
+      this.undelegateEvents();\r
+      for (var key in events) {\r
+        var method = events[key];\r
+        if (!_.isFunction(method)) method = this[events[key]];\r
+        if (!method) throw new Error('Event "' + events[key] + '" does not exist');\r
+        var match = key.match(eventSplitter);\r
+        var eventName = match[1], selector = match[2];\r
+        method = _.bind(method, this);\r
+        eventName += '.delegateEvents' + this.cid;\r
+        if (selector === '') {\r
+          this.$el.bind(eventName, method);\r
+        } else {\r
+          this.$el.delegate(selector, eventName, method);\r
+        }\r
+      }\r
+    },\r
+\r
+    // Clears all callbacks previously bound to the view with `delegateEvents`.\r
+    // You usually don't need to use this, but may wish to if you have multiple\r
+    // Backbone views attached to the same DOM element.\r
+    undelegateEvents: function() {\r
+      this.$el.unbind('.delegateEvents' + this.cid);\r
+    },\r
+\r
+    // Performs the initial configuration of a View with a set of options.\r
+    // Keys with special meaning *(model, collection, id, className)*, are\r
+    // attached directly to the view.\r
+    _configure: function(options) {\r
+      if (this.options) options = _.extend({}, this.options, options);\r
+      for (var i = 0, l = viewOptions.length; i < l; i++) {\r
+        var attr = viewOptions[i];\r
+        if (options[attr]) this[attr] = options[attr];\r
+      }\r
+      this.options = options;\r
+    },\r
+\r
+    // Ensure that the View has a DOM element to render into.\r
+    // If `this.el` is a string, pass it through `$()`, take the first\r
+    // matching element, and re-assign it to `el`. Otherwise, create\r
+    // an element from the `id`, `className` and `tagName` properties.\r
+    _ensureElement: function() {\r
+      if (!this.el) {\r
+        var attrs = getValue(this, 'attributes') || {};\r
+        if (this.id) attrs.id = this.id;\r
+        if (this.className) attrs['class'] = this.className;\r
+        this.setElement(this.make(this.tagName, attrs), false);\r
+      } else {\r
+        this.setElement(this.el, false);\r
+      }\r
+    }\r
+\r
+  });\r
+\r
+  // The self-propagating extend function that Backbone classes use.\r
+  var extend = function (protoProps, classProps) {\r
+    var child = inherits(this, protoProps, classProps);\r
+    child.extend = this.extend;\r
+    return child;\r
+  };\r
+\r
+  // Set up inheritance for the model, collection, and view.\r
+  Backbone.Model.extend = Backbone.Collection.extend =\r
+    Backbone.Router.extend = Backbone.View.extend = extend;\r
+\r
+  // Backbone.sync\r
+  // -------------\r
+\r
+  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.\r
+  var methodMap = {\r
+    'create': 'POST',\r
+    'update': 'PUT',\r
+    'delete': 'DELETE',\r
+    'read':   'GET'\r
+  };\r
+\r
+  // Override this function to change the manner in which Backbone persists\r
+  // models to the server. You will be passed the type of request, and the\r
+  // model in question. By default, makes a RESTful Ajax request\r
+  // to the model's `url()`. Some possible customizations could be:\r
+  //\r
+  // * Use `setTimeout` to batch rapid-fire updates into a single request.\r
+  // * Send up the models as XML instead of JSON.\r
+  // * Persist models via WebSockets instead of Ajax.\r
+  //\r
+  // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests\r
+  // as `POST`, with a `_method` parameter containing the true HTTP method,\r
+  // as well as all requests with the body as `application/x-www-form-urlencoded`\r
+  // instead of `application/json` with the model in a param named `model`.\r
+  // Useful when interfacing with server-side languages like **PHP** that make\r
+  // it difficult to read the body of `PUT` requests.\r
+  Backbone.sync = function(method, model, options) {\r
+    var type = methodMap[method];\r
+\r
+    // Default JSON-request options.\r
+    var params = {type: type, dataType: 'json'};\r
+\r
+    // Ensure that we have a URL.\r
+    if (!options.url) {\r
+      params.url = getValue(model, 'url') || urlError();\r
+    }\r
+\r
+    // Ensure that we have the appropriate request data.\r
+    if (!options.data && model && (method == 'create' || method == 'update')) {\r
+      params.contentType = 'application/json';\r
+      params.data = JSON.stringify(model.toJSON());\r
+    }\r
+\r
+    // For older servers, emulate JSON by encoding the request into an HTML-form.\r
+    if (Backbone.emulateJSON) {\r
+      params.contentType = 'application/x-www-form-urlencoded';\r
+      params.data = params.data ? {model: params.data} : {};\r
+    }\r
+\r
+    // For older servers, emulate HTTP by mimicking the HTTP method with `_method`\r
+    // And an `X-HTTP-Method-Override` header.\r
+    if (Backbone.emulateHTTP) {\r
+      if (type === 'PUT' || type === 'DELETE') {\r
+        if (Backbone.emulateJSON) params.data._method = type;\r
+        params.type = 'POST';\r
+        params.beforeSend = function(xhr) {\r
+          xhr.setRequestHeader('X-HTTP-Method-Override', type);\r
+        };\r
+      }\r
+    }\r
+\r
+    // Don't process data on a non-GET request.\r
+    if (params.type !== 'GET' && !Backbone.emulateJSON) {\r
+      params.processData = false;\r
+    }\r
+\r
+    // Make the request, allowing the user to override any Ajax options.\r
+    return $.ajax(_.extend(params, options));\r
+  };\r
+\r
+  // Wrap an optional error callback with a fallback error event.\r
+  Backbone.wrapError = function(onError, originalModel, options) {\r
+    return function(model, resp) {\r
+      var resp = model === originalModel ? resp : model;\r
+      if (onError) {\r
+        onError(model, resp, options);\r
+      } else {\r
+        originalModel.trigger('error', model, resp, options);\r
+      }\r
+    };\r
+  };\r
+\r
+  // Helpers\r
+  // -------\r
+\r
+  // Shared empty constructor function to aid in prototype-chain creation.\r
+  var ctor = function(){};\r
+\r
+  // Helper function to correctly set up the prototype chain, for subclasses.\r
+  // Similar to `goog.inherits`, but uses a hash of prototype properties and\r
+  // class properties to be extended.\r
+  var inherits = function(parent, protoProps, staticProps) {\r
+    var child;\r
+\r
+    // The constructor function for the new subclass is either defined by you\r
+    // (the "constructor" property in your `extend` definition), or defaulted\r
+    // by us to simply call the parent's constructor.\r
+    if (protoProps && protoProps.hasOwnProperty('constructor')) {\r
+      child = protoProps.constructor;\r
+    } else {\r
+      child = function(){ parent.apply(this, arguments); };\r
+    }\r
+\r
+    // Inherit class (static) properties from parent.\r
+    _.extend(child, parent);\r
+\r
+    // Set the prototype chain to inherit from `parent`, without calling\r
+    // `parent`'s constructor function.\r
+    ctor.prototype = parent.prototype;\r
+    child.prototype = new ctor();\r
+\r
+    // Add prototype properties (instance properties) to the subclass,\r
+    // if supplied.\r
+    if (protoProps) _.extend(child.prototype, protoProps);\r
+\r
+    // Add static properties to the constructor function, if supplied.\r
+    if (staticProps) _.extend(child, staticProps);\r
+\r
+    // Correctly set child's `prototype.constructor`.\r
+    child.prototype.constructor = child;\r
+\r
+    // Set a convenience property in case the parent's prototype is needed later.\r
+    child.__super__ = parent.prototype;\r
+\r
+    return child;\r
+  };\r
+\r
+  // Helper function to get a value from a Backbone object as a property\r
+  // or as a function.\r
+  var getValue = function(object, prop) {\r
+    if (!(object && object[prop])) return null;\r
+    return _.isFunction(object[prop]) ? object[prop]() : object[prop];\r
+  };\r
+\r
+  // Throw an error when a URL is needed, and none is supplied.\r
+  var urlError = function() {\r
+    throw new Error('A "url" property or function must be specified');\r
+  };\r
+\r
+}).call(this);\r
old mode 100644 (file)
new mode 100755 (executable)
index 05bca87..cf02cf3
@@ -8,7 +8,7 @@ kiwi.front.events = {
     /**\r
     *   Binds all of the event handlers to their events\r
     */\r
-       bindAll: function () {\r
+    bindAll: function () {\r
         $(kiwi.gateway).bind('onmsg', this.onMsg)\r
             .bind('onnotice', this.onNotice)\r
             .bind('onaction', this.onAction)\r
@@ -41,7 +41,7 @@ kiwi.front.events = {
             .bind('onctcp_response', this.onCTCPResponse)\r
             .bind('onirc_error', this.onIRCError)\r
             .bind('onkiwi', this.onKiwi);\r
-       },\r
+    },\r
 \r
     /**\r
     *   Handles the msg event\r
@@ -62,11 +62,11 @@ kiwi.front.events = {
         if (!plugin_event) {\r
             return;\r
         }\r
-        tab = Tabview.getTab(plugin_event.destination);\r
-        if (!tab) {\r
-            tab = new Tabview(plugin_event.destination);\r
+        \r
+        var chan = kiwi.channels.getByName(plugin_event.destination);\r
+        if (chan) {\r
+            chan.addMsg(null, plugin_event.nick, plugin_event.msg);\r
         }\r
-        tab.addMsg(null, plugin_event.nick, plugin_event.msg);\r
     },\r
 \r
     /**\r
@@ -96,11 +96,12 @@ kiwi.front.events = {
             destination = data.channel;\r
         }\r
 \r
-        tab = Tabview.getTab(destination);\r
-        if (!tab) {\r
-            tab = new Tabview(destination);\r
+        var chan = kiwi.channels.getByName(destination);\r
+        if (chan) {\r
+            chan.addMsg(null, ' ', '* ' + data.nick + ' ' + data.msg, 'action', 'color:#555;');\r
+        } else {\r
+            kiwi.channels.server.addMsg(null, ' ', '* ' + data.nick + ' ' + data.msg, 'action', 'color:#555;');\r
         }\r
-        tab.addMsg(null, ' ', '* ' + data.nick + ' ' + data.msg, 'action', 'color:#555;');\r
     },\r
 \r
     /**\r
@@ -109,9 +110,10 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onTopic: function (e, data) {\r
-        var tab = Tabview.getTab(data.channel);\r
-        if (tab) {\r
-            tab.changeTopic(data.topic);\r
+        var chan = kiwi.channels.getByName(data.channel);\r
+        if (chan) {\r
+            chan.set({"topic": data.topic});\r
+            chan.addMsg(null, ' ', '=== Topic for ' + data.channel + ' is: ' + data.topic, 'topic');\r
         }\r
     },\r
 \r
@@ -121,10 +123,11 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onTopicSetBy: function (e, data) {\r
-        var when, tab = Tabview.getTab(data.channel);\r
-        if (tab) {\r
+        var when,\r
+            chan = kiwi.channels.getByName(data.channel);\r
+        if (chan) {\r
             when = new Date(data.when * 1000).toLocaleString();\r
-            tab.addMsg(null, '', 'Topic set by ' + data.nick + ' at ' + when, 'topic');\r
+            chan.addMsg(null, '', 'Topic set by ' + data.nick + ' at ' + when, 'topic');\r
         }\r
     },\r
 \r
@@ -135,14 +138,14 @@ kiwi.front.events = {
     */\r
     onNotice: function (e, data) {\r
         var nick = (data.nick === undefined) ? '' : data.nick,\r
-            enick = '[' + nick + ']';\r
+            enick = '[' + nick + ']',\r
+            chan;\r
 \r
-        if (Tabview.tabExists(data.target)) {\r
-            Tabview.getTab(data.target).addMsg(null, enick, data.msg, 'notice');\r
-        } else if (Tabview.tabExists(nick)) {\r
-            Tabview.getTab(nick).addMsg(null, enick, data.msg, 'notice');\r
+        chan = kiwi.channels.getByName(data.channel);\r
+        if (chan) {\r
+            chan.addMsg(null, enick, data.msg, 'notice');\r
         } else {\r
-            Tabview.getServerTab().addMsg(null, enick, data.msg, 'notice');\r
+            kiwi.channels.server.addMsg(null, enick, data.msg, 'notice');\r
         }\r
     },\r
 \r
@@ -164,7 +167,8 @@ kiwi.front.events = {
             kiwi.gateway.ctcp(false, 'TIME', data.nick, (new Date()).toLocaleString());\r
             break;\r
         }\r
-        Tabview.getServerTab().addMsg(null, 'CTCP Request', '[from ' + data.nick + '] ' + data.msg, 'ctcp');\r
+        kiwi.channels.server.addMsg(null, 'CTCP Request', '[from ' + data.nick + '] ' + data.msg, 'ctcp');\r
+        \r
     },\r
 \r
     /**\r
@@ -173,7 +177,7 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onCTCPResponse: function (e, data) {\r
-        Tabview.getServerTab().addMsg(null, 'CTCP Reply', '[from ' + data.nick + '] ' + data.msg, 'ctcp');\r
+        kiwi.channels.server.addMsg(null, 'CTCP Reply', '[from ' + data.nick + '] ' + data.msg, 'ctcp');\r
     },\r
 \r
     /**\r
@@ -201,15 +205,15 @@ kiwi.front.events = {
                 err_box.parent().removeClass('disconnect');\r
 \r
                 // Rejoin channels\r
-                channels = '';\r
-                _.each(Tabview.getAllTabs(), function (tabview) {\r
-                    if (tabview.name === 'server') {\r
-                        return;\r
-                    }\r
-                    channels += tabview.name + ',';\r
-                });\r
-                console.log('Rejoining: ' + channels);\r
-                kiwi.gateway.join(channels);\r
+                //channels = '';\r
+                //_.each(Tabview.getAllTabs(), function (tabview) {\r
+                //    if (tabview.name === 'server') {\r
+                //        return;\r
+                //    }\r
+                //    channels += tabview.name + ',';\r
+                //});\r
+                //console.log('Rejoining: ' + channels);\r
+                //kiwi.gateway.join(channels);\r
                 return;\r
             }\r
 \r
@@ -218,13 +222,13 @@ kiwi.front.events = {
                 kiwi.front.ui.doLayout();\r
             }\r
 \r
-            Tabview.getServerTab().addMsg(null, ' ', '=== Connected OK :)', 'status');\r
+            kiwi.channels.server.addMsg(null, ' ', '=== Connected OK :)', 'status');\r
             if (typeof init_data.channel === "string") {\r
                 kiwi.front.joinChannel(init_data.channel);\r
             }\r
             kiwi.plugs.run('connect', {success: true});\r
         } else {\r
-            Tabview.getServerTab().addMsg(null, ' ', '=== Failed to connect :(', 'status');\r
+            kiwi.channels.server.addMsg(null, ' ', '=== Failed to connect :(', 'status');\r
             kiwi.plugs.run('connect', {success: false});\r
         }\r
 \r
@@ -238,7 +242,7 @@ kiwi.front.events = {
     */\r
     onConnectFail: function (e, data) {\r
         var reason = (typeof data.reason === 'string') ? data.reason : '';\r
-        Tabview.getServerTab().addMsg(null, '', 'There\'s a problem connecting! (' + reason + ')', 'error');\r
+        kiwi.channels.server.addMsg(null, '', 'There\'s a problem connecting! (' + reason + ')', 'error');\r
         kiwi.plugs.run('connect', {success: false});\r
     },\r
     /**\r
@@ -248,10 +252,10 @@ kiwi.front.events = {
     */\r
     onDisconnect: function (e, data) {\r
         var tab, tabs;\r
-        tabs = Tabview.getAllTabs();\r
-        for (tab in tabs) {\r
-            tabs[tab].addMsg(null, '', 'Disconnected from server!', 'error disconnect');\r
-        }\r
+        //tabs = Tabview.getAllTabs();\r
+        //for (tab in tabs) {\r
+        //    tabs[tab].addMsg(null, '', 'Disconnected from server!', 'error disconnect');\r
+        //}\r
         kiwi.plugs.run('disconnect', {success: false});\r
     },\r
     /**\r
@@ -301,7 +305,7 @@ kiwi.front.events = {
     */\r
     onOptions: function (e, data) {\r
         if (typeof kiwi.gateway.network_name === "string" && kiwi.gateway.network_name !== "") {\r
-            Tabview.getServerTab().setTabText(kiwi.gateway.network_name);\r
+            kiwi.channels.server.set({"name": kiwi.gateway.network_name});\r
         }\r
     },\r
     /**\r
@@ -310,7 +314,7 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onMOTD: function (e, data) {\r
-        Tabview.getServerTab().addMsg(null, data.server, data.msg, 'motd');\r
+        kiwi.channels.server.addMsg(null, data.server, data.msg, 'motd');\r
     },\r
     /**\r
     *   Handles the whois event\r
@@ -330,7 +334,7 @@ kiwi.front.events = {
             idle_time = idle_time.h.toString().lpad(2, "0") + ':' + idle_time.m.toString().lpad(2, "0") + ':' + idle_time.s.toString().lpad(2, "0");\r
         }\r
 \r
-        tab = Tabview.getCurrentTab();\r
+        tab = kiwi.currentPanel;\r
         if (data.msg) {\r
             tab.addMsg(null, data.nick, data.msg, 'whois');\r
         } else if (data.logon) {\r
@@ -349,15 +353,23 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onMode: function (e, data) {\r
-        var tab;\r
+        var tab, mem;\r
         if ((typeof data.channel === 'string') && (typeof data.effected_nick === 'string')) {\r
-            tab = Tabview.getTab(data.channel);\r
-            tab.addMsg(null, ' ', '[' + data.mode + '] ' + data.effected_nick + ' by ' + data.nick, 'mode', '');\r
-            if (tab.userlist.hasUser(data.effected_nick)) {\r
-                tab.userlist.changeUserMode(data.effected_nick, data.mode.substr(1), (data.mode[0] === '+'));\r
+            chan = kiwi.channels.getByName(data.channel);\r
+            if (chan) {\r
+                chan.addMsg(null, ' ', '[' + data.mode + '] ' + data.effected_nick + ' by ' + data.nick, 'mode', '');\r
+                mem = _.detect(chan.get("members"), function (m) {\r
+                    return data.effected_nick === m.get("nick");\r
+                });\r
+                if (mem) {\r
+                    if (data.mode[0] === '+') {\r
+                        mem.addMode(data.mode);\r
+                    } else {\r
+                        mem.removeMode(data.mode);\r
+                    }\r
+                }\r
             }\r
         }\r
-\r
         // TODO: Other mode changes that aren't +/- qaohv. - JA\r
     },\r
     /**\r
@@ -366,24 +378,21 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onUserList: function (e, data) {\r
-        var tab;\r
-\r
-        tab = Tabview.getTab(data.channel);\r
-        if (!tab) {\r
-            return;\r
-        }\r
-\r
-        if ((!kiwi.front.cache.userlist) || (!kiwi.front.cache.userlist.updating)) {\r
-            if (!kiwi.front.cache.userlist) {\r
-                kiwi.front.cache.userlist = {updating: true};\r
-            } else {\r
-                kiwi.front.cache.userlist.updating = true;\r
+        var tab, chan;\r
+        chan = kiwi.channels.getByName(data.channel);\r
+        if (chan) {\r
+            if ((!kiwi.front.cache.userlist) || (!kiwi.front.cache.userlist.updating)) {\r
+                if (!kiwi.front.cache.userlist) {\r
+                    kiwi.front.cache.userlist = {updating: true};\r
+                } else {\r
+                    kiwi.front.cache.userlist.updating = true;\r
+                }\r
+                chan.get("members").reset([],{"silent": true});\r
             }\r
-            tab.userlist.empty();\r
+            _.forEach(data.users, function (u) {\r
+                chan.get("members").add(new kiwi.model.Member(u), {"silent": true});\r
+            });\r
         }\r
-\r
-        tab.userlist.addUser(data.users);\r
-\r
     },\r
     /**\r
     *   Handles the userListEnd event\r
@@ -391,10 +400,15 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onUserListEnd: function (e, data) {\r
+        var chan;\r
         if (!kiwi.front.cache.userlist) {\r
             kiwi.front.cache.userlist = {};\r
         }\r
         kiwi.front.cache.userlist.updating = false;\r
+        chan = kiwi.channels.getByName(data.channel);\r
+        if (chan) {\r
+            chan.get("members").trigger("change");\r
+        }\r
     },\r
 \r
     /**\r
@@ -447,18 +461,15 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onJoin: function (e, data) {\r
-        var tab = Tabview.getTab(data.channel);\r
-        if (!tab) {\r
-            tab = new Tabview(data.channel.toLowerCase());\r
-        }\r
-\r
-        tab.addMsg(null, ' ', '--> ' + data.nick + ' [' + data.ident + '@' + data.hostname + '] has joined', 'action join', 'color:#009900;');\r
-\r
-        if (data.nick === kiwi.gateway.nick) {\r
-            return; // Not needed as it's already in nicklist\r
+        var chan = kiwi.channels.getByName(data.channel);\r
+        if (!chan) {\r
+            chan = new kiwi.model.Channel({"name": data.channel.toLowerCase()});\r
+            kiwi.channels.add(chan);\r
+            // No need to add ourselves to the MemberList as RPL_NAMESREPLY will be next\r
+            chan.view.show();\r
+        } else {\r
+            chan.get("members").add(new kiwi.model.Member({"nick": data.nick, "modes": [], "ident": data.ident, "hostname": data.hostname}));\r
         }\r
-\r
-        tab.userlist.addUser({nick: data.nick, modes: []});\r
     },\r
     /**\r
     *   Handles the part event\r
@@ -466,17 +477,17 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onPart: function (e, data) {\r
-        var tab = Tabview.getTab(data.channel);\r
-        if (tab) {\r
-            // If this is us, close the tabview\r
+        var chan, members, cid;\r
+        chan = kiwi.channels.getByName(data.channel);\r
+        if (chan) {\r
             if (data.nick === kiwi.gateway.nick) {\r
-                tab.close();\r
-                Tabview.getServerTab().show();\r
-                return;\r
+                chan.trigger("close");\r
+            } else {\r
+                members = chan.get("members");\r
+                members.remove(members.detect(function (m) {\r
+                    return data.nick === m.get("nick");\r
+                }).cid, {"message": data.message})\r
             }\r
-\r
-            tab.addMsg(null, ' ', '<-- ' + data.nick + ' has left (' + data.message + ')', 'action part', 'color:#990000;');\r
-            tab.userlist.removeUser(data.nick);\r
         }\r
     },\r
     /**\r
@@ -485,19 +496,17 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onKick: function (e, data) {\r
-        var tab = Tabview.getTab(data.channel);\r
-        if (tab) {\r
-            // If this is us, close the tabview\r
+        var panel = kiwi.channels.getByName(data.channel);\r
+        if (panel) {\r
+            // If this is us, close the panel\r
             if (data.kicked === kiwi.gateway.nick) {\r
-                //tab.close();\r
-                tab.addMsg(null, ' ', '=== You have been kicked from ' + data.channel + '. ' + data.message, 'status kick');\r
-                tab.safe_to_close = true;\r
-                tab.userlist.remove();\r
+                kiwi.channels.remove(panel);\r
+                kiwi.channels.server.addMsg(null, ' ', '=== You have been kicked from ' + data.channel + '. ' + data.message, 'status kick');\r
                 return;\r
             }\r
 \r
-            tab.addMsg(null, ' ', '<-- ' + data.kicked + ' kicked by ' + data.nick + '(' + data.message + ')', 'action kick', 'color:#990000;');\r
-            tab.userlist.removeUser(data.nick);\r
+            panel.addMsg(null, ' ', '<-- ' + data.kicked + ' kicked by ' + data.nick + '(' + data.message + ')', 'action kick', 'color:#990000;');\r
+            panel.userlist.removeUser(data.nick);\r
         }\r
     },\r
     /**\r
@@ -511,10 +520,13 @@ kiwi.front.events = {
             kiwi.front.ui.doLayout();\r
         }\r
 \r
-        _.each(Tabview.getAllTabs(), function (tab) {\r
-            if (tab.userlist.hasUser(data.nick)) {\r
-                tab.userlist.renameUser(data.nick, data.newnick);\r
-                tab.addMsg(null, ' ', '=== ' + data.nick + ' is now known as ' + data.newnick, 'action changenick');\r
+        kiwi.channels.each(function (panel) {\r
+            if (panel.isChannel) {\r
+                var member = panel.get("members").getByNick(data.nick);\r
+                if (member) {\r
+                    member.set({"nick": data.newnick});\r
+                    panel.addMsg(null, ' ', '=== ' + data.nick + ' is now known as ' + data.newnick, 'action changenick');\r
+                }\r
             }\r
         });\r
     },\r
@@ -524,10 +536,15 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onQuit: function (e, data) {\r
-        _.each(Tabview.getAllTabs(), function (tab) {\r
-            if (tab.userlist.hasUser(data.nick)) {\r
-                tab.userlist.removeUser(data.nick);\r
-                tab.addMsg(null, ' ', '<-- ' + data.nick + ' has quit (' + data.message + ')', 'action quit', 'color:#990000;');\r
+        var chan, members, member;\r
+        kiwi.channels.forEach(function (chan) {\r
+            members = chan.get("members");\r
+            member = members.detect(function (m) {\r
+                return data.nick === m.get("nick");\r
+            });\r
+            if (member) {\r
+                members.trigger("quit", {"member": member, "message": data.message});\r
+                members.remove(member.cid);\r
             }\r
         });\r
     },\r
@@ -537,10 +554,10 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onChannelRedirect: function (e, data) {\r
-        var tab = Tabview.getTab(data.from);\r
-        tab.close();\r
-        tab = new Tabview(data.to);\r
-        tab.addMsg(null, ' ', '=== Redirected from ' + data.from, 'action');\r
+        //var tab = Tabview.getTab(data.from);\r
+        //tab.close();\r
+        //tab = new Tabview(data.to);\r
+        //tab.addMsg(null, ' ', '=== Redirected from ' + data.from, 'action');\r
     },\r
 \r
     /**\r
@@ -549,7 +566,7 @@ kiwi.front.events = {
     *   @param  {Object}        data    The event data\r
     */\r
     onIRCError: function (e, data) {\r
-        var t_view,\r
+        /*var t_view,\r
             tab = Tabview.getTab(data.channel);\r
         if (data.channel !== undefined && tab) {\r
             t_view = data.channel;\r
@@ -595,8 +612,8 @@ kiwi.front.events = {
             break;\r
         default:\r
             // We don't know what data contains, so don't do anything with it.\r
-            //kiwi.front.tabviews.server.addMsg(null, ' ', '=== ' + data, 'status');\r
-        }\r
+            console.log(e, data);\r
+        }*/\r
     },\r
 \r
 \r
old mode 100644 (file)
new mode 100755 (executable)
index 8928632..005cffa
@@ -101,7 +101,10 @@ kiwi.front = {
                 console.log('whoaa');
             }
 
-            Tabview.getCurrentTab().userlist.setWidth(new_width);
+            var members = kiwi.currentPanel.get("members");
+            if (members) {
+                $(members.view.el).width(new_width);
+            }
             $('#windows').css('right', ul.outerWidth(true));
         }});
 
@@ -128,9 +131,12 @@ kiwi.front = {
 
             kiwi.front.ui.doLayout();
             try {
-                tmp = '/connect ' + netsel.val() + ' ' + netport.val() + ' ';
-                tmp += (netssl.is(':checked') ? 'true' : 'false') + ' ' + netpass.val();
-                kiwi.front.run(tmp);
+                kiwi.gateway.connect(netsel.val(), netport.val(), netssl.is(':checked'), netpass.val(), function () {
+                    setTimeout(function () {
+                        kiwi.channels.server.set({"name": netsel.val()});
+                        kiwi.channels.view.render();
+                    }, 0);
+                });
             } catch (e) {
                 console.log(e);
             }
@@ -168,15 +174,18 @@ kiwi.front = {
         kiwi.front.ui.doLayout();
         kiwi.front.ui.barsHide();
 
-        server_tabview = new Tabview('server');
-        server_tabview.userlist.setWidth(0); // Disable the userlist
-        server_tabview.setIcon('/img/app_menu.png');
-        $('.icon', server_tabview.tab).tipTip({
-            delay: 0,
-            keepAlive: true,
-            content: $('#tmpl_network_menu').tmpl({}).html(),
-            activation: 'click'
-        });
+        kiwi.channels = new kiwi.model.PanelList();
+        
+        
+        //server_tabview = new Tabview('server');
+        //server_tabview.userlist.setWidth(0); // Disable the userlist
+        //server_tabview.setIcon('/img/app_menu.png');
+        //$('.icon', server_tabview.tab).tipTip({
+        //    delay: 0,
+        //    keepAlive: true,
+        //    content: $('#tmpl_network_menu').tmpl({}).html(),
+        //    activation: 'click'
+        //});
 
         // Any pre-defined nick?
         if (typeof window.init_data.nick === "string") {
@@ -220,8 +229,9 @@ kiwi.front = {
             var chan, text;
             text = $(this).text();
             if (text !== kiwi.front.cache.original_topic) {
-                chan = Tabview.getCurrentTab().name;
-                kiwi.gateway.topic(chan, text);
+                if (kiwi.currentPanel.isChannel) {
+                    kiwi.gateway.topic(kiwi.currentPannel.get("name"), text);
+                }
             }
         });
 
@@ -252,15 +262,12 @@ kiwi.front = {
         var chans = chan_name.split(','),
             i,
             chan,
-            tab;
+            panel;
         for (i in chans) {
             chan = chans[i];
-            tab = Tabview.getTab(chan);
-            if ((!tab) || (tab.safe_to_close === true)) {
+            panel = kiwi.channels.getByName(chan);
+            if (!panel) {
                 kiwi.gateway.join(chan);
-                tab = new Tabview(chan);
-            } else {
-                tab.show();
             }
         }
     },
@@ -270,7 +277,7 @@ kiwi.front = {
     *   @param  {String}    msg The message string to parse
     */
     run: function (msg) {
-        var parts, dest, t, pos, textRange, plugin_event, msg_sliced, tab, nick;
+        var parts, dest, t, pos, textRange, plugin_event, msg_sliced, tab, nick, panel;
 
         // Run through any plugins
         plugin_event = {command: msg};
@@ -309,7 +316,8 @@ kiwi.front = {
                     parts[3] = true;
                 }
 
-                Tabview.getCurrentTab().addMsg(null, ' ', '=== Connecting to ' + parts[1] + ' on port ' + parts[2] + (parts[3] ? ' using SSL' : '') + '...', 'status');
+                kiwi.channels.server.addMsg(null, ' ', '=== Connecting to ' + parts[1] + ' on port ' + parts[2] + (parts[3] ? ' using SSL' : '') + '...', 'status');
+                console.log('Connecting to ' + parts[1] + ' on port ' + parts[2] + (parts[3] ? ' using SSL' : '') + '...');
                 kiwi.gateway.connect(parts[1], parts[2], parts[3], parts[4]);
                 break;
 
@@ -326,10 +334,8 @@ kiwi.front = {
 
             case '/part':
                 if (typeof parts[1] === "undefined") {
-                    if (Tabview.getCurrentTab().safe_to_close) {
-                        Tabview.getCurrentTab().close();
-                    } else {
-                        kiwi.gateway.part(Tabview.getCurrentTab().name);
+                    if (kiwi.currentPanel.isChannel) {
+                        kiwi.gateway.part(kiwi.currentPanel.get("name"));
                     }
                 } else {
                     kiwi.gateway.part(msg.substring(6));
@@ -339,6 +345,10 @@ kiwi.front = {
             case '/names':
                 if (typeof parts[1] !== "undefined") {
                     kiwi.gateway.raw(msg.substring(1));
+                } else {
+                    if (kiwi.currentPanel.isChannel) {
+                        kiwi.gateway.raw("NAMES " + kiwi.currentPanel.get("name"));
+                    }
                 }
                 break;
 
@@ -349,7 +359,7 @@ kiwi.front = {
             case '/q':
             case '/query':
                 if (typeof parts[1] !== "undefined") {
-                    tab = new Tabview(parts[1]);
+                    //tab = new Tabview(parts[1]);
                 }
                 break;
 
@@ -360,11 +370,11 @@ kiwi.front = {
                     msg_sliced = msg.split(' ').slice(2).join(' ');
                     kiwi.gateway.privmsg(parts[1], msg_sliced);
 
-                    tab = Tabview.getTab(parts[1]);
-                    if (!tab) {
-                        tab = new Tabview(parts[1]);
+                    // TODO: Queries
+                    panel = kiwi.channels.getByName(parts[1]);
+                    if (panel) {
+                        panel.addMsg(null, kiwi.gateway.nick, msg_sliced);
                     }
-                    tab.addMsg(null, kiwi.gateway.nick, msg_sliced);
                 }
                 break;
 
@@ -375,7 +385,7 @@ kiwi.front = {
                 }
                 t = msg.split(' ', 3);
                 nick = t[1];
-                kiwi.gateway.kick(Tabview.getCurrentTab().name, nick, t[2]);
+                //kiwi.gateway.kick(Tabview.getCurrentTab().name, nick, t[2]);
                 break;
 
             case '/quote':
@@ -383,24 +393,28 @@ kiwi.front = {
                 break;
 
             case '/me':
-                tab = Tabview.getCurrentTab();
-                kiwi.gateway.ctcp(true, 'ACTION', tab.name, msg.substring(4));
-                tab.addMsg(null, ' ', '* ' + kiwi.gateway.nick + ' ' + msg.substring(4), 'action', 'color:#555;');
+                if (kiwi.currentPanel.isChannel) {
+                    kiwi.gateway.ctcp(true, 'ACTION', tab.name, msg.substring(4), function () {
+                        kiwi.currentPanel.addMsg(null, ' ', '* ' + kiwi.gateway.nick + ' ' + msg.substring(4), 'action', 'color:#555;');
+                    });
+                }
+                //TODO: Queries
                 break;
 
             case '/notice':
                 dest = parts[1];
                 msg = parts.slice(2).join(' ');
 
-                kiwi.gateway.notice(dest, msg);
-                kiwi.front.events.onNotice({}, {nick: kiwi.gateway.nick, channel: dest, msg: msg});
+                kiwi.gateway.notice(dest, msg, function () {
+                    kiwi.front.events.onNotice({}, {nick: kiwi.gateway.nick, channel: dest, msg: msg});
+                });
                 break;
 
-            case '/win':
+            /*case '/win':
                 if (parts[1] !== undefined) {
                     kiwi.front.ui.windowsShow(parseInt(parts[1], 10));
                 }
-                break;
+                break;*/
 
             case '/quit':
                 kiwi.gateway.quit(parts.slice(1).join(' '));
@@ -420,12 +434,14 @@ kiwi.front = {
                         t.setSelectionRange(pos, pos);
                     }
                 } else {
-                    kiwi.gateway.topic(Tabview.getCurrentTab().name, msg.split(' ', 2)[1]);
+                    if (kiwi.currentPanel.isChannel) {
+                        kiwi.gateway.topic(kiwi.currentPanel.get("name"), msg.split(' ', 2)[1]);
+                    }
                 }
                 break;
 
             case '/kiwi':
-                kiwi.gateway.ctcp(true, 'KIWI', Tabview.getCurrentTab().name, msg.substring(6));
+                kiwi.gateway.ctcp(true, 'KIWI', kiwi.currentPanel.get("name"), msg.substring(6));
                 break;
 
             case '/ctcp':
@@ -435,11 +451,12 @@ kiwi.front = {
                 msg = parts.join(' ');
                 console.log(parts);
                 
-                kiwi.gateway.ctcp(true, t, dest, msg);
-                Tabview.getServerTab().addMsg(null, 'CTCP Request', '[to ' + dest + '] ' + t + ' ' + msg, 'ctcp');
+                kiwi.gateway.ctcp(true, t, dest, msg, function () {
+                    kiwi.channels.server.addMsg(null, 'CTCP Request', '[to ' + dest + '] ' + t + ' ' + msg, 'ctcp');
+                });
                 break;
             default:
-                //Tabview.getCurrentTab().addMsg(null, ' ', '--> Invalid command: '+parts[0].substring(1));
+                kiwi.currentPanel.addMsg(null, ' ', '--> Invalid command: '+parts[0].substring(1));
                 kiwi.gateway.raw(msg.substring(1));
                 break;
             }
@@ -449,9 +466,10 @@ kiwi.front = {
             if (msg.trim() === '') {
                 return;
             }
-            if (Tabview.getCurrentTab().name !== 'server') {
-                kiwi.gateway.privmsg(Tabview.getCurrentTab().name, msg);
-                Tabview.getCurrentTab().addMsg(null, kiwi.gateway.nick, msg);
+            if (kiwi.currentPanel.isChannel) {
+                kiwi.gateway.privmsg(kiwi.currentPanel.get("name"), msg, function () {
+                    kiwi.currentPanel.addMsg(null, kiwi.gateway.nick, msg);
+                });
             }
         }
     },
@@ -467,7 +485,7 @@ kiwi.front = {
         var win_list = $('#kiwi .windowlist ul'),
             listitems = win_list.children('li').get();
         
-        listitems.sort(function (a, b) {
+        /*listitems.sort(function (a, b) {
             if (a === Tabview.getServerTab().tab[0]) {
                 return -1;
             }
@@ -481,7 +499,7 @@ kiwi.front = {
 
         $.each(listitems, function(idx, itm) {
             win_list.append(itm);
-        });
+        });*/
     },
 
 
@@ -748,460 +766,6 @@ var ChannelList = function () {
     };
 };
 
-
-/**
-*   @constructor
-*   @param  {String}    name    The name of the UserList
-*/
-var UserList = function (name) {
-    /*globals User */
-    var userlist, list_html, sortUsers;
-
-    userlist = [];
-
-    $('#kiwi .userlist').append($('<ul id="kiwi_userlist_' + name + '"></ul>'));
-    list_html = $('#kiwi_userlist_' + name);
-    $('a.nick', list_html[0]).live('click', this.clickHandler);
-
-    /**
-    *   @inner
-    */
-    sortUsers = function () {
-        var parent;
-        parent = list_html.parent();
-        list_html = list_html.detach();
-
-        // Not sure this is needed. 
-        // It's O(n^2) as well, so need to test to see if it works without.
-        // Alternative to test: list_html.children('li').detach();
-        list_html.children().each(function (child) {
-            var i, nick;
-            child = $(child);
-            nick = child.data('nick');
-            for (i = 0; i < userlist.length; i++) {
-                if (userlist[i].nick === nick) {
-                    userlist[i].html = child.detach();
-                    break;
-                }
-            }
-        });
-
-        userlist.sort(User.compare);
-
-        _.each(userlist, function (user) {
-            user.html = user.html.appendTo(list_html);
-        });
-
-        list_html = list_html.appendTo(parent);
-    };
-
-    /**
-    *   Adds a user or users to the UserList.
-    *   Chainable method.
-    *   @param      {Object}    users   The user or Array of users to add
-    *   @returns    {UserList}          This UserList
-    */
-    this.addUser = function (users) {
-        if (!_.isArray(users)) {
-            users = [users];
-        }
-        _.each(users, function (user) {
-            user = new User(user.nick, user.modes);
-            user.html = $('<li><a class="nick">' + user.prefix + user.nick + '</a></li>');
-            user.html.data('user', user);
-            userlist.push(user);
-        });
-        sortUsers();
-
-        return this;
-    };
-
-    /**
-    *   Removes a user or users from the UserList.
-    *   Chainable method.
-    *   @param      {String}    nicks   The nick or Array of nicks to remove
-    *   @returns    {UserList}          This UserList
-    */
-    this.removeUser = function (nicks) {
-        var toRemove;
-        if (!_.isArray(nicks)) {
-            nicks = [nicks];
-        }
-        toRemove = _.select(userlist, function (user) {
-            return _.any(nicks, function (n) {
-                return n === user.nick;
-            });
-        });
-
-        _.each(toRemove, function (user) {
-            user.html.remove();
-        });
-
-        userlist = _.difference(userlist, toRemove);
-
-        return this;
-    };
-
-    /**
-    *   Renames a user in the UserList.
-    *   Chainable method.
-    *   @param      {String}    oldNick The old nick
-    *   @param      {String}    newNick The new nick
-    *   @returns    {UserList}          This UserList
-    */
-    this.renameUser = function (oldNick, newNick) {
-        var user = _.detect(userlist, function (u) {
-            return u.nick === oldNick;
-        });
-        if (user) {
-            user.nick = newNick;
-            user.html.text(User.getPrefix(user.modes) + newNick);
-        }
-
-        sortUsers();
-
-        return this;
-    };
-
-    /**
-    *   Lists the users in this UserList.
-    *   @param      {Boolean}   modesort    True to enable sorting by mode, false for lexicographical sort
-    *   @param      {Array}     mode        If specified, only return those users who have the specified modes
-    *   @returns    {Array}                 The users in the UserList that match the criteria
-    */
-    this.listUsers = function (modesort, modes) {
-        var users = userlist;
-        if (modes) {
-            users = _.select(users, function (user) {
-                return _.any(modes, function (m) {
-                    return _.any(user.modes, function (um) {
-                        return m === um;
-                    });
-                });
-            });
-        }
-        if ((modesort === true) || (typeof modesort === undefined)) {
-            return users;
-        } else {
-            return _.sortBy(users, function (user) {
-                return user.nick;
-            });
-        }
-    };
-
-    /**
-    *   Remove this UserList from the DOM.
-    */
-    this.remove = function () {
-        list_html.remove();
-        list_html = null;
-        userlist = null;
-    };
-
-    /**
-    *   Empty the UserList.
-    *   Chainable method.
-    *   @returns    {UserList}  This UserList
-    */
-    this.empty = function () {
-        list_html.children().remove();
-        userlist = [];
-
-        return this;
-    };
-
-    /**
-    *   Checks whether a given nick is in the UserList.
-    *   @param      {String}    nick    The nick to search for
-    *   @returns    {Boolean}           True if the nick is in the userlist, false otherwise
-    */
-    this.hasUser = function (nick) {
-        return _.any(userlist, function (user) {
-            return user.nick === nick;
-        });
-    };
-
-    /**
-    *   Returns the object representing the user with the given nick, if it is in the UserList.
-    *   @param      {String}    nick    The nick to retrieve
-    *   @returns    {Object}            An object representing the user, if it exists, null otherwise
-    */
-    this.getUser = function (nick) {
-        if (this.hasUser(nick)) {
-            return _.detect(userlist, function (user) {
-                return user.nick === nick;
-            });
-        } else {
-            return null;
-        }
-    };
-
-    /**
-    *   Sets the UserList's activity.
-    *   Chainable method.
-    *   @param      {Boolean}   active  If true, sets the UserList to active. If False, sets it to inactive
-    *   @returns    {UserList}          This UserList
-    */
-    this.active = function (active) {
-        if ((arguments.length === 0) || (active)) {
-            list_html.addClass('active');
-            list_html.show();
-        } else {
-            list_html.removeClass('active');
-            list_html.hide();
-        }
-
-        return this;
-    };
-
-    /**
-    *   Updates a user's mode.
-    *   Chainable method.
-    *   @param      {String}    nick    The nick of the user to modify
-    *   @param      {String}    mode    The mode to add or remove
-    *   @param      {Boolean}   add     Adds the mode if true, removes it otherwise
-    *   @returns    {UserList}          This UserList
-    */
-    this.changeUserMode = function (nick, mode, add) {
-        var user, prefix;
-        if (this.hasUser(nick)) {
-            user = _.detect(userlist, function (u) {
-                return u.nick === nick;
-            });
-
-            prefix = user.prefix;
-            if ((arguments.length < 3) || (add)) {
-                user.addMode(mode);
-            } else {
-                user.removeMode(mode);
-            }
-            if (prefix !== user.prefix) {
-                user.html.children('a:first').text(user.prefix + user.nick);
-            }
-            sortUsers();
-        }
-
-        return this;
-    };
-};
-/**
-*   @memberOf UserList
-*/
-UserList.prototype.width = 100;     // 0 to disable
-/**
-*   Sets the width of the UserList.
-*   Chainable method.
-*   @param      {Number}    newWidth    The new width of the UserList
-*   @returns    {UserList}              This UserList
-*/
-UserList.prototype.setWidth = function (newWidth) {
-    var w, u;
-    if (typeof newWidth === 'number') {
-        this.width = newWidth;
-    }
-
-    w = $('#windows');
-    u = $('#kiwi .userlist');
-
-    u.width(this.width);
-
-    return this;
-};
-/**
-*   The click handler for this UserList
-*/
-UserList.prototype.clickHandler = function () {
-    var li = $(this).parent(),
-        user = li.data('user'),
-        userbox;
-    
-    // Remove any existing userboxes
-    $('#kiwi .userbox').remove();
-
-    if (li.data('userbox') === true) {
-        // This li already has the userbox, show remove it instead
-        li.removeData('userbox');
-
-    } else {
-        // We don't have a userbox so create one
-        userbox = $('#tmpl_user_box').tmpl({nick: user.nick}).appendTo(li);
-
-        $('.userbox_query', userbox).click(function (ev) {
-            var nick = $('#kiwi .userbox_nick').val();
-            kiwi.front.run('/query ' + nick);
-        });
-
-        $('.userbox_whois', userbox).click(function (ev) {
-            var nick = $('#kiwi .userbox_nick').val();
-            kiwi.front.run('/whois ' + nick);
-        });
-        li.data('userbox', true);
-    }
-};
-
-
-
-/**
-*   @constructor
-*   The User class. Represents a user on a channel.
-*   @param      {String}    nick    The user's nickname
-*   @param      {Array}     modes   An array of channel user modes
-*/
-var User = function (nick, modes) {
-    var sortModes;
-    /**
-    *   @inner
-    */
-    sortModes = function (modes) {
-        return modes.sort(function (a, b) {
-            var a_idx, b_idx, i;
-            for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
-                if (kiwi.gateway.user_prefixes[i].mode === a) {
-                    a_idx = i;
-                }
-            }
-            for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
-                if (kiwi.gateway.user_prefixes[i].mode === b) {
-                    b_idx = i;
-                }
-            }
-            if (a_idx < b_idx) {
-                return -1;
-            } else if (a_idx > b_idx) {
-                return 1;
-            } else {
-                return 0;
-            }
-        });
-    };
-
-    this.nick = User.stripPrefix(nick);
-    this.modes = modes || [];
-    this.modes = sortModes(this.modes);
-    this.prefix = User.getPrefix(this.modes);
-
-    /**
-    *   @inner
-    */
-    this.addMode = function (mode) {
-        this.modes.push(mode);
-        this.modes = sortModes(this.modes);
-        this.prefix = User.getPrefix(this.modes);
-        return this;
-    };
-};
-
-/**
-*   Removes a channel mode from the user
-*   @param      {String}    mode    The mode(s) to remove
-*   @returns    {User}              Returns the User object to allow chaining
-*/
-User.prototype.removeMode = function (mode) {
-    this.modes = _.reject(this.modes, function (m) {
-        return m === mode;
-    });
-    this.prefix = User.getPrefix(this.modes);
-    return this;
-};
-
-/**
-*   Checks to see if the user is an op on the channel
-*   @returns    {Boolean}   True if the user is an op, false otherwise
-*/
-User.prototype.isOp = function () {
-    // return true if this.mode[0] > o
-    return false;
-};
-
-/**
-*   Returns the highest user prefix (e.g.~, @, or +) that matches the modes given
-*   @param      {Array} modes   An array of mode letters
-*   @returns    {String}        The user's prefix
-*/
-User.getPrefix = function (modes) {
-    var prefix = '';
-    if (typeof modes[0] !== 'undefined') {
-        prefix = _.detect(kiwi.gateway.user_prefixes, function (prefix) {
-            return prefix.mode === modes[0];
-        });
-        prefix = (prefix) ? prefix.symbol : '';
-    }
-    return prefix;
-};
-
-/**
-*   Returns the user's nick without the mode prefix
-*   @param      {String}    nick    The nick to strip the prefix from
-*   @returns    {String}            The nick without the prefix
-*/
-User.stripPrefix = function (nick) {
-    var tmp = nick, i, j, k;
-    i = 0;
-    for (j = 0; j < nick.length; j++) {
-        for (k = 0; k < kiwi.gateway.user_prefixes.length; k++) {
-            if (nick.charAt(j) === kiwi.gateway.user_prefixes[k].symbol) {
-                i++;
-                break;
-            }
-        }
-    }
-
-    return tmp.substr(i);
-};
-
-/**
-*   Comparison function to order nicks based on their modes and/or nicks
-*   @param      {User}      a   The first User to evaluate
-*   @param      {User}      b   The second User to evaluate
-*   @returns    {Number}        -1 if a should be sorted before b, 1 if b should be sorted before a, and 0 if the two Users are the same.
-*/
-User.compare = function (a, b) {
-    var i, a_idx, b_idx, a_nick, b_nick;
-    // Try to sort by modes first
-    if (a.modes.length > 0) {
-        // a has modes, but b doesn't so a should appear first
-        if (b.modes.length === 0) {
-            return -1;
-        }
-        a_idx = b_idx = -1;
-        // Compare the first (highest) mode
-        for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
-            if (kiwi.gateway.user_prefixes[i].mode === a.modes[0]) {
-                a_idx = i;
-            }
-        }
-        for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
-            if (kiwi.gateway.user_prefixes[i].mode === b.modes[0]) {
-                b_idx = i;
-            }
-        }
-        if (a_idx < b_idx) {
-            return -1;
-        } else if (a_idx > b_idx) {
-            return 1;
-        }
-        // If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting
-
-    } else if (b.modes.length > 0) {
-        // b has modes but a doesn't so b should appear first
-        return 1;
-    }
-    a_nick = a.nick.toLocaleUpperCase();
-    b_nick = b.nick.toLocaleUpperCase();
-    // Lexicographical sorting
-    if (a_nick < b_nick) {
-        return -1;
-    } else if (a_nick > b_nick) {
-        return 1;
-    } else {
-        // This should never happen; both users have the same nick.
-        console.log('Something\'s gone wrong somewhere - two users have the same nick!');
-        return 0;
-    }
-};
-
-
-
 /*
  *   MISC VIEW
  */
@@ -1318,369 +882,6 @@ Utilityview.prototype.clearPartImage = function () {
     $('#kiwi .toolbars .tab_part').remove();
 };
 
-
-
-
-
-/*
- *
- *   TABVIEWS
- *
- */
-
-/**
-*   @constructor
-*   A tab to show a channel or query window
-*   @param  {String}    v_name  The window's target's name (i.e. channel name or nickname)
-*/
-var Tabview = function (v_name) {
-    /*global Tabview, UserList */
-    var re, htmlsafe_name, tmp_divname, tmp_userlistname, tmp_tabname, tmp_tab, userlist_enabled = true;
-
-    if (v_name.charAt(0) === kiwi.gateway.channel_prefix) {
-        htmlsafe_name = 'chan_' + randomString(15);
-    } else {
-        htmlsafe_name = 'query_' + randomString(15);
-        userlist_enabled = false;
-    }
-
-    tmp_divname = 'kiwi_window_' + htmlsafe_name;
-    tmp_userlistname = 'kiwi_userlist_' + htmlsafe_name;
-    tmp_tabname = 'kiwi_tab_' + htmlsafe_name;
-
-    if (!Tabview.tabExists(v_name)) {
-        // Create the window
-        $('#kiwi .windows .scroller').append('<div id="' + tmp_divname + '" class="messages"></div>');
-        
-        // Create the window tab
-        tmp_tab = $('<li id="' + tmp_tabname + '"><span></span></li>');
-        $('span', tmp_tab).text(v_name);
-        $('#kiwi .windowlist ul').append(tmp_tab);
-        tmp_tab.click(function (e) {
-            var tab = Tabview.getTab(v_name);
-            if (tab) {
-                tab.show();
-            }
-        });
-
-        kiwi.front.sortWindowList();
-    }
-
-    kiwi.front.tabviews[v_name.toLowerCase()] = this;
-    this.name = v_name;
-    this.div = $('#' + tmp_divname);
-    this.userlist = new UserList(htmlsafe_name);
-    this.tab = $('#' + tmp_tabname);
-    this.panel = $('#panel1');
-
-    if (!userlist_enabled) {
-        this.userlist.setWidth(0);
-    }
-    this.show();
-
-    if (typeof registerTouches === "function") {
-        //alert("Registering touch interface");
-        //registerTouches($('#'+tmp_divname));
-        registerTouches(document.getElementById(tmp_divname));
-    }
-
-    kiwi.front.ui.doLayoutSize();
-};
-Tabview.prototype.name = null;
-Tabview.prototype.div = null;
-Tabview.prototype.userlist = null;
-Tabview.prototype.tab = null;
-Tabview.prototype.topic = "";
-Tabview.prototype.safe_to_close = false;                // If we have been kicked/banned/etc from this channel, don't wait for a part message
-Tabview.prototype.panel = null;
-Tabview.prototype.msg_count = 0;
-/**
-*   Brings this view to the foreground
-*/
-Tabview.prototype.show = function () {
-    var w, u;
-
-    $('.messages', this.panel).removeClass("active");
-    $('#kiwi .userlist ul').removeClass("active");
-    $('#kiwi .toolbars ul li').removeClass("active");
-
-    w = $('#windows');
-    u = $('#kiwi .userlist');
-
-    this.panel.css('overflow-y', 'scroll');
-
-    // Set the window size accordingly
-    if (this.userlist.width > 0) {
-        this.userlist.setWidth();
-        w.css('right', u.outerWidth(true));
-        this.userlist.active(true);
-        // Enable the userlist resizer
-        $('#nicklist_resize').css('display', 'block');
-    } else {
-        w.css('right', 0);
-        // Disable the userlist resizer
-        $('#nicklist_resize').css('display', 'none');
-    }
-
-    this.div.addClass('active');
-    this.tab.addClass('active');
-
-    // Add the part image to the tab
-    this.addPartImage();
-
-    this.clearHighlight();
-    kiwi.front.ui.setTopicText(this.topic);
-    kiwi.front.cur_channel = this;
-
-    // If we're using fancy scrolling, refresh it
-    if (touch_scroll) {
-        touch_scroll.refresh();
-    }
-
-    this.scrollBottom();
-    if (!touchscreen) {
-        $('#kiwi_msginput').focus();
-    }
-};
-/**
-*   Removes the panel from the UI and destroys its contents
-*/
-Tabview.prototype.close = function () {
-    this.div.remove();
-    this.userlist.remove();
-    this.userlist = null;
-    this.tab.remove();
-
-    if (kiwi.front.cur_channel === this) {
-        kiwi.front.tabviews.server.show();
-    }
-    delete kiwi.front.tabviews[this.name.toLowerCase()];
-};
-/**
-*   Adds the close image to the tab
-*/
-Tabview.prototype.addPartImage = function () {
-    this.clearPartImage();
-
-    // We can't close this tab, so don't have the close image
-    if (this.name === 'server') {
-        return;
-    }
-
-    var del_html = '<img src="/img/redcross.png" class="tab_part" />';
-    this.tab.append(del_html);
-
-    $('.tab_part', this.tab).click(function () {
-        if (kiwi.front.isChannel($(this).parent().text())) {
-            kiwi.front.run("/part");
-        } else {
-            // Make sure we don't close the server tab
-            if (kiwi.front.cur_channel.name !== 'server') {
-                kiwi.front.cur_channel.close();
-            }
-        }
-    });
-};
-/**
-*   Removes the close image from the tab
-*/
-Tabview.prototype.clearPartImage = function () {
-    $('#kiwi .toolbars .tab_part').remove();
-};
-/**
-*   Sets the tab's icon
-*   @param  {String}    url     The URL of the icon to display
-*/
-Tabview.prototype.setIcon = function (url) {
-    this.tab.prepend('<img src="' + url + '" class="icon" />');
-    this.tab.css('padding-left', '33px');
-};
-/**
-*   Sets the tab's label
-*/
-Tabview.prototype.setTabText = function (text) {
-    $('span', this.tab).text(text);
-};
-/**
-*   Adds a message to the window.
-*   This method will automatically format the message (bold, underline, colours etc.)
-*   @param      {Date}      time    The timestamp of the message. May be null.
-*   @param      {String}    nick    The origin of the message
-*   @param      {String}    msg     The message to display
-*   @param      {String}    type    The CSS class to assign to the whole message line
-*   @param      {String}    style   Extra CSS commands to apply just to the msg
-*/
-Tabview.prototype.addMsg = function (time, nick, msg, type, style) {
-    var self, tmp, d, re, line_msg;
-
-    self = this;
-
-    tmp = {msg: msg, time: time, nick: nick, tabview: this.name};
-    tmp = kiwi.plugs.run('addmsg', tmp);
-    if (!tmp) {
-        return;
-    }
-
-
-    msg = tmp.msg;
-    time = tmp.time;
-    nick = tmp.nick;
-
-    if (time === null) {
-        d = new Date();
-        time = d.getHours().toString().lpad(2, "0") + ":" + d.getMinutes().toString().lpad(2, "0") + ":" + d.getSeconds().toString().lpad(2, "0");
-    }
-
-    // The CSS class (action, topic, notice, etc)
-    if (typeof type !== "string") {
-        type = '';
-    }
-
-    // Make sure we don't have NaN or something
-    if (typeof msg !== "string") {
-        msg = '';
-    }
-
-    // Make the channels clickable
-    re = new RegExp('\\B(' + kiwi.gateway.channel_prefix + '[^ ,.\\007]+)', 'g');
-    msg = msg.replace(re, function (match) {
-        return '<a class="chan">' + match + '</a>';
-    });
-
-    msg = kiwi.front.formatIRCMsg(msg);
-
-    // Build up and add the line
-    line_msg = $('<div class="msg ' + type + '"><div class="time">' + time + '</div><div class="nick">' + nick + '</div><div class="text" style="' + style + '">' + msg + ' </div></div>');
-    this.div.append(line_msg);
-
-    this.msg_count++;
-    if (this.msg_count > 250) {
-        $('.msg:first', this.div).remove();
-        this.msg_count--;
-    }
-
-    if (!touchscreen) {
-        this.scrollBottom();
-    } else {
-        touch_scroll.refresh();
-        //console.log(this.div.attr("scrollHeight") +" - "+ $('#windows').height());
-        this.scrollBottom();
-        //if(this.div.attr("scrollHeight") > $('#windows').height()){
-        //    touch_scroll.scrollTo(0, this.div.height());
-        //}
-    }
-};
-/**
-*   Scroll to the bottom of the window
-*/
-Tabview.prototype.scrollBottom = function () {
-    var panel = this.panel;
-    panel[0].scrollTop = panel[0].scrollHeight;
-};
-/**
-*   Change a user's nick on the channel
-*   @param      {String}    newNick     The new nick
-*   @param      {String}    oldNick     The old nick
-*/
-Tabview.prototype.changeNick = function (newNick, oldNick) {
-    var inChan = this.userlist.hasUser(oldNick);
-    if (inChan) {
-        this.userlist.renameUser(oldNick, newNick);
-        this.addMsg(null, ' ', '=== ' + oldNick + ' is now known as ' + newNick, 'action changenick');
-    }
-};
-/**
-*   Highlight the tab
-*/
-Tabview.prototype.highlight = function () {
-    this.tab.addClass('highlight');
-};
-/**
-*   Indicate activity on the tab
-*/
-Tabview.prototype.activity = function () {
-    this.tab.addClass('activity');
-};
-/**
-*   Clear the tab's highlight
-*/
-Tabview.prototype.clearHighlight = function () {
-    this.tab.removeClass('highlight');
-    this.tab.removeClass('activity');
-};
-/**
-*   Change the channel's topic
-*   @param      {String}    new_topic   The new channel topic
-*/
-Tabview.prototype.changeTopic = function (new_topic) {
-    this.topic = new_topic;
-    this.addMsg(null, ' ', '=== Topic for ' + this.name + ' is: ' + new_topic, 'topic');
-    if (kiwi.front.cur_channel.name === this.name) {
-        kiwi.front.ui.setTopicText(new_topic);
-    }
-};
-// Static functions
-/**
-*   Checks to see if a tab by the given name exists
-*   @param      {String}    name    The name to check
-*   @returns    {Boolean}           True if the tab exists, false otherwise
-*/
-Tabview.tabExists = function (name) {
-    return (Tabview.getTab(name) !== null);
-};
-/**
-*   Returns the tab which has the given name
-*   @param      {String}    name    The name of the tab to return
-*   @returns    {Tabview}           The Tabview with the given name, or null if it does not exist
-*/
-Tabview.getTab = function (name) {
-    var tab;
-
-    // Make sure we actually have a name
-    if (typeof name !== 'string') {
-        return null;
-    }
-
-    // Go through each tabview and pick out the matching one
-    $.each(kiwi.front.tabviews, function (i, item) {
-        if (item.name.toLowerCase() === name.toLowerCase()) {
-            tab = item;
-            return false;
-        }
-    });
-
-    // If we we didn't find one, return null instead
-    tab = tab || null;
-
-    return tab;
-};
-/**
-*   Returns the tab that corresponds to the server
-*   @retruns    {Tabview}       The server Tabview
-*/
-Tabview.getServerTab = function () {
-    return Tabview.getTab('server');
-};
-/**
-*   Returns all tabs
-*   @returns    {Array}         All of the tabs
-*/
-Tabview.getAllTabs = function () {
-    return kiwi.front.tabviews;
-};
-/**
-*   Returns the tab that's currently showing
-*   @returns    {Tabview}       The tab that's currently showing
-*/
-Tabview.getCurrentTab = function () {
-    return kiwi.front.cur_channel;
-};
-
-
-
-
-
-
 /**
 *   @constructor
 *   Floating message box
old mode 100644 (file)
new mode 100755 (executable)
index 2d5ea43..e7d2eb4
@@ -345,7 +345,7 @@ kiwi.front.ui = {
     *   Displays the next tab\r
     */\r
     windowsNext: function () {\r
-        var tab, tabs, curTab, next;\r
+        /*var tab, tabs, curTab, next;\r
         next = false;\r
         tabs = Tabview.getAllTabs();\r
         curTab = Tabview.getCurrentTab();\r
@@ -359,14 +359,14 @@ kiwi.front.ui = {
                 tabs[tab].show();\r
                 return;\r
             }\r
-        }\r
+        }*/\r
     },\r
 \r
     /**\r
     *   Displays the previous tab\r
     */\r
     windowsPrevious: function () {\r
-        var tab, tabs, curTab, prev_tab, next;\r
+        /*var tab, tabs, curTab, prev_tab, next;\r
         next = false;\r
         tabs = Tabview.getAllTabs();\r
         curTab = Tabview.getCurrentTab();\r
@@ -378,7 +378,7 @@ kiwi.front.ui = {
                 return;\r
             }\r
             prev_tab = tabs[tab];\r
-        }\r
+        }*/\r
     },\r
 \r
     /**\r
@@ -386,7 +386,7 @@ kiwi.front.ui = {
     *   @param  {Number}    num The index of the tab to show\r
     */\r
     windowsShow: function (num) {\r
-        num = parseInt(num, 10);\r
+        /*num = parseInt(num, 10);\r
         console.log('Showing window ' + num.toString());\r
         var i = 0, tab, tabs;\r
         tabs = Tabview.getAllTabs();\r
@@ -396,7 +396,7 @@ kiwi.front.ui = {
                 return;\r
             }\r
             i++;\r
-        }\r
+        }*/\r
     },\r
 \r
 \r
old mode 100644 (file)
new mode 100755 (executable)
index 37fec1a..c9dbce7
@@ -119,6 +119,7 @@ kiwi.gateway = {
             kiwi.gateway.socket.on('reconnect_failed', function () {
                 console.log("kiwi.gateway.socket.on('reconnect_failed')");
             });
+            kiwi.gateway.socket.on('error', console.log);
         }
     },
 
@@ -146,8 +147,6 @@ kiwi.gateway = {
     */
     parse: function (item) {
         if (item.event !== undefined) {
-            $(kiwi.gateway).trigger('on' + item.event, item);
-
             switch (item.event) {
             case 'options':
                 $.each(item.options, function (name, value) {
@@ -176,6 +175,8 @@ kiwi.gateway = {
                 $(kiwi.gateway).trigger('kiwi.' + item.namespace, item.data);
                 break;
             }
+
+            $(kiwi.gateway).trigger('on' + item.event, item);
         }
     },
 
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
diff --git a/client/js/model.js b/client/js/model.js
new file mode 100755 (executable)
index 0000000..335fd88
--- /dev/null
@@ -0,0 +1,232 @@
+/*jslint white:true, regexp: true, nomen: true, devel: true, undef: true, browser: true, continue: true, sloppy: true, forin: true, newcap: true, plusplus: true, maxerr: 50, indent: 4 */\r
+/*global kiwi */\r
+kiwi.model = {};\r
+\r
+kiwi.model.MemberList = Backbone.Collection.extend({\r
+    model: kiwi.model.Member,\r
+    comparator: function (a, b) {\r
+        var i, a_modes, b_modes, a_idx, b_idx, a_nick, b_nick;\r
+        a_modes = a.get("modes");\r
+        b_modes = b.get("modes");\r
+        // Try to sort by modes first\r
+        if (a_modes.length > 0) {\r
+            // a has modes, but b doesn't so a should appear first\r
+            if (b_modes.length === 0) {\r
+                return -1;\r
+            }\r
+            a_idx = b_idx = -1;\r
+            // Compare the first (highest) mode\r
+            for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {\r
+                if (kiwi.gateway.user_prefixes[i].mode === a_modes[0]) {\r
+                    a_idx = i;\r
+                }\r
+            }\r
+            for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {\r
+                if (kiwi.gateway.user_prefixes[i].mode === b_modes[0]) {\r
+                    b_idx = i;\r
+                }\r
+            }\r
+            if (a_idx < b_idx) {\r
+                return -1;\r
+            } else if (a_idx > b_idx) {\r
+                return 1;\r
+            }\r
+            // If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting\r
+\r
+        } else if (b_modes.length > 0) {\r
+            // b has modes but a doesn't so b should appear first\r
+            return 1;\r
+        }\r
+        a_nick = a.get("nick").toLocaleUpperCase();\r
+        b_nick = b.get("nick").toLocaleUpperCase();\r
+        // Lexicographical sorting\r
+        if (a_nick < b_nick) {\r
+            return -1;\r
+        } else if (a_nick > b_nick) {\r
+            return 1;\r
+        } else {\r
+            // This should never happen; both users have the same nick.\r
+            console.log('Something\'s gone wrong somewhere - two users have the same nick!');\r
+            return 0;\r
+        }\r
+    },\r
+    initialize: function (options) {\r
+        this.view = new kiwi.view.MemberList({"model": this, "name": options.name});\r
+    },\r
+    getByNick: function (nick) {\r
+        return this.find(function (m) {\r
+            return nick === m.get("nick");\r
+        });\r
+    }\r
+});\r
+\r
+kiwi.model.Member = Backbone.Model.extend({\r
+    sortModes: function (modes) {\r
+        return modes.sort(function (a, b) {\r
+            var a_idx, b_idx, i;\r
+            for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {\r
+                if (kiwi.gateway.user_prefixes[i].mode === a) {\r
+                    a_idx = i;\r
+                }\r
+            }\r
+            for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {\r
+                if (kiwi.gateway.user_prefixes[i].mode === b) {\r
+                    b_idx = i;\r
+                }\r
+            }\r
+            if (a_idx < b_idx) {\r
+                return -1;\r
+            } else if (a_idx > b_idx) {\r
+                return 1;\r
+            } else {\r
+                return 0;\r
+            }\r
+        });\r
+    },\r
+    initialize: function (attributes) {\r
+        var nick, modes, prefix;\r
+        nick = this.stripPrefix(this.get("nick"));\r
+\r
+        modes = this.get("modes");\r
+        modes = modes || [];\r
+        this.sortModes(modes);\r
+        this.set({"nick": nick, "modes": modes, "prefix": this.getPrefix(modes)}, {silent: true});\r
+    },\r
+    addMode: function (mode) {\r
+        var modes, prefix;\r
+        modes = this.get("modes");\r
+        modes.push(mode);\r
+        modes = this.sortModes(modes);\r
+        this.set({"prefix": this.getPrefix(modes), "modes": modes});\r
+    },\r
+    removeMode: function (mode) {\r
+        var modes, prefix;\r
+        modes = this.get("modes");\r
+        modes = _.reject(modes, function(m) {\r
+            return m === mode;\r
+        });\r
+        this.set({"prefix": this.getPrefix(modes), "modes": modes});\r
+    },\r
+    getPrefix: function (modes) {\r
+        var prefix = '';\r
+        if (typeof modes[0] !== 'undefined') {\r
+            prefix = _.detect(kiwi.gateway.user_prefixes, function (prefix) {\r
+                return prefix.mode === modes[0];\r
+            });\r
+            prefix = (prefix) ? prefix.symbol : '';\r
+        }\r
+        return prefix;\r
+    },\r
+    stripPrefix: function (nick) {\r
+        var tmp = nick, i, j, k;\r
+        i = 0;\r
+        for (j = 0; j < nick.length; j++) {\r
+            for (k = 0; k < kiwi.gateway.user_prefixes.length; k++) {\r
+                if (nick.charAt(j) === kiwi.gateway.user_prefixes[k].symbol) {\r
+                    i++;\r
+                    break;\r
+                }\r
+            }\r
+        }\r
+\r
+        return tmp.substr(i);\r
+    }\r
+});\r
+\r
+kiwi.model.PanelList = Backbone.Collection.extend({\r
+    model: kiwi.model.Panel,\r
+    comparator: function (chan) {\r
+        return chan.get("name");\r
+    },\r
+    initialize: function () {\r
+        this.server = new kiwi.model.Server({"name": kiwi.gateway.network_name});\r
+        this.view = new kiwi.view.Tabs({"el": $('#kiwi .windowlist ul')[0], "model": this});\r
+        kiwi.currentPanel = this.server;\r
+    },\r
+    getByName: function (name) {\r
+        return this.find(function (c) {\r
+            return name === c.get("name");\r
+        });\r
+    }\r
+});\r
+\r
+kiwi.model.Panel = Backbone.Model.extend({\r
+    initialize: function (attributes) {\r
+        var name = this.get("name") || "";\r
+        this.view = new kiwi.view.Panel({"model": this, "name": name});\r
+        this.set({\r
+            "backscroll": [],\r
+            "name": name\r
+        }, {"silent": true});\r
+\r
+        this.isChannel = false;\r
+    },\r
+    addMsg: function (time, nick, msg, type, style) {\r
+        var tmp, bs;\r
+\r
+        tmp = {"msg": msg, "time": time, "nick": nick, "chan": this.get("name"), "style": style};\r
+        tmp = kiwi.plugs.run('addmsg', tmp);\r
+        if (!tmp) {\r
+            return;\r
+        }\r
+        if (tmp.time === null) {\r
+            d = new Date();\r
+            tmp.time = d.getHours().toString().lpad(2, "0") + ":" + d.getMinutes().toString().lpad(2, "0") + ":" + d.getSeconds().toString().lpad(2, "0");\r
+        }\r
+\r
+        // The CSS class (action, topic, notice, etc)\r
+        if (typeof tmp.type !== "string") {\r
+            tmp.type = '';\r
+        }\r
+\r
+        // Make sure we don't have NaN or something\r
+        if (typeof tmp.msg !== "string") {\r
+            tmp.msg = '';\r
+        }\r
+\r
+        bs = this.get("backscroll");\r
+        bs.push(tmp)\r
+        this.set({"backscroll": bs}, {silent:true});\r
+        this.trigger("msg", tmp);\r
+    }\r
+});\r
+\r
+kiwi.model.Server = kiwi.model.Panel.extend({\r
+    initialize: function (attributes) {\r
+        var name = "Server";\r
+        this.view = new kiwi.view.Panel({"model": this, "name": name});\r
+        this.set({\r
+            "backscroll": [],\r
+            "name": name\r
+        }, {"silent": true});\r
+        this.isChannel = false;\r
+    }\r
+});\r
+\r
+// TODO: Channel modes\r
+kiwi.model.Channel = kiwi.model.Panel.extend({\r
+    initialize: function (attributes) {\r
+        var name = this.get("name") || "",\r
+            members;\r
+        this.view = new kiwi.view.Panel({"model": this, "name": name});\r
+        this.set({\r
+            "members": new kiwi.model.MemberList({"name": this.view.htmlsafe_name}),\r
+            "name": name,\r
+            "backscroll": [],\r
+            "topic": ""\r
+        }, {"silent": true});\r
+        this.addMsg(null, ' ', '--> You have joined ' + name, 'action join', 'color:#009900;');\r
+        members = this.get("members");\r
+        members.bind("add", function (member) {\r
+            this.addMsg(null, ' ', '--> ' + member.get("nick") + ' [' + member.get("ident") + '@' + member.get("hostname") + '] has joined', 'action join', 'color:#009900;');\r
+        }, this);\r
+        members.bind("remove", function (member, options) {\r
+            this.addMsg(null, ' ', '<-- ' + member.get("nick") + ' has left ' + ((options.message) ? '(' + options.message + ')' : ''), 'action join', 'color:#009900;');\r
+        }, this);\r
+        members.bind("quit", function (args) {\r
+            this.addMsg(null, ' ', '<-- ' + args.member.get("nick") + ' has quit ' + ((args.message) ? '(' + args.message + ')' : ''), 'action join', 'color:#009900;');\r
+        }, this);\r
+\r
+        this.isChannel = true;\r
+    }\r
+});
\ No newline at end of file
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
old mode 100644 (file)
new mode 100755 (executable)
index 86f284c..c6658dc
@@ -14,7 +14,7 @@ var kiwi = {};
 *   @param  {Boolean}   debug   Whether to re-enable console.log or not
 */
 function manageDebug(debug) {
-    var log, consoleBackUp;
+/*    var log, consoleBackUp;
     if (window.console) {
         consoleBackUp = window.console.log;
         window.console.log = function () {
@@ -30,7 +30,7 @@ function manageDebug(debug) {
                 log(str);
             }
         };
-    }
+    }*/
 }
 
 /**
@@ -139,9 +139,9 @@ var plugins = [
     {
         name: "activity",
         onaddmsg: function (event, opts) {
-            if (kiwi.front.cur_channel.name.toLowerCase() !== kiwi.front.tabviews[event.tabview.toLowerCase()].name) {
-                kiwi.front.tabviews[event.tabview].activity();
-            }
+            //if (kiwi.front.cur_channel.name.toLowerCase() !== kiwi.front.tabviews[event.tabview.toLowerCase()].name) {
+            //    kiwi.front.tabviews[event.tabview].activity();
+            //}
 
             return event;
         }
@@ -150,24 +150,24 @@ var plugins = [
     {
         name: "highlight",
         onaddmsg: function (event, opts) {
-            var tab = Tabviews.getTab(event.tabview.toLowerCase());
+            //var tab = Tabviews.getTab(event.tabview.toLowerCase());
 
             // If we have a highlight...
-            if (event.msg.toLowerCase().indexOf(kiwi.gateway.nick.toLowerCase()) > -1) {
-                if (Tabview.getCurrentTab() !== tab) {
-                    tab.highlight();
-                }
-                if (kiwi.front.isChannel(tab.name)) {
-                    event.msg = '<span style="color:red;">' + event.msg + '</span>';
-                }
-            }
+            //if (event.msg.toLowerCase().indexOf(kiwi.gateway.nick.toLowerCase()) > -1) {
+            //    if (Tabview.getCurrentTab() !== tab) {
+            //        tab.highlight();
+            //    }
+            //    if (kiwi.front.isChannel(tab.name)) {
+            //        event.msg = '<span style="color:red;">' + event.msg + '</span>';
+            //    }
+            //}
 
             // If it's a PM, highlight
-            if (!kiwi.front.isChannel(tab.name) && tab.name !== "server"
-                && Tabview.getCurrentTab().name.toLowerCase() !== tab.name
-            ) {
-                tab.highlight();
-            }
+            //if (!kiwi.front.isChannel(tab.name) && tab.name !== "server"
+            //    && Tabview.getCurrentTab().name.toLowerCase() !== tab.name
+            //) {
+            //    tab.highlight();
+            //}
 
             return event;
         }
@@ -281,8 +281,8 @@ var plugins = [
             }
             return false;
 
-               }
-       },
+        }
+    },
     */
 
     {
@@ -292,15 +292,16 @@ var plugins = [
                 return event;
             }
 
-            if (typeof kiwi.front.tabviews[event.tabview].nick_colours === 'undefined') {
-                kiwi.front.tabviews[event.tabview].nick_colours = {};
-            }
+            //if (typeof kiwi.front.tabviews[event.tabview].nick_colours === 'undefined') {
+            //    kiwi.front.tabviews[event.tabview].nick_colours = {};
+            //}
 
-            if (typeof kiwi.front.tabviews[event.tabview].nick_colours[event.nick] === 'undefined') {
-                kiwi.front.tabviews[event.tabview].nick_colours[event.nick] = this.randColour();
-            }
+            //if (typeof kiwi.front.tabviews[event.tabview].nick_colours[event.nick] === 'undefined') {
+            //    kiwi.front.tabviews[event.tabview].nick_colours[event.nick] = this.randColour();
+            //}
 
-            var c = kiwi.front.tabviews[event.tabview].nick_colours[event.nick];
+            //var c = kiwi.front.tabviews[event.tabview].nick_colours[event.nick];
+            var c = this.randColour();
             event.nick = '<span style="color:' + c + ';">' + event.nick + '</span>';
 
             return event;
@@ -321,16 +322,16 @@ var plugins = [
         }
     },
 
-       {
-               name: "kiwitest",
-               oninit: function (event, opts) {
-                       console.log('registering namespace');
-                       $(gateway).bind("kiwi.lol.browser", function (e, data) {
-                               console.log('YAY kiwitest');
-                               console.log(data);
-                       });
-               }
-       }
+    {
+        name: "kiwitest",
+        oninit: function (event, opts) {
+            console.log('registering namespace');
+            $(gateway).bind("kiwi.lol.browser", function (e, data) {
+                console.log('YAY kiwitest');
+                console.log(data);
+            });
+        }
+    }
 ];
 
 
diff --git a/client/js/view.js b/client/js/view.js
new file mode 100755 (executable)
index 0000000..c971627
--- /dev/null
@@ -0,0 +1,155 @@
+/*jslint white:true, regexp: true, nomen: true, devel: true, undef: true, browser: true, continue: true, sloppy: true, forin: true, newcap: true, plusplus: true, maxerr: 50, indent: 4 */\r
+/*global kiwi */\r
+\r
+kiwi.view = {};\r
+\r
+kiwi.view.MemberList = Backbone.View.extend({\r
+    tagName: "ul",\r
+    events: {\r
+        "click .nick": "nickClick"\r
+    },\r
+    initialize: function (options) {\r
+        $(this.el).attr("id", 'kiwi_userlist_' + options.name);\r
+        this.model.bind('all', this.render, this);\r
+        $(this.el).appendTo('#kiwi .userlist');\r
+    },\r
+    render: function () {\r
+        var $this = $(this.el);\r
+        $this.empty();\r
+        this.model.forEach(function (member) {\r
+            $('<li><a class="nick"><span class="prefix">' + member.get("prefix") + '</span>' + member.get("nick") + '</a></li>').appendTo($this).data('member', member);\r
+        });\r
+    },\r
+    nickClick: function (x) {\r
+        console.log(x);\r
+    },\r
+    show: function () {\r
+        $('#kiwi .userlist').children().css('display', 'none');\r
+        $(this.el).css('display', 'block');\r
+    }\r
+});\r
+\r
+kiwi.view.Panel = Backbone.View.extend({\r
+    tagName: "div",\r
+    className: "messages",\r
+    events: {\r
+        "click .chan": "chanClick"\r
+    },\r
+    initialize: function (options) {\r
+        this.htmlsafe_name = 'panel_' + randomString(15);\r
+        $(this.el).attr("id", 'kiwi_panel_' + this.htmlsafe_name).css('display', 'none');\r
+        this.el = $(this.el).appendTo('#panel1 .scroller')[0];\r
+        this.model.bind('msg', this.newMsg, this);\r
+        this.msg_count = 0;\r
+        this.model.set({"view": this}, {"silent": true});\r
+    },\r
+    render: function () {\r
+        var $this = $(this.el);\r
+        $this.empty();\r
+        this.model.get("backscroll").forEach(this.newMsg);\r
+    },\r
+    newMsg: function (msg) {\r
+        // TODO: make sure that the message pane is scrolled to the bottom\r
+        var re, line_msg, $this = $(this.el);\r
+        // Make the channels clickable\r
+        re = new RegExp('\\B(' + kiwi.gateway.channel_prefix + '[^ ,.\\007]+)', 'g');\r
+        msg.msg = msg.msg.replace(re, function (match) {\r
+            return '<a class="chan">' + match + '</a>';\r
+        });\r
+\r
+        msg.msg = kiwi.front.formatIRCMsg(msg.msg);\r
+\r
+        // Build up and add the line\r
+        line_msg = $('<div class="msg ' + msg.type + '"><div class="time">' + msg.time + '</div><div class="nick">' + msg.nick + '</div><div class="text" style="' + msg.style + '">' + msg.msg + ' </div></div>');\r
+        $this.append(line_msg);\r
+        this.msg_count++;\r
+        if (this.msg_count > 250) {\r
+            $('.msg:first', this.div).remove();\r
+            this.msg_count--;\r
+        }\r
+    },\r
+    chanClick: function (x) {\r
+        console.log(x);\r
+    },\r
+    show: function () {\r
+        var $this = $(this.el);\r
+        $('#panel1 .scroller').children().css('display','none');\r
+        $this.css('display', 'block');\r
+        var members = this.model.get("members");\r
+        if (members) {\r
+            members.view.show();\r
+        } else {\r
+            $('#kiwi .userlist').children().css('display', 'none');\r
+        }\r
+        kiwi.front.ui.setTopicText(this.model.get("topic") || "")\r
+        // TODO: Have Kiwi remember the scoll locations of each panel\r
+        $('#panel1').scrollTop($this.height());\r
+        kiwi.currentPanel = this.model;\r
+    }\r
+});\r
+\r
+kiwi.view.Channel = kiwi.view.Panel.extend({\r
+    initialize: function (options) {\r
+        this.htmlsafe_name = 'chan_' + randomString(15);\r
+        $(this.el).attr("id", 'kiwi_window_' + this.htmlsafe_name).css('display', 'none');\r
+        this.el = $(this.el).appendTo('#panel1 .scroller')[0];\r
+        this.model.bind('msg', this.newMsg, this);\r
+        this.model.bind('change[topic]', this.topic, this);\r
+        this.msg_count = 0;\r
+        this.model.set({"view": this}, {"silent": true});\r
+        this.show();\r
+    },\r
+    topic: function (topic) {\r
+        console.log(topic);\r
+        if (!topic) {\r
+            topic = this.model.get("topic");\r
+        }\r
+        this.model.addMsg(null, ' ', '=== Topic for ' + this.model.get("name") + ' is: ' + topic, 'topic');\r
+        if ($(this.el).css('display') === 'block') {\r
+            kiwi.front.ui.setTopicText(this.model.get("topic"))\r
+        }\r
+    }\r
+});\r
+\r
+kiwi.view.Tabs = Backbone.View.extend({\r
+    events: {\r
+        "click li": "tabClick"\r
+    },\r
+    initialize: function () {\r
+        this.model.bind("add", this.addTab, this);\r
+        this.model.bind("remove", this.removeTab, this);\r
+        this.model.bind("reset", this.render, this);\r
+        this.model.server.bind("change", this.render, this);\r
+    },\r
+    render: function () {\r
+        $this = $(this.el);\r
+        $this.empty();\r
+        $('<li id="tab_server"><span>' + kiwi.gateway.network_name + '</span></li>').data('pane', this.model.server).appendTo($this);\r
+        this.model.forEach(function (tab) {\r
+            var tabname = $(tab.get("view").el).attr("id");\r
+            $('<li id="tab_' + tabname + '"><span>' + tab.get("name") + '</span></li>').data('pane', tab).appendTo($this);\r
+        });\r
+    },\r
+    addTab: function (tab) {\r
+        var tabname = $(tab.get("view").el).attr("id"),\r
+            $this = $(this.el);\r
+        $('<li id="tab_' + tabname + '"><span>' + tab.get("name") + '</span></li>').data('pane', tab).appendTo($this);\r
+    },\r
+    removeTab: function (tab) {\r
+        $('#tab_' + $(tab.get("view").el).attr("id")).remove();\r
+    },\r
+    tabClick: function (e) {\r
+        $(e.currentTarget).data('pane').view.show();\r
+    }\r
+});\r
+\r
+\r
+\r
+\r
+\r
+\r
+\r
+\r
+\r
+\r
+\r
old mode 100644 (file)
new mode 100755 (executable)
index 31863e8..8460449
-kiwi.model.Gateway = Backbone.Model.extend(new (function () {\r
-    var that = this;\r
-\r
-    this.defaults = {\r
-        /**\r
-        *   The name of the network\r
-        *   @type    String\r
-        */\r
-        name: 'Server',\r
-\r
-        /**\r
-        *   The address (URL) of the network\r
-        *   @type    String\r
-        */\r
-        address: '',\r
-\r
-        /**\r
-        *   The current nickname\r
-        *   @type   String\r
-        */\r
-        nick: '',\r
-\r
-        /**\r
-        *   The channel prefix for this network\r
-        *   @type    String\r
-        */\r
-        channel_prefix: '#',\r
-\r
-        /**\r
-        *   The user prefixes for channel owner/admin/op/voice etc. on this network\r
-        *   @type   Array\r
-        */\r
-        user_prefixes: ['~', '&', '@', '+'],\r
-\r
-        /**\r
-        *   The URL to the Kiwi server\r
-        *   @type   String\r
-        */\r
-        //kiwi_server: '//kiwi'\r
-        kiwi_server: 'http://localhost:7778/kiwi'\r
-    };\r
-\r
-\r
-    this.initialize = function () {\r
-        // Update `that` with this new Model object\r
-        that = this;\r
-\r
-        // For ease of access. The socket.io object\r
-        this.socket = this.get('socket');\r
-\r
-        // Redundant perhaps? Legacy\r
-        this.session_id = '';\r
-\r
-        network = this;\r
-    };\r
-\r
-\r
-    /**\r
-    *   Connects to the server\r
-    *   @param  {String}    host        The hostname or IP address of the IRC server to connect to\r
-    *   @param  {Number}    port        The port of the IRC server to connect to\r
-    *   @param  {Boolean}   ssl         Whether or not to connect to the IRC server using SSL\r
-    *   @param  {String}    password    The password to supply to the IRC server during registration\r
-    *   @param  {Function}  callback    A callback function to be invoked once Kiwi's server has connected to the IRC server\r
-    */\r
-    this.connect = function (host, port, ssl, password, callback) {\r
-        this.socket = io.connect(this.get('kiwi_server'), {\r
-            'try multiple transports': true,\r
-            'connect timeout': 3000,\r
-            'max reconnection attempts': 7,\r
-            'reconnection delay': 2000\r
-        });\r
-        this.socket.on('connect_failed', function (reason) {\r
-            // TODO: When does this even actually get fired? I can't find a case! ~Darren\r
-            console.debug('Unable to connect Socket.IO', reason);\r
-            console.log("kiwi.gateway.socket.on('connect_failed')");\r
-            //kiwi.front.tabviews.server.addMsg(null, ' ', 'Unable to connect to Kiwi IRC.\n' + reason, 'error');\r
-            this.socket.disconnect();\r
-            this.emit("connect_fail", {reason: reason});\r
-        });\r
-\r
-        this.socket.on('error', function (e) {\r
-            this.emit("connect_fail", {reason: e});\r
-            console.log("kiwi.gateway.socket.on('error')", {reason: e});\r
-        });\r
-\r
-        this.socket.on('connecting', function (transport_type) {\r
-            console.log("kiwi.gateway.socket.on('connecting')");\r
-            this.emit("connecting");\r
-            that.trigger("connecting");\r
-        });\r
-\r
-        this.socket.on('connect', function () {\r
-            this.emit('irc connect', that.get('nick'), host, port, ssl, password, callback);\r
-            that.trigger('connect', {});\r
-        });\r
-\r
-        this.socket.on('too_many_connections', function () {\r
-            this.emit("connect_fail", {reason: 'too_many_connections'});\r
-        });\r
-\r
-        this.socket.on('message', this.parse);\r
-\r
-        this.socket.on('disconnect', function () {\r
-            that.trigger("disconnect", {});\r
-            console.log("kiwi.gateway.socket.on('disconnect')");\r
-        });\r
-\r
-        this.socket.on('close', function () {\r
-            console.log("kiwi.gateway.socket.on('close')");\r
-        });\r
-\r
-        this.socket.on('reconnecting', function (reconnectionDelay, reconnectionAttempts) {\r
-            console.log("kiwi.gateway.socket.on('reconnecting')");\r
-            that.trigger("reconnecting", {delay: reconnectionDelay, attempts: reconnectionAttempts});\r
-        });\r
-\r
-        this.socket.on('reconnect_failed', function () {\r
-            console.log("kiwi.gateway.socket.on('reconnect_failed')");\r
-        });\r
-    };\r
-\r
-\r
-    /*\r
-        Events:\r
-            msg\r
-            action\r
-            server_connect\r
-            options\r
-            motd\r
-            notice\r
-            userlist\r
-            nick\r
-            join\r
-            topic\r
-            part\r
-            kick\r
-            quit\r
-            whois\r
-            syncchannel_redirect\r
-            debug\r
-    */\r
-    /**\r
-    *   Parses the response from the server\r
-    */\r
-    this.parse = function (item) {\r
-        //console.log('gateway event', item);\r
-        if (item.event !== undefined) {\r
-            that.trigger('on' + item.event, item);\r
-\r
-            switch (item.event) {\r
-            case 'options':\r
-                $.each(item.options, function (name, value) {\r
-                    switch (name) {\r
-                    case 'CHANTYPES':\r
-                        // TODO: Check this. Why is it only getting the first char?\r
-                        that.set('channel_prefix', value.charAt(0));\r
-                        break;\r
-                    case 'NETWORK':\r
-                        that.set('name', value);\r
-                        break;\r
-                    case 'PREFIX':\r
-                        that.set('user_prefixes', value);\r
-                        break;\r
-                    }\r
-                });\r
-                break;\r
-\r
-            case 'connect':\r
-                that.set('nick', item.nick);\r
-                break;\r
-\r
-            case 'nick':\r
-                if (item.nick === that.get('nick')) {\r
-                    that.set('nick', item.newnick);\r
-                }\r
-                break;\r
-            /*\r
-            case 'sync':\r
-                if (kiwi.gateway.onSync && kiwi.gateway.syncing) {\r
-                    kiwi.gateway.syncing = false;\r
-                    kiwi.gateway.onSync(item);\r
-                }\r
-                break;\r
-            */\r
-\r
-            case 'kiwi':\r
-                this.emit('kiwi.' + item.namespace, item.data);\r
-                break;\r
-            }\r
-        }\r
-    };\r
-\r
-    /**\r
-    *   Sends data to the server\r
-    *   @private\r
-    *   @param  {Object}    data        The data to send\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.sendData = function (data, callback) {\r
-        this.socket.emit('message', {sid: this.session_id, data: JSON.stringify(data)}, callback);\r
-    };\r
-\r
-    /**\r
-    *   Sends a PRIVMSG message\r
-    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
-    *   @param  {String}    msg         The message to send\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.privmsg = function (target, msg, callback) {\r
-        var data = {\r
-            method: 'privmsg',\r
-            args: {\r
-                target: target,\r
-                msg: msg\r
-            }\r
-        };\r
-\r
-        this.sendData(data, callback);\r
-    };\r
-\r
-    /**\r
-    *   Sends a NOTICE message\r
-    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
-    *   @param  {String}    msg         The message to send\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.notice = function (target, msg, callback) {\r
-        var data = {\r
-            method: 'notice',\r
-            args: {\r
-                target: target,\r
-                msg: msg\r
-            }\r
-        };\r
-\r
-        this.sendData(data, callback);\r
-    };\r
-\r
-    /**\r
-    *   Sends a CTCP message\r
-    *   @param  {Boolean}   request     Indicates whether this is a CTCP request (true) or reply (false)\r
-    *   @param  {String}    type        The type of CTCP message, e.g. 'VERSION', 'TIME', 'PING' etc.\r
-    *   @param  {String}    target      The target of the message, e.g a channel or nick\r
-    *   @param  {String}    params      Additional paramaters\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.ctcp = function (request, type, target, params, callback) {\r
-        var data = {\r
-            method: 'ctcp',\r
-            args: {\r
-                request: request,\r
-                type: type,\r
-                target: target,\r
-                params: params\r
-            }\r
-        };\r
-\r
-        this.sendData(data, callback);\r
-    };\r
-\r
-    /**\r
-    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
-    *   @param  {String}    msg         The message to send\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.action = function (target, msg, callback) {\r
-        this.ctcp(true, 'ACTION', target, msg, callback);\r
-    };\r
-\r
-    /**\r
-    *   Joins a channel\r
-    *   @param  {String}    channel     The channel to join\r
-    *   @param  {String}    key         The key to the channel\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.join = function (channel, key, callback) {\r
-        var data = {\r
-            method: 'join',\r
-            args: {\r
-                channel: channel,\r
-                key: key\r
-            }\r
-        };\r
-\r
-        this.sendData(data, callback);\r
-    };\r
-\r
-    /**\r
-    *   Leaves a channel\r
-    *   @param  {String}    channel     The channel to part\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.part = function (channel, callback) {\r
-        var data = {\r
-            method: 'part',\r
-            args: {\r
-                channel: channel\r
-            }\r
-        };\r
-\r
-        this.sendData(data, callback);\r
-    };\r
-\r
-    /**\r
-    *   Queries or modifies a channell topic\r
-    *   @param  {String}    channel     The channel to query or modify\r
-    *   @param  {String}    new_topic   The new topic to set\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.topic = function (channel, new_topic, callback) {\r
-        var data = {\r
-            method: 'topic',\r
-            args: {\r
-                channel: channel,\r
-                topic: new_topic\r
-            }\r
-        };\r
-\r
-        this.sendData(data, callback);\r
-    };\r
-\r
-    /**\r
-    *   Kicks a user from a channel\r
-    *   @param  {String}    channel     The channel to kick the user from\r
-    *   @param  {String}    nick        The nick of the user to kick\r
-    *   @param  {String}    reason      The reason for kicking the user\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.kick = function (channel, nick, reason, callback) {\r
-        var data = {\r
-            method: 'kick',\r
-            args: {\r
-                channel: channel,\r
-                nick: nick,\r
-                reason: reason\r
-            }\r
-        };\r
-\r
-        this.sendData(data, callback);\r
-    };\r
-\r
-    /**\r
-    *   Disconnects us from the server\r
-    *   @param  {String}    msg         The quit message to send to the IRC server\r
-    *   @param  {Function}   callback    A callback function\r
-    */\r
-    this.quit = function (msg, callback) {\r
-        msg = msg || "";\r
-        var data = {\r
-            method: 'quit',\r
-            args: {\r
-                message: msg\r
-            }\r
-        };\r
-\r
-        this.sendData(data, callback);\r
-    };\r
-\r
-    /**\r
-    *   Sends a string unmodified to the IRC server\r
-    *   @param  {String}    data        The data to send to the IRC server\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.raw = function (data, callback) {\r
-        data = {\r
-            method: 'raw',\r
-            args: {\r
-                data: data\r
-            }\r
-        };\r
-\r
-        this.sendData(data, callback);\r
-    };\r
-\r
-    /**\r
-    *   Changes our nickname\r
-    *   @param  {String}    new_nick    Our new nickname\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.changeNick = function (new_nick, callback) {\r
-        var data = {\r
-            method: 'nick',\r
-            args: {\r
-                nick: new_nick\r
-            }\r
-        };\r
-\r
-        this.sendData(data, callback);\r
-    };\r
-\r
-    /**\r
-    *   Sends data to a fellow Kiwi IRC user\r
-    *   @param  {String}    target      The nick of the Kiwi IRC user to send to\r
-    *   @param  {String}    data        The data to send\r
-    *   @param  {Function}  callback    A callback function\r
-    */\r
-    this.kiwi = function (target, data, callback) {\r
-        data = {\r
-            method: 'kiwi',\r
-            args: {\r
-                target: target,\r
-                data: data\r
-            }\r
-        };\r
-\r
-        this.sendData(data, callback);\r
-    };\r
+kiwi.model.Gateway = Backbone.Model.extend(new (function () {
+    var that = this;
+
+    this.defaults = {
+        /**
+        *   The name of the network
+        *   @type    String
+        */
+        name: 'Server',
+
+        /**
+        *   The address (URL) of the network
+        *   @type    String
+        */
+        address: '',
+
+        /**
+        *   The current nickname
+        *   @type   String
+        */
+        nick: '',
+
+        /**
+        *   The channel prefix for this network
+        *   @type    String
+        */
+        channel_prefix: '#',
+
+        /**
+        *   The user prefixes for channel owner/admin/op/voice etc. on this network
+        *   @type   Array
+        */
+        user_prefixes: ['~', '&', '@', '+'],
+
+        /**
+        *   The URL to the Kiwi server
+        *   @type   String
+        */
+        //kiwi_server: '//kiwi'
+        kiwi_server: document.location.protocol + '//' + document.location.host + '/kiwi'
+    };
+
+
+    this.initialize = function () {
+        // Update `that` with this new Model object
+        that = this;
+
+        // For ease of access. The socket.io object
+        this.socket = this.get('socket');
+
+        this.server_num = null;
+
+        // Global variable? ~Jack
+        network = this;
+    };
+
+
+    /**
+    *   Connects to the server
+    *   @param  {String}    host        The hostname or IP address of the IRC server to connect to
+    *   @param  {Number}    port        The port of the IRC server to connect to
+    *   @param  {Boolean}   ssl         Whether or not to connect to the IRC server using SSL
+    *   @param  {String}    password    The password to supply to the IRC server during registration
+    *   @param  {Function}  callback    A callback function to be invoked once Kiwi's server has connected to the IRC server
+    */
+    this.connect = function (host, port, ssl, password, callback) {
+        this.socket = io.connect(this.get('kiwi_server'), {
+            'try multiple transports': true,
+            'connect timeout': 3000,
+            'max reconnection attempts': 7,
+            'reconnection delay': 2000
+        });
+        this.socket.on('connect_failed', function (reason) {
+            // TODO: When does this even actually get fired? I can't find a case! ~Darren
+            console.debug('Unable to connect Socket.IO', reason);
+            console.log("kiwi.gateway.socket.on('connect_failed')");
+            //kiwi.front.tabviews.server.addMsg(null, ' ', 'Unable to connect to Kiwi IRC.\n' + reason, 'error');
+            this.socket.disconnect();
+            this.emit("connect_fail", {reason: reason});
+        });
+
+        this.socket.on('error', function (e) {
+            this.emit("connect_fail", {reason: e});
+            console.log("kiwi.gateway.socket.on('error')", {reason: e});
+        });
+
+        this.socket.on('connecting', function (transport_type) {
+            console.log("kiwi.gateway.socket.on('connecting')");
+            this.emit("connecting");
+        });
+
+        this.socket.on('connect', function () {
+            //{command: 'connect', nick: kiwi.gateway.nick, hostname: host, port: port, ssl: ssl, password: password}
+            this.emit('kiwi', {command: 'connect', nick: that.get('nick'), hostname: host, port: port, ssl: ssl, password:password}, function (err, server_num) {
+                console.log('err, server_num', err, server_num);
+                if (!err) {
+                    that.server_num = server_num;
+                    console.log("kiwi.gateway.socket.on('connect')");
+                } else {
+                    console.log("kiwi.gateway.socket.on('error')", {reason: err});
+                }
+            });
+        });
+
+        this.socket.on('too_many_connections', function () {
+            this.emit("connect_fail", {reason: 'too_many_connections'});
+        });
+
+        this.socket.on('irc', function (data, callback) {
+            that.parse(data.command, data.data);
+        });
+
+        this.socket.on('disconnect', function () {
+            this.emit("disconnect", {});
+            console.log("kiwi.gateway.socket.on('disconnect')");
+        });
+
+        this.socket.on('close', function () {
+            console.log("kiwi.gateway.socket.on('close')");
+        });
+
+        this.socket.on('reconnecting', function (reconnectionDelay, reconnectionAttempts) {
+            console.log("kiwi.gateway.socket.on('reconnecting')");
+            this.emit("reconnecting", {delay: reconnectionDelay, attempts: reconnectionAttempts});
+        });
+
+        this.socket.on('reconnect_failed', function () {
+            console.log("kiwi.gateway.socket.on('reconnect_failed')");
+        });
+    };
+
+
+    /*
+        Events:
+            msg
+            action
+            server_connect
+            options
+            motd
+            notice
+            userlist
+            nick
+            join
+            topic
+            part
+            kick
+            quit
+            whois
+            syncchannel_redirect
+            debug
+    */
+    /**
+    *   Parses the response from the server
+    */
+    this.parse = function (command, data) {
+        console.log('gateway event', command, data);
+        if (command !== undefined) {
+            that.trigger('on' + command, data);
+
+            switch (command) {
+            case 'options':
+                $.each(data.options, function (name, value) {
+                    switch (name) {
+                    case 'CHANTYPES':
+                        // TODO: Check this. Why is it only getting the first char?
+                        that.set('channel_prefix', value.join('').charAt(0));
+                        break;
+                    case 'NETWORK':
+                        that.set('name', value);
+                        break;
+                    case 'PREFIX':
+                        that.set('user_prefixes', value);
+                        break;
+                    }
+                });
+                break;
+
+            case 'connect':
+                that.set('nick', data.nick);
+                break;
+
+            case 'nick':
+                that.set('nick', data.newnick);
+                break;
+            /*
+            case 'sync':
+                if (kiwi.gateway.onSync && kiwi.gateway.syncing) {
+                    kiwi.gateway.syncing = false;
+                    kiwi.gateway.onSync(item);
+                }
+                break;
+            */
+
+            case 'kiwi':
+                this.emit('kiwi.' + data.namespace, data);
+                break;
+            }
+        }
+    };
+
+    /**
+    *   Sends data to the server
+    *   @private
+    *   @param  {Object}    data        The data to send
+    *   @param  {Function}  callback    A callback function
+    */
+    this.sendData = function (data, callback) {
+        //this.socket.emit('message', {sid: this.session_id, data: JSON.stringify(data)}, callback);
+        //kiwi.gateway.socket.emit('irc', {server: this.server_num, data: $.toJSON(data)}, callback);
+        this.socket.emit('irc', {server: this.server_num, data: JSON.stringify(data)}, callback);
+    };
+
+    /**
+    *   Sends a PRIVMSG message
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)
+    *   @param  {String}    msg         The message to send
+    *   @param  {Function}  callback    A callback function
+    */
+    this.privmsg = function (target, msg, callback) {
+        var data = {
+            method: 'privmsg',
+            args: {
+                params: [target],
+                trailing: msg
+            }
+        };
+
+        this.sendData(data, callback);
+    };
+
+    /**
+    *   Sends a NOTICE message
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)
+    *   @param  {String}    msg         The message to send
+    *   @param  {Function}  callback    A callback function
+    */
+    this.notice = function (target, msg, callback) {
+        var data = {
+            method: 'notice',
+            args: {
+                params: [target],
+                trailing: msg
+            }
+        };
+
+        this.sendData(data, callback);
+    };
+
+    /**
+    *   Sends a CTCP message
+    *   @param  {Boolean}   request     Indicates whether this is a CTCP request (true) or reply (false)
+    *   @param  {String}    type        The type of CTCP message, e.g. 'VERSION', 'TIME', 'PING' etc.
+    *   @param  {String}    target      The target of the message, e.g a channel or nick
+    *   @param  {String}    params      Additional paramaters
+    *   @param  {Function}  callback    A callback function
+    */
+    this.ctcp = function (request, type, target, params, callback) {
+        var data = {
+            method: 'ctcp',
+            args: {
+                request: request,
+                type: type,
+                target: target,
+                params: params
+            }
+        };
+
+        this.sendData(data, callback);
+    };
+
+    /**
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)
+    *   @param  {String}    msg         The message to send
+    *   @param  {Function}  callback    A callback function
+    */
+    this.action = function (target, msg, callback) {
+        this.ctcp(true, 'ACTION', target, msg, callback);
+    };
+
+    /**
+    *   Joins a channel
+    *   @param  {String}    channel     The channel to join
+    *   @param  {String}    key         The key to the channel
+    *   @param  {Function}  callback    A callback function
+    */
+    this.join = function (channel, key, callback) {
+        var data = {
+            method: 'join',
+            args: {
+                params: [channel, key]
+            }
+        };
+
+        this.sendData(data, callback);
+    };
+
+    /**
+    *   Leaves a channel
+    *   @param  {String}    channel     The channel to part
+    *   @param  {Function}  callback    A callback function
+    */
+    this.part = function (channel, callback) {
+        var data = {
+            method: 'part',
+            args: {
+                params: channel
+            }
+        };
+
+        this.sendData(data, callback);
+    };
+
+    /**
+    *   Queries or modifies a channell topic
+    *   @param  {String}    channel     The channel to query or modify
+    *   @param  {String}    new_topic   The new topic to set
+    *   @param  {Function}  callback    A callback function
+    */
+    this.topic = function (channel, new_topic, callback) {
+        var data = {
+            method: 'topic',
+            args: {
+                params: [channel],
+                trailing: new_topic
+            }
+        };
+
+        this.sendData(data, callback);
+    };
+
+    /**
+    *   Kicks a user from a channel
+    *   @param  {String}    channel     The channel to kick the user from
+    *   @param  {String}    nick        The nick of the user to kick
+    *   @param  {String}    reason      The reason for kicking the user
+    *   @param  {Function}  callback    A callback function
+    */
+    this.kick = function (channel, nick, reason, callback) {
+        var data = {
+            method: 'kick',
+            args: {
+                params: [channel, nick],
+                trailing: reason
+            }
+        };
+
+        this.sendData(data, callback);
+    };
+
+    /**
+    *   Disconnects us from the server
+    *   @param  {String}    msg         The quit message to send to the IRC server
+    *   @param  {Function}   callback    A callback function
+    */
+    this.quit = function (msg, callback) {
+        msg = msg || "";
+        var data = {
+            method: 'quit',
+            args: {
+                trailing: msg
+            }
+        };
+
+        this.sendData(data, callback);
+    };
+
+    /**
+    *   Sends a string unmodified to the IRC server
+    *   @param  {String}    data        The data to send to the IRC server
+    *   @param  {Function}  callback    A callback function
+    */
+    this.raw = function (data, callback) {
+        data = {
+            method: 'raw',
+            args: {
+                data: data
+            }
+        };
+
+        this.sendData(data, callback);
+    };
+
+    /**
+    *   Changes our nickname
+    *   @param  {String}    new_nick    Our new nickname
+    *   @param  {Function}  callback    A callback function
+    */
+    this.changeNick = function (new_nick, callback) {
+        var data = {
+            method: 'nick',
+            args: {
+                params: [new_nick]
+            }
+        };
+
+        this.sendData(data, callback);
+    };
+
+    /**
+    *   Sends data to a fellow Kiwi IRC user
+    *   @param  {String}    target      The nick of the Kiwi IRC user to send to
+    *   @param  {String}    data        The data to send
+    *   @param  {Function}  callback    A callback function
+    */
+    this.kiwi = function (target, data, callback) {
+        data = {
+            method: 'kiwi',
+            args: {
+                target: target,
+                data: data
+            }
+        };
+
+        this.sendData(data, callback);
+    };
 })());
\ No newline at end of file
diff --git a/client_backbone/index.jade b/client_backbone/index.jade
new file mode 100644 (file)
index 0000000..4f66130
--- /dev/null
@@ -0,0 +1,31 @@
+html
+    head
+        meta(http-equiv='Content-Type', content='text/html; charset=UTF-8')
+        title KiwiIRC 
+        link(rel='stylesheet', type='text/css', href='/css/style.css')
+        script
+            document.write(unescape('%3Cscript type="text/javascript" src="' + document.location.protocol + '//' + document.location.host + '/socket.io/socket.io.js"%3E%3C/script%3E'));
+    body
+        #kiwi
+            #toolbar
+                ul.panellist.channels
+                #topic
+                    input(type='text')
+            #panels
+                .panel_container.container1
+            #memberlists
+            #controlbox
+                .input
+                    span.nick    
+                    .input_wrap
+                        input.inp(type='text')
+        script#tmpl_userbox(type='text/x-jquery-tmpl')
+            <div class="userbox">
+            <a class="query">Message</a>
+            <a class="info">Info</a>
+            </div>
+        script(src='all.js')
+        script
+            $(function () {
+                kiwi.app = new kiwi.model.Application({container: $('body')[0]});
+            });
\ No newline at end of file
old mode 100644 (file)
new mode 100755 (executable)
similarity index 100%
rename from client_backbone/backbone-git.js
rename to client_backbone/js/backbone-git.js
old mode 100644 (file)
new mode 100755 (executable)
similarity index 100%
rename from client_backbone/jquery-1.7.1.min.js
rename to client_backbone/js/jquery-1.7.1.min.js
old mode 100644 (file)
new mode 100755 (executable)
similarity index 100%
rename from client_backbone/underscore-min.js
rename to client_backbone/js/underscore-min.js
diff --git a/client_backbone/manifest.json b/client_backbone/manifest.json
new file mode 100644 (file)
index 0000000..48eec3d
--- /dev/null
@@ -0,0 +1,13 @@
+{\r
+    "js": [\r
+        "jquery-1.7.1.min.js",\r
+        "underscore-min.js",\r
+        "backbone-git.js",\r
+        "utils.js",\r
+        "model.js",\r
+        "model_application.js",\r
+        "model_gateway.js",\r
+        "view.js"\r
+    ],\r
+    "css": ["style.css"]\r
+}
\ No newline at end of file
diff --git a/server/app.js b/server/app.js
deleted file mode 100644 (file)
index 7f544a5..0000000
+++ /dev/null
@@ -1,1231 +0,0 @@
-/*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,
-    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;
-    net = objs.net;
-    http = objs.http;
-    https = objs.https;
-    fs = objs.fs;
-    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);
-};
-
-
-
-
-
-
-/*
- * Some process changes
- */
-this.setTitle = function () {
-    process.title = 'kiwiirc';
-};
-
-this.changeUser = function () {
-    if (typeof kiwi.config.group !== 'undefined' && kiwi.config.group !== '') {
-        try {
-            process.setgid(kiwi.config.group);
-        } catch (err) {
-            kiwi.log('Failed to set gid: ' + err);
-            process.exit();
-        }
-    }
-
-    if (typeof kiwi.config.user !== 'undefined' && kiwi.config.user !== '') {
-        try {
-            process.setuid(kiwi.config.user);
-        } catch (e) {
-            kiwi.log('Failed to set uid: ' + e);
-            process.exit();
-        }
-    }
-};
-
-
-
-
-
-
-
-
-
-var ircNumerics = {
-    RPL_WELCOME:            '001',
-    RPL_MYINFO:             '004',
-    RPL_ISUPPORT:           '005',
-    RPL_WHOISUSER:          '311',
-    RPL_WHOISSERVER:        '312',
-    RPL_WHOISOPERATOR:      '313',
-    RPL_WHOISIDLE:          '317',
-    RPL_ENDOFWHOIS:         '318',
-    RPL_WHOISCHANNELS:      '319',
-    RPL_LISTSTART:          '321',
-    RPL_LIST:               '322',
-    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',
-    ERR_TOOMANYCHANNELS:    '405',
-    ERR_NICKNAMEINUSE:      '433',
-    ERR_USERNOTINCHANNEL:   '441',
-    ERR_NOTONCHANNEL:       '442',
-    ERR_NOTREGISTERED:      '451',
-    ERR_LINKCHANNEL:        '470',
-    ERR_CHANNELISFULL:      '471',
-    ERR_INVITEONLYCHAN:     '473',
-    ERR_BANNEDFROMCHAN:     '474',
-    ERR_BADCHANNELKEY:      '475',
-    ERR_CHANOPRIVSNEEDED:   '482',
-    RPL_STARTTLS:           '670'
-};
-
-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});
-        };
-
-    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');
-                }
-            }
-        }
-
-        websocket.sendClientEvent('options', {server: '', "options": irc_connection.IRC.options});
-    });
-
-    bindCommand(ircNumerics.RPL_ENDOFWHOIS, function (msg) {
-        websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: true});
-    });
-
-    bindCommand(ircNumerics.RPL_WHOISUSER, function (msg) {
-        websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
-    });
-
-    bindCommand(ircNumerics.RPL_WHOISSERVER, function (msg) {
-        websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
-    });
-
-    bindCommand(ircNumerics.RPL_WHOISOPERATOR, function (msg) {
-        websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
-    });
-
-    bindCommand(ircNumerics.RPL_WHOISCHANNELS, function (msg) {
-        websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
-    });
-
-    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);
-    });
-
-    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 < 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 {
-            kiwi.log("oops");
-        }
-    });
-
-    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 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);
-    });
-
-    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('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 {
-                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) {
-                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;
-        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;
-                listeners = ircSocket.listeners('data');
-                ircSocket.removeAllListeners('data');
-                ssl_socket = starttls(ircSocket, {}, function () {
-                    ssl_socket.on("data", function (data) {
-                        ircSocketDataHandler(data, websocket, ssl_socket);
-                    });
-                    ircSocket = ssl_socket;
-                    ircSocket.IRC = IRC;
-                    _.each(listeners, function (listener) {
-                        ircSocket.addListener('data', listener);
-                    });
-                });
-                //log(ircSocket);
-            } catch (e) {
-                kiwi.log(e);
-            }
-            break;*/
-    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});
-    });
-
-    bindCommand(ircNumerics.ERR_BANNEDFROMCHAN, function (msg) {
-        websocket.sendClientEvent('irc_error', {error: 'banned_from_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
-    });
-
-    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, obj, args, ircuri, target, modifiers, query,
-        secure = (typeof request.client.encrypted === 'object');
-
-    try {
-        if (kiwi.config.handle_http) {
-            // Run through any plugins..
-            args = {request: request, response: response, ssl: secure};
-            obj = kiwi.kiwi_mod.run('http', args);
-            if (obj === null) {
-                return;
-            }
-            response = args.response;
-
-            uri = url.parse(request.url, true);
-            uri_parts = uri.pathname.split('/');
-
-            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:') {
-                    uri_parts = /^\/([^,\?]*)((,[^,\?]*)*)?$/.exec(ircuri.pathname);
-                    target = uri_parts[1];
-                    modifiers = (typeof uri_parts[2] !== 'undefined') ? uri_parts[2].split(',') : [];
-                    query = ircuri.query;
-
-                    nick = _.detect(modifiers, function (mod) {
-                        return mod === ',isnick';
-                    });
-                    kiwi.log(request.headers);
-                    response.statusCode = 303;
-                    response.setHeader('Location', 'http' + ((secure) ? 's' : '') + '://' + request.headers.host + '/client/' + ircuri.host + '/' + ((!nick) ? target : ''));
-                    response.end();
-                }
-            } else if (uri.pathname === '/js/all.js') {
-                if (kiwi.cache.alljs === '') {
-
-                    min.underscore = fs.readFileSync(public_http_path + 'js/underscore.min.js');
-                    min.util = fs.readFileSync(public_http_path + 'js/util.js');
-                    min.gateway = fs.readFileSync(public_http_path + 'js/gateway.js');
-                    min.front = fs.readFileSync(public_http_path + 'js/front.js');
-                    min.front_events = fs.readFileSync(public_http_path + 'js/front.events.js');
-                    min.front_ui = fs.readFileSync(public_http_path + 'js/front.ui.js');
-                    min.iscroll = fs.readFileSync(public_http_path + 'js/iscroll.js');
-                    min.ast = jsp.parse(min.underscore + min.util + min.gateway + min.front + min.front_events + min.front_ui + min.iscroll);
-                    min.ast = pro.ast_mangle(min.ast);
-                    min.ast = pro.ast_squeeze(min.ast);
-                    min.final_code = pro.gen_code(min.ast);
-                    kiwi.cache.alljs = min.final_code;
-                    hash = crypto.createHash('md5').update(kiwi.cache.alljs);
-                    kiwi.cache.alljs_hash = hash.digest('base64');
-                }
-                if (request.headers['if-none-match'] === kiwi.cache.alljs_hash) {
-                    response.statusCode = 304;
-                } else {
-                    response.setHeader('Content-type', 'application/javascript');
-                    response.setHeader('ETag', kiwi.cache.alljs_hash);
-                    response.write(kiwi.cache.alljs);
-                }
-                response.end();
-            } else if ((subs === '/js/') || (subs === '/css') || (subs === '/img')) {
-                request.addListener('end', function () {
-                    kiwi.fileServer.serve(request, response);
-                });
-            } else if (uri.pathname === '/' || uri_parts[1] === 'client') {
-                useragent = (typeof request.headers === 'string') ? request.headers['user-agent'] : '';
-                if (useragent.match(/android/i) !== -1) {
-                    agent = 'android';
-                    touchscreen = true;
-                } else if (useragent.match(/iphone/) !== -1) {
-                    agent = 'iphone';
-                    touchscreen = true;
-                } else if (useragent.match(/ipad/) !== -1) {
-                    agent = 'ipad';
-                    touchscreen = true;
-                } else if (useragent.match(/ipod/) !== -1) {
-                    agent = 'ipod';
-                    touchscreen = true;
-                } else {
-                    agent = 'normal';
-                    touchscreen = false;
-                }
-                agent = 'normal';
-                touchscreen = false;
-
-                debug = (typeof uri.query.debug !== 'undefined');
-
-                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 || kiwi.config.client_defaults.server;
-                        nick = uri.query.nick || '';
-                    } else {
-                        server_set = false;
-                        server = kiwi.config.client_defaults.server;
-                        nick = '';
-                    }
-                } else {
-                    server_set = ((typeof uri_parts[2] !== 'undefined') && (uri_parts[2] !== ''));
-                    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] === '+') {
-                            port = port.substring(1);
-                            ssl = true;
-                        } else {
-                            ssl = false;
-                        }
-                    }
-                    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')
-                    .update(server_set ? 't' : 'f')
-                    .update(secure ? 't' : 'f')
-                    .update(server)
-                    .update(port.toString())
-                    .update(ssl  ? 't' : 'f')
-                    .update(nick)
-                    .update(agent)
-                    .update(JSON.stringify(kiwi.config))
-                    .digest('base64');
-                if (kiwi.cache.html[hash]) {
-                    if (request.headers['if-none-match'] === kiwi.cache.html[hash].hash) {
-                        response.statusCode = 304;
-                    } else {
-                        response.setHeader('Etag', kiwi.cache.html[hash].hash);
-                        response.setHeader('Content-type', 'text/html');
-                        response.write(kiwi.cache.html[hash].html);
-                    }
-                    response.end();
-                } else {
-                    fs.readFile(public_http_path + 'index.html.jade', 'utf8', function (err, str) {
-                        var html, hash2;
-                        if (!err) {
-                            try {
-                                html = kiwi.jade.compile(str)({ "touchscreen": touchscreen, "debug": debug, "secure": secure, "server_set": server_set, "server": server, "port": port, "ssl": ssl, "nick": nick, "agent": agent, "config": kiwi.config });
-                                hash2 = crypto.createHash('md5').update(html).digest('base64');
-                                kiwi.cache.html[hash] = {"html": html, "hash": hash2};
-                                if (request.headers['if-none-match'] === hash2) {
-                                    response.statusCode = 304;
-                                } else {
-                                    response.setHeader('Etag', hash2);
-                                    response.setHeader('Content-type', 'text/html');
-                                    response.write(html);
-                                }
-                            } catch (e) {
-                                response.statusCode = 500;
-                                kiwi.log(e);
-                            }
-                        } else {
-                            kiwi.log(err);
-                            response.statusCode = 500;
-                        }
-                        response.end();
-                    });
-                }
-            } else if (uri.pathname.substr(0, 10) === '/socket.io') {
-                return;
-            } else {
-                response.statusCode = 404;
-                response.end();
-            }
-        }
-
-    } catch (e) {
-        kiwi.log('ERROR app.httpHandler()');
-        kiwi.log(e);
-    }
-};
-
-
-
-
-this.websocketListen = function (servers, handler) {
-    if (kiwi.httpServers.length > 0) {
-        _.each(kiwi.httpServers, function (hs) {
-            hs.close();
-        });
-        kiwi.httpsServers = [];
-    }
-
-    _.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(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(server.port, server.address);
-            kiwi.log('Listening on ' + server.address + ':' + server.port.toString() + ' without SSL');
-        }
-
-        kiwi.httpServers.push(hs);
-    });
-
-    _.each(kiwi.io, function (io) {
-        io.set('log level', 1);
-        io.enable('browser client minification');
-        io.enable('browser client etag');
-        io.set('transports', kiwi.config.transports);
-
-        io.of('/kiwi').authorization(function (handshakeData, callback) {
-            var address = handshakeData.address.address;
-            if (typeof kiwi.connections[address] === 'undefined') {
-                kiwi.connections[address] = {count: 0, sockets: []};
-            }
-            callback(null, true);
-        }).on('connection', kiwi.websocketConnection);
-    });
-};
-
-
-
-
-
-
-this.websocketConnection = function (websocket) {
-    var con;
-    kiwi.log("New connection!");
-    websocket.kiwi = {address: websocket.handshake.address.address, buffer: {list: []}};
-    con = kiwi.connections[websocket.kiwi.address];
-
-    if (con.count >= kiwi.config.max_client_conns) {
-        websocket.emit('too_many_connections');
-        websocket.disconnect();
-    } else {
-        con.count += 1;
-        con.sockets.push(websocket);
-
-        websocket.sendClientEvent = function (event_name, data) {
-            var ev = kiwi.kiwi_mod.run(event_name, data, {websocket: this});
-            if (ev === null) {
-                return;
-            }
-
-            data.event = event_name;
-            websocket.emit('message', data);
-        };
-
-        websocket.sendServerLine = function (data, eol, callback) {
-            if ((arguments.length < 3) && (typeof eol === 'function')) {
-                callback = eol;
-            }
-            eol = (typeof eol !== 'string') ? '\r\n' : eol;
-
-            try {
-                websocket.ircConnection.write(data + eol, 'utf-8', callback);
-            } catch (e) { }
-        };
-
-        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');
-        });
-    };
-
-    if (!ssl) {
-        ircSocket = net.createConnection(port, host);
-        ircSocket.on('connect', onConnectHandler);
-    } else {
-        ircSocket = tls.connect(port, host, {}, onConnectHandler);
-    }
-
-    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);
-        }
-    });
-
-    ircSocket.on('error', function (e) {
-        that.connected = false;
-        that.emit('error', e);
-        that.destroySoon();
-    });
-
-    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 (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;
-                }
-
-                // 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, 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 '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, callback);
-                }
-            }
-            break;
-        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, 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('PART ' + chan, callback);
-                });
-            }
-            break;
-
-        case 'topic':
-            if (args.channel) {
-                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.ircConnection.end('QUIT :' + args.message + '\r\n');
-            websocket.sentQUIT = true;
-            websocket.ircConnection.destroySoon();
-            websocket.disconnect();
-            break;
-
-        case 'notice':
-            if ((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:
-        }
-    //} catch (e) {
-    //    kiwi.log("Caught error (app.websocketMessage): " + e);
-    //}
-};
-
-
-
-this.websocketDisconnect = function (websocket) {
-    var con;
-
-    if ((!websocket.sentQUIT) && (websocket.ircConnection.connected)) {
-        try {
-            websocket.ircConnection.end('QUIT :' + kiwi.config.quit_message + '\r\n');
-            websocket.sentQUIT = true;
-            websocket.ircConnection.destroySoon();
-        } catch (e) {
-        }
-    }
-    con = kiwi.connections[websocket.kiwi.address];
-    con.count -= 1;
-    con.sockets = _.reject(con.sockets, function (sock) {
-        return sock === websocket;
-    });
-};
-
-
-
-
-
-
-this.rehash = function () {
-    var changes, i,
-        reload_config = kiwi.loadConfig();
-
-    // If loading the new config errored out, dont attempt any changes
-    if (reload_config === false) {
-        return false;
-    }
-
-    // We just want the settings that have been changed
-    changes = reload_config[1];
-
-    if (Object.keys(changes).length !== 0) {
-        kiwi.log('%s config changes: \n', Object.keys(changes).length, changes);
-        for (i in changes) {
-            switch (i) {
-            case 'servers':
-                kiwi.websocketListen(kiwi.config.servers, kiwi.httpHandler);
-                delete changes.ports;
-                delete changes.bind_address;
-                delete changes.ssl_key;
-                delete changes.ssl_cert;
-                break;
-            case 'user':
-            case 'group':
-                kiwi.changeUser();
-                delete changes.user;
-                delete changes.group;
-                break;
-            case 'module_dir':
-            case 'modules':
-                kiwi.kiwi_mod.loadModules(kiwi_root, kiwi.config);
-                kiwi.kiwi_mod.printMods();
-                delete changes.module_dir;
-                delete changes.modules;
-                break;
-            }
-        }
-    }
-
-    // Also clear the kiwi.cached javascript and HTML
-    if (kiwi.config.handle_http) {
-        kiwi.cache = {alljs: '', html: []};
-    }
-
-    return true;
-};
-
-
-
-
-
-/*
- * KiwiIRC controlling via STDIN
- */
-this.manageControll = function (data) {
-    var parts = data.toString().trim().split(' '),
-        connections_cnt = 0,
-        i;
-    switch (parts[0]) {
-    case 'rehash':
-        kiwi.log('Rehashing...');
-        kiwi.log(kiwi.rehash() ? 'Rehash complete' : 'Rehash failed');
-        break;
-
-    case 'recode':
-        kiwi.log('Recoding...');
-        kiwi.log(kiwi.recode() ? 'Recode complete' : 'Recode failed');
-        break;
-
-    case 'mod':
-        if (parts[1] === 'reload') {
-            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;
-
-    case 'cache':
-        if (parts[1] === 'clear') {
-            kiwi.cache.html = {};
-            kiwi.cache.alljs = '';
-            kiwi.log('HTML cache cleared');
-        }
-        break;
-
-    case 'status':
-        for (i in kiwi.connections) {
-            connections_cnt = connections_cnt + parseInt(kiwi.connections[i].count, 10);
-        }
-        kiwi.log(connections_cnt.toString() + ' connected clients');
-        break;
-
-    default:
-        kiwi.log('Unknown command \'' + parts[0] + '\'');
-    }
-};
diff --git a/server/client.js b/server/client.js
new file mode 100755 (executable)
index 0000000..4276043
--- /dev/null
@@ -0,0 +1,150 @@
+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;
+    
+    events.EventEmitter.call(this);
+    this.websocket = websocket;
+    
+    this.IRC_connections = [];
+    this.next_connection = 0;
+    
+    this.buffer = {
+        list: [],
+        motd: ''
+    };
+    
+    websocket.on('irc', function () {
+        IRC_command.apply(c, arguments);
+    });
+    websocket.on('kiwi', function () {
+        kiwi_command.apply(c, arguments);
+    });
+    websocket.on('disconnect', function () {
+        disconnect.apply(c, arguments);
+    });
+    websocket.on('error', function () {
+        error.apply(c, arguments);
+    });
+};
+util.inherits(Client, events.EventEmitter);
+
+module.exports.Client = Client;
+
+// Callback API:
+// Callbacks SHALL accept 2 arguments, error and response, in that order.
+// error MUST be null where the command is successul.
+// 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) {
+    var c = {command: command, data: data};
+    console.log('C<--', c);
+    this.websocket.emit('irc', c, callback);
+};
+
+Client.prototype.sendKiwiCommand = function (command, callback) {
+    this.websocket.emit('kiwi', command, 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);
+    }
+};
+
+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();
+    }
+};
+
+var extension_command = function (command, callback) {
+    if (typeof callback === 'function') {
+        callback('not implemented');
+    }
+};
+
+var disconnect = function () {
+    _.each(this.IRC_connections, function (irc_connection, i, cons) {
+        if (irc_connection) {
+            irc_connection.end('QUIT :Kiwi IRC');
+            cons[i] = null;
+        }
+    });
+    this.emit('destroy');
+};
+
+var error = function () {
+    this.emit('destroy');
+};
old mode 100644 (file)
new mode 100755 (executable)
index 75a38be..686a384
@@ -2,6 +2,7 @@
     "servers":            [
                             {
                                 "secure":   true,
+                                "hsts": true,
                                 "port":   7777,
                                 "address": "0.0.0.0",
 
diff --git a/server/http-handler.js b/server/http-handler.js
new file mode 100755 (executable)
index 0000000..aa3d2c0
--- /dev/null
@@ -0,0 +1,169 @@
+var fs          = require('fs'),
+    crypto      = require('crypto');
+    url         = require('url'),
+    _           = require('underscore'),
+    uglify      = require('uglify-js'),
+    jade        = require('jade'),
+    node_static = require ('node-static');
+
+var HTTPHandler = function (config) {
+    var site = config.site;
+    this.config = config;
+    var files;
+    files = fs.readdirSync('client');
+    if ((typeof site !== 'undefined') && (typeof site === 'string') && (_.include(files, site))) {
+        this.site = site;
+        this.static_file_server = new StaticFileServer(site);
+    }
+    else {
+        this.site = 'default';
+        this.static_file_server = null;
+    }
+    
+};
+
+module.exports.HTTPHandler = HTTPHandler;
+
+var default_static_file_server = new node_static.Server('client_backbone/');
+
+var StaticFileServer = function (site) {
+    this.fileServer = new node_static.Server('client_backbone/');
+};
+
+StaticFileServer.prototype.serve = function (request, response) {
+    this.fileServer.serve(request, response, function (err) {
+        if (err) {
+            default_static_file_server.serve(request, response);
+        }
+    });
+};
+
+var serve_static_file = function (request, response) {
+    if (this.static_file_server !== null) {
+        this.static_file_server.serve(request, response);
+    } else {
+        default_static_file_server.serve(request, response);
+    }
+};
+
+HTTPHandler.prototype.handler = function (request, response) {
+    var file_list, default_file_list, hash, uri, site, subs, self = this;
+    
+    site = 'default';
+    uri = url.parse(request.url, true);
+    subs = uri.pathname.substr(0, 4);
+    
+    if (uri.pathname === '/all.js') {
+        hash = is_cached(site,'all.js');
+        if (!hash) {
+            file_list = [];
+                       default_file_list = [];
+            console.log('a');
+            fs.readFile('client_backbone/manifest.json', 'utf-8', function (err, manifest) {
+                console.log('b');
+                var js = '';
+                manifest = JSON.parse(manifest);
+                _.each(manifest.js, function (file) {
+                    console.log(file)
+                    js += fs.readFileSync('client_backbone/js/' + file, 'utf-8') + '\r\n';
+                });
+                
+                // TODO: Replace false with check for debug flag
+                if (/* debug === */ false) {
+                    js = uglify.uglify.gen_code(uglify.uglify.ast_squeeze(uglify.uglify.ast_mangle(uglify.parser.parse(js))));
+                }
+                
+                hash = set_cache(site, 'all.js', js);
+                if (request.headers['if-none-match'] === hash) {
+                    response.statusCode = 304;
+                } else {
+                    response.setHeader('Content-type', 'application/javascript');
+                    response.setHeader('ETag', hash);
+                    response.write(js);
+                }
+                response.end();
+            });
+        } else {
+            if (request.headers['if-none-match'] === hash) {
+                response.statusCode = 304;
+            } else {
+                response.setHeader('Content-type', 'application/javascript');
+                response.setHeader('ETag', hash);
+                response.write(get_cache(site, 'all.js'));
+            }
+            response.end();
+        }
+    } else if (uri.pathname === '/') {
+        var jadefile = '';
+        
+        hash = is_cached(site, '/');
+        
+        if (!hash) {
+            try {
+                fs.readFile('client_backbone/index.jade', 'utf-8', function (err, str) {
+                    if (err) {
+                        console.log(err + '');
+                        response.end();
+                    } else {
+                        jadefile = str;
+                    }
+                    hash = set_cache('default', '/', jade.compile(jadefile, {pretty: true})());
+                    if (response.statusCode !== 500) {
+                        if (request.headers['if-none-match'] === hash) {
+                            response.statusCode = 304;
+                        } else {
+                            response.setHeader('Content-type', 'text/html; charset=utf-8');
+                            response.setHeader('ETag', hash);
+                            response.write(get_cache(site, '/'));
+                        }
+                    }
+                    response.end();
+                });
+                
+            } catch (e) {
+                console.log(e);
+                response.statusCode = 500;
+                response.end();
+            }
+        } else {
+            if (request.headers['if-none-match'] === hash) {
+                response.statusCode = 304;
+            } else {
+                response.setHeader('Content-type', 'text/html; charset=utf-8');
+                response.setHeader('ETag', hash);
+                response.write(get_cache(site, '/'));
+            }
+            response.end();
+        }
+    } else if ((subs === '/img') || (subs === '/css')) {
+        serve_static_file.call(this, request, response);
+    } else if (uri.pathname.substr(0, 10) === '/socket.io') {
+        return;
+    } else {
+        response.statusCode = 404;
+        response.end();
+    }
+};
+
+var cache = Object.create(null);
+
+var set_cache = function (site, file, data) {
+    if (!cache[site]) {
+        cache[site] = Object.create(null);
+    }
+    var hash = crypto.createHash('md5').update(data).digest('base64');
+    cache[site][file] = {'data': data, 'hash': hash};
+    return hash;
+};
+
+var is_cached = function (site, file) {
+    if ((cache[site]) && (cache[site][file])) {
+        return cache[site][file].hash;
+    } else {
+        return false;
+    }
+};
+
+var get_cache = function (site, file) {
+    return cache[site][file].data;
+};
diff --git a/server/irc-commands.js b/server/irc-commands.js
new file mode 100755 (executable)
index 0000000..b777d86
--- /dev/null
@@ -0,0 +1,480 @@
+var _ = require('underscore');
+
+var irc_numerics = {
+    RPL_WELCOME:            '001',
+    RPL_MYINFO:             '004',
+    RPL_ISUPPORT:           '005',
+    RPL_WHOISUSER:          '311',
+    RPL_WHOISSERVER:        '312',
+    RPL_WHOISOPERATOR:      '313',
+    RPL_WHOISIDLE:          '317',
+    RPL_ENDOFWHOIS:         '318',
+    RPL_WHOISCHANNELS:      '319',
+    RPL_LISTSTART:          '321',
+    RPL_LIST:               '322',
+    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',
+    ERR_TOOMANYCHANNELS:    '405',
+    ERR_NICKNAMEINUSE:      '433',
+    ERR_USERNOTINCHANNEL:   '441',
+    ERR_NOTONCHANNEL:       '442',
+    ERR_NOTREGISTERED:      '451',
+    ERR_LINKCHANNEL:        '470',
+    ERR_CHANNELISFULL:      '471',
+    ERR_INVITEONLYCHAN:     '473',
+    ERR_BANNEDFROMCHAN:     '474',
+    ERR_BADCHANNELKEY:      '475',
+    ERR_CHANOPRIVSNEEDED:   '482',
+    RPL_STARTTLS:           '670'
+};
+
+
+var Binder = function (irc_connection, con_num, client) {
+    this.irc_connection = irc_connection;
+    this.con_num = con_num;
+    this.client = client;
+};
+module.exports.Binder = Binder;
+
+Binder.prototype.bind_irc_commands = function () {
+    var that = this;
+    _.each(listeners, function (listener, command) {
+        var s = command.substr(0, 4);
+        if ((s === 'RPL_') || (s === 'ERR_')) {
+            command = irc_numerics[command];
+        }
+        that.irc_connection.on('irc_' + command, function () {
+            listener.apply(that, arguments);
+        });
+    });
+};
+
+var listeners = {
+    'RPL_WELCOME':            function (command) {
+                var nick =  command.params[0];
+                this.irc_connection.registered = true;
+                this.client.sendKiwiCommand({server: this.con_num, command: 'connect', nick: nick});
+            },
+    'RPL_ISUPPORT':           function (command) {
+                var options, i, option, matches, j;
+                options = command.params;
+                for (i = 1; i < options.length; i++) {
+                    option = options[i].split("=", 2);
+                    option[0] = option[0].toUpperCase();
+                    this.irc_connection.options[option[0]] = (typeof option[1] !== 'undefined') ? option[1] : true;
+                    if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES', 'CHANMODES', 'NAMESX'], option[0])) {
+                        if (option[0] === 'PREFIX') {
+                            matches = /\(([^)]*)\)(.*)/.exec(option[1]);
+                            if ((matches) && (matches.length === 3)) {
+                                this.irc_connection.options.PREFIX = [];
+                                for (j = 0; j < matches[2].length; j++) {
+                                    this.irc_connection.options.PREFIX.push({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)});
+                                }
+                            }
+                                               } else if (option[0] === 'CHANTYPES') {
+                                                       this.irc_connection.options.CHANTYPES = this.irc_connection.options.CHANTYPES.split('');
+                                               } else if (option[0] === 'CHANMODES') {
+                                                       this.irc_connection.options.CHANMODES = option[1].split(',');
+                        } else if (option[0] === 'NAMESX') {
+                            this.irc_connection.write('PROTOCTL NAMESX');
+                        }
+                    }
+                }
+                //this.client.sendIRCCommand({server: this.con_num, command: 'RPL_ISUPPORT', options: this.irc_connection.options});
+                //websocket.sendClientEvent('options', {server: '', "options": irc_connection.IRC.options});
+                this.client.sendIRCCommand('options', {server: this.con_num, options: this.irc_connection.options});
+            },
+    'RPL_ENDOFWHOIS':         function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_ENDOFWHOIS';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: true});
+                this.client.sendIRCCommand('whois', {server: this.con_num, nick: command.params[0], msg: command.trailing, end: true});
+            },
+    'RPL_WHOISUSER':          function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_WHOISUSER';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
+                this.client.sendIRCCommand('whois', {server: this.con_num, nick: command.params[0], msg: command.trailing, end: false});
+            },
+    'RPL_WHOISSERVER':        function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_WHOISSERVER';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
+                this.client.sendIRCCommand('whois', {server: this.con_num, nick: command.params[0], msg: command.trailing, end: false});
+            },
+    'RPL_WHOISOPERATOR':      function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_WHOISOPERATOR';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
+                this.client.sendIRCCommand('whois', {server: this.con_num, nick: command.params[0], msg: command.trailing, end: false});
+            },
+    'RPL_WHOISCHANNELS':      function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_WHOISCHANNELS';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
+                this.client.sendIRCCommand('whois', {server: this.con_num, nick: command.params[0], msg: command.trailing, end: false});
+            },
+    'RPL_WHOISMODES':         function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_WHOISMODES';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
+                this.client.sendIRCCommand('whois', {server: this.con_num, nick: command.params[0], msg: command.trailing, end: false});
+            },
+    'RPL_WHOISIDLE':          function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_WHOISIDLE';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
+                this.client.sendIRCCommand('whois', {server: this.con_num, nick: command.params[0], msg: command.trailing, end: false});
+            },
+    'RPL_LISTSTART':          function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_LISTSTART';
+                               this.client.sendIRCCommand(command);*/
+                this.client.sendIRCCommand('list_start', {server: this.con_num});
+                this.client.buffer.list = [];
+            },
+    'RPL_LISTEND':            function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_LISTEND';
+                               this.client.sendIRCCommand(command);*/
+                if (this.client.buffer.list.length > 0) {
+                    this.client.buffer.list = _.sortBy(this.client.buffer.list, function (channel) {
+                        return channel.num_users;
+                    });
+                    this.client.sendIRCCommand('list_channel', {server: this.con_num, chans: this.client.buffer.list});
+                    this.client.buffer.list = [];
+                }
+                this.client.sendIRCCommand('list_end', {server: this.con_num});
+            },
+    'RPL_LIST':               function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_LIST';
+                               this.client.sendIRCCommand(command);*/
+                this.client.buffer.list.push({server: this.con_num, channel: command.params[1], num_users: parseInt(command.params[2]), topic: command.trailing});
+                if (this.client.buffer.list.length > 200){
+                    this.client.buffer.list = _.sortBy(this.client.buffer.list, function (channel) {
+                        return channel.num_users;
+                    });
+                    this.client.sendIRCCommand('list_channel', {server: this.con_num, chans: this.client.buffer.list});
+                    this.client.buffer.list = [];
+                }
+            },
+    'RPL_MOTD':               function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_MOTD';
+                               this.client.sendIRCCommand(command);*/
+                this.client.buffer.motd += command.trailing + '\n';
+            },
+    'RPL_MOTDSTART':          function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_MOTDSTART';
+                               this.client.sendIRCCommand(command);*/
+                this.client.buffer.motd = '';
+            },
+    'RPL_ENDOFMOTD':          function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_ENDOFMOTD';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('motd', {server: '', 'msg': websocket.kiwi.buffer.motd});
+                this.client.sendIRCCommand('motd', {server: this.con_num, msg: this.client.buffer.motd});
+            },
+    'RPL_NAMEREPLY':          function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_NAMEREPLY';
+                               this.client.sendIRCCommand(command);*/
+                var members = command.trailing.split(' ');
+                var member_list = [];
+                var that = this;
+                var i = 0;
+                _.each(members, function (member) {
+                    var j, k, modes = [];
+                    for (j = 0; j < member.length; j++) {
+                        for (k = 0; k < that.irc_connection.options.PREFIX.length; k++) {
+                            if (member.charAt(j) === that.irc_connection.options.PREFIX[k].symbol) {
+                                modes.push(that.irc_connection.options.PREFIX[k].mode);
+                                i++;
+                            }
+                        }
+                    }
+                    member_list.push({nick: member, modes: modes});
+                    if (i++ >= 50) {
+                        that.client.sendIRCCommand('userlist', {server: that.con_num, users: member_list, channel: command.params[2]});
+                        member_list = [];
+                        i = 0;
+                    }
+                });
+                if (i > 0) {
+                    this.client.sendIRCCommand('userlist', {server: this.con_num, users: member_list, channel: command.params[2]});
+                }
+            },
+    'RPL_ENDOFNAMES':         function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_ENDOFNAMES';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('userlist_end', {server: '', channel: msg.params.split(" ")[1]});
+                this.client.sendIRCCommand('userlist_end', {server: this.con_num, channel: command.params[1]});
+            },
+    'RPL_BANLIST':            function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_BANLIST';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('banlist', {server: '', channel: params[1], banned: params[2], banned_by: params[3], banned_at: params[4]});
+                this.client.sendIRCCommand('banlist', {server: this.con_num, channel: command.params[1], banned: command.params[2], banned_by: command.params[3], banned_at: command.params[4]});
+            },
+    'RPL_ENDOFBANLIST':       function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_ENDOFBANLIST';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('banlist_end', {server: '', channel: msg.params.split(" ")[1]});
+                this.client.sendIRCCommand('banlist_end', {server: this.con_num, channel: command.params[1]});
+            },
+    'RPL_TOPIC':              function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_TOPIC';
+                               this.client.sendIRCCommand(command);*/
+                //{nick: '', channel: msg.params.split(" ")[1], topic: msg.trailing};
+                this.client.sendIRCCommand('topic', {server: this.con_num, nick: '', channel: command.params[1], topic: command.trailing});
+            },
+    'RPL_NOTOPIC':            function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_NOTOPIC';
+                               this.client.sendIRCCommand(command);*/
+                this.client.sendIRCCommand('topic', {server: this.con_num, nick: '', channel: command.params[1], topic: ''});
+            },
+    'RPL_TOPICWHOTIME':       function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'RPL_TOPICWHOTIME';
+                               this.client.sendIRCCommand(command);*/
+                //{nick: nick, channel: channel, when: when};
+                this.client.sendIRCCommand('topicsetby', {server: this.con_num, nick: command.params[2], channel: command.params[1], when: command.params[3]});
+            },
+    'PING':                 function (command) {
+                this.irc_connection.write('PONG ' + command.trailing);
+            },
+    'JOIN':                 function (command) {
+                               var channel;
+                               if (typeof command.trailing === 'string' && command.trailing !== '') {
+                                       channel = command.trailing;
+                               } else if (typeof command.params[0] === 'string' && command.params[0] !== '') {
+                                       channel = command.params[0];
+                               }
+                               /*command.server = this.con_num;
+                               command.command = 'JOIN';
+                               command.params = [channel];
+                this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('join', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: channel});
+                this.client.sendIRCCommand('join', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, channel: channel});
+                
+                if (command.nick === this.nick) {
+                    this.irc_connection.write('NAMES ' + channel);
+                }
+            },
+    'PART':                 function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'PART';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('part', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), message: msg.trailing});
+                this.client.sendIRCCommand('part', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, channel: command.params[0], message: command.trailing});
+            },
+    'KICK':                 function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'KICK';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('kick', {kicked: params[1], nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: params[0].trim(), message: msg.trailing});
+                this.client.sendIRCCommand('kick', {server: this.con_num, kicked: command.params[1], nick: command.nick, ident: command.ident, hostname: command.hostname, channel: params[0], message: command.trailing});
+            },
+    'QUIT':                 function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'QUIT';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('quit', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, message: msg.trailing});
+                this.client.sendIRCCommand('quit', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, message: command.trailing});
+            },
+    'NOTICE':               function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'NOTICE';
+                               this.client.sendIRCCommand(command);*/
+                if ((command.trailing.charAt(0) === String.fromCharCode(1)) && (command.trailing.charAt(command.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)});
+                    this.client.sendIRCCommand('ctcp_response', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, channel: command.params[0], msg: command.trailing.substr(1, command.trailing.length - 2)});
+                } else {
+                    //websocket.sendClientEvent('notice', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, target: msg.params.trim(), msg: msg.trailing});
+                    this.client.sendIRCCommand('notice', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, target: command.params[0], msg: command.trailing});
+                }
+            },
+    'NICK':                 function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'NICK';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('nick', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, newnick: msg.trailing});
+                this.client.sendIRCCommand('nick', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, newnick: command.params[0]});
+            },
+    'TOPIC':                function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'TOPIC';
+                               this.client.sendIRCCommand(command);*/
+                //{nick: msg.nick, channel: msg.params, topic: msg.trailing};
+                this.client.sendIRCCommand('topic', {server: this.con_num, nick: command.nick, channel: msg.params[0], topic: command.trailing});
+            },
+    'MODE':                 function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'MODE';
+                               this.client.sendIRCCommand(command);*/
+                var ret = { server: this.con_num, nick: command.nick }
+                switch (command.params.length) {
+                    case 1:
+                        ret.affected_nick = command.params[0];
+                        ret.mode = command.trailing;
+                        break;
+                    case 2:
+                        ret.channel = command.params[0];
+                        ret.mode = command.params[1];
+                        break;
+                    default:
+                        ret.channel = command.params[0];
+                        ret.mode = command.params[1];
+                        ret.affected_nick = command.params[2];
+                        break;
+                }
+                this.client.sendIRCCommand('mode', ret);
+            },
+    'PRIVMSG':              function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'PRIVMSG';
+                               this.client.sendIRCCommand(command);*/
+                var tmp, namespace;
+                if ((command.trailing.charAt(0) === String.fromCharCode(1)) && (command.trailing.charAt(command.trailing.length - 1) === String.fromCharCode(1))) {
+                    //CTCP request
+                    if (command.trailing.substr(1, 6) === 'ACTION') {
+                        this.client.sendIRCCommand('action', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, channel: command.params[0], msg: command.trailing.substr(7, command.trailing.length - 2)});
+                    } else if (command.trailing.substr(1, 4) === 'KIWI') {
+                        tmp = msg.trailing.substr(6, msg.trailing.length - 2);
+                        namespace = tmp.split(' ', 1)[0];
+                        this.client.sendIRCCommand('kiwi', {server: this.con_num, namespace: namespace, data: tmp.substr(namespace.length + 1)});
+                    } else if (msg.trailing.substr(1, 7) === 'VERSION') {
+                        this.irc_connection.write('NOTICE ' + command.nick + ' :' + String.fromCharCode(1) + 'VERSION KiwiIRC' + String.fromCharCode(1));
+                    } else {
+                        this.client.sendIRCCommand('ctcp_request', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, channel: command.params[0], msg: command.trailing.substr(1, command.trailing.length - 2)});
+                    }
+                } else {
+                    //{nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing}
+                    this.client.sendIRCCommand('msg', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, channel: command.params[0], msg: command.trailing});
+                }
+            },
+    'ERROR':                function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERROR';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'error', reason: msg.trailing});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'error', reason: command.trailing});
+            },
+    ERR_LINKCHANNEL:        function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_LINKCHANNEL';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('channel_redirect', {from: params[1], to: params[2]});
+                this.client.sendIRCCommand('channel_redirect', {server: this.con_num, from: command.params[1], to: command.params[2]});
+            },
+    ERR_NOSUCHNICK:         function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_NOSUCHNICK';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'no_such_nick', nick: msg.params.split(" ")[1], reason: msg.trailing});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'no_such_nick', nick: command.params[1], reason: command.trailing});
+            },
+    ERR_CANNOTSENDTOCHAN:   function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_CANNOTSENDTOCHAN';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'cannot_send_to_chan', channel: msg.params.split(" ")[1], reason: msg.trailing});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'cannot_send_to_chan', channel: command.params[1], reason: command.trailing});
+            },
+    ERR_TOOMANYCHANNELS:    function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_TOOMANYCHANNELS';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'too_many_channels', channel: msg.params.split(" ")[1], reason: msg.trailing});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'too_many_channels', channel: command.params[1], reason: command.trailing});
+            },
+    ERR_USERNOTINCHANNEL:   function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_USERNOTINCHANNEL';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'user_not_in_channel', nick: params[0], channel: params[1], reason: msg.trainling});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'user_not_in_channel', nick: command.params[0], channel: command.params[1], reason: command.trailing});
+            },
+    ERR_NOTONCHANNEL:       function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_NOTONCHANNEL';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'not_on_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'not_on_channel', channel: command.params[1], reason: command.trailing});
+            },
+    ERR_CHANNELISFULL:      function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_CHANNELISFULL';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'channel_is_full', channel: msg.params.split(" ")[1], reason: msg.trailing});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'channel_is_full', channel: command.params[1], reason: command.trailing});
+            },
+    ERR_INVITEONLYCHAN:     function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_INVITEONLYCHAN';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'invite_only_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'invite_only_channel', channel: command.params[1], reason: command.trailing});
+            },
+    ERR_BANNEDFROMCHAN:     function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_BANNEDFROMCHAN';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'banned_from_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'banned_from_channel', channel: command.params[1], reason: command.trailing});
+            },
+    ERR_BADCHANNELKEY:      function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_BADCHANNELKEY';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'bad_channel_key', channel: msg.params.split(" ")[1], reason: msg.trailing});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'bad_channel_key', channel: command.params[1], reason: command.trailing});
+            },
+    ERR_CHANOPRIVSNEEDED:   function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_CHANOPRIVSNEEDED';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'chanop_privs_needed', channel: msg.params.split(" ")[1], reason: msg.trailing});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'chanop_privs_needed', channel: command.params[1], reason: command.trailing});
+            },
+    ERR_NICKNAMEINUSE:      function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_NICKNAMEINUSE';
+                               this.client.sendIRCCommand(command);*/
+                //websocket.sendClientEvent('irc_error', {error: 'nickname_in_use', nick: _.last(msg.params.split(" ")), reason: msg.trailing});
+                this.client.sendIRCCommand('irc_error', {server: this.con_num, error: 'nickname_in_use', nick: command.params[1], reason: command.trailing});
+            },
+    ERR_NOTREGISTERED:      function (command) {
+                               /*command.server = this.con_num;
+                               command.command = 'ERR_NOTREGISTERED';
+                               this.client.sendIRCCommand(command);*/
+            }
+};
diff --git a/server/irc-connection.js b/server/irc-connection.js
new file mode 100755 (executable)
index 0000000..2f3b457
--- /dev/null
@@ -0,0 +1,138 @@
+var net     = require('net'),
+    tls     = require('tls'),
+    events  = require('events'),
+    util    = require('util');
+
+var IRCConnection = function (hostname, port, ssl, nick, user, pass, webirc) {
+    var that = this;
+    events.EventEmitter.call(this);
+    
+    if (ssl) {
+        this.socket = tls.connect(port, hostname, {}, connect_handler);
+    } else {
+        this.socket = net.createConnection(port, hostname);
+        this.socket.on('connect', function () {
+            connect_handler.apply(that, arguments);
+        });
+    }
+    
+    this.socket.on('error', function () {
+        var a = Array.prototype.slice.call(arguments);
+        a.unshift('error');
+        that.emit.apply(this, a);
+    });
+    
+    this.socket.setEncoding('utf-8');
+    
+    this.socket.on('data', function () {
+        parse.apply(that, arguments);
+    });
+    
+    this.socket.on('close', function () {
+        that.emit('close');
+    });
+    
+    this.connected = false;
+    this.registered = false;
+    this.nick = nick;
+    this.user = user;
+    this.ssl = !(!ssl);
+    this.options = Object.create(null);
+    
+    this.webirc = webirc;
+    this.password = pass;
+    this.hold_last = false;
+    this.held_data = '';
+};
+util.inherits(IRCConnection, events.EventEmitter);
+
+IRCConnection.prototype.write = function (data, callback) {
+    console.log('S<--', data);
+    write.call(this, data + '\r\n', 'utf-8', callback);
+};
+
+IRCConnection.prototype.end = function (data, callback) {
+    console.log('S<--', data);
+    console.log('Closing docket');
+    end.call(this, data + '\r\n', 'utf-8', callback);
+}
+
+var write = function (data, encoding, callback) {
+    this.socket.write(data, encoding, callback);
+};
+
+var end = function (data, encoding, callback) {
+    this.socket.end(data, encoding, callback);
+};
+
+module.exports.IRCConnection = IRCConnection;
+
+var connect_handler = function () {
+    if (this.webirc) {
+        this.write('WEBIRC ' + webirc.pass + ' KiwiIRC ' + this.user.hostname + ' ' + this.user.address);
+    }
+    if (this.password) {
+        this.write('PASS ' + password);
+    }
+    //this.write('CAP LS');
+    this.write('NICK ' + this.nick);
+    this.write('USER kiwi_' + this.nick.replace(/[^0-9a-zA-Z\-_.]/, '') + ' 0 0 :' + this.nick);
+    
+    this.connected = true;
+    console.log("IRCConnection.emit('connected')");
+    this.emit('connected');
+};
+
+//parse_regex = /^(?::(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
+alt_regex   = /(?::(([0-9a-z][\x2d0-9a-z]*[0-9a-z]*(?:\x2e[0-9a-z][\x2d0-9a-z]*[0-9a-z]*)*|[\x5b-\x7d][\x2d0-9\x5b-\x7d]{0,8})(?:(?:!([\x01-\t\v\f\x0e-\x1f!-\x3f\x5b-\xff]+))?@([0-9a-z][\x2d0-9a-z]*[0-9a-z]*(?:\x2e[0-9a-z][\x2d0-9a-z]*[0-9a-z]*)*|\d{1,3}\x2e\d{1,3}\x2e\d{1,3}\x2e\d{1,3}|[0-9a-f]+(?::[0-9a-f]+){7}|0:0:0:0:0:(?:0|ffff):\d{1,3}\x2e\d{1,3}\x2e\d{1,3}\x2e\d{1,3}))?)\x20)?([a-z]+|\d{3})((?:\x20[\x01-\t\v\f\x0e-\x1f!-9;-@\x5b-\xff][\x01-\t\v\f\x0e-\x1f!-@\x5b-\xff]*){0,14}(?:\x20:[\x01-\t\v\f\x0e-@\x5b-\xff]*)?|(?:\x20[\x01-\t\v\f\x0e-\x1f!-9;-@\x5b-\xff][\x01-\t\v\f\x0e-\x1f!-@\x5b-\xff]*){14}(?:\x20:?[\x01-\t\v\f\x0e-@\x5b-\xff]*)?)?/i;
+
+var parse = function (data) {
+    var i,
+        msg,
+               msg2,
+               trm;
+    
+    if ((this.hold_last) && (this.held_data !== '')) {
+        data = this.held_data + data;
+        this.hold_last = false;
+        this.held_data = '';
+    }
+    if (data.substr(-1) !== '\n') {
+        this.hold_last = true;
+    }
+    data = data.split("\n");
+    for (i = 0; i < data.length; i++) {
+        if (data[i]) {
+            if ((this.hold_last) && (i === data.length - 1)) {
+                this.held_data = data[i];
+                break;
+            }
+
+            // We have a complete line of data, parse it!
+            //msg = parse_regex.exec(data[i].replace(/^\r+|\r+$/, ''));
+                       msg2 = alt_regex.exec(data[i].replace(/^\r+|\r+$/, ''));
+                       //console.log(msg2);
+            if (msg2) {
+                msg = {
+                    prefix:     msg2[1],
+                    nick:       msg2[2],
+                    ident:      msg2[3],
+                    hostname:   msg2[4],
+                    command:    msg2[5]
+                };
+                               trm = msg2[6].indexOf(':');
+                               if (trm !== -1){
+                                       msg.params = msg2[6].substr(0, trm - 1).trim().split(" ");
+                                       msg.trailing = msg2[6].substr(trm + 1).trim();
+                               } else {
+                                       msg.params = msg2[6].trim().split(" ");
+                               }
+                console.log('S-->', data[i]);
+                               //console.log(msg);
+                this.emit('irc_' + msg.command.toUpperCase(), msg);
+            } else {
+                console.log("Malformed IRC line: " + data[i].replace(/^\r+|\r+$/, ''));
+            }
+        }
+    }
+};
old mode 100644 (file)
new mode 100755 (executable)
index 1afdb41..24c26d9
-/*jslint continue: true, forin: true, regexp: true, undef: false, node: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
-"use strict";
-var tls = require('tls'),
-    net = require('net'),
-    http = require('http'),
-    https = require('https'),
-    fs = require('fs'),
-    url = require('url'),
-    dns = require('dns'),
-    crypto = require('crypto'),
-    events = require("events"),
-    util = require('util'),
-    ws = require('socket.io'),
-    jsp = require("uglify-js").parser,
-    pro = require("uglify-js").uglify,
-    _ = require('./lib/underscore.min.js'),
-    starttls = require('./lib/starttls.js'),
-    app = require(__dirname + '/app.js');
+var fs          = require('fs'),
+    WebListener = require('./web.js').WebListener;
 
+//load config
 
-// Libraries may need to know kiwi.js path as __dirname
-// only gives that librarys path. Set it here for usage later.
-this.kiwi_root = __dirname;
-
-
-
-// How to handle log output
-this.log = function(str, level) {
-    level = level || 0;
-    console.log(str);
-}
-
-
-/*
- * Configuration and rehashing routines
- */
 var config_filename = 'config.json',
-    config_dirs = ['/etc/kiwiirc/', this.kiwi_root + '/'];
-
-this.config = {};
-this.loadConfig = function () {
-    var i, j,
-        nconf = {},
-        cconf = {},
-        found_config = false;
-    
-    for (i in config_dirs) {
-        try {
-            if (fs.lstatSync(config_dirs[i] + config_filename).isDirectory() === false) {
-                found_config = true;
-                nconf = JSON.parse(fs.readFileSync(config_dirs[i] + config_filename, 'ascii'));
-                for (j in nconf) {
-                    // If this has changed from the previous config, mark it as changed
-                    if (!_.isEqual(this.config[j], nconf[j])) {
-                        cconf[j] = nconf[j];
-                    }
-
-                    this.config[j] = nconf[j];
-                }
-
-                this.log('Loaded config file ' + config_dirs[i] + config_filename);
-                break;
-            }
-        } catch (e) {
-            switch (e.code) {
-            case 'ENOENT':      // No file/dir
-                break;
-            default:
-                this.log('An error occured parsing the config file ' + config_dirs[i] + config_filename + ': ' + e.message);
-                return false;
-            }
-            continue;
+    config_dirs = ['/etc/kiwiirc/', __dirname + '/'];
+
+var config = Object.create(null);
+for (var i in config_dirs) {
+    try {
+        if (fs.lstatSync(config_dirs[i] + config_filename).isDirectory() === false) {
+            config = JSON.parse(fs.readFileSync(config_dirs[i] + config_filename, 'utf-8'));
+            console.log('Loaded config file ' + config_dirs[i] + config_filename);
+            break;
         }
-    }
-    if (Object.keys(this.config).length === 0) {
-        if (!found_config) {
-            this.log('Couldn\'t find a config file!');
+    } catch (e) {
+        switch (e.code) {
+        case 'ENOENT':      // No file/dir
+            break;
+        default:
+            console.log('An error occured parsing the config file ' + config_dirs[i] + config_filename + ': ' + e.message);
+            return false;
         }
-        return false;
-    }
-    return [nconf, cconf];
-};
-
-
-// Reloads the config during runtime
-this.rehash = function () {
-    return app.rehash();
-}
-
-// Reloads app.js during runtime for any recoding
-this.recode = function () {
-    if (typeof require.cache[this.kiwi_root + '/app.js'] !== 'undefined'){
-        delete require.cache[this.kiwi_root + '/app.js'];
+        continue;
     }
-
-    app = null;
-    app = require(__dirname + '/app.js');
-
-    var objs = {tls:tls, net:net, http:http, https:https, fs:fs, url:url, dns:dns, crypto:crypto, events:events, util:util, ws:ws, jsp:jsp, pro:pro, _:_, starttls:starttls};
-    app.init(objs);
-    app.rebindIRCCommands();
-
-    return true;
 }
 
-
-
-
-
-
-/*
- * Before we continue we need the config loaded
- */
-if (!this.loadConfig()) {
-    process.exit(0);
+if (Object.keys(config).length === 0) {
+    console.log('Couldn\'t find a valid config file!');
+    process.exit(1);
 }
 
-
-
-
-
-
-
-/*
- * HTTP file serving
- */
-if (this.config.handle_http) {
-    this.fileServer = new (require('node-static').Server)(__dirname + this.config.public_http);
-    this.jade = require('jade');
-    this.cache = {alljs: '', html: []};
-}
-this.httpServers = [];
-this.httpHandler = function (request, response) {
-    return app.httpHandler(request, response);
-}
-
-
-
-
-
-
-/*
- * Websocket handling
- */
-this.connections = {};
-this.io = [];
-this.websocketListen = function (servers, handler) {
-    return app.websocketListen(servers, handler);
-}
-this.websocketConnection = function (websocket) {
-    return app.websocketConnection(websocket);
-}
-this.websocketDisconnect = function () {
-    return app.websocketDisconnect(this);
-}
-this.websocketMessage = function (msg, callback) {
-    return app.websocketMessage(this, msg, callback);
-}
-this.websocketIRCConnect = function (nick, host, port, ssl, callback) {
-    return app.websocketIRCConnect(this, nick, host, port, ssl, callback);
+if ((!config.servers) || (config.servers.length < 1)) {
+    console.log('No servers defined in config file');
+    process.exit(2);
 }
 
+//Create web listeners
 
+var clients = [];
+_.each(config.servers, function (server) {
+    var wl = new WebListener(server, config.transports);
+    wl.on('connection', function (client) {
+        clients.push(client);
+    });
+    wl.on('destroy', function (client) {
+        clients = _.reject(clients, function (c) {
+            return client === c;
+        });
+    });
+});
 
 
-/*
- * IRC handling
- */
-this.parseIRCMessage = function (websocket, ircSocket, data) {
-    return app.parseIRCMessage(websocket, ircSocket, data);
-}
-this.ircSocketDataHandler = function (data, websocket, ircSocket) {
-    return app.ircSocketDataHandler(data, websocket, ircSocket);
-}
-this.IRCConnection = function (websocket, nick, host, port, ssl, password, callback) {
-    return app.IRCConnection.call(this, websocket, nick, host, port, ssl, password, callback);
-}
-util.inherits(this.IRCConnection, events.EventEmitter);
+//Set process title
+process.title = 'Kiwi IRC';
 
-this.bindIRCCommands = function (irc_connection, websocket) {
-    return app.bindIRCCommands.call(this, irc_connection, websocket);
+//Change UID/GID
+if ((config.user) && (config.user !== '')) {
+    process.setuid(config.user);
 }
-this.rebindIRCCommands = function () {
-    return app.rebindIRCCommands.call(this);
-}
-
-
-
-
-
-
-/*
- * Load up main application source
- */
-if (!this.recode()) {
-    process.exit(0);
+if ((config.group) && (config.group !== '')) {
+    process.setgid(config.group);
 }
 
-
-
-// Set the process title
-app.setTitle();
-
-
-
-/*
- * Load the modules as set in the config and print them out
- */
-this.kiwi_mod = require('./lib/kiwi_mod.js');
-this.kiwi_mod.loadModules(this.kiwi_root, this.config);
-this.kiwi_mod.printMods();
-
-
-// Make sure Kiwi doesn't simply quit on an exception
-process.on('uncaughtException', function (e) {
-    console.log('[Uncaught exception] ' + e);
-});
-
-// Start the server up
-this.websocketListen(this.config.servers, this.httpHandler);
-
-// Now we're listening on the network, set our UID/GIDs if required
-app.changeUser();
-
-// Listen for controll messages
+//Listen to STDIN
 process.stdin.resume();
-process.stdin.on('data', function (data) { app.manageControll(data); });
-
-
-
-
+process.stdin.on('data', function (data) {
+    console.log(data.toString());
+});
diff --git a/server/kiwi_modules/forcessl.js b/server/kiwi_modules/forcessl.js
deleted file mode 100644 (file)
index d510299..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * forcessl Kiwi module
- * Force clients to use an SSL port by redirecting them
- */
-
-var kiwi = require('../kiwi.js');
-
-
-exports.onhttp = function (ev, opts) {
-       var host, port = null, i;
-
-       if (!ev.ssl) {
-           host = ev.request.headers.host;
-
-           // Remove the port if one is set
-           if (host.search(/:/) > -1) {
-               host = host.substring(0, host.search(/:/));
-           }
-
-       for (i in kiwi.config.servers) {
-               if (kiwi.config.servers[i].secure) {
-                       port = kiwi.config.servers[i].port;
-                       break;
-               }
-       }
-
-           // If we didn't find an SSL listener, don't redirect
-           if (port == null) {
-               return ev;
-           }
-
-           // No need to specify port 443 since it's the standard
-           if (port !== 443) {
-               host += ':' + port.toString();
-           }
-           
-           ev.response.writeHead(302, {'Location': 'https://' + host + ev.request.url});
-           ev.response.end();
-
-           return null;
-       }
-
-       return ev;
-}
\ No newline at end of file
diff --git a/server/kiwi_modules/spamfilter.js b/server/kiwi_modules/spamfilter.js
deleted file mode 100644 (file)
index 5f3ff76..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Example Kiwi module.
- * This is by no means is a production ready module.
- */
-
-var filters;
-var compiled_regex;
-
-exports.onload = function(){
-       filters = [];
-
-       if (filter.length > 0) {
-               compiled_regex = new RegExp(filters.join('|'), 'im');
-       }
-}
-
-
-exports.onmsg = function(msg){
-       if (typeof compiled_regex !== 'undefined' && msg.msg.search(compiled_regex) > -1) {
-               return null;
-       }
-
-       return msg;
-}
\ No newline at end of file
diff --git a/server/kiwi_modules/statistics.js b/server/kiwi_modules/statistics.js
deleted file mode 100644 (file)
index 09b4546..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Example Kiwi module.
- * This is by no means is a production ready module.
- */
-
-var kiwi = require('../kiwi.js');
-var stats = {msgs: 0, topic_changes: 0};
-
-exports.onmsgsend = function (msg, opts) {
-    stats.msgs++;
-
-    var connections_cnt = 0;
-    for (var i in kiwi.connections) {
-        connections_cnt = connections_cnt + parseInt(kiwi.connections[i].count, 10);
-    }
-
-    if (msg.msg === '!kiwistats') {
-        msg.msg = '';
-        msg.msg += 'Connections: ' + connections_cnt.toString() + '. ';
-        msg.msg += 'Messages sent: ' + stats.msgs.toString() + '. ';
-        msg.msg += 'Topics set: ' + stats.topic_changes.toString() + '. ';
-
-        opts.websocket.sendClientEvent('msg', {nick: msg.target, ident: '', hostname: '', channel: msg.target, msg: msg.msg});
-        return null;
-    }
-
-    return msg;
-}
-
-exports.ontopic = function (topic, opts) {
-    stats.topic_changes++;
-
-    return topic;
-}
diff --git a/server/lib/kiwi_mod.js b/server/lib/kiwi_mod.js
deleted file mode 100644 (file)
index bdac9c3..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-/*jslint node: true, sloppy: true, forin: true, maxerr: 50, indent: 4 */
-/*
- * Kiwi module handler
- *
- * To run module events:
- *     kiwi_mod.run(event_name, obj);
- *
- * - Each module call must return obj, with or without changes.
- * - If a module call returns null, the event is considered cancelled
- *   and null is passed back to the caller to take action.
- *   For example, if null is returned for onmsg, kiwi stops sending
- *   the message to any clients.
- */
-
-var kiwi = require('../kiwi.js');
-var fs = require('fs');
-this.loaded_modules = {};
-
-
-/*
- * Load any unloaded modules as set in config
- */
-exports.loadModules = function (kiwi_root, config) {
-    var i, mod_name;
-    // Warn each module it is about to be unloaded
-    //this.run('unload');
-    //this.loaded_modules = {};
-
-    // Load each module and run the onload event
-    for (i in kiwi.config.modules) {
-        mod_name = kiwi.config.modules[i];
-        if (typeof this.loaded_modules[mod_name] !== 'undefined') continue;
-
-        this.loaded_modules[mod_name] = require(kiwi.kiwi_root + '/' + kiwi.config.module_dir + mod_name);
-    }
-    this.run('load');
-};
-
-
-/*
- * Unload and reload a specific module
- */
-exports.reloadModule = function (mod_name) {
-    fs.realpath(kiwi.kiwi_root + '/' + kiwi.config.module_dir + mod_name + '.js', function(err, resolvedPath){
-        try {
-            var mod_path = resolvedPath;
-
-            if (typeof kiwi.kiwi_mod.loaded_modules[mod_name] !== 'undefined') {
-                delete kiwi.kiwi_mod.loaded_modules[mod_name];
-            }
-            if (typeof require.cache[mod_path] !== 'undefined') {
-                delete require.cache[mod_path];
-            }
-
-            kiwi.kiwi_mod.loaded_modules[mod_name] = null;
-            kiwi.kiwi_mod.loaded_modules[mod_name] = require(mod_path);
-
-            kiwi.log('Module ' + mod_name + ' reloaded.');
-        } catch (e) {
-            kiwi.log('reloadModule error!');
-            kiwi.log(e);
-            return false;
-        }
-    });
-
-    //return this.loaded_modules[mod_name] ? true : false;
-};
-
-
-/*
- * Run an event against all loaded modules
- */
-exports.run = function (event_name, event_data, opts) {
-    var ret = event_data,
-        ret_tmp, mod_name;
-    
-    event_data = (typeof event_data === 'undefined') ? {} : event_data;
-    opts = (typeof opts === 'undefined') ? {} : opts;
-    
-    for (mod_name in this.loaded_modules) {
-        if (typeof this.loaded_modules[mod_name]['on' + event_name] === 'function') {
-            try {
-                ret_tmp = this.loaded_modules[mod_name]['on' + event_name](ret, opts);
-                if (ret_tmp === null) {
-                    return null;
-                }
-                ret = ret_tmp;
-            } catch (e) {
-            }
-        }
-    }
-
-    return ret;
-};
-
-exports.printMods = function () {
-    var mod_name;
-    kiwi.log('Loaded Kiwi modules:');
-    for (mod_name in this.loaded_modules) {
-        kiwi.log(' - ' + mod_name);
-    }
-};
diff --git a/server/lib/starttls.js b/server/lib/starttls.js
deleted file mode 100644 (file)
index e7f2240..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-// Target API:
-//
-//  var s = require('net').createStream(25, 'smtp.example.com');
-//  s.on('connect', function() {
-//   require('starttls')(s, options, function() {
-//      if (!s.authorized) {
-//        s.destroy();
-//        return;
-//      }
-//
-//      s.end("hello world\n");
-//    });
-//  });
-//
-//
-module.exports = function starttls(socket, options, cb) {
-
-  var sslcontext = require('crypto').createCredentials(options);
-
-  var pair = require('tls').createSecurePair(sslcontext, false);
-
-  var cleartext = pipe(pair, socket);
-
-  pair.on('secure', function() {
-    var verifyError = pair.ssl.verifyError();
-
-    if (verifyError) {
-      cleartext.authorized = false;
-      cleartext.authorizationError = verifyError;
-    } else {
-      cleartext.authorized = true;
-    }
-
-    if (cb) cb();
-  });
-
-  cleartext._controlReleased = true;
-  return cleartext;
-};
-
-
-function pipe(pair, socket) {
-  pair.encrypted.pipe(socket);
-  socket.pipe(pair.encrypted);
-
-  pair.fd = socket.fd;
-  var cleartext = pair.cleartext;
-  cleartext.socket = socket;
-  cleartext.encrypted = pair.encrypted;
-  cleartext.authorized = false;
-
-  function onerror(e) {
-    if (cleartext._controlReleased) {
-      cleartext.emit('error', e);
-    }
-  }
-
-  function onclose() {
-    socket.removeListener('error', onerror);
-    socket.removeListener('close', onclose);
-  }
-
-  socket.on('error', onerror);
-  socket.on('close', onclose);
-
-  return cleartext;
-}
-
diff --git a/server/lib/underscore.min.js b/server/lib/underscore.min.js
deleted file mode 100644 (file)
index ad19500..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-// Underscore.js 1.2.2
-// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
-// Underscore is freely distributable under the MIT license.
-// Portions of Underscore are inspired or borrowed from Prototype,
-// Oliver Steele's Functional, and John Resig's Micro-Templating.
-// For all details and documentation:
-// http://documentcloud.github.com/underscore
-(function(){function r(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(b.isFunction(a.isEqual))return a.isEqual(c);if(b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return false;switch(e){case "[object String]":return String(a)==String(c);case "[object Number]":return a=+a,c=+c,a!=a?c!=c:a==0?1/a==1/c:a==c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
-c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if(typeof a!="object"||typeof c!="object")return false;for(var f=d.length;f--;)if(d[f]==a)return true;d.push(a);var f=0,g=true;if(e=="[object Array]"){if(f=a.length,g=f==c.length)for(;f--;)if(!(g=f in a==f in c&&r(a[f],c[f],d)))break}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return false;for(var h in a)if(m.call(a,h)&&(f++,!(g=m.call(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(m.call(c,
-h)&&!f--)break;g=!f}}d.pop();return g}var s=this,F=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,G=k.unshift,l=p.toString,m=p.hasOwnProperty,v=k.forEach,w=k.map,x=k.reduce,y=k.reduceRight,z=k.filter,A=k.every,B=k.some,q=k.indexOf,C=k.lastIndexOf,p=Array.isArray,H=Object.keys,t=Function.prototype.bind,b=function(a){return new n(a)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports)exports=module.exports=b;exports._=b}else typeof define==="function"&&define.amd?
-define("underscore",function(){return b}):s._=b;b.VERSION="1.2.2";var j=b.each=b.forEach=function(a,c,b){if(a!=null)if(v&&a.forEach===v)a.forEach(c,b);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(b,a[e],e,a)===o)break}else for(e in a)if(m.call(a,e)&&c.call(b,a[e],e,a)===o)break};b.map=function(a,c,b){var e=[];if(a==null)return e;if(w&&a.map===w)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=
-d!==void 0;a==null&&(a=[]);if(x&&a.reduce===x)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){a==null&&(a=[]);if(y&&a.reduceRight===y)return e&&(c=b.bind(c,e)),d!==void 0?a.reduceRight(c,d):a.reduceRight(c);a=(b.isArray(a)?a.slice():b.toArray(a)).reverse();return b.reduce(a,c,d,e)};b.find=b.detect=function(a,c,b){var e;
-D(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.filter===z)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(A&&a.every===A)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,a,g,h)))return o});
-return e};var D=b.some=b.any=function(a,c,d){var c=c||b.identity,e=false;if(a==null)return e;if(B&&a.some===B)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return q&&a.indexOf===q?a.indexOf(c)!=-1:b=D(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(c.call?c||a:a[c]).apply(a,d)})};b.pluck=function(a,c){return b.map(a,function(a){return a[c]})};
-b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&(e={value:a,computed:b})});return e.value};b.shuffle=function(a){var c=[],b;
-j(a,function(a,f){f==0?c[0]=a:(b=Math.floor(Math.random()*(f+1)),c[f]=c[b],c[b]=a)});return c};b.sortBy=function(a,c,d){return b.pluck(b.map(a,function(a,b,g){return{value:a,criteria:c.call(d,a,b,g)}}).sort(function(a,c){var b=a.criteria,d=c.criteria;return b<d?-1:b>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,c){var b=e(a,c);(d[b]||(d[b]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<
-f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:a.toArray?a.toArray():b.isArray(a)?i.call(a):b.isArguments(a)?i.call(a):b.values(a)};b.size=function(a){return b.toArray(a).length};b.first=b.head=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=b.tail=function(a,b,d){return i.call(a,b==null||
-d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,e=[];b.reduce(d,function(d,g,h){if(0==h||(c===true?b.last(d)!=g:!b.include(d,g)))d[d.length]=g,e[e.length]=a[h];return d},[]);return e};b.union=function(){return b.uniq(b.flatten(arguments,
-true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a,c){return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d)return d=b.sortedIndex(a,c),a[d]===c?d:-1;if(q&&a.indexOf===q)return a.indexOf(c);
-for(d=0,e=a.length;d<e;d++)if(a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(C&&a.lastIndexOf===C)return a.lastIndexOf(b);for(var d=a.length;d--;)if(a[d]===b)return d;return-1};b.range=function(a,b,d){arguments.length<=1&&(b=a||0,a=0);for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;)g[f++]=a,a+=d;return g};var E=function(){};b.bind=function(a,c){var d,e;if(a.bind===t&&t)return t.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;
-e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));E.prototype=a.prototype;var b=new E,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var b=c.apply(this,arguments);return m.call(d,b)?d[b]:d[b]=a.apply(this,arguments)}};b.delay=
-function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(a,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i=b.debounce(function(){h=g=false},c);return function(){d=this;e=arguments;var b;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);i()},c));g?h=true:a.apply(d,e);i();g=true}};b.debounce=function(a,b){var d;return function(){var e=this,f=arguments;clearTimeout(d);d=setTimeout(function(){d=
-null;a.apply(e,f)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments));return b.apply(this,d)}};b.compose=function(){var a=i.call(arguments);return function(){for(var b=i.call(arguments),d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=H||function(a){if(a!==
-Object(a))throw new TypeError("Invalid object");var b=[],d;for(d in a)m.call(a,d)&&(b[b.length]=d);return b};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)b[d]!==void 0&&(a[d]=b[d])});return a};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?
-a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(m.call(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=l.call(arguments)=="[object Arguments]"?function(a){return l.call(a)=="[object Arguments]"}:
-function(a){return!(!a||!m.call(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};
-b.isUndefined=function(a){return a===void 0};b.noConflict=function(){s._=F;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")};b.mixin=function(a){j(b.functions(a),function(c){I(c,b[c]=a[c])})};var J=0;b.uniqueId=function(a){var b=J++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,
-interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape,function(a,b){return"',_.escape("+b.replace(/\\'/g,"'")+"),'"}).replace(d.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(d.evaluate||null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,
-"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e(a,b)}};var n=function(a){this._wrapped=a};b.prototype=n.prototype;var u=function(a,c){return c?b(a).chain():a},I=function(a,c){n.prototype[a]=function(){var a=i.call(arguments);G.call(a,this._wrapped);return u(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];n.prototype[a]=function(){b.apply(this._wrapped,
-arguments);return u(this._wrapped,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];n.prototype[a]=function(){return u(b.apply(this._wrapped,arguments),this._chain)}});n.prototype.chain=function(){this._chain=true;return this};n.prototype.value=function(){return this._wrapped}}).call(this);
-
diff --git a/server/web.js b/server/web.js
new file mode 100755 (executable)
index 0000000..6751e00
--- /dev/null
@@ -0,0 +1,72 @@
+var ws          = require('socket.io'),
+    events      = require('events'),
+    http        = require('http'),
+    https       = require('https'),
+    util        = require('util'),
+    fs          = require('fs'),
+    dns         = require('dns'),
+    _           = require('underscore'),
+    Client   = require('./client.js').Client;
+    HTTPHandler = require('./http-handler.js').HTTPHandler;
+
+var WebListener = function (config, transports) {
+    var handler,
+        hs,
+        opts,
+        that = this;
+
+    events.EventEmitter.call(this);
+    
+    http_handler = new HTTPHandler(config);
+    
+    if (config.secure) {
+        opts = {
+            key: fs.readFileSync(__dirname + '/' + config.ssl_key),
+            cert: fs.readFileSync(__dirname + '/' + config.ssl_cert)
+        };
+        // Do we have an intermediate certificate?
+        if (typeof config.ssl_ca !== 'undefined') {
+            opts.ca = fs.readFileSync(__dirname + '/' + config.ssl_ca);
+        }
+        hs = https.createServer(opts, function (request, response) {
+            http_handler.handler(request, response);
+        });
+        
+        this.ws = ws.listen(hs, {secure: true});
+        hs.listen(config.port, config.address);
+        console.log('Listening on ' + config.address + ':' + config.port.toString() + ' with SSL');
+    } else {
+        // Start some plain-text server up
+        hs = http.createServer(function (request, response) {
+            http_handler.handler(request, response);
+        });
+        this.ws = ws.listen(hs, {secure: false});
+        hs.listen(config.port, config.address);
+        console.log('Listening on ' + config.address + ':' + config.port.toString() + ' without SSL');
+    }
+    
+    this.ws.set('log level', 1);
+    this.ws.enable('browser client minification');
+    this.ws.enable('browser client etag');
+    this.ws.set('transports', transports);
+
+    this.ws.of('/kiwi').authorization(authorisation).on('connection', function () {
+        connection.apply(that, arguments);
+    });
+    this.ws.of('/kiwi').on('error', console.log);
+};
+util.inherits(WebListener, events.EventEmitter);
+
+module.exports.WebListener = WebListener;
+
+var authorisation = function (handshakeData, callback) {
+    dns.reverse(handshakeData.address.address, function (err, domains) {
+        handshakeData.revdns = (err) ? handshakeData.address.address : _.first(domains);
+        callback(null, true);
+    });
+};
+
+var connection = function (websocket) {
+    //console.log(websocket);
+    this.emit('connection', new Client(websocket));
+};