*.DS_*
node/node_modules/
node_modules/
+doc/
\ No newline at end of file
- 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")
--- /dev/null
+// 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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")},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);
+
--- /dev/null
+// 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
/**\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
.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
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
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
* @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
* @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
*/\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
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
* @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
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
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
*/\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
*/\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
*/\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
* @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
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
* @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
* @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
* @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
* @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
* @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
* @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
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
* @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
* @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
* @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
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
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));
}});
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);
}
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") {
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);
+ }
}
});
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();
}
}
},
* @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};
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;
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));
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;
case '/q':
case '/query':
if (typeof parts[1] !== "undefined") {
- tab = new Tabview(parts[1]);
+ //tab = new Tabview(parts[1]);
}
break;
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;
}
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':
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(' '));
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':
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;
}
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);
+ });
}
}
},
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;
}
$.each(listitems, function(idx, itm) {
win_list.append(itm);
- });
+ });*/
},
};
};
-
-/**
-* @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
*/
$('#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
* 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
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
return;\r
}\r
prev_tab = tabs[tab];\r
- }\r
+ }*/\r
},\r
\r
/**\r
* @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
return;\r
}\r
i++;\r
- }\r
+ }*/\r
},\r
\r
\r
kiwi.gateway.socket.on('reconnect_failed', function () {
console.log("kiwi.gateway.socket.on('reconnect_failed')");
});
+ kiwi.gateway.socket.on('error', console.log);
}
},
*/
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) {
$(kiwi.gateway).trigger('kiwi.' + item.namespace, item.data);
break;
}
+
+ $(kiwi.gateway).trigger('on' + item.event, item);
}
},
--- /dev/null
+/*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
* @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 () {
log(str);
}
};
- }
+ }*/
}
/**
{
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;
}
{
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;
}
}
return false;
- }
- },
+ }
+ },
*/
{
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;
}
},
- {
- 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);
+ });
+ }
+ }
];
--- /dev/null
+/*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
-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
--- /dev/null
+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
--- /dev/null
+{\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
+++ /dev/null
-/*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] + '\'');
- }
-};
--- /dev/null
+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');
+};
"servers": [
{
"secure": true,
+ "hsts": true,
"port": 7777,
"address": "0.0.0.0",
--- /dev/null
+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;
+};
--- /dev/null
+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);*/
+ }
+};
--- /dev/null
+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+$/, ''));
+ }
+ }
+ }
+};
-/*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());
+});
+++ /dev/null
-/*
- * 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
+++ /dev/null
-/*
- * 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
+++ /dev/null
-/*
- * 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;
-}
+++ /dev/null
-/*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);
- }
-};
+++ /dev/null
-// 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;
-}
-
+++ /dev/null
-// 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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};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);
-
--- /dev/null
+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));
+};