├── .gitignore ├── LICENSE.txt ├── README.md ├── client ├── assets │ ├── css │ │ ├── custom.css │ │ ├── font-awesome.css │ │ ├── font-awesome.min.css │ │ └── style.css │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ ├── img │ │ ├── emoticons.png │ │ ├── favicon.ico │ │ ├── ico.png │ │ ├── loader.gif │ │ └── resize_handle.png │ ├── libs │ │ ├── ace │ │ │ ├── ace.js │ │ │ ├── ext-elastic_tabstops_lite.js │ │ │ ├── ext-emmet.js │ │ │ ├── ext-searchbox.js │ │ │ ├── ext-spellcheck.js │ │ │ ├── ext-static_highlight.js │ │ │ ├── ext-textarea.js │ │ │ ├── ext-whitespace.js │ │ │ ├── keybinding-emacs.js │ │ │ ├── keybinding-vim.js │ │ │ ├── mode-css.js │ │ │ ├── mode-html.js │ │ │ ├── mode-javascript.js │ │ │ ├── mode-json.js │ │ │ ├── theme-ambiance.js │ │ │ ├── theme-chaos.js │ │ │ ├── theme-chrome.js │ │ │ ├── theme-clouds.js │ │ │ ├── theme-clouds_midnight.js │ │ │ ├── theme-cobalt.js │ │ │ ├── theme-crimson_editor.js │ │ │ ├── theme-dawn.js │ │ │ ├── theme-dreamweaver.js │ │ │ ├── theme-eclipse.js │ │ │ ├── theme-github.js │ │ │ ├── theme-idle_fingers.js │ │ │ ├── theme-kr.js │ │ │ ├── theme-merbivore.js │ │ │ ├── theme-merbivore_soft.js │ │ │ ├── theme-mono_industrial.js │ │ │ ├── theme-monokai.js │ │ │ ├── theme-pastel_on_dark.js │ │ │ ├── theme-solarized_dark.js │ │ │ ├── theme-solarized_light.js │ │ │ ├── theme-terminal.js │ │ │ ├── theme-textmate.js │ │ │ ├── theme-tomorrow.js │ │ │ ├── theme-tomorrow_night.js │ │ │ ├── theme-tomorrow_night_blue.js │ │ │ ├── theme-tomorrow_night_bright.js │ │ │ ├── theme-tomorrow_night_eighties.js │ │ │ ├── theme-twilight.js │ │ │ ├── theme-vibrant_ink.js │ │ │ ├── theme-xcode.js │ │ │ ├── worker-css.js │ │ │ ├── worker-javascript.js │ │ │ └── worker-json.js │ │ ├── backbone.min.js │ │ ├── engine.io.js │ │ ├── engine.io.tools.js │ │ ├── jed.js │ │ ├── jquery-1.11.1.min.js │ │ ├── lodash.min.js │ │ ├── promise.min.js │ │ └── soundmanager2 │ │ │ ├── soundmanager2-jsmin.js │ │ │ ├── soundmanager2-nodebug-jsmin.js │ │ │ ├── soundmanager2-nodebug.js │ │ │ ├── soundmanager2.js │ │ │ ├── soundmanager2.swf │ │ │ ├── soundmanager2_debug.swf │ │ │ ├── soundmanager2_flash9.swf │ │ │ ├── soundmanager2_flash9_debug.swf │ │ │ └── soundmanager2_flash_xdomain.zip │ ├── plugins │ │ ├── emoticonlist.html │ │ ├── filepicker.html │ │ ├── plugin_example.html │ │ └── textstyle.html │ ├── sound │ │ └── highlight.mp3 │ ├── text_themes │ │ └── default.json │ └── themes │ │ ├── basic │ │ ├── background-light.png │ │ ├── style.css │ │ └── theme.json │ │ ├── cli │ │ ├── style.css │ │ └── theme.json │ │ ├── mini │ │ ├── style.css │ │ └── theme.json │ │ └── relaxed │ │ ├── background-light.png │ │ ├── style.css │ │ └── theme.json └── src │ ├── app.js │ ├── applets │ ├── chanlist.js │ ├── scripteditor.js │ ├── settings.js │ └── startup.js │ ├── helpers │ ├── desktopnotifications.js │ ├── formatdate.js │ ├── plugininterface.js │ └── utils.js │ ├── index.html.tmpl │ ├── misc │ └── clientuicommands.js │ ├── models │ ├── applet.js │ ├── application.js │ ├── channel.js │ ├── channelinfo.js │ ├── datastore.js │ ├── gateway.js │ ├── ignorelist.js │ ├── member.js │ ├── memberlist.js │ ├── network.js │ ├── networkpanellist.js │ ├── newconnection.js │ ├── panel.js │ ├── panellist.js │ ├── pluginmanager.js │ ├── query.js │ └── server.js │ ├── translations │ ├── bs.po │ ├── ca.po │ ├── cs.po │ ├── de-de.po │ ├── el.po │ ├── en-gb.po │ ├── es-419.po │ ├── es.po │ ├── fr.po │ ├── gl.po │ ├── he.po │ ├── it.po │ ├── ko-kr.po │ ├── mk.po │ ├── nl.po │ ├── no.po │ ├── pl.po │ ├── pt-br.po │ ├── ro.po │ ├── ru.po │ ├── sq.po │ ├── sr.po │ ├── template.po │ ├── tr.po │ ├── translations.json │ ├── uk.po │ ├── vi.po │ └── zh-tw.po │ └── views │ ├── applet.js │ ├── application.js │ ├── apptoolbar.js │ ├── autocomplete.js │ ├── channel.js │ ├── channelinfo.js │ ├── channeltools.js │ ├── controlbox.js │ ├── favicon.js │ ├── mediamessage.js │ ├── member.js │ ├── memberlist.js │ ├── menubox.js │ ├── networktabs.js │ ├── nickchangebox.js │ ├── notification.js │ ├── panel.js │ ├── resizehandler.js │ ├── rightbar.js │ ├── serverselect.js │ ├── statusmessage.js │ ├── tabs.js │ ├── topicbar.js │ └── userbox.js ├── config.example.js ├── kiwi ├── kiwi.bat ├── man └── kiwiirc.1 ├── package.json ├── server ├── .jshintrc ├── client.js ├── clientcommands.js ├── configuration.js ├── controlinterface.js ├── ee.js ├── helpers │ ├── build.js │ ├── configloader.js │ └── launcher.js ├── httphandler.js ├── identd.js ├── irc │ ├── channel.js │ ├── commands.js │ ├── commands │ │ ├── channel.js │ │ ├── messaging.js │ │ ├── misc.js │ │ ├── registration.js │ │ └── user.js │ ├── connection.js │ ├── eventbinder.js │ ├── server.js │ ├── state.js │ └── user.js ├── kiwi.js ├── modules.js ├── plugininterface.js ├── proxy.js ├── rehash.js ├── settingsgenerator.js ├── stats.js ├── weblistener.js └── websocketrpc.js ├── server_modules ├── client_file_watcher.js ├── control.js ├── dnsbl.js ├── example.js ├── force_https.js ├── proxychecker.js ├── stats.js └── web_agent_debugger.js └── translations.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_* 2 | node/node_modules/ 3 | node_modules/ 4 | doc/ 5 | client/assets/libs/engine.io.bundle.* 6 | client/assets/kiwi.js 7 | client/assets/kiwi.min.js 8 | client/assets/locales/*.json 9 | client/index.html 10 | kiwi.log 11 | kiwiirc.pid 12 | config.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Note - This version is no longer in active development! 2 | # The new, better version is here: https://github.com/kiwiirc/kiwiirc 3 | 4 | ### This version of Kiwi IRC is only receiving bug and security updates 5 | 6 | This is a complete re-write and is now using much more modern development tools and includes over 5 years worth of knowledge on how people use web IRC clients. If you are looking for the latest and greatest IRC features then this new version is the way to go! 7 | 8 | ### Kiwi IRC - A hand-crafted IRC client 9 | Kiwi IRC is a fully featured IRC client that can be extended to suit almost any needs. 10 | Using the web application is extremly simple even without any IRC knowledge as all the common needs are built directly into the UI. 11 | 12 | For more information see https://kiwiirc.com or live instance of the application can be found at https://kiwiirc.com/client/. 13 | Our development IRC channel is on the Freenode network, irc.freenode.net #kiwiirc. 14 | 15 | **Developing? Please use the development branch - not the master branch!** 16 | 17 | [![Visit our IRC channel](https://kiwiirc.com/buttons/irc.freenode.net/kiwiirc.png)](https://kiwiirc.com/client/irc.freenode.net/#kiwiirc) 18 | 19 | 20 | ### Installation 21 | 22 | *Note: This requires Node.js to run. Make sure you have installed Node.js first! http://nodejs.org/download/* 23 | 24 | 1. Download the Kiwi source or clone the git repository: 25 | 26 | `$ git clone https://github.com/prawnsalad/KiwiIRC.git && cd KiwiIRC` 27 | 28 | 2. Install the dependencies: 29 | 30 | `$ npm install` 31 | 32 | 3. Copy and edit the configuration file as needed: 33 | 34 | `$ cp config.example.js config.js` 35 | 36 | `$ nano config.js` 37 | 38 | 4. Make sure the client code is built: 39 | 40 | `$ ./kiwi build` 41 | 42 | 43 | ### Running 44 | From the source folder: `$ ./kiwi start` 45 | 46 | You can also run kiwi in the foreground to see any output by using the `-f` flag. Eg: `$ ./kiwi -f` 47 | 48 | Open your new Kiwi instance in your browser. By default: http://localhost:7778/ 49 | 50 | 51 | ### Bugs 52 | Report bugs using the issue tracker on github: https://github.com/prawnsalad/KiwiIRC/issues 53 | 54 | ### Translations 55 | Kiwi IRC has been translated to 25 different languages. The translators can be found in translations.md 56 | 57 | ### Licence 58 | GNU Affero 59 | http://www.gnu.org/licenses/agpl.html 60 | -------------------------------------------------------------------------------- /client/assets/css/custom.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/css/custom.css -------------------------------------------------------------------------------- /client/assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /client/assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /client/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /client/assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /client/assets/img/emoticons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/img/emoticons.png -------------------------------------------------------------------------------- /client/assets/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/img/favicon.ico -------------------------------------------------------------------------------- /client/assets/img/ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/img/ico.png -------------------------------------------------------------------------------- /client/assets/img/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/img/loader.gif -------------------------------------------------------------------------------- /client/assets/img/resize_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/img/resize_handle.png -------------------------------------------------------------------------------- /client/assets/libs/ace/ext-elastic_tabstops_lite.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/ext/elastic_tabstops_lite",["require","exports","module","ace/editor","ace/config"],function(e,t,n){var r=function(e){this.$editor=e;var t=this,n=[],r=!1;this.onAfterExec=function(){r=!1,t.processRows(n),n=[]},this.onExec=function(){r=!0},this.onChange=function(e){var t=e.data.range;r&&(n.indexOf(t.start.row)==-1&&n.push(t.start.row),t.end.row!=t.start.row&&n.push(t.end.row))}};(function(){this.processRows=function(e){this.$inChange=!0;var t=[];for(var n=0,r=e.length;n-1)continue;var s=this.$findCellWidthsForBlock(i),o=this.$setBlockCellWidthsToMax(s.cellWidths),u=s.firstRow;for(var a=0,f=o.length;a=0){n=this.$cellWidthsForRow(r);if(n.length==0)break;t.unshift(n),r--}var i=r+1;r=e;var s=this.$editor.session.getLength();while(r0&&(this.$editor.session.getDocument().insertInLine({row:e,column:f+1},Array(l+1).join(" ")+" "),this.$editor.session.getDocument().removeInLine(e,f,f+1),r+=l),l<0&&p>=-l&&(this.$editor.session.getDocument().removeInLine(e,f+l,f),r+=l)}},this.$izip_longest=function(e){if(!e[0])return[];var t=e[0].length,n=e.length;for(var r=1;rt&&(t=i)}var s=[];for(var o=0;o=t.length?t.length:e.length,r=[];for(var i=0;i1?e=n[0]:e=="php"&&(e="html"))}return e},getProfileName:function(){switch(this.getSyntax()){case"css":return css;case"xml":case"xsl":return"xml";case"html":var e=s.require("resources").getVariable("profile");return e||(e=this.ace.session.getLines(0,2).join("").search(/]+XHTML/i)!=-1?"xhtml":"html"),e}return"xhtml"},prompt:function(e){return prompt(e)},getSelection:function(){return this.ace.session.getTextRange()},getFilePath:function(){return""}};var u={expand_abbreviation:{mac:"ctrl+alt+e",win:"alt+e"},match_pair_outward:{mac:"ctrl+d",win:"ctrl+,"},match_pair_inward:{mac:"ctrl+j",win:"ctrl+shift+0"},matching_pair:{mac:"ctrl+alt+j",win:"alt+j"},next_edit_point:"alt+right",prev_edit_point:"alt+left",toggle_comment:{mac:"command+shift+/",win:"ctrl+shift+/"},split_join_tag:{mac:"shift+command+'",win:"shift+ctrl+`"},remove_tag:{mac:"command+'",win:"shift+ctrl+;"},evaluate_math_expression:{mac:"shift+command+y",win:"shift+ctrl+y"},increment_number_by_1:"ctrl+up",decrement_number_by_1:"ctrl+down",increment_number_by_01:"alt+up",decrement_number_by_01:"alt+down",increment_number_by_10:{mac:"alt+command+up",win:"shift+alt+up"},decrement_number_by_10:{mac:"alt+command+down",win:"shift+alt+down"},select_next_item:{mac:"shift+command+.",win:"shift+ctrl+."},select_previous_item:{mac:"shift+command+,",win:"shift+ctrl+,"},reflect_css_value:{mac:"shift+command+r",win:"shift+ctrl+r"},encode_decode_data_url:{mac:"shift+ctrl+d",win:"ctrl+'"},expand_abbreviation_with_tab:"Tab"},a=new o;t.commands=new r;for(var l in u)t.commands.addCommand({name:l,bindKey:u[l],exec:f});var c=function(e,n){var r=n;if(!r)return;var i=r.session.$modeId,s=i&&/css|less|sass|html|php/.test(i);e.enableEmmet===!1&&(s=!1),s?r.keyBinding.addKeyboardHandler(t.commands):r.keyBinding.removeKeyboardHandler(t.commands)};t.AceEmmetEditor=o,e("ace/config").defineOptions(i.prototype,"editor",{enableEmmet:{set:function(e){this[e?"on":"removeListener"]("changeMode",c),c({enableEmmet:!!e},this)},value:!0}}),t.setCore=function(e){s=e}}) -------------------------------------------------------------------------------- /client/assets/libs/ace/ext-spellcheck.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/ext/spellcheck",["require","exports","module","ace/lib/event","ace/editor","ace/config"],function(e,t,n){var r=e("../lib/event");t.contextMenuHandler=function(e){var t=e.target,n=t.textInput.getElement();if(!t.selection.isEmpty())return;var i=t.getCursorPosition(),s=t.session.getWordRange(i.row,i.column),o=t.session.getTextRange(s);t.session.tokenRe.lastIndex=0;if(!t.session.tokenRe.test(o))return;var u="",a=o+" "+u;n.value=a,n.setSelectionRange(o.length+1,o.length+1),n.setSelectionRange(0,0);var f=!1;r.addListener(n,"keydown",function l(){r.removeListener(n,"keydown",l),f=!0}),t.textInput.setInputHandler(function(e){console.log(e,a,n.selectionStart,n.selectionEnd);if(e==a)return"";if(e.lastIndexOf(a,0)===0)return e.slice(a.length);if(e.substr(n.selectionEnd)==a)return e.slice(0,-a.length);if(e.slice(-2)==u){var r=e.slice(0,-2);if(r.slice(-1)==" ")return f?r.substring(0,n.selectionEnd):(r=r.slice(0,-1),t.session.replace(s,r),"")}return e})};var i=e("../editor").Editor;e("../config").defineOptions(i.prototype,"editor",{spellcheck:{set:function(e){var n=this.textInput.getElement();n.spellcheck=!!e,e?this.on("nativecontextmenu",t.contextMenuHandler):this.removeListener("nativecontextmenu",t.contextMenuHandler)},value:!0}})}) -------------------------------------------------------------------------------- /client/assets/libs/ace/ext-static_highlight.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/ext/static_highlight",["require","exports","module","ace/edit_session","ace/layer/text"],function(e,t,n){var r=e("../edit_session").EditSession,i=e("../layer/text").Text,s=".ace_editor {font-family: 'Monaco', 'Menlo', 'Droid Sans Mono', 'Courier New', monospace;font-size: 12px;}.ace_editor .ace_gutter { width: 25px !important;display: block;float: left;text-align: right; padding: 0 3px 0 0; margin-right: 3px;}.ace_line { clear: both; }*.ace_gutter-cell {-moz-user-select: -moz-none;-khtml-user-select: none;-webkit-user-select: none;user-select: none;}";t.render=function(e,t,n,o,u){o=parseInt(o||1,10);var a=new r("");a.setMode(t),a.setUseWorker(!1);var f=new i(document.createElement("div"));f.setSession(a),f.config={characterWidth:10,lineHeight:20},a.setValue(e);var l=[],c=a.getLength();for(var h=0;h"),u||l.push(""+(h+o)+""),f.$renderLine(l,h,!0,!1),l.push("");var p="
:code
".replace(/:cssClass/,n.cssClass).replace(/:code/,l.join(""));return f.destroy(),{css:s+n.cssText,html:p}}}) -------------------------------------------------------------------------------- /client/assets/libs/ace/ext-whitespace.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/ext/whitespace",["require","exports","module","ace/lib/lang"],function(e,t,n){var r=e("../lib/lang");t.$detectIndentation=function(e,t){function h(e){var t=0;for(var r=e;r0&&!(s%c)&&!(l%c)&&(r[c]=(r[c]||0)+1),n[l]=(n[l]||0)+1}s=l;while(a[a.length-1]=="\\")a=e[u++]}var p=r.reduce(function(e,t){return e+t},0),d={score:0,length:0},v=0;for(var u=1;u<12;u++){if(u==1){v=h(u);var m=1}else var m=h(u)/v;r[u]&&(m+=r[u]/p),m>d.score&&(d={score:m,length:u})}if(d.score&&d.score>1.4)var g=d.length;if(i>v+1)return{ch:" ",length:g};if(v+1>i)return{ch:" ",length:g}},t.detectIndentation=function(e){var n=e.getLines(0,1e3),r=t.$detectIndentation(n)||{};return r.ch&&e.setUseSoftTabs(r.ch==" "),r.length&&e.setTabSize(r.length),r},t.trimTrailingSpace=function(e){var t=e.getDocument(),n=t.getAllLines();for(var r=0,i=n.length;r span {font-weight: normal !important;}.ace-github .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-github .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-github .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-github .ace_gutter-active-line {background-color : rgba(0, 0, 0, 0.07);}.ace-github .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-github .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-github .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}';var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-idle_fingers.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/idle_fingers",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-idle-fingers",t.cssText=".ace-idle-fingers .ace_gutter {background: #3b3b3b;color: #fff}.ace-idle-fingers .ace_print-margin {width: 1px;background: #3b3b3b}.ace-idle-fingers .ace_scroller {background-color: #323232}.ace-idle-fingers .ace_text-layer {color: #FFFFFF}.ace-idle-fingers .ace_cursor {border-left: 2px solid #91FF00}.ace-idle-fingers .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #91FF00}.ace-idle-fingers .ace_marker-layer .ace_selection {background: rgba(90, 100, 126, 0.88)}.ace-idle-fingers.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #323232;border-radius: 2px}.ace-idle-fingers .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-idle-fingers .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #404040}.ace-idle-fingers .ace_marker-layer .ace_active-line {background: #353637}.ace-idle-fingers .ace_gutter-active-line {background-color: #353637}.ace-idle-fingers .ace_marker-layer .ace_selected-word {border: 1px solid rgba(90, 100, 126, 0.88)}.ace-idle-fingers .ace_invisible {color: #404040}.ace-idle-fingers .ace_keyword,.ace-idle-fingers .ace_meta {color: #CC7833}.ace-idle-fingers .ace_constant,.ace-idle-fingers .ace_constant.ace_character,.ace-idle-fingers .ace_constant.ace_character.ace_escape,.ace-idle-fingers .ace_constant.ace_other,.ace-idle-fingers .ace_support.ace_constant {color: #6C99BB}.ace-idle-fingers .ace_invalid {color: #FFFFFF;background-color: #FF0000}.ace-idle-fingers .ace_fold {background-color: #CC7833;border-color: #FFFFFF}.ace-idle-fingers .ace_support.ace_function {color: #B83426}.ace-idle-fingers .ace_variable.ace_parameter {font-style: italic}.ace-idle-fingers .ace_string {color: #A5C261}.ace-idle-fingers .ace_string.ace_regexp {color: #CCCC33}.ace-idle-fingers .ace_comment {font-style: italic;color: #BC9458}.ace-idle-fingers .ace_meta.ace_tag {color: #FFE5BB}.ace-idle-fingers .ace_entity.ace_name {color: #FFC66D}.ace-idle-fingers .ace_markup.ace_underline {text-decoration: underline}.ace-idle-fingers .ace_collab.ace_user1 {color: #323232;background-color: #FFF980}.ace-idle-fingers .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWMwMjL6zzBz5sz/ABEUBGCqhK6UAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-kr.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/kr_theme",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-kr-theme",t.cssText=".ace-kr-theme .ace_gutter {background: #1c1917;color: #FCFFE0}.ace-kr-theme .ace_print-margin {width: 1px;background: #1c1917}.ace-kr-theme .ace_scroller {background-color: #0B0A09}.ace-kr-theme .ace_text-layer {color: #FCFFE0}.ace-kr-theme .ace_cursor {border-left: 2px solid #FF9900}.ace-kr-theme .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #FF9900}.ace-kr-theme .ace_marker-layer .ace_selection {background: rgba(170, 0, 255, 0.45)}.ace-kr-theme.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #0B0A09;border-radius: 2px}.ace-kr-theme .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-kr-theme .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(255, 177, 111, 0.32)}.ace-kr-theme .ace_marker-layer .ace_active-line {background: #38403D}.ace-kr-theme .ace_gutter-active-line {background-color : #38403D}.ace-kr-theme .ace_marker-layer .ace_selected-word {border: 1px solid rgba(170, 0, 255, 0.45)}.ace-kr-theme .ace_invisible {color: rgba(255, 177, 111, 0.32)}.ace-kr-theme .ace_keyword,.ace-kr-theme .ace_meta {color: #949C8B}.ace-kr-theme .ace_constant,.ace-kr-theme .ace_constant.ace_character,.ace-kr-theme .ace_constant.ace_character.ace_escape,.ace-kr-theme .ace_constant.ace_other {color: rgba(210, 117, 24, 0.76)}.ace-kr-theme .ace_invalid {color: #F8F8F8;background-color: #A41300}.ace-kr-theme .ace_support {color: #9FC28A}.ace-kr-theme .ace_support.ace_constant {color: #C27E66}.ace-kr-theme .ace_fold {background-color: #949C8B;border-color: #FCFFE0}.ace-kr-theme .ace_support.ace_function {color: #85873A}.ace-kr-theme .ace_storage {color: #FFEE80}.ace-kr-theme .ace_string {color: rgba(164, 161, 181, 0.8)}.ace-kr-theme .ace_string.ace_regexp {color: rgba(125, 255, 192, 0.65)}.ace-kr-theme .ace_comment {font-style: italic;color: #706D5B}.ace-kr-theme .ace_variable {color: #D1A796}.ace-kr-theme .ace_variable.ace_language {color: #FF80E1}.ace-kr-theme .ace_meta.ace_tag {color: #BABD9C}.ace-kr-theme .ace_markup.ace_underline {text-decoration: underline}.ace-kr-theme .ace_markup.ace_list {background-color: #0F0040}.ace-kr-theme .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPg5uL8zzBz5sz/AA1WA+hUYIqjAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-merbivore.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/merbivore",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-merbivore",t.cssText=".ace-merbivore .ace_gutter {background: #202020;color: #E6E1DC}.ace-merbivore .ace_print-margin {width: 1px;background: #555651}.ace-merbivore .ace_scroller {background-color: #161616}.ace-merbivore .ace_text-layer {color: #E6E1DC}.ace-merbivore .ace_cursor {border-left: 2px solid #FFFFFF}.ace-merbivore .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #FFFFFF}.ace-merbivore .ace_marker-layer .ace_selection {background: #454545}.ace-merbivore.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #161616;border-radius: 2px}.ace-merbivore .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-merbivore .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #404040}.ace-merbivore .ace_marker-layer .ace_active-line {background: #333435}.ace-merbivore .ace_gutter-active-line {background-color: #333435}.ace-merbivore .ace_marker-layer .ace_selected-word {border: 1px solid #454545}.ace-merbivore .ace_invisible {color: #404040}.ace-merbivore .ace_entity.ace_name.ace_tag,.ace-merbivore .ace_keyword,.ace-merbivore .ace_meta,.ace-merbivore .ace_meta.ace_tag,.ace-merbivore .ace_storage,.ace-merbivore .ace_support.ace_function {color: #FC6F09}.ace-merbivore .ace_constant,.ace-merbivore .ace_constant.ace_character,.ace-merbivore .ace_constant.ace_character.ace_escape,.ace-merbivore .ace_constant.ace_other,.ace-merbivore .ace_support.ace_type {color: #1EDAFB}.ace-merbivore .ace_constant.ace_character.ace_escape {color: #519F50}.ace-merbivore .ace_constant.ace_language {color: #FDC251}.ace-merbivore .ace_constant.ace_library,.ace-merbivore .ace_string,.ace-merbivore .ace_support.ace_constant {color: #8DFF0A}.ace-merbivore .ace_constant.ace_numeric {color: #58C554}.ace-merbivore .ace_invalid {color: #FFFFFF;background-color: #990000}.ace-merbivore .ace_fold {background-color: #FC6F09;border-color: #E6E1DC}.ace-merbivore .ace_comment {font-style: italic;color: #AD2EA4}.ace-merbivore .ace_entity.ace_other.ace_attribute-name {color: #FFFF89}.ace-merbivore .ace_markup.ace_underline {text-decoration: underline}.ace-merbivore .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWMQExP7zzBz5sz/AA50BAyDznYhAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-merbivore_soft.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/merbivore_soft",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-merbivore-soft",t.cssText=".ace-merbivore-soft .ace_gutter {background: #262424;color: #E6E1DC}.ace-merbivore-soft .ace_print-margin {width: 1px;background: #262424}.ace-merbivore-soft .ace_scroller {background-color: #1C1C1C}.ace-merbivore-soft .ace_text-layer {color: #E6E1DC}.ace-merbivore-soft .ace_cursor {border-left: 2px solid #FFFFFF}.ace-merbivore-soft .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #FFFFFF}.ace-merbivore-soft .ace_marker-layer .ace_selection {background: #494949}.ace-merbivore-soft.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #1C1C1C;border-radius: 2px}.ace-merbivore-soft .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-merbivore-soft .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #404040}.ace-merbivore-soft .ace_marker-layer .ace_active-line {background: #333435}.ace-merbivore-soft .ace_gutter-active-line {background-color: #333435}.ace-merbivore-soft .ace_marker-layer .ace_selected-word {border: 1px solid #494949}.ace-merbivore-soft .ace_invisible {color: #404040}.ace-merbivore-soft .ace_entity.ace_name.ace_tag,.ace-merbivore-soft .ace_keyword,.ace-merbivore-soft .ace_meta,.ace-merbivore-soft .ace_meta.ace_tag,.ace-merbivore-soft .ace_storage {color: #FC803A}.ace-merbivore-soft .ace_constant,.ace-merbivore-soft .ace_constant.ace_character,.ace-merbivore-soft .ace_constant.ace_character.ace_escape,.ace-merbivore-soft .ace_constant.ace_other,.ace-merbivore-soft .ace_support.ace_type {color: #68C1D8}.ace-merbivore-soft .ace_constant.ace_character.ace_escape {color: #B3E5B4}.ace-merbivore-soft .ace_constant.ace_language {color: #E1C582}.ace-merbivore-soft .ace_constant.ace_library,.ace-merbivore-soft .ace_string,.ace-merbivore-soft .ace_support.ace_constant {color: #8EC65F}.ace-merbivore-soft .ace_constant.ace_numeric {color: #7FC578}.ace-merbivore-soft .ace_invalid,.ace-merbivore-soft .ace_invalid.ace_deprecated {color: #FFFFFF;background-color: #FE3838}.ace-merbivore-soft .ace_fold {background-color: #FC803A;border-color: #E6E1DC}.ace-merbivore-soft .ace_comment,.ace-merbivore-soft .ace_meta {font-style: italic;color: #AC4BB8}.ace-merbivore-soft .ace_entity.ace_other.ace_attribute-name {color: #EAF1A3}.ace-merbivore-soft .ace_markup.ace_underline {text-decoration: underline}.ace-merbivore-soft .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWOQkZH5zzBz5sz/AA8EBB6crd1rAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-mono_industrial.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/mono_industrial",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-mono-industrial",t.cssText=".ace-mono-industrial .ace_gutter {background: #1d2521;color: #C5C9C9}.ace-mono-industrial .ace_print-margin {width: 1px;background: #555651}.ace-mono-industrial .ace_scroller {background-color: #222C28}.ace-mono-industrial .ace_text-layer {color: #FFFFFF}.ace-mono-industrial .ace_cursor {border-left: 2px solid #FFFFFF}.ace-mono-industrial .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #FFFFFF}.ace-mono-industrial .ace_marker-layer .ace_selection {background: rgba(145, 153, 148, 0.40)}.ace-mono-industrial.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #222C28;border-radius: 2px}.ace-mono-industrial .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-mono-industrial .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(102, 108, 104, 0.50)}.ace-mono-industrial .ace_marker-layer .ace_active-line {background: rgba(12, 13, 12, 0.25)}.ace-mono-industrial .ace_gutter-active-line {background-color: rgba(12, 13, 12, 0.25)}.ace-mono-industrial .ace_marker-layer .ace_selected-word {border: 1px solid rgba(145, 153, 148, 0.40)}.ace-mono-industrial .ace_invisible {color: rgba(102, 108, 104, 0.50)}.ace-mono-industrial .ace_string {background-color: #151C19;color: #FFFFFF}.ace-mono-industrial .ace_keyword,.ace-mono-industrial .ace_meta {color: #A39E64}.ace-mono-industrial .ace_constant,.ace-mono-industrial .ace_constant.ace_character,.ace-mono-industrial .ace_constant.ace_character.ace_escape,.ace-mono-industrial .ace_constant.ace_numeric,.ace-mono-industrial .ace_constant.ace_other {color: #E98800}.ace-mono-industrial .ace_entity.ace_name.ace_function,.ace-mono-industrial .ace_keyword.ace_operator,.ace-mono-industrial .ace_variable {color: #A8B3AB}.ace-mono-industrial .ace_invalid {color: #FFFFFF;background-color: rgba(153, 0, 0, 0.68)}.ace-mono-industrial .ace_support.ace_constant {color: #C87500}.ace-mono-industrial .ace_fold {background-color: #A8B3AB;border-color: #FFFFFF}.ace-mono-industrial .ace_support.ace_function {color: #588E60}.ace-mono-industrial .ace_entity.ace_name,.ace-mono-industrial .ace_support.ace_class,.ace-mono-industrial .ace_support.ace_type {color: #5778B6}.ace-mono-industrial .ace_storage {color: #C23B00}.ace-mono-industrial .ace_variable.ace_language,.ace-mono-industrial .ace_variable.ace_parameter {color: #648BD2}.ace-mono-industrial .ace_comment {color: #666C68;background-color: #151C19}.ace-mono-industrial .ace_entity.ace_other.ace_attribute-name {color: #909993}.ace-mono-industrial .ace_markup.ace_underline {text-decoration: underline}.ace-mono-industrial .ace_entity.ace_name.ace_tag {color: #A65EFF}.ace-mono-industrial .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNQ0tH4zzBz5sz/ABAOBECKH+evAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-monokai.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-monokai",t.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai .ace_scroller {background-color: #272822}.ace-monokai .ace_text-layer {color: #F8F8F2}.ace-monokai .ace_cursor {border-left: 2px solid #F8F8F0}.ace-monokai .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;border-radius: 2px}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_markup.ace_underline {text-decoration: underline}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNQ11D6z7Bq1ar/ABCKBG6g04U2AAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-pastel_on_dark.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/pastel_on_dark",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-pastel-on-dark",t.cssText=".ace-pastel-on-dark .ace_gutter {background: #353030;color: #8F938F}.ace-pastel-on-dark .ace_print-margin {width: 1px;background: #353030}.ace-pastel-on-dark .ace_scroller {background-color: #2C2828}.ace-pastel-on-dark .ace_text-layer {color: #8F938F}.ace-pastel-on-dark .ace_cursor {border-left: 2px solid #A7A7A7}.ace-pastel-on-dark .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #A7A7A7}.ace-pastel-on-dark .ace_marker-layer .ace_selection {background: rgba(221, 240, 255, 0.20)}.ace-pastel-on-dark.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #2C2828;border-radius: 2px}.ace-pastel-on-dark .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-pastel-on-dark .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(255, 255, 255, 0.25)}.ace-pastel-on-dark .ace_marker-layer .ace_active-line {background: rgba(255, 255, 255, 0.031)}.ace-pastel-on-dark .ace_gutter-active-line {background-color: rgba(255, 255, 255, 0.031)}.ace-pastel-on-dark .ace_marker-layer .ace_selected-word {border: 1px solid rgba(221, 240, 255, 0.20)}.ace-pastel-on-dark .ace_invisible {color: rgba(255, 255, 255, 0.25)}.ace-pastel-on-dark .ace_keyword,.ace-pastel-on-dark .ace_meta {color: #757aD8}.ace-pastel-on-dark .ace_constant,.ace-pastel-on-dark .ace_constant.ace_character,.ace-pastel-on-dark .ace_constant.ace_character.ace_escape,.ace-pastel-on-dark .ace_constant.ace_other {color: #4FB7C5}.ace-pastel-on-dark .ace_keyword.ace_operator {color: #797878}.ace-pastel-on-dark .ace_constant.ace_character {color: #AFA472}.ace-pastel-on-dark .ace_constant.ace_language {color: #DE8E30}.ace-pastel-on-dark .ace_constant.ace_numeric {color: #CCCCCC}.ace-pastel-on-dark .ace_invalid,.ace-pastel-on-dark .ace_invalid.ace_illegal {color: #F8F8F8;background-color: rgba(86, 45, 86, 0.75)}.ace-pastel-on-dark .ace_invalid.ace_deprecated {text-decoration: underline;font-style: italic;color: #D2A8A1}.ace-pastel-on-dark .ace_fold {background-color: #757aD8;border-color: #8F938F}.ace-pastel-on-dark .ace_support.ace_function {color: #AEB2F8}.ace-pastel-on-dark .ace_string {color: #66A968}.ace-pastel-on-dark .ace_string.ace_regexp {color: #E9C062}.ace-pastel-on-dark .ace_comment {color: #A6C6FF}.ace-pastel-on-dark .ace_variable {color: #BEBF55}.ace-pastel-on-dark .ace_variable.ace_language {color: #C1C144}.ace-pastel-on-dark .ace_xml-pe {color: #494949}.ace-pastel-on-dark .ace_markup.ace_underline {text-decoration: underline}.ace-pastel-on-dark .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0dD4z9DR0fEfAA+vBBPqhbn1AAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-solarized_dark.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/solarized_dark",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-solarized-dark",t.cssText=".ace-solarized-dark .ace_gutter {background: #01313f;color: #d0edf7}.ace-solarized-dark .ace_print-margin {width: 1px;background: #33555E}.ace-solarized-dark .ace_scroller {background-color: #002B36}.ace-solarized-dark .ace_entity.ace_other.ace_attribute-name,.ace-solarized-dark .ace_storage,.ace-solarized-dark .ace_text-layer {color: #93A1A1}.ace-solarized-dark .ace_cursor {border-left: 2px solid #D30102}.ace-solarized-dark .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #D30102}.ace-solarized-dark .ace_marker-layer .ace_active-line,.ace-solarized-dark .ace_marker-layer .ace_selection {background: rgba(255, 255, 255, 0.1)}.ace-solarized-dark.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #002B36;border-radius: 2px}.ace-solarized-dark .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-solarized-dark .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(147, 161, 161, 0.50)}.ace-solarized-dark .ace_gutter-active-line {background-color: #0d3440}.ace-solarized-dark .ace_marker-layer .ace_selected-word {border: 1px solid #073642}.ace-solarized-dark .ace_invisible {color: rgba(147, 161, 161, 0.50)}.ace-solarized-dark .ace_keyword,.ace-solarized-dark .ace_meta,.ace-solarized-dark .ace_support.ace_class,.ace-solarized-dark .ace_support.ace_type {color: #859900}.ace-solarized-dark .ace_constant.ace_character,.ace-solarized-dark .ace_constant.ace_other {color: #CB4B16}.ace-solarized-dark .ace_constant.ace_language {color: #B58900}.ace-solarized-dark .ace_constant.ace_numeric {color: #D33682}.ace-solarized-dark .ace_fold {background-color: #268BD2;border-color: #93A1A1}.ace-solarized-dark .ace_entity.ace_name.ace_function,.ace-solarized-dark .ace_entity.ace_name.ace_tag,.ace-solarized-dark .ace_support.ace_function,.ace-solarized-dark .ace_variable,.ace-solarized-dark .ace_variable.ace_language {color: #268BD2}.ace-solarized-dark .ace_string {color: #2AA198}.ace-solarized-dark .ace_string.ace_regexp {color: #D30102}.ace-solarized-dark .ace_comment {font-style: italic;color: #657B83}.ace-solarized-dark .ace_markup.ace_underline {text-decoration: underline}.ace-solarized-dark .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNg0Db7zzBz5sz/AA82BCv7wOIDAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-solarized_light.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/solarized_light",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-solarized-light",t.cssText=".ace-solarized-light .ace_gutter {background: #fbf1d3;color: #333}.ace-solarized-light .ace_print-margin {width: 1px;background: #e8e8e8}.ace-solarized-light .ace_scroller {background-color: #FDF6E3}.ace-solarized-light .ace_text-layer {color: #586E75}.ace-solarized-light .ace_cursor {border-left: 2px solid #000000}.ace-solarized-light .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #000000}.ace-solarized-light .ace_marker-layer .ace_selection {background: #073642}.ace-solarized-light.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #FDF6E3;border-radius: 2px}.ace-solarized-light .ace_marker-layer .ace_step {background: rgb(255, 255, 0)}.ace-solarized-light .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(147, 161, 161, 0.50)}.ace-solarized-light .ace_marker-layer .ace_active-line {background: #EEE8D5}.ace-solarized-light .ace_gutter-active-line {background-color : #dcdcdc}.ace-solarized-light .ace_marker-layer .ace_selected-word {border: 1px solid #073642}.ace-solarized-light .ace_invisible {color: rgba(147, 161, 161, 0.50)}.ace-solarized-light .ace_keyword,.ace-solarized-light .ace_meta,.ace-solarized-light .ace_support.ace_class,.ace-solarized-light .ace_support.ace_type {color: #859900}.ace-solarized-light .ace_constant.ace_character,.ace-solarized-light .ace_constant.ace_other {color: #CB4B16}.ace-solarized-light .ace_constant.ace_language {color: #B58900}.ace-solarized-light .ace_constant.ace_numeric {color: #D33682}.ace-solarized-light .ace_fold {background-color: #268BD2;border-color: #586E75}.ace-solarized-light .ace_entity.ace_name.ace_function,.ace-solarized-light .ace_entity.ace_name.ace_tag,.ace-solarized-light .ace_support.ace_function,.ace-solarized-light .ace_variable,.ace-solarized-light .ace_variable.ace_language {color: #268BD2}.ace-solarized-light .ace_storage {color: #073642}.ace-solarized-light .ace_string {color: #2AA198}.ace-solarized-light .ace_string.ace_regexp {color: #D30102}.ace-solarized-light .ace_comment,.ace-solarized-light .ace_entity.ace_other.ace_attribute-name {color: #93A1A1}.ace-solarized-light .ace_markup.ace_underline {text-decoration: underline}.ace-solarized-light .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4++3xf4ZVq1b9BwAjxwbT1g3hiwAAAABJRU5ErkJggg==) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-terminal.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/terminal",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-terminal-theme",t.cssText=".ace-terminal-theme .ace_gutter {background: #1a0005;color: steelblue}.ace-terminal-theme .ace_print-margin {width: 1px;background: #1a1a1a}.ace-terminal-theme .ace_scroller {background-color: black}.ace-terminal-theme .ace_text-layer {color: #DEDEDE}.ace-terminal-theme .ace_cursor {border-left: 2px solid springgreen}.ace-terminal-theme .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #9F9F9F}.ace-terminal-theme .ace_marker-layer .ace_selection {background: #424242}.ace-terminal-theme.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px black;border-radius: 2px}.ace-terminal-theme .ace_marker-layer .ace_step {background: rgb(0, 0, 0)}.ace-terminal-theme .ace_marker-layer .ace_bracket {background: #090;}.ace-terminal-theme .ace_marker-layer .ace_bracket-start {background: #090;}.ace-terminal-theme .ace_marker-layer .ace_bracket-unmatched {margin: -1px 0 0 -1px;border: 1px solid #900}.ace-terminal-theme .ace_marker-layer .ace_active-line {background: #2A2A2A}.ace-terminal-theme .ace_gutter-active-line {background-color: #2A112A}.ace-terminal-theme .ace_marker-layer .ace_selected-word {border: 1px solid #424242}.ace-terminal-theme .ace_invisible {color: #343434}.ace-terminal-theme .ace_keyword,.ace-terminal-theme .ace_meta,.ace-terminal-theme .ace_storage,.ace-terminal-theme .ace_storage.ace_type,.ace-terminal-theme .ace_support.ace_type {color: tomato}.ace-terminal-theme .ace_keyword.ace_operator {color: deeppink}.ace-terminal-theme .ace_constant.ace_character,.ace-terminal-theme .ace_constant.ace_language,.ace-terminal-theme .ace_constant.ace_numeric,.ace-terminal-theme .ace_keyword.ace_other.ace_unit,.ace-terminal-theme .ace_support.ace_constant,.ace-terminal-theme .ace_variable.ace_parameter {color: #E78C45}.ace-terminal-theme .ace_constant.ace_other {color: gold}.ace-terminal-theme .ace_invalid {color: yellow;background-color: red}.ace-terminal-theme .ace_invalid.ace_deprecated {color: #CED2CF;background-color: #B798BF}.ace-terminal-theme .ace_fold {background-color: #7AA6DA;border-color: #DEDEDE}.ace-terminal-theme .ace_entity.ace_name.ace_function,.ace-terminal-theme .ace_support.ace_function,.ace-terminal-theme .ace_variable {color: #7AA6DA}.ace-terminal-theme .ace_support.ace_class,.ace-terminal-theme .ace_support.ace_type {color: #E7C547}.ace-terminal-theme .ace_markup.ace_heading,.ace-terminal-theme .ace_string {color: #B9CA4A}.ace-terminal-theme .ace_entity.ace_name.ace_tag,.ace-terminal-theme .ace_entity.ace_other.ace_attribute-name,.ace-terminal-theme .ace_meta.ace_tag,.ace-terminal-theme .ace_string.ace_regexp,.ace-terminal-theme .ace_variable {color: #D54E53}.ace-terminal-theme .ace_comment {color: orangered}.ace-terminal-theme .ace_markup.ace_underline {text-decoration: underline}.ace-terminal-theme .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGD4z7Bq1ar/AAz9A/2naJQKAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-textmate.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/libs/ace/theme-textmate.js -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-tomorrow.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/tomorrow",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-tomorrow",t.cssText=".ace-tomorrow .ace_gutter {background: #f6f6f6;color: #4D4D4C}.ace-tomorrow .ace_print-margin {width: 1px;background: #f6f6f6}.ace-tomorrow .ace_scroller {background-color: #FFFFFF}.ace-tomorrow .ace_text-layer {color: #4D4D4C}.ace-tomorrow .ace_cursor {border-left: 2px solid #AEAFAD}.ace-tomorrow .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #AEAFAD}.ace-tomorrow .ace_marker-layer .ace_selection {background: #D6D6D6}.ace-tomorrow.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #FFFFFF;border-radius: 2px}.ace-tomorrow .ace_marker-layer .ace_step {background: rgb(255, 255, 0)}.ace-tomorrow .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #D1D1D1}.ace-tomorrow .ace_marker-layer .ace_active-line {background: #EFEFEF}.ace-tomorrow .ace_gutter-active-line {background-color : #dcdcdc}.ace-tomorrow .ace_marker-layer .ace_selected-word {border: 1px solid #D6D6D6}.ace-tomorrow .ace_invisible {color: #D1D1D1}.ace-tomorrow .ace_keyword,.ace-tomorrow .ace_meta,.ace-tomorrow .ace_storage,.ace-tomorrow .ace_storage.ace_type,.ace-tomorrow .ace_support.ace_type {color: #8959A8}.ace-tomorrow .ace_keyword.ace_operator {color: #3E999F}.ace-tomorrow .ace_constant.ace_character,.ace-tomorrow .ace_constant.ace_language,.ace-tomorrow .ace_constant.ace_numeric,.ace-tomorrow .ace_keyword.ace_other.ace_unit,.ace-tomorrow .ace_support.ace_constant,.ace-tomorrow .ace_variable.ace_parameter {color: #F5871F}.ace-tomorrow .ace_constant.ace_other {color: #666969}.ace-tomorrow .ace_invalid {color: #FFFFFF;background-color: #C82829}.ace-tomorrow .ace_invalid.ace_deprecated {color: #FFFFFF;background-color: #8959A8}.ace-tomorrow .ace_fold {background-color: #4271AE;border-color: #4D4D4C}.ace-tomorrow .ace_entity.ace_name.ace_function,.ace-tomorrow .ace_support.ace_function,.ace-tomorrow .ace_variable {color: #4271AE}.ace-tomorrow .ace_support.ace_class,.ace-tomorrow .ace_support.ace_type {color: #C99E00}.ace-tomorrow .ace_markup.ace_heading,.ace-tomorrow .ace_string {color: #718C00}.ace-tomorrow .ace_entity.ace_name.ace_tag,.ace-tomorrow .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow .ace_meta.ace_tag,.ace-tomorrow .ace_string.ace_regexp,.ace-tomorrow .ace_variable {color: #C82829}.ace-tomorrow .ace_comment {color: #8E908C}.ace-tomorrow .ace_markup.ace_underline {text-decoration: underline}.ace-tomorrow .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-tomorrow_night.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/tomorrow_night",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-tomorrow-night",t.cssText=".ace-tomorrow-night .ace_gutter {background: #25282c;color: #C5C8C6}.ace-tomorrow-night .ace_print-margin {width: 1px;background: #25282c}.ace-tomorrow-night .ace_scroller {background-color: #1D1F21}.ace-tomorrow-night .ace_text-layer {color: #C5C8C6}.ace-tomorrow-night .ace_cursor {border-left: 2px solid #AEAFAD}.ace-tomorrow-night .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #AEAFAD}.ace-tomorrow-night .ace_marker-layer .ace_selection {background: #373B41}.ace-tomorrow-night.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #1D1F21;border-radius: 2px}.ace-tomorrow-night .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-tomorrow-night .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #4B4E55}.ace-tomorrow-night .ace_marker-layer .ace_active-line {background: #282A2E}.ace-tomorrow-night .ace_gutter-active-line {background-color: #282A2E}.ace-tomorrow-night .ace_marker-layer .ace_selected-word {border: 1px solid #373B41}.ace-tomorrow-night .ace_invisible {color: #4B4E55}.ace-tomorrow-night .ace_keyword,.ace-tomorrow-night .ace_meta,.ace-tomorrow-night .ace_storage,.ace-tomorrow-night .ace_storage.ace_type,.ace-tomorrow-night .ace_support.ace_type {color: #B294BB}.ace-tomorrow-night .ace_keyword.ace_operator {color: #8ABEB7}.ace-tomorrow-night .ace_constant.ace_character,.ace-tomorrow-night .ace_constant.ace_language,.ace-tomorrow-night .ace_constant.ace_numeric,.ace-tomorrow-night .ace_keyword.ace_other.ace_unit,.ace-tomorrow-night .ace_support.ace_constant,.ace-tomorrow-night .ace_variable.ace_parameter {color: #DE935F}.ace-tomorrow-night .ace_constant.ace_other {color: #CED1CF}.ace-tomorrow-night .ace_invalid {color: #CED2CF;background-color: #DF5F5F}.ace-tomorrow-night .ace_invalid.ace_deprecated {color: #CED2CF;background-color: #B798BF}.ace-tomorrow-night .ace_fold {background-color: #81A2BE;border-color: #C5C8C6}.ace-tomorrow-night .ace_entity.ace_name.ace_function,.ace-tomorrow-night .ace_support.ace_function,.ace-tomorrow-night .ace_variable {color: #81A2BE}.ace-tomorrow-night .ace_support.ace_class,.ace-tomorrow-night .ace_support.ace_type {color: #F0C674}.ace-tomorrow-night .ace_markup.ace_heading,.ace-tomorrow-night .ace_string {color: #B5BD68}.ace-tomorrow-night .ace_entity.ace_name.ace_tag,.ace-tomorrow-night .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow-night .ace_meta.ace_tag,.ace-tomorrow-night .ace_string.ace_regexp,.ace-tomorrow-night .ace_variable {color: #CC6666}.ace-tomorrow-night .ace_comment {color: #969896}.ace-tomorrow-night .ace_markup.ace_underline {text-decoration: underline}.ace-tomorrow-night .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWOQlVf8z7Bq1ar/AA/hBFp7egmpAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-tomorrow_night_blue.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/tomorrow_night_blue",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-tomorrow-night-blue",t.cssText=".ace-tomorrow-night-blue .ace_gutter {background: #00204b;color: #7388b5}.ace-tomorrow-night-blue .ace_print-margin {width: 1px;background: #00204b}.ace-tomorrow-night-blue .ace_scroller {background-color: #002451}.ace-tomorrow-night-blue .ace_constant.ace_other,.ace-tomorrow-night-blue .ace_text-layer {color: #FFFFFF}.ace-tomorrow-night-blue .ace_cursor {border-left: 2px solid #FFFFFF}.ace-tomorrow-night-blue .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #FFFFFF}.ace-tomorrow-night-blue .ace_marker-layer .ace_selection {background: #003F8E}.ace-tomorrow-night-blue.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #002451;border-radius: 2px}.ace-tomorrow-night-blue .ace_marker-layer .ace_step {background: rgb(127, 111, 19)}.ace-tomorrow-night-blue .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #404F7D}.ace-tomorrow-night-blue .ace_marker-layer .ace_active-line {background: #00346E}.ace-tomorrow-night-blue .ace_gutter-active-line {background-color: #022040}.ace-tomorrow-night-blue .ace_marker-layer .ace_selected-word {border: 1px solid #003F8E}.ace-tomorrow-night-blue .ace_invisible {color: #404F7D}.ace-tomorrow-night-blue .ace_keyword,.ace-tomorrow-night-blue .ace_meta,.ace-tomorrow-night-blue .ace_storage,.ace-tomorrow-night-blue .ace_storage.ace_type,.ace-tomorrow-night-blue .ace_support.ace_type {color: #EBBBFF}.ace-tomorrow-night-blue .ace_keyword.ace_operator {color: #99FFFF}.ace-tomorrow-night-blue .ace_constant.ace_character,.ace-tomorrow-night-blue .ace_constant.ace_language,.ace-tomorrow-night-blue .ace_constant.ace_numeric,.ace-tomorrow-night-blue .ace_keyword.ace_other.ace_unit,.ace-tomorrow-night-blue .ace_support.ace_constant,.ace-tomorrow-night-blue .ace_variable.ace_parameter {color: #FFC58F}.ace-tomorrow-night-blue .ace_invalid {color: #FFFFFF;background-color: #F99DA5}.ace-tomorrow-night-blue .ace_invalid.ace_deprecated {color: #FFFFFF;background-color: #EBBBFF}.ace-tomorrow-night-blue .ace_fold {background-color: #BBDAFF;border-color: #FFFFFF}.ace-tomorrow-night-blue .ace_entity.ace_name.ace_function,.ace-tomorrow-night-blue .ace_support.ace_function,.ace-tomorrow-night-blue .ace_variable {color: #BBDAFF}.ace-tomorrow-night-blue .ace_support.ace_class,.ace-tomorrow-night-blue .ace_support.ace_type {color: #FFEEAD}.ace-tomorrow-night-blue .ace_markup.ace_heading,.ace-tomorrow-night-blue .ace_string {color: #D1F1A9}.ace-tomorrow-night-blue .ace_entity.ace_name.ace_tag,.ace-tomorrow-night-blue .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow-night-blue .ace_meta.ace_tag,.ace-tomorrow-night-blue .ace_string.ace_regexp,.ace-tomorrow-night-blue .ace_variable {color: #FF9DA4}.ace-tomorrow-night-blue .ace_comment {color: #7285B7}.ace-tomorrow-night-blue .ace_markup.ace_underline {text-decoration: underline}.ace-tomorrow-night-blue .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgUAn8z7Bq1ar/ABBUBHJ4/r3JAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-tomorrow_night_bright.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/tomorrow_night_bright",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-tomorrow-night-bright",t.cssText=".ace-tomorrow-night-bright .ace_gutter {background: #1a1a1a;color: #DEDEDE}.ace-tomorrow-night-bright .ace_print-margin {width: 1px;background: #1a1a1a}.ace-tomorrow-night-bright .ace_scroller {background-color: #000000}.ace-tomorrow-night-bright .ace_text-layer {color: #DEDEDE}.ace-tomorrow-night-bright .ace_cursor {border-left: 2px solid #9F9F9F}.ace-tomorrow-night-bright .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #9F9F9F}.ace-tomorrow-night-bright .ace_marker-layer .ace_selection {background: #424242}.ace-tomorrow-night-bright.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #000000;border-radius: 2px}.ace-tomorrow-night-bright .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-tomorrow-night-bright .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #343434}.ace-tomorrow-night-bright .ace_marker-layer .ace_active-line {background: #2A2A2A}.ace-tomorrow-night-bright .ace_gutter-active-line {background-color: #2A2A2A}.ace-tomorrow-night-bright .ace_marker-layer .ace_selected-word {border: 1px solid #424242}.ace-tomorrow-night-bright .ace_invisible {color: #343434}.ace-tomorrow-night-bright .ace_keyword,.ace-tomorrow-night-bright .ace_meta,.ace-tomorrow-night-bright .ace_storage,.ace-tomorrow-night-bright .ace_storage.ace_type,.ace-tomorrow-night-bright .ace_support.ace_type {color: #C397D8}.ace-tomorrow-night-bright .ace_keyword.ace_operator {color: #70C0B1}.ace-tomorrow-night-bright .ace_constant.ace_character,.ace-tomorrow-night-bright .ace_constant.ace_language,.ace-tomorrow-night-bright .ace_constant.ace_numeric,.ace-tomorrow-night-bright .ace_keyword.ace_other.ace_unit,.ace-tomorrow-night-bright .ace_support.ace_constant,.ace-tomorrow-night-bright .ace_variable.ace_parameter {color: #E78C45}.ace-tomorrow-night-bright .ace_constant.ace_other {color: #EEEEEE}.ace-tomorrow-night-bright .ace_invalid {color: #CED2CF;background-color: #DF5F5F}.ace-tomorrow-night-bright .ace_invalid.ace_deprecated {color: #CED2CF;background-color: #B798BF}.ace-tomorrow-night-bright .ace_fold {background-color: #7AA6DA;border-color: #DEDEDE}.ace-tomorrow-night-bright .ace_entity.ace_name.ace_function,.ace-tomorrow-night-bright .ace_support.ace_function,.ace-tomorrow-night-bright .ace_variable {color: #7AA6DA}.ace-tomorrow-night-bright .ace_support.ace_class,.ace-tomorrow-night-bright .ace_support.ace_type {color: #E7C547}.ace-tomorrow-night-bright .ace_markup.ace_heading,.ace-tomorrow-night-bright .ace_string {color: #B9CA4A}.ace-tomorrow-night-bright .ace_entity.ace_name.ace_tag,.ace-tomorrow-night-bright .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow-night-bright .ace_meta.ace_tag,.ace-tomorrow-night-bright .ace_string.ace_regexp,.ace-tomorrow-night-bright .ace_variable {color: #D54E53}.ace-tomorrow-night-bright .ace_comment {color: #969896}.ace-tomorrow-night-bright .ace_markup.ace_underline {text-decoration: underline}.ace-tomorrow-night-bright .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGD4z7Bq1ar/AAz9A/2naJQKAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-tomorrow_night_eighties.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/tomorrow_night_eighties",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-tomorrow-night-eighties",t.cssText=".ace-tomorrow-night-eighties .ace_gutter {background: #272727;color: #CCC}.ace-tomorrow-night-eighties .ace_print-margin {width: 1px;background: #272727}.ace-tomorrow-night-eighties .ace_scroller {background-color: #2D2D2D}.ace-tomorrow-night-eighties .ace_constant.ace_other,.ace-tomorrow-night-eighties .ace_text-layer {color: #CCCCCC}.ace-tomorrow-night-eighties .ace_cursor {border-left: 2px solid #CCCCCC}.ace-tomorrow-night-eighties .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #CCCCCC}.ace-tomorrow-night-eighties .ace_marker-layer .ace_selection {background: #515151}.ace-tomorrow-night-eighties.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #2D2D2D;border-radius: 2px}.ace-tomorrow-night-eighties .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-tomorrow-night-eighties .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #6A6A6A}.ace-tomorrow-night-eighties .ace_marker-layer .ace_active-line {background: #393939}.ace-tomorrow-night-eighties .ace_gutter-active-line {background-color: #393939}.ace-tomorrow-night-eighties .ace_marker-layer .ace_selected-word {border: 1px solid #515151}.ace-tomorrow-night-eighties .ace_invisible {color: #6A6A6A}.ace-tomorrow-night-eighties .ace_keyword,.ace-tomorrow-night-eighties .ace_meta,.ace-tomorrow-night-eighties .ace_storage,.ace-tomorrow-night-eighties .ace_storage.ace_type,.ace-tomorrow-night-eighties .ace_support.ace_type {color: #CC99CC}.ace-tomorrow-night-eighties .ace_keyword.ace_operator {color: #66CCCC}.ace-tomorrow-night-eighties .ace_constant.ace_character,.ace-tomorrow-night-eighties .ace_constant.ace_language,.ace-tomorrow-night-eighties .ace_constant.ace_numeric,.ace-tomorrow-night-eighties .ace_keyword.ace_other.ace_unit,.ace-tomorrow-night-eighties .ace_support.ace_constant,.ace-tomorrow-night-eighties .ace_variable.ace_parameter {color: #F99157}.ace-tomorrow-night-eighties .ace_invalid {color: #CDCDCD;background-color: #F2777A}.ace-tomorrow-night-eighties .ace_invalid.ace_deprecated {color: #CDCDCD;background-color: #CC99CC}.ace-tomorrow-night-eighties .ace_fold {background-color: #6699CC;border-color: #CCCCCC}.ace-tomorrow-night-eighties .ace_entity.ace_name.ace_function,.ace-tomorrow-night-eighties .ace_support.ace_function,.ace-tomorrow-night-eighties .ace_variable {color: #6699CC}.ace-tomorrow-night-eighties .ace_support.ace_class,.ace-tomorrow-night-eighties .ace_support.ace_type {color: #FFCC66}.ace-tomorrow-night-eighties .ace_markup.ace_heading,.ace-tomorrow-night-eighties .ace_string {color: #99CC99}.ace-tomorrow-night-eighties .ace_comment {color: #999999}.ace-tomorrow-night-eighties .ace_entity.ace_name.ace_tag,.ace-tomorrow-night-eighties .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow-night-eighties .ace_meta.ace_tag,.ace-tomorrow-night-eighties .ace_variable {color: #F2777A}.ace-tomorrow-night-eighties .ace_markup.ace_underline {text-decoration: underline}.ace-tomorrow-night-eighties .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ1dX9z7Bq1ar/ABE1BITwhhuFAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-twilight.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/twilight",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-twilight",t.cssText=".ace-twilight .ace_gutter {background: #232323;color: #E2E2E2}.ace-twilight .ace_print-margin {width: 1px;background: #232323}.ace-twilight .ace_scroller {background-color: #141414}.ace-twilight .ace_text-layer {color: #F8F8F8}.ace-twilight .ace_cursor {border-left: 2px solid #A7A7A7}.ace-twilight .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #A7A7A7}.ace-twilight .ace_marker-layer .ace_selection {background: rgba(221, 240, 255, 0.20)}.ace-twilight.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #141414;border-radius: 2px}.ace-twilight .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-twilight .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(255, 255, 255, 0.25)}.ace-twilight .ace_marker-layer .ace_active-line {background: rgba(255, 255, 255, 0.031)}.ace-twilight .ace_gutter-active-line {background-color: rgba(255, 255, 255, 0.031)}.ace-twilight .ace_marker-layer .ace_selected-word {border: 1px solid rgba(221, 240, 255, 0.20)}.ace-twilight .ace_invisible {color: rgba(255, 255, 255, 0.25)}.ace-twilight .ace_keyword,.ace-twilight .ace_meta {color: #CDA869}.ace-twilight .ace_constant,.ace-twilight .ace_constant.ace_character,.ace-twilight .ace_constant.ace_character.ace_escape,.ace-twilight .ace_constant.ace_other,.ace-twilight .ace_markup.ace_heading,.ace-twilight .ace_support.ace_constant {color: #CF6A4C}.ace-twilight .ace_invalid.ace_illegal {color: #F8F8F8;background-color: rgba(86, 45, 86, 0.75)}.ace-twilight .ace_invalid.ace_deprecated {text-decoration: underline;font-style: italic;color: #D2A8A1}.ace-twilight .ace_support {color: #9B859D}.ace-twilight .ace_fold {background-color: #AC885B;border-color: #F8F8F8}.ace-twilight .ace_support.ace_function {color: #DAD085}.ace-twilight .ace_markup.ace_list,.ace-twilight .ace_storage {color: #F9EE98}.ace-twilight .ace_entity.ace_name.ace_function,.ace-twilight .ace_meta.ace_tag,.ace-twilight .ace_variable {color: #AC885B}.ace-twilight .ace_string {color: #8F9D6A}.ace-twilight .ace_string.ace_regexp {color: #E9C062}.ace-twilight .ace_comment {font-style: italic;color: #5F5A60}.ace-twilight .ace_variable {color: #7587A6}.ace-twilight .ace_xml-pe {color: #494949}.ace-twilight .ace_markup.ace_underline {text-decoration: underline}.ace-twilight .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWMQERH5zzBz5sz/AA5EBAYqeZXWAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-vibrant_ink.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/vibrant_ink",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-vibrant-ink",t.cssText=".ace-vibrant-ink .ace_gutter {background: #1a1a1a;color: #BEBEBE}.ace-vibrant-ink .ace_print-margin {width: 1px;background: #1a1a1a}.ace-vibrant-ink .ace_scroller {background-color: #0F0F0F}.ace-vibrant-ink .ace_text-layer {color: #FFFFFF}.ace-vibrant-ink .ace_cursor {border-left: 2px solid #FFFFFF}.ace-vibrant-ink .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #FFFFFF}.ace-vibrant-ink .ace_marker-layer .ace_selection {background: #6699CC}.ace-vibrant-ink.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #0F0F0F;border-radius: 2px}.ace-vibrant-ink .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-vibrant-ink .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #404040}.ace-vibrant-ink .ace_marker-layer .ace_active-line {background: #333333}.ace-vibrant-ink .ace_gutter-active-line {background-color: #333333}.ace-vibrant-ink .ace_marker-layer .ace_selected-word {border: 1px solid #6699CC}.ace-vibrant-ink .ace_invisible {color: #404040}.ace-vibrant-ink .ace_keyword,.ace-vibrant-ink .ace_meta {color: #FF6600}.ace-vibrant-ink .ace_constant,.ace-vibrant-ink .ace_constant.ace_character,.ace-vibrant-ink .ace_constant.ace_character.ace_escape,.ace-vibrant-ink .ace_constant.ace_other {color: #339999}.ace-vibrant-ink .ace_constant.ace_numeric {color: #99CC99}.ace-vibrant-ink .ace_invalid,.ace-vibrant-ink .ace_invalid.ace_deprecated {color: #CCFF33;background-color: #000000}.ace-vibrant-ink .ace_fold {background-color: #FFCC00;border-color: #FFFFFF}.ace-vibrant-ink .ace_entity.ace_name.ace_function,.ace-vibrant-ink .ace_support.ace_function,.ace-vibrant-ink .ace_variable {color: #FFCC00}.ace-vibrant-ink .ace_variable.ace_parameter {font-style: italic}.ace-vibrant-ink .ace_string {color: #66FF00}.ace-vibrant-ink .ace_string.ace_regexp {color: #44B4CC}.ace-vibrant-ink .ace_comment {color: #9933CC}.ace-vibrant-ink .ace_entity.ace_other.ace_attribute-name {font-style: italic;color: #99CC99}.ace-vibrant-ink .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPg5+f/z7Bq1ar/AA5lBCqoLxsgAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/ace/theme-xcode.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/xcode",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-xcode",t.cssText="/* THIS THEME WAS AUTOGENERATED BY Theme.tmpl.css (UUID: EE3AD170-2B7F-4DE1-B724-C75F13FE0085) */.ace-xcode .ace_gutter {background: #e8e8e8;color: #333}.ace-xcode .ace_print-margin {width: 1px;background: #e8e8e8}.ace-xcode .ace_scroller {background-color: #FFFFFF}.ace-xcode .ace_text-layer {color: #000000}.ace-xcode .ace_cursor {border-left: 2px solid #000000}.ace-xcode .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #000000}.ace-xcode .ace_marker-layer .ace_selection {background: #B5D5FF}.ace-xcode.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #FFFFFF;border-radius: 2px}.ace-xcode .ace_marker-layer .ace_step {background: rgb(198, 219, 174)}.ace-xcode .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #BFBFBF}.ace-xcode .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.071)}.ace-xcode .ace_gutter-active-line {background-color: rgba(0, 0, 0, 0.071)}.ace-xcode .ace_marker-layer .ace_selected-word {border: 1px solid #B5D5FF}.ace-xcode .ace_constant.ace_language,.ace-xcode .ace_keyword,.ace-xcode .ace_meta,.ace-xcode .ace_variable.ace_language {color: #C800A4}.ace-xcode .ace_invisible {color: #BFBFBF}.ace-xcode .ace_constant.ace_character,.ace-xcode .ace_constant.ace_other {color: #275A5E}.ace-xcode .ace_constant.ace_numeric {color: #3A00DC}.ace-xcode .ace_entity.ace_other.ace_attribute-name,.ace-xcode .ace_support.ace_constant,.ace-xcode .ace_support.ace_function {color: #450084}.ace-xcode .ace_fold {background-color: #C800A4;border-color: #000000}.ace-xcode .ace_entity.ace_name.ace_tag,.ace-xcode .ace_support.ace_class,.ace-xcode .ace_support.ace_type {color: #790EAD}.ace-xcode .ace_storage {color: #C900A4}.ace-xcode .ace_string {color: #DF0002}.ace-xcode .ace_comment {color: #008E00}.ace-xcode .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==) right repeat-y;}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /client/assets/libs/soundmanager2/soundmanager2.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/libs/soundmanager2/soundmanager2.swf -------------------------------------------------------------------------------- /client/assets/libs/soundmanager2/soundmanager2_debug.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/libs/soundmanager2/soundmanager2_debug.swf -------------------------------------------------------------------------------- /client/assets/libs/soundmanager2/soundmanager2_flash9.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/libs/soundmanager2/soundmanager2_flash9.swf -------------------------------------------------------------------------------- /client/assets/libs/soundmanager2/soundmanager2_flash9_debug.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/libs/soundmanager2/soundmanager2_flash9_debug.swf -------------------------------------------------------------------------------- /client/assets/libs/soundmanager2/soundmanager2_flash_xdomain.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/libs/soundmanager2/soundmanager2_flash_xdomain.zip -------------------------------------------------------------------------------- /client/assets/plugins/emoticonlist.html: -------------------------------------------------------------------------------- 1 | 20 | 21 | 44 | 45 | 79 | -------------------------------------------------------------------------------- /client/assets/plugins/filepicker.html: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /client/assets/plugins/plugin_example.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 |
7 |
8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /client/assets/sound/highlight.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/sound/highlight.mp3 -------------------------------------------------------------------------------- /client/assets/text_themes/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "channel_join": "→ %nick %text", 3 | "channel_part": "← %nick %text", 4 | "channel_quit": "← %nick %text", 5 | "channel_kicked": "← %nick %text", 6 | "channel_selfkick": "× %text", 7 | "channel_badpassword": "× %text", 8 | "channel_topic": "ⓘ %text", 9 | "channel_banned": "× %text", 10 | "channel_badkey": "⚠ %text", 11 | "channel_inviteonly": "⚠ %channel %text", 12 | "channel_alreadyin": "⚠ %nick %text", 13 | "channel_limitreached": "⚠ %channel %text", 14 | "channel_invalid_name": "⚠ %channel %text", 15 | "channel_topic_setby": "ⓘ %text", 16 | "channel_has_been_invited": "ⓘ %nick %text", 17 | "server_connecting": "%text", 18 | "server_connecting_error": "%text", 19 | "mode": "ⓘ %nick %text", 20 | "selfmode": "ⓘ %nick %text", 21 | "nickname_alreadyinuse": "⚠ %text", 22 | "network_disconnected": "⚠ %text", 23 | "whois_channels": "%text", 24 | "whois_idle_and_signon": "%text", 25 | "whois_away": "%text", 26 | "whois_server": "%text", 27 | "whois_idle": "%text", 28 | "whois_notfound": "ⓘ %text", 29 | "nick_changed": "ⓘ %nick %text", 30 | "applet_notfound": "⚠ %text", 31 | "encoding_changed": "ⓘ %text", 32 | "encoding_invalid": "⚠ %text", 33 | "settings_saved": "ⓘ %text", 34 | "ignore_title": "%text:", 35 | "ignore_none": "%text", 36 | "ignore_nick": "%text", 37 | "ignore_stop_notice": "%text", 38 | "ignore_stopped": "%text", 39 | "chanop_privs_needed": "⚠ %text", 40 | "no_such_nick": "ⓘ %nick: %text", 41 | "unknown_command": "ⓘ %text", 42 | "motd": "%text", 43 | "ctcp": "[CTCP] %text", 44 | "privmsg": "%text", 45 | "notice": "%text", 46 | "action": "* %nick %text", 47 | "whois_ident": "%nick [%nick!%ident@%host] * %text", 48 | "whois": "%text", 49 | "who": "%nick [%nick!%ident@%host] * %realname", 50 | "quit": "%text", 51 | "rejoin": "%text", 52 | "set_setting": "ⓘ %text", 53 | "list_aliases": "ⓘ %text", 54 | "ignored_pattern": "ⓘ %text", 55 | "wallops": "[WALLOPS] %text", 56 | "message_nick": "%prefix%nick", 57 | "general_error": "%text" 58 | } -------------------------------------------------------------------------------- /client/assets/themes/basic/background-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/themes/basic/background-light.png -------------------------------------------------------------------------------- /client/assets/themes/basic/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Basic", 3 | "thumbnail_colour": "#e7e7e7" 4 | } -------------------------------------------------------------------------------- /client/assets/themes/cli/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CLI", 3 | "thumbnail_colour": "#222", 4 | "nick_lightness": 60 5 | } 6 | -------------------------------------------------------------------------------- /client/assets/themes/mini/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mini", 3 | "thumbnail_colour": "#fff" 4 | } -------------------------------------------------------------------------------- /client/assets/themes/relaxed/background-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prawnsalad/KiwiIRC/e54d8e5b98f15a903f915335f985d6dc56011fcf/client/assets/themes/relaxed/background-light.png -------------------------------------------------------------------------------- /client/assets/themes/relaxed/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Relaxed", 3 | "thumbnail_colour": "#e7e7e7" 4 | } -------------------------------------------------------------------------------- /client/src/applets/scripteditor.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var view = Backbone.View.extend({ 3 | events: { 4 | 'click .btn_save': 'onSave' 5 | }, 6 | 7 | initialize: function (options) { 8 | var that = this, 9 | text = { 10 | save: _kiwi.global.i18n.translate('client_applets_scripteditor_save').fetch() 11 | }; 12 | this.$el = $(_.template($('#tmpl_script_editor').html().trim())(text)); 13 | 14 | this.model.on('applet_loaded', function () { 15 | that.$el.parent().css('height', '100%'); 16 | $script(_kiwi.app.get('base_path') + '/assets/libs/ace/ace.js', function (){ that.createAce(); }); 17 | }); 18 | }, 19 | 20 | 21 | createAce: function () { 22 | var editor_id = 'editor_' + Math.floor(Math.random()*10000000).toString(); 23 | this.editor_id = editor_id; 24 | 25 | this.$el.find('.editor').attr('id', editor_id); 26 | 27 | this.editor = ace.edit(editor_id); 28 | this.editor.setTheme("ace/theme/monokai"); 29 | this.editor.getSession().setMode("ace/mode/javascript"); 30 | 31 | var script_content = _kiwi.global.settings.get('user_script') || ''; 32 | this.editor.setValue(script_content); 33 | }, 34 | 35 | 36 | onSave: function (event) { 37 | var script_content, user_fn; 38 | 39 | // Build the user script up with some pre-defined components 40 | script_content = 'var network = kiwi.components.Network();\n'; 41 | script_content += 'var input = kiwi.components.ControlInput();\n'; 42 | script_content += 'var events = kiwi.components.Events();\n'; 43 | script_content += this.editor.getValue() + '\n'; 44 | 45 | // Add a dispose method to the user script for cleaning up 46 | script_content += 'this._dispose = function(){ network.off(); input.off(); events.dispose(); if(this.dispose) this.dispose(); }'; 47 | 48 | // Try to compile the user script 49 | try { 50 | user_fn = new Function(script_content); 51 | 52 | // Dispose any existing user script 53 | if (_kiwi.user_script && _kiwi.user_script._dispose) 54 | _kiwi.user_script._dispose(); 55 | 56 | // Create and run the new user script 57 | _kiwi.user_script = new user_fn(); 58 | 59 | } catch (err) { 60 | this.setStatus(_kiwi.global.i18n.translate('client_applets_scripteditor_error').fetch(err.toString())); 61 | return; 62 | } 63 | 64 | // If we're this far, no errors occured. Save the user script 65 | _kiwi.global.settings.set('user_script', this.editor.getValue()); 66 | _kiwi.global.settings.save(); 67 | 68 | this.setStatus(_kiwi.global.i18n.translate('client_applets_scripteditor_saved').fetch() + ' :)'); 69 | }, 70 | 71 | 72 | setStatus: function (status_text) { 73 | var $status = this.$el.find('.toolbar .status'); 74 | 75 | status_text = status_text || ''; 76 | $status.slideUp('fast', function() { 77 | $status.text(status_text); 78 | $status.slideDown(); 79 | }); 80 | } 81 | }); 82 | 83 | 84 | 85 | var applet = Backbone.Model.extend({ 86 | initialize: function () { 87 | var that = this; 88 | 89 | this.set('title', _kiwi.global.i18n.translate('client_applets_scripteditor_title').fetch()); 90 | this.view = new view({model: this}); 91 | 92 | } 93 | }); 94 | 95 | 96 | _kiwi.model.Applet.register('kiwi_script_editor', applet); 97 | //_kiwi.model.Applet.loadOnce('kiwi_script_editor'); 98 | })(); -------------------------------------------------------------------------------- /client/src/applets/startup.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var view = Backbone.View.extend({ 3 | events: {}, 4 | 5 | 6 | initialize: function (options) { 7 | this.showConnectionDialog(); 8 | }, 9 | 10 | 11 | showConnectionDialog: function() { 12 | var connection_dialog = this.connection_dialog = new _kiwi.model.NewConnection(); 13 | connection_dialog.populateDefaultServerSettings(); 14 | 15 | connection_dialog.view.$el.addClass('initial'); 16 | this.$el.append(connection_dialog.view.$el); 17 | 18 | var $info = $($('#tmpl_new_connection_info').html().trim()); 19 | 20 | if ($info.html()) { 21 | connection_dialog.view.infoBoxSet($info); 22 | } else { 23 | $info = null; 24 | } 25 | 26 | this.listenTo(connection_dialog, 'connected', this.newConnectionConnected); 27 | 28 | _.defer(function(){ 29 | if ($info) { 30 | connection_dialog.view.infoBoxShow(); 31 | } 32 | 33 | // Only set focus if we're not within an iframe. (firefox auto scrolls to the embedded client on page load - bad) 34 | if (window == window.top) { 35 | connection_dialog.view.$el.find('.nick').select(); 36 | } 37 | }); 38 | }, 39 | 40 | 41 | newConnectionConnected: function(network) { 42 | // Once connected, reset the connection form to be used again in future 43 | this.connection_dialog.view.reset(); 44 | } 45 | }); 46 | 47 | 48 | 49 | var applet = Backbone.Model.extend({ 50 | initialize: function () { 51 | this.view = new view({model: this}); 52 | } 53 | }); 54 | 55 | 56 | _kiwi.model.Applet.register('kiwi_startup', applet); 57 | })(); 58 | -------------------------------------------------------------------------------- /client/src/helpers/desktopnotifications.js: -------------------------------------------------------------------------------- 1 | _kiwi.utils.notifications = (function () { 2 | if (!window.Notification) { 3 | return { 4 | allowed: _.constant(false), 5 | requestPermission: _.constant($.Deferred().reject()) 6 | }; 7 | } 8 | 9 | var notifications = { 10 | /** 11 | * Check if desktop notifications have been allowed by the user. 12 | * 13 | * @returns {?Boolean} `true` - they have been allowed. 14 | * `false` - they have been blocked. 15 | * `null` - the user hasn't answered yet. 16 | */ 17 | allowed: function () { 18 | return Notification.permission === 'granted' ? true 19 | : Notification.permission === 'denied' ? false 20 | : null; 21 | }, 22 | 23 | /** 24 | * Ask the user their permission to display desktop notifications. 25 | * This will return a promise which will be resolved if the user allows notifications, or rejected if they blocked 26 | * notifictions or simply closed the dialog. If the user had previously given their preference, the promise will be 27 | * immediately resolved or rejected with their previous answer. 28 | * 29 | * @example 30 | * notifications.requestPermission().then(function () { 'allowed' }, function () { 'not allowed' }); 31 | * 32 | * @returns {Promise} 33 | */ 34 | requestPermission: function () { 35 | var deferred = $.Deferred(); 36 | Notification.requestPermission(function (permission) { 37 | deferred[(permission === 'granted') ? 'resolve' : 'reject'](); 38 | }); 39 | return deferred.promise(); 40 | }, 41 | 42 | /** 43 | * Create a new notification. If the user has not yet given permission to display notifications, they will be asked 44 | * to confirm first. The notification will show afterwards if they allow it. 45 | * 46 | * Notifications implement Backbone.Events (so you can use `on` and `off`). They trigger four different events: 47 | * - 'click' 48 | * - 'close' 49 | * - 'error' 50 | * - 'show' 51 | * 52 | * @example 53 | * notifications 54 | * .create('Cool notification', { icon: 'logo.png' }) 55 | * .on('click', function () { 56 | * window.focus(); 57 | * }) 58 | * .closeAfter(5000); 59 | * 60 | * @param {String} title 61 | * @param {Object} options 62 | * @param {String=} options.body A string representing an extra content to display within the notification 63 | * @param {String=} options.dir The direction of the notification; it can be auto, ltr, or rtl 64 | * @param {String=} options.lang Specify the lang used within the notification. This string must be a valid BCP 65 | * 47 language tag. 66 | * @param {String=} options.tag An ID for a given notification that allows to retrieve, replace or remove it if necessary 67 | * @param {String=} options.icon The URL of an image to be used as an icon by the notification 68 | * @returns {Notifier} 69 | */ 70 | create: function (title, options) { 71 | return new Notifier(title, options); 72 | } 73 | }; 74 | 75 | function Notifier(title, options) { 76 | createNotification.call(this, title, options); 77 | } 78 | _.extend(Notifier.prototype, Backbone.Events, { 79 | closed: false, 80 | _closeTimeout: null, 81 | 82 | /** 83 | * Close the notification after a given number of milliseconds. 84 | * @param {Number} timeout 85 | * @returns {this} 86 | */ 87 | closeAfter: function (timeout) { 88 | if (!this.closed) { 89 | if (this.notification) { 90 | this._closeTimeout = this._closeTimeout || setTimeout(_.bind(this.close, this), timeout); 91 | } else { 92 | this.once('show', _.bind(this.closeAfter, this, timeout)); 93 | } 94 | } 95 | return this; 96 | }, 97 | 98 | /** 99 | * Close the notification immediately. 100 | * @returns {this} 101 | */ 102 | close: function () { 103 | if (this.notification && !this.closed) { 104 | this.notification.close(); 105 | this.closed = true; 106 | } 107 | return this; 108 | } 109 | }); 110 | 111 | function createNotification(title, options) { 112 | switch (notifications.allowed()) { 113 | case true: 114 | this.notification = new Notification(title, options); 115 | _.each(['click', 'close', 'error', 'show'], function (eventName) { 116 | this.notification['on' + eventName] = _.bind(this.trigger, this, eventName); 117 | }, this); 118 | break; 119 | case null: 120 | notifications.requestPermission().done(_.bind(createNotification, this, title, options)); 121 | break; 122 | } 123 | } 124 | 125 | return notifications; 126 | }()); 127 | -------------------------------------------------------------------------------- /client/src/models/applet.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.Applet = _kiwi.model.Panel.extend({ 2 | initialize: function (attributes) { 3 | // Temporary name 4 | var name = "applet_"+(new Date().getTime().toString()) + Math.ceil(Math.random()*100).toString(); 5 | this.view = new _kiwi.view.Applet({model: this, name: name}); 6 | 7 | this.set({ 8 | "name": name 9 | }, {"silent": true}); 10 | 11 | // Holds the loaded applet 12 | this.loaded_applet = null; 13 | }, 14 | 15 | 16 | // Load an applet within this panel 17 | load: function (applet_object, applet_name) { 18 | if (typeof applet_object === 'object') { 19 | // Make sure this is a valid Applet 20 | if (applet_object.get || applet_object.extend) { 21 | 22 | // Try find a title for the applet 23 | this.set('title', applet_object.get('title') || _kiwi.global.i18n.translate('client_models_applet_unknown').fetch()); 24 | 25 | // Update the tabs title if the applet changes it 26 | applet_object.bind('change:title', function (obj, new_value) { 27 | this.set('title', new_value); 28 | }, this); 29 | 30 | // If this applet has a UI, add it now 31 | this.view.$el.html(''); 32 | if (applet_object.view) { 33 | this.view.$el.append(applet_object.view.$el); 34 | } 35 | 36 | // Keep a reference to this applet 37 | this.loaded_applet = applet_object; 38 | 39 | this.loaded_applet.trigger('applet_loaded'); 40 | } 41 | 42 | } else if (typeof applet_object === 'string') { 43 | // Treat this as a URL to an applet script and load it 44 | this.loadFromUrl(applet_object, applet_name); 45 | } 46 | 47 | return this; 48 | }, 49 | 50 | 51 | loadFromUrl: function(applet_url, applet_name) { 52 | var that = this; 53 | 54 | this.view.$el.html(_kiwi.global.i18n.translate('client_models_applet_loading').fetch()); 55 | $script(applet_url, function () { 56 | // Check if the applet loaded OK 57 | if (!_kiwi.applets[applet_name]) { 58 | that.view.$el.html(_kiwi.global.i18n.translate('client_models_applet_notfound').fetch()); 59 | return; 60 | } 61 | 62 | // Load a new instance of this applet 63 | that.load(new _kiwi.applets[applet_name]()); 64 | }); 65 | }, 66 | 67 | 68 | close: function () { 69 | this.view.$el.remove(); 70 | this.destroy(); 71 | 72 | this.view = undefined; 73 | 74 | // Call the applets dispose method if it has one 75 | if (this.loaded_applet && this.loaded_applet.dispose) { 76 | this.loaded_applet.dispose(); 77 | } 78 | 79 | // Call the inherited close() 80 | this.constructor.__super__.close.apply(this, arguments); 81 | }, 82 | 83 | isApplet: function () { 84 | return true; 85 | } 86 | }, 87 | 88 | 89 | { 90 | // Load an applet type once only. If it already exists, return that 91 | loadOnce: function (applet_name) { 92 | 93 | // See if we have an instance loaded already 94 | var applet = _.find(_kiwi.app.panels('applets'), function(panel) { 95 | // Ignore if it's not an applet 96 | if (!panel.isApplet()) return; 97 | 98 | // Ignore if it doesn't have an applet loaded 99 | if (!panel.loaded_applet) return; 100 | 101 | if (panel.loaded_applet.get('_applet_name') === applet_name) { 102 | return true; 103 | } 104 | }); 105 | 106 | if (applet) return applet; 107 | 108 | 109 | // If we didn't find an instance, load a new one up 110 | return this.load(applet_name); 111 | }, 112 | 113 | 114 | load: function (applet_name, options) { 115 | var applet, applet_obj; 116 | 117 | options = options || {}; 118 | 119 | applet_obj = this.getApplet(applet_name); 120 | 121 | if (!applet_obj) 122 | return; 123 | 124 | // Create the applet and load the content 125 | applet = new _kiwi.model.Applet(); 126 | applet.load(new applet_obj({_applet_name: applet_name})); 127 | 128 | // Add it into the tab list if needed (default) 129 | if (!options.no_tab) 130 | _kiwi.app.applet_panels.add(applet); 131 | 132 | 133 | return applet; 134 | }, 135 | 136 | 137 | getApplet: function (applet_name) { 138 | return _kiwi.applets[applet_name] || null; 139 | }, 140 | 141 | 142 | register: function (applet_name, applet) { 143 | _kiwi.applets[applet_name] = applet; 144 | } 145 | }); -------------------------------------------------------------------------------- /client/src/models/channel.js: -------------------------------------------------------------------------------- 1 | // TODO: Channel modes 2 | // TODO: Listen to gateway events for anythign related to this channel 3 | _kiwi.model.Channel = _kiwi.model.Panel.extend({ 4 | initialize: function (attributes) { 5 | var name = this.get("name") || "", 6 | members; 7 | 8 | this.set({ 9 | "members": new _kiwi.model.MemberList(), 10 | "name": name, 11 | "scrollback": [], 12 | "topic": "" 13 | }, {"silent": true}); 14 | 15 | this.view = new _kiwi.view.Channel({"model": this, "name": name}); 16 | 17 | members = this.get("members"); 18 | members.channel = this; 19 | members.bind("add", function (member, members, options) { 20 | var show_message = _kiwi.global.settings.get('show_joins_parts'); 21 | if (show_message === false) { 22 | return; 23 | } 24 | 25 | this.addMsg(' ', styleText('channel_join', {member: member.getMaskParts(), text: translateText('client_models_channel_join'), channel: name}), 'action join', {time: options.kiwi.time}); 26 | }, this); 27 | 28 | members.bind("remove", function (member, members, options) { 29 | var show_message = _kiwi.global.settings.get('show_joins_parts'); 30 | var msg = (options.kiwi.message) ? '(' + options.kiwi.message + ')' : ''; 31 | 32 | if (options.kiwi.type === 'quit' && show_message) { 33 | this.addMsg(' ', styleText('channel_quit', {member: member.getMaskParts(), text: translateText('client_models_channel_quit', [msg]), channel: name}), 'action quit', {time: options.kiwi.time}); 34 | 35 | } else if (options.kiwi.type === 'kick') { 36 | 37 | if (!options.kiwi.current_user_kicked) { 38 | //If user kicked someone, show the message regardless of settings. 39 | if (show_message || options.kiwi.current_user_initiated) { 40 | this.addMsg(' ', styleText('channel_kicked', {member: member.getMaskParts(), text: translateText('client_models_channel_kicked', [options.kiwi.by, msg]), channel: name}), 'action kick', {time: options.kiwi.time}); 41 | } 42 | } else { 43 | this.addMsg(' ', styleText('channel_selfkick', {text: translateText('client_models_channel_selfkick', [options.kiwi.by, msg]), channel: name}), 'action kick', {time: options.kiwi.time}); 44 | } 45 | } else if (show_message) { 46 | this.addMsg(' ', styleText('channel_part', {member: member.getMaskParts(), text: translateText('client_models_channel_part', [msg]), channel: name}), 'action part', {time: options.kiwi.time}); 47 | 48 | } 49 | }, this); 50 | 51 | _kiwi.global.events.emit('panel:created', {panel: this}); 52 | }, 53 | 54 | 55 | addMsg: function (nick, msg, type, opts) { 56 | var message_obj, bs, d, members, member, 57 | scrollback = (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250); 58 | 59 | opts = opts || {}; 60 | 61 | // Time defaults to now 62 | if (typeof opts.time === 'number') { 63 | opts.time = new Date(opts.time); 64 | } else { 65 | opts.time = new Date(); 66 | } 67 | 68 | // CSS style defaults to empty string 69 | if (!opts || typeof opts.style === 'undefined') { 70 | opts.style = ''; 71 | } 72 | 73 | // Create a message object 74 | message_obj = {"msg": msg, "date": opts.date, "time": opts.time, "nick": nick, "chan": this.get("name"), "type": type, "style": opts.style}; 75 | 76 | // If this user has one, get its prefix 77 | members = this.get('members'); 78 | if (members) { 79 | member = members.getByNick(message_obj.nick); 80 | if (member) { 81 | message_obj.nick_prefix = member.get('prefix'); 82 | } 83 | } 84 | 85 | // The CSS class (action, topic, notice, etc) 86 | if (typeof message_obj.type !== "string") { 87 | message_obj.type = ''; 88 | } 89 | 90 | // Make sure we don't have NaN or something 91 | if (typeof message_obj.msg !== "string") { 92 | message_obj.msg = ''; 93 | } 94 | 95 | // Update the scrollback 96 | bs = this.get("scrollback"); 97 | if (bs) { 98 | bs.push(message_obj); 99 | 100 | // Keep the scrolback limited 101 | if (bs.length > scrollback) { 102 | bs = _.takeRight(bs, scrollback); 103 | } 104 | this.set({"scrollback": bs}, {silent: true}); 105 | } 106 | 107 | this.trigger("msg", message_obj); 108 | }, 109 | 110 | 111 | clearMessages: function () { 112 | this.set({'scrollback': []}, {silent: true}); 113 | this.addMsg('', 'Window cleared'); 114 | 115 | this.view.render(); 116 | }, 117 | 118 | 119 | setMode: function(mode_string) { 120 | this.get('network').gateway.mode(this.get('name'), mode_string); 121 | }, 122 | 123 | isChannel: function() { 124 | return true; 125 | } 126 | }); 127 | -------------------------------------------------------------------------------- /client/src/models/channelinfo.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.ChannelInfo = Backbone.Model.extend({ 2 | initialize: function () { 3 | this.view = new _kiwi.view.ChannelInfo({"model": this}); 4 | } 5 | }); -------------------------------------------------------------------------------- /client/src/models/datastore.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.DataStore = Backbone.Model.extend({ 2 | initialize: function () { 3 | this._namespace = ''; 4 | this.new_data = {}; 5 | this.stored_attributes = {}; 6 | }, 7 | 8 | namespace: function (new_namespace) { 9 | if (new_namespace) this._namespace = new_namespace; 10 | return this._namespace; 11 | }, 12 | 13 | // Overload the original save() method 14 | save: function () { 15 | // Save the current data and update the stored_attributes with a copy 16 | var stringified = JSON.stringify(this.attributes); 17 | localStorage.setItem(this._namespace, stringified); 18 | this.stored_attributes = JSON.parse(stringified); 19 | }, 20 | 21 | // Save only one attribute to storage 22 | saveOne: function (key_name) { 23 | this.stored_attributes[key_name] = this.get(key_name); 24 | localStorage.setItem(this._namespace, JSON.stringify(this.stored_attributes)); 25 | }, 26 | 27 | // Overload the original load() method 28 | load: function () { 29 | if (!localStorage) return; 30 | 31 | var raw, data, stored_data; 32 | 33 | try { 34 | raw = localStorage.getItem(this._namespace); 35 | data = JSON.parse(raw) || {}; 36 | stored_data = JSON.parse(raw) || {}; 37 | } catch (error) { 38 | data = {}; 39 | stored_data = {}; 40 | } 41 | 42 | this.attributes = data; 43 | this.stored_attributes = stored_data; 44 | } 45 | }, 46 | 47 | { 48 | // Generates a new instance of DataStore with a set namespace 49 | instance: function (namespace, attributes) { 50 | var datastore = new _kiwi.model.DataStore(attributes); 51 | datastore.namespace(namespace); 52 | return datastore; 53 | } 54 | }); -------------------------------------------------------------------------------- /client/src/models/ignorelist.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.IgnoreList = Backbone.Collection.extend({ 2 | initialize: function() { 3 | this.network_address = ''; 4 | this.ignore_data = _kiwi.model.DataStore.instance('kiwi.ignore_list'); 5 | this.ignore_data.load(); 6 | 7 | this.on('add', _.bind(this.onAdd, this)); 8 | this.on('add', _.bind(this.saveList, this)); 9 | this.on('remove', _.bind(this.saveList, this)); 10 | }, 11 | 12 | 13 | onAdd: function(entry) { 14 | if (!entry.get('mask')) return; 15 | 16 | if (!entry.get('time')) { 17 | entry.set('time', (new Date()).getTime()); 18 | } 19 | 20 | if (!entry.get('regex')) { 21 | entry.set('regex', toUserMask(entry.get('mask'), true)[1]); 22 | } 23 | }, 24 | 25 | 26 | loadFromNetwork: function(network) { 27 | this.network_address = network.get('address').toLowerCase(); 28 | 29 | var ignore_list = this.ignore_data.get(this.network_address) || []; 30 | 31 | _.each(ignore_list, function(item, idx) { 32 | if (!item || !item.mask) return; 33 | 34 | // Make the regex for the given user mask 35 | item.regex = toUserMask(item.mask, true)[1]; 36 | }); 37 | 38 | this.reset(ignore_list); 39 | }, 40 | 41 | 42 | saveList: function() { 43 | var list = []; 44 | 45 | this.forEach(function(entry) { 46 | var obj = _.clone(entry.attributes); 47 | delete obj.regex; 48 | list.push(obj); 49 | }); 50 | 51 | this.ignore_data.set(this.network_address, list); 52 | this.ignore_data.save(); 53 | }, 54 | 55 | 56 | addMask: function(mask) { 57 | return this.add({mask: mask}); 58 | }, 59 | 60 | 61 | removeMask: function(mask) { 62 | var entry = this.find(function(entry) { 63 | return entry.get('mask') == mask; 64 | }); 65 | 66 | if (entry) { 67 | this.remove(entry); 68 | } 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /client/src/models/memberlist.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.MemberList = Backbone.Collection.extend({ 2 | model: _kiwi.model.Member, 3 | comparator: function (a, b) { 4 | var i, a_modes, b_modes, a_idx, b_idx, a_nick, b_nick; 5 | var user_prefixes = this.channel.get('network').get('user_prefixes'); 6 | 7 | a_modes = a.get("modes"); 8 | b_modes = b.get("modes"); 9 | 10 | // Try to sort by modes first 11 | if (a_modes.length > 0) { 12 | // a has modes, but b doesn't so a should appear first 13 | if (b_modes.length === 0) { 14 | return -1; 15 | } 16 | a_idx = b_idx = -1; 17 | // Compare the first (highest) mode 18 | for (i = 0; i < user_prefixes.length; i++) { 19 | if (user_prefixes[i].mode === a_modes[0]) { 20 | a_idx = i; 21 | } 22 | } 23 | for (i = 0; i < user_prefixes.length; i++) { 24 | if (user_prefixes[i].mode === b_modes[0]) { 25 | b_idx = i; 26 | } 27 | } 28 | if (a_idx < b_idx) { 29 | return -1; 30 | } else if (a_idx > b_idx) { 31 | return 1; 32 | } 33 | // If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting 34 | 35 | } else if (b_modes.length > 0) { 36 | // b has modes but a doesn't so b should appear first 37 | return 1; 38 | } 39 | a_nick = a.get("nick").toLocaleLowerCase(); 40 | b_nick = b.get("nick").toLocaleLowerCase(); 41 | // Lexicographical sorting 42 | if (a_nick < b_nick) { 43 | return -1; 44 | } else if (a_nick > b_nick) { 45 | return 1; 46 | } else { 47 | return 0; 48 | } 49 | }, 50 | 51 | 52 | initialize: function (options) { 53 | this.view = new _kiwi.view.MemberList({"model": this}); 54 | this.initNickCache(); 55 | }, 56 | 57 | 58 | /* 59 | * Keep a reference to each member by the nick. Speeds up .getByNick() 60 | * so it doesn't need to loop over every model for each nick lookup 61 | */ 62 | initNickCache: function() { 63 | var updateRegex = _.bind(function () { 64 | // Allows checking for a nick that contains 'the_nick' or 'the_nick' 65 | // .. where is any character not allowed in an IRC nick 66 | var regex_valid_nick_chars = 'a-z0-9_\\-{}[\\]^`|\\\\'; 67 | var regex_nicks = Object.keys(this.nick_cache) 68 | .map(_.escapeRegExp) 69 | .join('|'); 70 | 71 | this.nick_regex = new RegExp( 72 | '^[^'+regex_valid_nick_chars+']?(' + regex_nicks + ')[^'+regex_valid_nick_chars+']?$', 'i' 73 | ); 74 | }, this); 75 | 76 | function getNick (member) { 77 | return member.get('nick').toLowerCase(); 78 | }; 79 | 80 | this.nick_cache = Object.create(null); 81 | this.nick_regex = null; 82 | 83 | this.on('reset', function() { 84 | this.nick_cache = _.reduce(this.models, function(memo, member) { 85 | memo[getNick(member)] = member; 86 | return memo; 87 | }, Object.create(null)); 88 | updateRegex(); 89 | }); 90 | 91 | this.on('add', function(member) { 92 | this.nick_cache[getNick(member)] = member; 93 | updateRegex(); 94 | }); 95 | 96 | this.on('remove', function(member) { 97 | delete this.nick_cache[getNick(member)]; 98 | updateRegex(); 99 | }); 100 | 101 | this.on('change:nick', function(member) { 102 | this.nick_cache[getNick(member)] = member; 103 | delete this.nick_cache[member.previous('nick').toLowerCase()]; 104 | updateRegex(); 105 | }); 106 | }, 107 | 108 | getByNick: function (nick) { 109 | var matches; 110 | if (this.nick_regex && (matches = this.nick_regex.exec(nick))) { 111 | return this.nick_cache[matches[1].toLowerCase()]; 112 | } 113 | } 114 | }); 115 | -------------------------------------------------------------------------------- /client/src/models/networkpanellist.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.NetworkPanelList = Backbone.Collection.extend({ 2 | model: _kiwi.model.Network, 3 | 4 | initialize: function() { 5 | this.view = new _kiwi.view.NetworkTabs({model: this}); 6 | 7 | this.on('add', this.onNetworkAdd, this); 8 | this.on('remove', this.onNetworkRemove, this); 9 | 10 | // Current active connection / panel 11 | this.active_connection = undefined; 12 | this.active_panel = undefined; 13 | 14 | // TODO: Remove this - legacy 15 | this.active = undefined; 16 | }, 17 | 18 | getByConnectionId: function(id) { 19 | return this.find(function(connection){ 20 | return connection.get('connection_id') == id; 21 | }); 22 | }, 23 | 24 | panels: function() { 25 | var panels = []; 26 | 27 | this.each(function(network) { 28 | panels = panels.concat(network.panels.models); 29 | }); 30 | 31 | return panels; 32 | }, 33 | 34 | 35 | onNetworkAdd: function(network) { 36 | network.panels.on('active', this.onPanelActive, this); 37 | 38 | // if it's our first connection, set it active 39 | if (this.models.length === 1) { 40 | this.active_connection = network; 41 | this.active_panel = network.panels.server; 42 | 43 | // TODO: Remove this - legacy 44 | this.active = this.active_panel; 45 | } 46 | }, 47 | 48 | onNetworkRemove: function(network) { 49 | network.panels.off('active', this.onPanelActive, this); 50 | }, 51 | 52 | onPanelActive: function(panel) { 53 | var connection = this.getByConnectionId(panel.tab.data('connection_id')); 54 | this.trigger('active', panel, connection); 55 | 56 | this.active_connection = connection; 57 | this.active_panel = panel; 58 | 59 | // TODO: Remove this - legacy 60 | this.active = panel; 61 | } 62 | }); -------------------------------------------------------------------------------- /client/src/models/newconnection.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.NewConnection = Backbone.Collection.extend({ 2 | initialize: function() { 3 | this.view = new _kiwi.view.ServerSelect({model: this}); 4 | 5 | this.view.bind('server_connect', this.onMakeConnection, this); 6 | 7 | }, 8 | 9 | 10 | populateDefaultServerSettings: function() { 11 | var defaults = _kiwi.global.defaultServerSettings(); 12 | this.view.populateFields(defaults); 13 | }, 14 | 15 | 16 | onMakeConnection: function(new_connection_event) { 17 | var that = this; 18 | 19 | this.connect_details = new_connection_event; 20 | 21 | this.view.networkConnecting(); 22 | 23 | _kiwi.gateway.newConnection({ 24 | nick: new_connection_event.nick, 25 | host: new_connection_event.server, 26 | port: new_connection_event.port, 27 | ssl: new_connection_event.ssl, 28 | password: new_connection_event.password, 29 | options: new_connection_event.options 30 | }, function(err, network) { 31 | that.onNewNetwork(err, network); 32 | }); 33 | }, 34 | 35 | 36 | onNewNetwork: function(err, network) { 37 | // Show any errors if given 38 | if (err) { 39 | this.view.showError(err); 40 | } 41 | 42 | if (network && this.connect_details) { 43 | network.auto_join = { 44 | channel: this.connect_details.channel, 45 | key: this.connect_details.channel_key 46 | }; 47 | 48 | this.trigger('new_network', network); 49 | } 50 | } 51 | }); -------------------------------------------------------------------------------- /client/src/models/panel.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.Panel = Backbone.Model.extend({ 2 | initialize: function (attributes) { 3 | var name = this.get("name") || ""; 4 | this.view = new _kiwi.view.Panel({"model": this, "name": name}); 5 | this.set({ 6 | "scrollback": [], 7 | "name": name 8 | }, {"silent": true}); 9 | 10 | _kiwi.global.events.emit('panel:created', {panel: this}); 11 | }, 12 | 13 | close: function () { 14 | _kiwi.app.panels.trigger('close', this); 15 | _kiwi.global.events.emit('panel:close', {panel: this}); 16 | 17 | if (this.view) { 18 | this.view.unbind(); 19 | this.view.remove(); 20 | this.view = undefined; 21 | delete this.view; 22 | } 23 | 24 | var members = this.get('members'); 25 | if (members) { 26 | members.reset([]); 27 | this.unset('members'); 28 | } 29 | 30 | this.get('panel_list').remove(this); 31 | 32 | this.unbind(); 33 | this.destroy(); 34 | }, 35 | 36 | isChannel: function () { 37 | return false; 38 | }, 39 | 40 | isQuery: function () { 41 | return false; 42 | }, 43 | 44 | isApplet: function () { 45 | return false; 46 | }, 47 | 48 | isServer: function () { 49 | return false; 50 | }, 51 | 52 | isActive: function () { 53 | return (_kiwi.app.panels().active === this); 54 | } 55 | }); -------------------------------------------------------------------------------- /client/src/models/panellist.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.PanelList = Backbone.Collection.extend({ 2 | model: _kiwi.model.Panel, 3 | 4 | comparator: function (chan) { 5 | return chan.get('name'); 6 | }, 7 | initialize: function (elements, network) { 8 | var that = this; 9 | 10 | // If this PanelList is associated with a network/connection 11 | if (network) { 12 | this.network = network; 13 | } 14 | 15 | this.view = new _kiwi.view.Tabs({model: this}); 16 | 17 | // Holds the active panel 18 | this.active = null; 19 | 20 | // Keep a tab on the active panel 21 | this.bind('active', function (active_panel) { 22 | this.active = active_panel; 23 | }, this); 24 | 25 | this.bind('add', function(panel) { 26 | panel.set('panel_list', this); 27 | }); 28 | }, 29 | 30 | 31 | 32 | getByCid: function (cid) { 33 | if (typeof name !== 'string') return; 34 | 35 | return this.find(function (c) { 36 | return cid === c.cid; 37 | }); 38 | }, 39 | 40 | 41 | 42 | getByName: function (name) { 43 | if (typeof name !== 'string') return; 44 | 45 | return this.find(function (c) { 46 | return name.toLowerCase() === c.get('name').toLowerCase(); 47 | }); 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /client/src/models/pluginmanager.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.PluginManager = Backbone.Model.extend({ 2 | initialize: function () { 3 | this.$plugin_holder = $('') 4 | .appendTo(_kiwi.app.view.$el); 5 | 6 | this.loading_plugins = 0; 7 | this.loaded_plugins = {}; 8 | }, 9 | 10 | // Load an applet within this panel 11 | load: function (url) { 12 | var that = this; 13 | 14 | if (this.loaded_plugins[url]) { 15 | this.unload(url); 16 | } 17 | 18 | this.loading_plugins++; 19 | 20 | this.loaded_plugins[url] = $('
'); 21 | this.loaded_plugins[url].appendTo(this.$plugin_holder) 22 | .load(url, _.bind(that.pluginLoaded, that)); 23 | }, 24 | 25 | 26 | unload: function (url) { 27 | if (!this.loaded_plugins[url]) { 28 | return; 29 | } 30 | 31 | this.loaded_plugins[url].remove(); 32 | delete this.loaded_plugins[url]; 33 | }, 34 | 35 | 36 | // Called after each plugin is loaded 37 | pluginLoaded: function() { 38 | this.loading_plugins--; 39 | 40 | if (this.loading_plugins === 0) { 41 | this.trigger('loaded'); 42 | } 43 | }, 44 | }); -------------------------------------------------------------------------------- /client/src/models/query.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.Query = _kiwi.model.Channel.extend({ 2 | initialize: function (attributes) { 3 | var name = this.get("name") || "", 4 | members; 5 | 6 | this.view = new _kiwi.view.Channel({"model": this, "name": name}); 7 | this.set({ 8 | "name": name, 9 | "scrollback": [] 10 | }, {"silent": true}); 11 | 12 | _kiwi.global.events.emit('panel:created', {panel: this}); 13 | }, 14 | 15 | isChannel: function () { 16 | return false; 17 | }, 18 | 19 | isQuery: function () { 20 | return true; 21 | } 22 | }); -------------------------------------------------------------------------------- /client/src/models/server.js: -------------------------------------------------------------------------------- 1 | _kiwi.model.Server = _kiwi.model.Channel.extend({ 2 | initialize: function (attributes) { 3 | var name = "Server"; 4 | this.view = new _kiwi.view.Channel({"model": this, "name": name}); 5 | this.set({ 6 | "scrollback": [], 7 | "name": name 8 | }, {"silent": true}); 9 | 10 | _kiwi.global.events.emit('panel:created', {panel: this}); 11 | }, 12 | 13 | isServer: function () { 14 | return true; 15 | }, 16 | 17 | isChannel: function () { 18 | return false; 19 | } 20 | }); -------------------------------------------------------------------------------- /client/src/translations/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "sq": "Albanian", 3 | "bs": "Bosnian", 4 | "ca": "Catalan", 5 | "cs": "Čeština", 6 | "de-de": "Deutsch", 7 | "en-gb": "English (British)", 8 | "es": "Español", 9 | "es-419": "Español (Latino America)", 10 | "fr": "Français", 11 | "gl": "Galego", 12 | "el": "Greek", 13 | "he": "Hebrew", 14 | "it": "Italiano", 15 | "ko-kr": "Korean", 16 | "mk": "Македонски", 17 | "nl": "Nederlands", 18 | "no": "Norsk", 19 | "pl": "Polski", 20 | "pt-br": "Português (Brasil)", 21 | "ro": "Română", 22 | "ru": "Русский", 23 | "sr": "Srpski", 24 | "tr": "Türkçe", 25 | "uk": "Українська", 26 | "vi": "Tiếng Việt", 27 | "zh-tw": "中文 (繁體)" 28 | } 29 | -------------------------------------------------------------------------------- /client/src/views/applet.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.Applet = _kiwi.view.Panel.extend({ 2 | className: 'panel applet', 3 | initialize: function (options) { 4 | this.initializePanel(options); 5 | } 6 | }); -------------------------------------------------------------------------------- /client/src/views/apptoolbar.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.AppToolbar = Backbone.View.extend({ 2 | events: { 3 | 'click .settings': 'clickSettings', 4 | 'click .startup': 'clickStartup' 5 | }, 6 | 7 | initialize: function () { 8 | // Remove the new connection/startup link if the server has disabled server changing 9 | if (_kiwi.app.server_settings.connection && !_kiwi.app.server_settings.connection.allow_change) { 10 | this.$('.startup').css('display', 'none'); 11 | } 12 | }, 13 | 14 | clickSettings: function (event) { 15 | event.preventDefault(); 16 | _kiwi.app.controlbox.processInput('/settings'); 17 | }, 18 | 19 | clickStartup: function (event) { 20 | event.preventDefault(); 21 | _kiwi.app.startup_applet.view.show(); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/views/channeltools.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.ChannelTools = Backbone.View.extend({ 2 | events: { 3 | 'click .channel_info': 'infoClick', 4 | 'click .channel_part': 'partClick' 5 | }, 6 | 7 | initialize: function () {}, 8 | 9 | infoClick: function (event) { 10 | new _kiwi.model.ChannelInfo({channel: _kiwi.app.panels().active}); 11 | }, 12 | 13 | partClick: function (event) { 14 | _kiwi.app.connections.active_connection.gateway.part(_kiwi.app.panels().active.get('name')); 15 | } 16 | }); -------------------------------------------------------------------------------- /client/src/views/favicon.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.Favicon = Backbone.View.extend({ 2 | initialize: function () { 3 | var that = this, 4 | $win = $(window); 5 | 6 | this.has_focus = true; 7 | this.highlight_count = 0; 8 | // Check for html5 canvas support 9 | this.has_canvas_support = !!window.CanvasRenderingContext2D; 10 | 11 | // Store the original favicon 12 | this.original_favicon = $('link[rel~="icon"]')[0].href; 13 | 14 | // Create our favicon canvas 15 | this._createCanvas(); 16 | 17 | // Reset favicon notifications when user focuses window 18 | $win.on('focus', function () { 19 | that.has_focus = true; 20 | that._resetHighlights(); 21 | }); 22 | $win.on('blur', function () { 23 | that.has_focus = false; 24 | }); 25 | }, 26 | 27 | newHighlight: function () { 28 | var that = this; 29 | if (!this.has_focus) { 30 | this.highlight_count++; 31 | if (this.has_canvas_support) { 32 | this._drawFavicon(function() { 33 | that._drawBubble(that.highlight_count.toString()); 34 | that._refreshFavicon(that.canvas.toDataURL()); 35 | }); 36 | } 37 | } 38 | }, 39 | 40 | _resetHighlights: function () { 41 | var that = this; 42 | this.highlight_count = 0; 43 | this._refreshFavicon(this.original_favicon); 44 | }, 45 | 46 | _drawFavicon: function (callback) { 47 | var that = this, 48 | canvas = this.canvas, 49 | context = canvas.getContext('2d'), 50 | favicon_image = new Image(); 51 | 52 | // Allow cross origin resource requests 53 | favicon_image.crossOrigin = 'anonymous'; 54 | // Trigger the load event 55 | favicon_image.src = this.original_favicon; 56 | 57 | favicon_image.onload = function() { 58 | // Clear canvas from prevous iteration 59 | context.clearRect(0, 0, canvas.width, canvas.height); 60 | // Draw the favicon itself 61 | context.drawImage(favicon_image, 0, 0, canvas.width, canvas.height); 62 | callback(); 63 | }; 64 | }, 65 | 66 | _drawBubble: function (label) { 67 | var letter_spacing, 68 | bubble_width = 0, bubble_height = 0, 69 | canvas = this.canvas, 70 | context = test_context = canvas.getContext('2d'), 71 | canvas_width = canvas.width, 72 | canvas_height = canvas.height; 73 | 74 | // Different letter spacing for MacOS 75 | if (navigator.appVersion.indexOf("Mac") !== -1) { 76 | letter_spacing = -1.5; 77 | } 78 | else { 79 | letter_spacing = -1; 80 | } 81 | 82 | // Setup a test canvas to get text width 83 | test_context.font = context.font = 'bold 10px Arial'; 84 | test_context.textAlign = 'right'; 85 | this._renderText(test_context, label, 0, 0, letter_spacing); 86 | 87 | // Calculate bubble width based on letter spacing and padding 88 | bubble_width = test_context.measureText(label).width + letter_spacing * (label.length - 1) + 2; 89 | // Canvas does not have any way of measuring text height, so we just do it manually and add 1px top/bottom padding 90 | bubble_height = 9; 91 | 92 | // Set bubble coordinates 93 | bubbleX = canvas_width - bubble_width; 94 | bubbleY = canvas_height - bubble_height; 95 | 96 | // Draw bubble background 97 | context.fillStyle = 'red'; 98 | context.fillRect(bubbleX, bubbleY, bubble_width, bubble_height); 99 | 100 | // Draw the text 101 | context.fillStyle = 'white'; 102 | this._renderText(context, label, canvas_width - 1, canvas_height - 1, letter_spacing); 103 | }, 104 | 105 | _refreshFavicon: function (url) { 106 | $('link[rel~="icon"]').remove(); 107 | $('').appendTo($('head')); 108 | }, 109 | 110 | _createCanvas: function () { 111 | var canvas = document.createElement('canvas'); 112 | canvas.width = 16; 113 | canvas.height = 16; 114 | 115 | this.canvas = canvas; 116 | }, 117 | 118 | _renderText: function (context, text, x, y, letter_spacing) { 119 | // A hacky solution for letter-spacing, but works well with small favicon text 120 | // Modified from http://jsfiddle.net/davidhong/hKbJ4/ 121 | var current, 122 | characters = text.split('').reverse(), 123 | index = 0, 124 | currentPosition = x; 125 | 126 | while (index < text.length) { 127 | current = characters[index++]; 128 | context.fillText(current, currentPosition, y); 129 | currentPosition += (-1 * (context.measureText(current).width + letter_spacing)); 130 | } 131 | 132 | return context; 133 | } 134 | }); 135 | -------------------------------------------------------------------------------- /client/src/views/member.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.Member = Backbone.View.extend({ 2 | tagName: "li", 3 | initialize: function (options) { 4 | this.model.bind('change', this.render, this); 5 | this.render(); 6 | }, 7 | render: function () { 8 | var $this = this.$el, 9 | prefix_css_class = (this.model.get('modes') || []).join(' '); 10 | 11 | $this.attr('class', 'mode ' + prefix_css_class); 12 | $this.html('' + this.model.get("prefix") + '' + this.model.get("nick") + ''); 13 | 14 | return this; 15 | } 16 | }); -------------------------------------------------------------------------------- /client/src/views/memberlist.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.MemberList = Backbone.View.extend({ 2 | tagName: "div", 3 | events: { 4 | "click .nick": "nickClick", 5 | "contextmenu .nick": "nickClick", 6 | "dblclick .nick": "nickClick", 7 | "click .channel_info": "channelInfoClick" 8 | }, 9 | 10 | initialize: function (options) { 11 | this.model.bind('all', this.render, this); 12 | this.$el.appendTo('#kiwi .memberlists'); 13 | 14 | // Holds meta data. User counts, etc 15 | this.$meta = $('
').appendTo(this.$el); 16 | 17 | // The list for holding the nicks 18 | this.$list = $('
    ').appendTo(this.$el); 19 | }, 20 | render: function () { 21 | var that = this; 22 | 23 | this.$list.empty(); 24 | this.model.forEach(function (member) { 25 | member.view.$el.data('member', member); 26 | that.$list.append(member.view.$el); 27 | }); 28 | 29 | // User count 30 | if(this.model.channel.isActive()) { 31 | this.renderMeta(); 32 | } 33 | 34 | return this; 35 | }, 36 | 37 | renderMeta: function() { 38 | var members_count = this.model.length + ' ' + translateText('client_applets_chanlist_users'); 39 | this.$meta.text(members_count); 40 | }, 41 | 42 | nickClick: function (event) { 43 | var $target = $(event.currentTarget).parent('li'), 44 | member = $target.data('member'); 45 | 46 | _kiwi.global.events.emit('nick:select', { 47 | target: $target, 48 | member: member, 49 | network: this.model.channel.get('network'), 50 | source: 'nicklist', 51 | $event: event 52 | }) 53 | .then(_.bind(this.openUserMenuForItem, this, $target)); 54 | }, 55 | 56 | 57 | // Open a user menu for the given userlist item (
  • ) 58 | openUserMenuForItem: function($target) { 59 | var member = $target.data('member'), 60 | userbox, 61 | are_we_an_op = !!this.model.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op'); 62 | 63 | userbox = new _kiwi.view.UserBox(); 64 | userbox.setTargets(member, this.model.channel); 65 | userbox.displayOpItems(are_we_an_op); 66 | 67 | var menu = new _kiwi.view.MenuBox(member.get('nick') || 'User'); 68 | menu.addItem('userbox', userbox.$el); 69 | menu.showFooter(false); 70 | 71 | _kiwi.global.events.emit('usermenu:created', {menu: menu, userbox: userbox, user: member}) 72 | .then(_.bind(function() { 73 | menu.show(); 74 | 75 | var target_offset = $target.offset(), 76 | t = target_offset.top, 77 | m_bottom = t + menu.$el.outerHeight(), // Where the bottom of menu will be 78 | memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight(), 79 | l = target_offset.left, 80 | m_right = l + menu.$el.outerWidth(), // Where the left of menu will be 81 | memberlist_right = this.$el.parent().offset().left + this.$el.parent().outerWidth(); 82 | 83 | // If the bottom of the userbox is going to be too low.. raise it 84 | if (m_bottom > memberlist_bottom){ 85 | t = memberlist_bottom - menu.$el.outerHeight(); 86 | } 87 | 88 | // If the top of the userbox is going to be too high.. lower it 89 | if (t < 0){ 90 | t = 0; 91 | } 92 | 93 | // If the right of the userbox is going off screen.. bring it in 94 | if (m_right > memberlist_right){ 95 | l = memberlist_right - menu.$el.outerWidth(); 96 | } 97 | 98 | // Set the new positon 99 | menu.$el.offset({ 100 | left: l, 101 | top: t 102 | }); 103 | 104 | }, this)) 105 | .then(null, _.bind(function() { 106 | userbox = null; 107 | 108 | menu.dispose(); 109 | menu = null; 110 | }, this)); 111 | }, 112 | 113 | 114 | channelInfoClick: function(event) { 115 | new _kiwi.model.ChannelInfo({channel: this.model.channel}); 116 | }, 117 | 118 | 119 | show: function () { 120 | $('#kiwi .memberlists').children().removeClass('active'); 121 | $(this.el).addClass('active'); 122 | 123 | this.renderMeta(); 124 | } 125 | }); -------------------------------------------------------------------------------- /client/src/views/menubox.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.MenuBox = Backbone.View.extend({ 2 | events: { 3 | 'click .ui_menu_foot .close, a.close_menu': 'dispose' 4 | }, 5 | 6 | initialize: function(title) { 7 | var that = this; 8 | 9 | this.$el = $('
    '); 10 | 11 | this._title = title || ''; 12 | this._items = {}; 13 | this._display_footer = true; 14 | this._close_on_blur = true; 15 | }, 16 | 17 | 18 | render: function() { 19 | var that = this, 20 | $title, 21 | $items = that.$el.find('.items'); 22 | 23 | $items.find('*').remove(); 24 | 25 | if (this._title) { 26 | $title = $('
    ') 27 | .text(this._title); 28 | 29 | this.$el.prepend($title); 30 | } 31 | 32 | _.each(this._items, function(item) { 33 | var $item = $('
    ') 34 | .append(item); 35 | 36 | $items.append($item); 37 | }); 38 | 39 | if (this._display_footer) 40 | this.$el.append(''); 41 | 42 | }, 43 | 44 | 45 | setTitle: function(new_title) { 46 | this._title = new_title; 47 | 48 | if (!this._title) 49 | return; 50 | 51 | this.$el.find('.ui_menu_title').text(this._title); 52 | }, 53 | 54 | 55 | onDocumentClick: function(event) { 56 | var $target = $(event.target); 57 | 58 | if (!this._close_on_blur) 59 | return; 60 | 61 | // If this is not itself AND we don't contain this element, dispose $el 62 | if ($target[0] != this.$el[0] && this.$el.has($target).length === 0) 63 | this.dispose(); 64 | }, 65 | 66 | 67 | dispose: function() { 68 | _.each(this._items, function(item) { 69 | item.dispose && item.dispose(); 70 | item.remove && item.remove(); 71 | }); 72 | 73 | this._items = null; 74 | this.remove(); 75 | 76 | if (this._close_proxy) 77 | $(document).off('click', this._close_proxy); 78 | }, 79 | 80 | 81 | addItem: function(item_name, $item) { 82 | if ($item.is('a')) $item.addClass('fa fa-chevron-right'); 83 | this._items[item_name] = $item; 84 | }, 85 | 86 | 87 | removeItem: function(item_name) { 88 | delete this._items[item_name]; 89 | }, 90 | 91 | 92 | showFooter: function(show) { 93 | this._display_footer = show; 94 | }, 95 | 96 | 97 | closeOnBlur: function(close_it) { 98 | this._close_on_blur = close_it; 99 | }, 100 | 101 | 102 | show: function() { 103 | var that = this, 104 | $controlbox, menu_height; 105 | 106 | this.render(); 107 | this.$el.appendTo(_kiwi.app.view.$el); 108 | 109 | // Ensure the menu doesn't get too tall to overlap the input bar at the bottom 110 | $controlbox = _kiwi.app.view.$el.find('.controlbox'); 111 | $items = this.$el.find('.items'); 112 | menu_height = this.$el.outerHeight() - $items.outerHeight(); 113 | 114 | $items.css({ 115 | 'overflow-y': 'auto', 116 | 'max-height': $controlbox.offset().top - this.$el.offset().top - menu_height 117 | }); 118 | 119 | // We add this document click listener on the next javascript tick. 120 | // If the current tick is handling an existing click event (such as the nicklist click handler), 121 | // the click event bubbles up and hits the document therefore calling this callback to 122 | // remove this menubox before it's even shown. 123 | setTimeout(function() { 124 | that._close_proxy = function(event) { 125 | that.onDocumentClick(event); 126 | }; 127 | $(document).on('click', that._close_proxy); 128 | }, 0); 129 | } 130 | }); 131 | -------------------------------------------------------------------------------- /client/src/views/networktabs.js: -------------------------------------------------------------------------------- 1 | // Model for this = _kiwi.model.NetworkPanelList 2 | _kiwi.view.NetworkTabs = Backbone.View.extend({ 3 | tagName: 'ul', 4 | className: 'connections', 5 | 6 | initialize: function() { 7 | this.model.on('add', this.networkAdded, this); 8 | this.model.on('remove', this.networkRemoved, this); 9 | 10 | this.$el.appendTo(_kiwi.app.view.$el.find('.tabs')); 11 | }, 12 | 13 | networkAdded: function(network) { 14 | $('
  • ') 15 | .append(network.panels.view.$el) 16 | .appendTo(this.$el); 17 | }, 18 | 19 | networkRemoved: function(network) { 20 | // Remove the containing list element 21 | network.panels.view.$el.parent().remove(); 22 | 23 | network.panels.view.remove(); 24 | 25 | _kiwi.app.view.doLayout(); 26 | } 27 | }); -------------------------------------------------------------------------------- /client/src/views/nickchangebox.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.NickChangeBox = Backbone.View.extend({ 2 | events: { 3 | 'submit': 'changeNick', 4 | 'click .cancel': 'close' 5 | }, 6 | 7 | initialize: function () { 8 | var text = { 9 | new_nick: _kiwi.global.i18n.translate('client_views_nickchangebox_new').fetch(), 10 | change: _kiwi.global.i18n.translate('client_views_nickchangebox_change').fetch(), 11 | cancel: _kiwi.global.i18n.translate('client_views_nickchangebox_cancel').fetch() 12 | }; 13 | this.$el = $(_.template($('#tmpl_nickchange').html().trim())(text)); 14 | }, 15 | 16 | render: function () { 17 | // Add the UI component and give it focus 18 | _kiwi.app.controlbox.$el.prepend(this.$el); 19 | this.$el.find('input').focus(); 20 | 21 | this.$el.css('bottom', _kiwi.app.controlbox.$el.outerHeight(true)); 22 | }, 23 | 24 | close: function () { 25 | this.$el.remove(); 26 | this.trigger('close'); 27 | }, 28 | 29 | changeNick: function (event) { 30 | event.preventDefault(); 31 | 32 | var connection = _kiwi.app.connections.active_connection; 33 | this.listenTo(connection, 'change:nick', function() { 34 | this.close(); 35 | }); 36 | 37 | connection.gateway.changeNick(this.$('input').val()); 38 | } 39 | }); -------------------------------------------------------------------------------- /client/src/views/notification.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.Notification = Backbone.View.extend({ 2 | className: 'notification', 3 | 4 | events: { 5 | 'click .close': 'close' 6 | }, 7 | 8 | initialize: function(title, content) { 9 | this.title = title; 10 | this.content = content; 11 | }, 12 | 13 | render: function() { 14 | this.$el.html($('#tmpl_notifications').html()); 15 | this.$('h6').text(this.title); 16 | 17 | // HTML string or jquery object 18 | if (typeof this.content === 'string') { 19 | this.$('.content').html(this.content); 20 | } else if (typeof this.content === 'object') { 21 | this.$('.content').empty().append(this.content); 22 | } 23 | 24 | return this; 25 | }, 26 | 27 | show: function() { 28 | var that = this; 29 | 30 | this.render().$el.appendTo(_kiwi.app.view.$el); 31 | 32 | // The element won't have any CSS transitions applied 33 | // until after a tick + paint. 34 | _.defer(function() { 35 | that.$el.addClass('show'); 36 | }); 37 | }, 38 | 39 | close: function() { 40 | this.remove(); 41 | } 42 | }); -------------------------------------------------------------------------------- /client/src/views/panel.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.Panel = Backbone.View.extend({ 2 | tagName: "div", 3 | className: "panel", 4 | 5 | events: { 6 | }, 7 | 8 | initialize: function (options) { 9 | this.initializePanel(options); 10 | }, 11 | 12 | initializePanel: function (options) { 13 | this.$el.css('display', 'none'); 14 | options = options || {}; 15 | 16 | // Containing element for this panel 17 | if (options.container) { 18 | this.$container = $(options.container); 19 | } else { 20 | this.$container = $('#kiwi .panels .container1'); 21 | } 22 | 23 | this.$el.appendTo(this.$container); 24 | 25 | this.alert_level = 0; 26 | 27 | this.model.set({"view": this}, {"silent": true}); 28 | 29 | this.listenTo(this.model, 'change:activity_counter', function(model, new_count) { 30 | var $act = this.model.tab.find('.activity'); 31 | 32 | if (new_count > 999) { 33 | $act.text('999+'); 34 | } else { 35 | $act.text(new_count); 36 | } 37 | 38 | if (new_count === 0) { 39 | $act.addClass('zero'); 40 | } else { 41 | $act.removeClass('zero'); 42 | } 43 | }); 44 | }, 45 | 46 | render: function () { 47 | }, 48 | 49 | 50 | show: function () { 51 | var $this = this.$el; 52 | 53 | // Hide all other panels and show this one 54 | this.$container.children('.panel').css('display', 'none'); 55 | $this.css('display', 'block'); 56 | 57 | // Show this panels memberlist 58 | var members = this.model.get("members"); 59 | if (members) { 60 | _kiwi.app.rightbar.show(); 61 | members.view.show(); 62 | } else { 63 | _kiwi.app.rightbar.hide(); 64 | } 65 | 66 | // Remove any alerts and activity counters for this panel 67 | this.alert('none'); 68 | this.model.set('activity_counter', 0); 69 | 70 | _kiwi.app.panels.trigger('active', this.model, _kiwi.app.panels().active); 71 | this.model.trigger('active', this.model); 72 | 73 | _kiwi.app.view.doLayout(); 74 | 75 | if (!this.model.isApplet()) 76 | this.scrollToBottom(true); 77 | }, 78 | 79 | 80 | alert: function (level) { 81 | // No need to highlight if this si the active panel 82 | if (this.model == _kiwi.app.panels().active) return; 83 | 84 | var types, type_idx; 85 | types = ['none', 'action', 'activity', 'highlight']; 86 | 87 | // Default alert level 88 | level = level || 'none'; 89 | 90 | // If this alert level does not exist, assume clearing current level 91 | type_idx = _.indexOf(types, level); 92 | if (!type_idx) { 93 | level = 'none'; 94 | type_idx = 0; 95 | } 96 | 97 | // Only 'upgrade' the alert. Never down (unless clearing) 98 | if (type_idx !== 0 && type_idx <= this.alert_level) { 99 | return; 100 | } 101 | 102 | // Clear any existing levels 103 | this.model.tab.removeClass(function (i, css) { 104 | return (css.match(/\balert_\S+/g) || []).join(' '); 105 | }); 106 | 107 | // Add the new level if there is one 108 | if (level !== 'none') { 109 | this.model.tab.addClass('alert_' + level); 110 | } 111 | 112 | this.alert_level = type_idx; 113 | }, 114 | 115 | 116 | // Scroll to the bottom of the panel 117 | scrollToBottom: function (force_down) { 118 | // If this isn't the active panel, don't scroll 119 | if (this.model !== _kiwi.app.panels().active) return; 120 | 121 | // Don't scroll down if we're scrolled up the panel a little 122 | if (force_down || this.$container.scrollTop() + this.$container.height() > this.$el.outerHeight() - 150) { 123 | this.$container[0].scrollTop = this.$container[0].scrollHeight; 124 | } 125 | } 126 | }); -------------------------------------------------------------------------------- /client/src/views/resizehandler.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.ResizeHandler = Backbone.View.extend({ 2 | events: { 3 | 'mousedown': 'startDrag', 4 | 'mouseup': 'stopDrag' 5 | }, 6 | 7 | initialize: function () { 8 | this.dragging = false; 9 | this.starting_width = {}; 10 | 11 | $(window).on('mousemove', $.proxy(this.onDrag, this)); 12 | }, 13 | 14 | startDrag: function (event) { 15 | this.dragging = true; 16 | }, 17 | 18 | stopDrag: function (event) { 19 | this.dragging = false; 20 | }, 21 | 22 | onDrag: function (event) { 23 | if (!this.dragging) return; 24 | 25 | var offset = $('#kiwi').offset().left; 26 | 27 | this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2) - offset); 28 | $('#kiwi .right_bar').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth())); 29 | _kiwi.app.view.doLayout(); 30 | } 31 | }); -------------------------------------------------------------------------------- /client/src/views/rightbar.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.RightBar = Backbone.View.extend({ 2 | events: { 3 | 'click .right-bar-toggle': 'onClickToggle', 4 | 'click .right-bar-toggle-inner': 'onClickToggle' 5 | }, 6 | 7 | initialize: function() { 8 | this.keep_hidden = false; 9 | this.hidden = this.$el.hasClass('disabled'); 10 | 11 | this.updateIcon(); 12 | }, 13 | 14 | 15 | hide: function() { 16 | this.hidden = true; 17 | this.$el.addClass('disabled'); 18 | 19 | this.updateIcon(); 20 | }, 21 | 22 | 23 | show: function() { 24 | this.hidden = false; 25 | 26 | if (!this.keep_hidden) 27 | this.$el.removeClass('disabled'); 28 | 29 | this.updateIcon(); 30 | }, 31 | 32 | 33 | // Toggle if the rightbar should be shown or not 34 | toggle: function(keep_hidden) { 35 | // Hacky, but we need to ignore the toggle() call from doLayout() as we are overriding it 36 | if (this.ignore_layout) 37 | return true; 38 | 39 | if (typeof keep_hidden === 'undefined') { 40 | this.keep_hidden = !this.keep_hidden; 41 | } else { 42 | this.keep_hidden = keep_hidden; 43 | } 44 | 45 | if (this.keep_hidden || this.hidden) { 46 | this.$el.addClass('disabled'); 47 | // Remove the right bar width if it has been set manually 48 | this.$el.css('width', ''); 49 | } else { 50 | this.$el.removeClass('disabled'); 51 | } 52 | 53 | this.updateIcon(); 54 | }, 55 | 56 | 57 | updateIcon: function() { 58 | var $toggle = this.$('.right-bar-toggle'), 59 | $icon = $toggle.find('i'); 60 | 61 | if (!this.hidden && this.keep_hidden) { 62 | $toggle.show(); 63 | } else { 64 | $toggle.hide(); 65 | } 66 | 67 | if (this.keep_hidden) { 68 | $icon.removeClass('fa fa-angle-double-right').addClass('fa fa-users'); 69 | } else { 70 | $icon.removeClass('fa fa-users').addClass('fa fa-angle-double-right'); 71 | } 72 | }, 73 | 74 | 75 | onClickToggle: function(event) { 76 | this.toggle(); 77 | 78 | // Hacky, but we need to ignore the toggle() call from doLayout() as we are overriding it 79 | this.ignore_layout = true; 80 | _kiwi.app.view.doLayout(); 81 | 82 | // No longer ignoring the toggle() call from doLayout() 83 | delete this.ignore_layout; 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /client/src/views/statusmessage.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.StatusMessage = Backbone.View.extend({ 2 | initialize: function () { 3 | this.$el.hide(); 4 | 5 | // Timer for hiding the message after X seconds 6 | this.tmr = null; 7 | }, 8 | 9 | text: function (text, opt) { 10 | // Defaults 11 | opt = opt || {}; 12 | opt.type = opt.type || ''; 13 | opt.timeout = opt.timeout || 5000; 14 | 15 | this.$el.text(text).addClass(opt.type); 16 | this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view)); 17 | 18 | if (opt.timeout) this.doTimeout(opt.timeout); 19 | }, 20 | 21 | html: function (html, opt) { 22 | // Defaults 23 | opt = opt || {}; 24 | opt.type = opt.type || ''; 25 | opt.timeout = opt.timeout || 5000; 26 | 27 | this.$el.html(html).addClass(opt.type); 28 | this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view)); 29 | 30 | if (opt.timeout) this.doTimeout(opt.timeout); 31 | }, 32 | 33 | hide: function () { 34 | this.$el.slideUp($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view)); 35 | }, 36 | 37 | doTimeout: function (length) { 38 | if (this.tmr) clearTimeout(this.tmr); 39 | var that = this; 40 | this.tmr = setTimeout(function () { that.hide(); }, length); 41 | } 42 | }); -------------------------------------------------------------------------------- /client/src/views/topicbar.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.TopicBar = Backbone.View.extend({ 2 | events: { 3 | 'keydown div': 'process' 4 | }, 5 | 6 | initialize: function () { 7 | _kiwi.app.panels.bind('active', function (active_panel) { 8 | // If it's a channel topic, update and make editable 9 | if (active_panel.isChannel()) { 10 | this.setCurrentTopicFromChannel(active_panel); 11 | this.$el.find('div').attr('contentEditable', true); 12 | 13 | } else { 14 | // Not a channel topic.. clear and make uneditable 15 | this.$el.find('div').attr('contentEditable', false) 16 | .text(''); 17 | } 18 | }, this); 19 | }, 20 | 21 | process: function (ev) { 22 | var inp = $(ev.currentTarget), 23 | inp_val = inp.text(); 24 | 25 | // Only allow topic editing if this is a channel panel 26 | if (!_kiwi.app.panels().active.isChannel()) { 27 | return false; 28 | } 29 | 30 | // If hit return key, update the current topic 31 | if (ev.keyCode === 13) { 32 | _kiwi.app.connections.active_connection.gateway.topic(_kiwi.app.panels().active.get('name'), inp_val); 33 | return false; 34 | } 35 | }, 36 | 37 | setCurrentTopic: function (new_topic) { 38 | new_topic = new_topic || ''; 39 | 40 | // We only want a plain text version 41 | $('div', this.$el).html(formatIRCMsg(_.escape(new_topic))); 42 | }, 43 | 44 | setCurrentTopicFromChannel: function(channel) { 45 | var set_by = channel.get('topic_set_by'), 46 | set_by_text = ''; 47 | 48 | this.setCurrentTopic(channel.get("topic")); 49 | 50 | if (set_by) { 51 | set_by_text += translateText('client_models_network_topic', [set_by.nick, _kiwi.utils.formatDate(set_by.when)]); 52 | this.$el.attr('title', set_by_text); 53 | } else { 54 | this.$el.attr('title', ''); 55 | } 56 | } 57 | }); -------------------------------------------------------------------------------- /client/src/views/userbox.js: -------------------------------------------------------------------------------- 1 | _kiwi.view.UserBox = Backbone.View.extend({ 2 | events: { 3 | 'click .query': 'queryClick', 4 | 'click .info': 'infoClick', 5 | 'change .ignore': 'ignoreChange', 6 | 'click .ignore': 'ignoreClick', 7 | 'click .op': 'opClick', 8 | 'click .deop': 'deopClick', 9 | 'click .voice': 'voiceClick', 10 | 'click .devoice': 'devoiceClick', 11 | 'click .kick': 'kickClick', 12 | 'click .ban': 'banClick' 13 | }, 14 | 15 | initialize: function () { 16 | var text = { 17 | op: _kiwi.global.i18n.translate('client_views_userbox_op').fetch(), 18 | de_op: _kiwi.global.i18n.translate('client_views_userbox_deop').fetch(), 19 | voice: _kiwi.global.i18n.translate('client_views_userbox_voice').fetch(), 20 | de_voice: _kiwi.global.i18n.translate('client_views_userbox_devoice').fetch(), 21 | kick: _kiwi.global.i18n.translate('client_views_userbox_kick').fetch(), 22 | ban: _kiwi.global.i18n.translate('client_views_userbox_ban').fetch(), 23 | message: _kiwi.global.i18n.translate('client_views_userbox_query').fetch(), 24 | info: _kiwi.global.i18n.translate('client_views_userbox_whois').fetch(), 25 | ignore: _kiwi.global.i18n.translate('client_views_userbox_ignore').fetch() 26 | }; 27 | this.$el = $(_.template($('#tmpl_userbox').html().trim())(text)); 28 | }, 29 | 30 | setTargets: function (user, channel) { 31 | this.user = user; 32 | this.channel = channel; 33 | 34 | var user_mask = toUserMask(this.user.get('nick')), 35 | is_ignored = _kiwi.app.connections.active_connection.isUserIgnored(user_mask); 36 | 37 | this.$('.ignore input').attr('checked', is_ignored ? 'checked' : false); 38 | }, 39 | 40 | displayOpItems: function(display_items) { 41 | if (display_items) { 42 | this.$el.find('.if_op').css('display', 'block'); 43 | } else { 44 | this.$el.find('.if_op').css('display', 'none'); 45 | } 46 | }, 47 | 48 | queryClick: function (event) { 49 | var nick = this.user.get('nick'); 50 | _kiwi.app.connections.active_connection.createQuery(nick); 51 | }, 52 | 53 | infoClick: function (event) { 54 | _kiwi.app.controlbox.processInput('/whois ' + this.user.get('nick')); 55 | }, 56 | 57 | ignoreClick: function (event) { 58 | // Stop the menubox from closing since it will not update the checkbox otherwise 59 | event.stopPropagation(); 60 | }, 61 | 62 | ignoreChange: function (event) { 63 | if ($(event.currentTarget).find('input').is(':checked')) { 64 | _kiwi.app.controlbox.processInput('/ignore ' + this.user.get('nick')); 65 | } else { 66 | _kiwi.app.controlbox.processInput('/unignore ' + this.user.get('nick')); 67 | } 68 | }, 69 | 70 | opClick: function (event) { 71 | _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +o ' + this.user.get('nick')); 72 | }, 73 | 74 | deopClick: function (event) { 75 | _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -o ' + this.user.get('nick')); 76 | }, 77 | 78 | voiceClick: function (event) { 79 | _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +v ' + this.user.get('nick')); 80 | }, 81 | 82 | devoiceClick: function (event) { 83 | _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -v ' + this.user.get('nick')); 84 | }, 85 | 86 | kickClick: function (event) { 87 | // TODO: Enable the use of a custom kick message 88 | _kiwi.app.controlbox.processInput('/kick ' + this.user.get('nick') + ' Bye!'); 89 | }, 90 | 91 | banClick: function (event) { 92 | // TODO: Set ban on host, not just on nick 93 | _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +b ' + this.user.get('nick') + '!*'); 94 | } 95 | }); 96 | -------------------------------------------------------------------------------- /kiwi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | basedir=`dirname "$0"` 3 | 4 | case `uname` in 5 | *CYGWIN*) basedir=`cygpath -w "$basedir"`;; 6 | esac 7 | 8 | $(command -v nodejs || command -v node) $basedir/server/helpers/launcher.js "$@" 9 | ret=$? 10 | 11 | exit $ret 12 | -------------------------------------------------------------------------------- /kiwi.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | node "%~dp0\server\helpers\launcher.js" %* -------------------------------------------------------------------------------- /man/kiwiirc.1: -------------------------------------------------------------------------------- 1 | .TH KIWIIRC 1 2 | .SH NAME 3 | kiwiirc \- the Kiwi IRC web client 4 | .SH SYNOPSIS 5 | .B kiwiirc 6 | [\fB\-f\fR | \fBstart\fR | \fBstop\fR | \fBrestart\fR | \fBstatus\fR | \fBreconfig\fR | \fBbuild\fR] [\fB\-c\fR\ \fIconfig_file\fR] [\fB\-p\fR\ \fIpid_file\fR] 7 | .SH DESCRIPTION 8 | Kiwi IRC is a web-based IRC client built using Node.js. 9 | .SH OPTIONS 10 | .TP 11 | .BR \-f\fR 12 | Run the kiwi server in the foreground. When run in the foreground kiwi will print all logs to stdout. 13 | .TP 14 | .BR start\fR 15 | Start the kiwi server and fork in to the background. 16 | .TP 17 | .BR stop\fR 18 | Stop the kiwi server if it is running. 19 | .TP 20 | .BR restart\fR 21 | Restart the kiwi server. If it is not running, it will be started. 22 | .TP 23 | .BR status\fR 24 | Output to stdout whether the kiwi server is running or not. If it is running it will also display the server's PID. 25 | .TP 26 | .BR reconfig\fR 27 | Forces the kiwi server to re-load its configuration file by sending the process \fBSIGUSR1\fR. 28 | .TP 29 | .BR build\fR 30 | Builds the necessary client files. This will process translation files, build the HTML file and bundle and minify the client-side JavaScript code. 31 | .TP 32 | .BR \-c\fR\ \fIconfig_file\fR 33 | Use the specified \fIconfig_file\fR instead of searching the pre-defined locations. By default the kiwi server will look in \fI/etc/kiwiirc\fR and the installation directory for a configuration file named \fIconfig.js\fR. 34 | .TP 35 | .BR \-p\fR\ \fIpid_file\fR 36 | Write the kiwi server's PID to this file. By default, kiwi will write the PID to a file called \fIkiwiirc.pid\fR in the installation directory unless it is started with the \fB\-f\fR flag. 37 | .SH FILES 38 | .TP 39 | .IR /etc/kiwiirc/config.js 40 | If this file exists, and the \fB\-c\fR option has not been specified, kiwi will use this file as its configuration file. If it does not exist, kiwi will try to locate \fIconfig.js\fR in its installation directory. 41 | .SH BUGS 42 | Please report bugs on the GitHub issues page at 43 | .UR https://github.com/prawnsalad/KiwiIRC/issues 44 | .UE 45 | or on the mailing list at 46 | .MT kiwiirc@googlegroups.com 47 | .ME . -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kiwiirc", 3 | "version": "0.9.4", 4 | "description": "A hand-crafted webirc client", 5 | "homepage": "https://www.kiwiirc.com/", 6 | "license": "AGPL-3.0", 7 | "preferGlobal": "true", 8 | "bin": { 9 | "kiwiirc": "./kiwi" 10 | }, 11 | "man": "./man/kiwiirc.1", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/prawnsalad/KiwiIRC.git" 15 | }, 16 | "dependencies": { 17 | "daemonize2": "0.4.2", 18 | "engine.io": "^1.5.4", 19 | "es6-promise": "^3.0.2", 20 | "eventemitter2": "0.4.14", 21 | "iconv-lite": "0.4.11", 22 | "ipaddr.js": "^1.0.3", 23 | "lodash": "^3.10.1", 24 | "negotiator": "^0.5.3", 25 | "node-static": "^0.7.7", 26 | "po2json": "^0.4.1", 27 | "socksjs": "^0.5.0", 28 | "spdy": "^2.0.5", 29 | "uglify-js": "^2.4.24", 30 | "winston": "^1.0.1" 31 | }, 32 | "engines": { 33 | "node": ">=0.10" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "immed": true, 6 | "indent": 4, 7 | "latedef": "nofunc", 8 | "newcap": true, 9 | "noarg": true, 10 | "undef": true, 11 | "unused": true, 12 | "trailing": true 13 | } -------------------------------------------------------------------------------- /server/configuration.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | events = require('events'), 3 | util = require('util'), 4 | path = require('path'), 5 | winston = require('winston'); 6 | 7 | var config_filename = 'config.js', 8 | config_dirs = ['/etc/kiwiirc/', __dirname + '/../'], 9 | environment = 'production', 10 | loaded_config = Object.create(null); 11 | 12 | var Config = function () { 13 | events.EventEmitter.call(this); 14 | }; 15 | util.inherits(Config, events.EventEmitter); 16 | 17 | Config.prototype.loadConfig = function (manual_config_file) { 18 | var new_config, 19 | conf_filepath, 20 | i; 21 | 22 | if ((manual_config_file) || (this.manual_config_file)) { 23 | manual_config_file = path.resolve(path.normalize(manual_config_file || this.manual_config_file)); 24 | if (fs.existsSync(manual_config_file)) { 25 | try { 26 | if (fs.lstatSync(manual_config_file).isFile() === true) { 27 | // Clear the loaded config cache 28 | delete require.cache[require.resolve(manual_config_file)]; 29 | 30 | // Try load the new config file 31 | new_config = require(manual_config_file); 32 | 33 | // Save location of configuration file so that we can re-load it later 34 | this.manual_config_file = manual_config_file; 35 | } 36 | } catch (e) { 37 | winston.error('An error occured parsing the config file %s: %s', manual_config_file, e.message); 38 | process.exit(1); 39 | } 40 | } else { 41 | winston.error('Could not find config file %s', manual_config_file); 42 | process.exit(1); 43 | } 44 | } else { 45 | // Loop through the possible config paths and find a usable one 46 | for (i = 0; i < config_dirs.length; i++) { 47 | conf_filepath = config_dirs[i] + config_filename; 48 | 49 | try { 50 | if (fs.lstatSync(conf_filepath).isFile() === true) { 51 | // Clear the loaded config cache 52 | delete require.cache[require.resolve(conf_filepath)]; 53 | 54 | // Try load the new config file 55 | new_config = require(conf_filepath); 56 | break; 57 | } 58 | } catch (e) { 59 | switch (e.code) { 60 | case 'ENOENT': // No file/dir 61 | break; 62 | default: 63 | winston.warn('An error occured parsing the config file %s%s: %s', config_dirs[i], config_filename, e.message); 64 | return false; 65 | } 66 | continue; 67 | } 68 | } 69 | } 70 | 71 | if (new_config) { 72 | loaded_config = new_config; 73 | global.config = new_config[environment] || {}; 74 | this.emit('loaded'); 75 | return loaded_config; 76 | } else { 77 | return false; 78 | } 79 | }; 80 | 81 | 82 | 83 | Config.prototype.setEnvironment = function (new_environment) { 84 | environment = new_environment; 85 | }; 86 | 87 | // Get the current config. Optionally for a different environment than currently set 88 | Config.prototype.get = function (specific_environment) { 89 | specific_environment = specific_environment || environment; 90 | 91 | return loaded_config[specific_environment] || {}; 92 | }; 93 | 94 | module.exports = new Config(); 95 | -------------------------------------------------------------------------------- /server/ee.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | EventEmitter2 = require('eventemitter2').EventEmitter2; 3 | 4 | 5 | var EE = function() { 6 | EventEmitter2.apply(this, arguments); 7 | }; 8 | util.inherits(EE, EventEmitter2); 9 | 10 | 11 | EE.prototype.emit = function() { 12 | arguments[0] = arguments[0].toLowerCase(); 13 | EventEmitter2.prototype.emit.apply(this, arguments); 14 | }; 15 | 16 | 17 | EE.prototype.on = function() { 18 | arguments[0] = arguments[0].toLowerCase(); 19 | EventEmitter2.prototype.on.apply(this, arguments); 20 | }; 21 | 22 | 23 | module.exports = EE; -------------------------------------------------------------------------------- /server/helpers/configloader.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var config = require('../configuration.js'), 3 | conf_switch = process.argv.indexOf('-c'); 4 | 5 | if (conf_switch !== -1) { 6 | if (process.argv[conf_switch + 1]) { 7 | return config.loadConfig(process.argv[conf_switch + 1]); 8 | } 9 | } 10 | 11 | return config.loadConfig(); 12 | }; 13 | -------------------------------------------------------------------------------- /server/helpers/launcher.js: -------------------------------------------------------------------------------- 1 | var kiwi_app = '../kiwi.js'; 2 | var pidfile = '../../kiwiirc.pid'; 3 | var pidfile_arg; 4 | 5 | // Check if a pidfile has been set as an argument 6 | if (process.argv.indexOf('-p') > -1) { 7 | pidfile_arg = process.argv[process.argv.indexOf('-p') + 1]; 8 | 9 | if (pidfile_arg) { 10 | // Don't set the relative path if we have an absolute path given to us 11 | if (['/', '\\', '.'].indexOf(pidfile_arg[0]) === -1) { 12 | pidfile = '../../' + pidfile_arg; 13 | } else { 14 | pidfile = pidfile_arg; 15 | } 16 | } 17 | } 18 | 19 | 20 | var daemon = require('daemonize2').setup({ 21 | main: kiwi_app, 22 | name: 'kiwiirc', 23 | pidfile: pidfile 24 | }); 25 | 26 | switch (process.argv[2]) { 27 | case '-f': 28 | require(kiwi_app); 29 | break; 30 | 31 | case 'start': 32 | if (process.argv.indexOf('-f') > -1) { 33 | require(kiwi_app); 34 | } else { 35 | daemon.start(); 36 | } 37 | break; 38 | 39 | case 'stop': 40 | daemon.stop(); 41 | break; 42 | 43 | case 'restart': 44 | daemon.stop(function(err) { 45 | daemon.start(); 46 | }); 47 | break; 48 | 49 | case 'status': 50 | var pid = daemon.status(); 51 | if (pid) 52 | console.log('Daemon running. PID: ' + pid); 53 | else 54 | console.log('Daemon is not running.'); 55 | break; 56 | 57 | case 'reconfig': 58 | console.log('Loading new config..'); 59 | daemon.sendSignal("SIGUSR1"); 60 | break; 61 | 62 | case 'stats': 63 | console.log('Writing stats to log file..'); 64 | daemon.sendSignal("SIGUSR2"); 65 | break; 66 | 67 | case 'build': 68 | require('./build.js'); 69 | break; 70 | 71 | default: 72 | console.log('Usage: [-f|start|stop|restart|status|reconfig|build [-v] [-c ] [-p ]]'); 73 | } 74 | -------------------------------------------------------------------------------- /server/identd.js: -------------------------------------------------------------------------------- 1 | var net = require('net'), 2 | winston = require('winston'); 3 | 4 | var IdentdServer = module.exports = function(opts) { 5 | 6 | var that = this; 7 | 8 | var default_user_id = 'kiwi', 9 | default_system_id = 'UNIX-KiwiIRC'; 10 | 11 | // Option defaults 12 | opts = opts || {}; 13 | opts.bind_addr = opts.bind_addr || '0.0.0.0'; 14 | opts.bind_port = opts.bind_port || 113; 15 | opts.system_id = opts.system_id || default_system_id; 16 | opts.user_id = opts.user_id || default_user_id; 17 | 18 | 19 | var server = net.createServer(function(socket) { 20 | var buffer = ''; 21 | 22 | socket.on('data', function(data){ 23 | var data_line, response; 24 | 25 | buffer += data.toString(); 26 | 27 | // If we exceeed 512 bytes, presume a flood and disconnect 28 | if (buffer.length < 512) { 29 | 30 | // Wait until we have a full line of data before processing it 31 | if (buffer.indexOf('\n') === -1) 32 | return; 33 | 34 | // Get the first line of data and process it for a rsponse 35 | data_line = buffer.split('\n')[0]; 36 | response = that.processLine(data_line); 37 | 38 | } 39 | 40 | // Close down the socket while sending the response 41 | socket.removeAllListeners(); 42 | socket.end(response); 43 | }); 44 | 45 | }); 46 | 47 | server.on('listening', function() { 48 | var addr = server.address(); 49 | winston.info('Ident Server listening on %s:%s', addr.address, addr.port); 50 | }); 51 | 52 | 53 | this.start = function() { 54 | server.listen(opts.bind_port, opts.bind_addr); 55 | }; 56 | 57 | this.stop = function(callback) { 58 | server.close(callback); 59 | }; 60 | 61 | 62 | /** 63 | * Process a line of data for an Identd response 64 | * 65 | * @param {String} The line of data to process 66 | * @return {String} Data to send back to the Identd client 67 | */ 68 | this.processLine = function(line) { 69 | var ports = line.split(','), 70 | port_here = 0, 71 | port_there = 0; 72 | 73 | // We need 2 port number to make this work 74 | if (ports.length < 2) 75 | return; 76 | 77 | port_here = parseInt(ports[0], 10); 78 | port_there = parseInt(ports[1], 10); 79 | 80 | // Make sure we have both ports to work with 81 | if (!port_here || !port_there) 82 | return; 83 | 84 | if (typeof opts.user_id === 'function') { 85 | user = (opts.user_id(port_here, port_there) || '').toString() || default_user_id; 86 | } else { 87 | user = opts.user_id.toString(); 88 | } 89 | 90 | if (typeof opts.system_id === 'function') { 91 | system = (opts.system_id(port_here, port_there) || '').toString() || default_system_id; 92 | } else { 93 | system = opts.system_id.toString(); 94 | } 95 | 96 | return port_here.toString() + ' , ' + port_there.toString() + ' : USERID : ' + system + ' : ' + user; 97 | }; 98 | }; 99 | -------------------------------------------------------------------------------- /server/irc/eventbinder.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | 4 | module.exports.bindIrcEvents = function (events_scope, event_map, context, irc_connection) { 5 | var namespace_prefix = events_scope ? 6 | events_scope + ' ' : 7 | ''; 8 | 9 | // Make sure we have a holder for the bound events 10 | if (!event_map._bound_events) 11 | event_map._bound_events = {}; 12 | 13 | _.each(event_map, function (fn, event_name) { 14 | if (event_name[0] === '_') return; 15 | 16 | // Bind the event to `context`, storing it with the event listing 17 | if (!event_map._bound_events[event_name]) { 18 | event_map._bound_events[event_name] = _.bind(fn, context); 19 | } 20 | 21 | // Add the listener to the IRC connection object 22 | irc_connection.on(namespace_prefix + event_name, event_map._bound_events[event_name]); 23 | }); 24 | }; 25 | 26 | 27 | module.exports.unbindIrcEvents = function (events_scope, event_map, irc_connection) { 28 | var namespace_prefix = events_scope ? 29 | events_scope + ' ' : 30 | ''; 31 | 32 | // No bound events? Then we have nothing to do 33 | if (!event_map._bound_events) return; 34 | 35 | _.each(event_map, function(fn, event_name) { 36 | if (event_name[0] === '_') return; 37 | 38 | if (event_map._bound_events[event_name]) { 39 | // Remove the listener from the IRC connection object 40 | irc_connection.removeListener(namespace_prefix + event_name, event_map._bound_events[event_name]); 41 | 42 | // Remove the bound function as no longer needed 43 | delete event_map._bound_events[event_name]; 44 | } 45 | }); 46 | 47 | delete event_map._bound_events; 48 | }; -------------------------------------------------------------------------------- /server/irc/state.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | events = require('events'), 3 | _ = require('lodash'), 4 | winston = require('winston'), 5 | IrcConnection = require('./connection.js').IrcConnection; 6 | 7 | var State = function (client, save_state) { 8 | var that = this; 9 | 10 | events.EventEmitter.call(this); 11 | this.client = client; 12 | this.save_state = save_state || false; 13 | 14 | this.irc_connections = []; 15 | this.next_connection = 0; 16 | 17 | this.client.on('dispose', function () { 18 | if (!that.save_state) { 19 | _.each(that.irc_connections, function (irc_connection, i, cons) { 20 | if (!irc_connection) { 21 | return; 22 | } 23 | 24 | if (irc_connection.connected) { 25 | irc_connection.once('close', irc_connection.dispose.bind(irc_connection)); 26 | irc_connection.end('QUIT :' + (irc_connection.quit_message || global.config.quit_message || '')); 27 | } else { 28 | irc_connection.dispose(); 29 | } 30 | 31 | global.servers.removeConnection(irc_connection); 32 | cons[i] = null; 33 | }); 34 | 35 | that.dispose(); 36 | } 37 | }); 38 | }; 39 | 40 | util.inherits(State, events.EventEmitter); 41 | 42 | module.exports = State; 43 | 44 | State.prototype.connect = function (hostname, port, ssl, nick, user, options, callback) { 45 | var that = this; 46 | var con, con_num; 47 | 48 | // Check the per-server limit on the number of connections 49 | if ((global.config.max_server_conns > 0) && 50 | (!global.config.restrict_server) && 51 | (!(global.config.webirc_pass && global.config.webirc_pass[hostname])) && 52 | (!(global.config.ip_as_username && _.contains(global.config.ip_as_username, hostname))) && 53 | (global.servers.numOnHost(hostname) >= global.config.max_server_conns)) 54 | { 55 | return callback('Too many connections to host', {host: hostname, limit: global.config.max_server_conns}); 56 | } 57 | 58 | con_num = this.next_connection++; 59 | con = new IrcConnection( 60 | hostname, 61 | port, 62 | ssl, 63 | nick, 64 | user, 65 | options, 66 | this, 67 | con_num); 68 | 69 | this.irc_connections[con_num] = con; 70 | 71 | con.on('connected', function IrcConnectionConnection() { 72 | global.servers.addConnection(this); 73 | return callback(null, con_num); 74 | }); 75 | 76 | con.on('error', function IrcConnectionError(err) { 77 | var context = ''; 78 | 79 | // If we have any of the last lines stored, include them in the log for context 80 | if (con.last_few_lines.length > 0) { 81 | context = '\n' + con.last_few_lines.join('\n') + '\n'; 82 | } 83 | 84 | winston.warn('irc_connection error (%s):' + context, hostname, err); 85 | return callback(err.message); 86 | }); 87 | 88 | con.on('reconnecting', function IrcConnectionReconnecting() { 89 | that.sendIrcCommand('disconnect', {connection_id: con.con_num, reason: 'IRC server reconnecting'}); 90 | }); 91 | 92 | con.on('close', function IrcConnectionClose() { 93 | // TODO: Can we get a better reason for the disconnection? Was it planned? 94 | that.sendIrcCommand('disconnect', {connection_id: con.con_num, reason: 'disconnected'}); 95 | 96 | that.irc_connections[con_num] = null; 97 | global.servers.removeConnection(this); 98 | }); 99 | 100 | // Call any modules before making the connection 101 | global.modules.emit('irc connecting', {state: this, connection: con}) 102 | .then(function () { 103 | con.connect(); 104 | }); 105 | }; 106 | 107 | State.prototype.sendIrcCommand = function () { 108 | this.client.sendIrcCommand.apply(this.client, arguments); 109 | }; 110 | 111 | State.prototype.sendKiwiCommand = function () { 112 | this.client.sendKiwicommand.apply(this.client, arguments); 113 | }; 114 | 115 | State.prototype.dispose = function () { 116 | this.emit('dispose'); 117 | this.removeAllListeners(); 118 | }; 119 | -------------------------------------------------------------------------------- /server/modules.js: -------------------------------------------------------------------------------- 1 | var events = require('events'), 2 | util = require('util'), 3 | path = require('path'), 4 | _ = require('lodash'), 5 | EventPublisher = require('./plugininterface.js'); 6 | 7 | 8 | /** 9 | * Publisher 10 | * The main point in which events are fired and bound to 11 | */ 12 | 13 | // Where events are bound to 14 | var active_publisher; 15 | 16 | 17 | // Create a publisher to allow event subscribing 18 | function Publisher (obj) { 19 | return new EventPublisher(); 20 | } 21 | 22 | 23 | // Register an already created Publisher() as the active instance 24 | function registerPublisher (obj) { 25 | active_publisher = obj; 26 | } 27 | 28 | 29 | 30 | 31 | 32 | 33 | /** 34 | * Keeping track of modules 35 | */ 36 | 37 | // Hold the loaded modules 38 | var registered_modules = []; 39 | 40 | function loadModule (module_file) { 41 | var module, 42 | full_module_filename = path.join(global.config.module_dir, module_file); 43 | 44 | // Make sure that the module is contained in the proper module directory 45 | if (full_module_filename.lastIndexOf(global.config.module_dir, 0) !== 0) { 46 | return false; 47 | } 48 | 49 | // Get an instance of the module and remove it from the cache 50 | try { 51 | module = require(full_module_filename); 52 | delete require.cache[require.resolve(full_module_filename)]; 53 | } catch (err) { 54 | // Module was not found 55 | return false; 56 | } 57 | 58 | return module; 59 | } 60 | 61 | 62 | // Find a registered collection, .dispose() of it and remove it 63 | function unloadModule (module) { 64 | var found_module = false; 65 | 66 | registered_modules = _.reject(registered_modules, function (registered_module) { 67 | if (module.toLowerCase() === registered_module.module_name.toLowerCase()) { 68 | found_module = true; 69 | 70 | registered_module.dispose(); 71 | return true; 72 | } 73 | }); 74 | 75 | return found_module; 76 | } 77 | 78 | 79 | 80 | 81 | 82 | 83 | /** 84 | * Module object 85 | * To be created by modules to bind to server events 86 | */ 87 | function Module (module_name) { 88 | registered_modules.push(this); 89 | this.module_name = module_name; 90 | 91 | // Holder for all the bound events by this module 92 | this._events = {}; 93 | } 94 | 95 | 96 | 97 | // Keep track of this modules events and bind 98 | Module.prototype.on = function (event_name, fn) { 99 | var internal_events = ['dispose']; 100 | 101 | this._events[event_name] = this._events[event_name] || []; 102 | this._events[event_name].push(fn); 103 | 104 | // If this is an internal event, do not propogate the event 105 | if (internal_events.indexOf(event_name) === -1) { 106 | active_publisher.on(event_name, fn); 107 | } 108 | }; 109 | 110 | 111 | // Keep track of this modules events and bind once 112 | Module.prototype.once = function (event_name, fn) { 113 | this._events[event_name] = this._events[event_name] || []; 114 | this._events[event_name].push(fn); 115 | 116 | active_publisher.once(event_name, fn); 117 | }; 118 | 119 | 120 | // Remove any events by this module only 121 | Module.prototype.off = function (event_name, fn) { 122 | if (typeof event_name === 'undefined') { 123 | // Remove all events 124 | _.each(this._events, function(events, event_name) { 125 | _.each(events, function(event_fn) { 126 | active_publisher.off(event_name, event_fn); 127 | }); 128 | }); 129 | 130 | this._events = {}; 131 | 132 | } else if (typeof fn === 'undefined') { 133 | // Remove all of 1 event type 134 | _.each(this._events[event_name], function(event_fn) { 135 | active_publisher.off(event_name, event_fn); 136 | }); 137 | 138 | delete this._events[event_name]; 139 | 140 | } else { 141 | // Remove a single event + callback 142 | if (this._events[event_name]) { 143 | this._events[event_name] = _.filter(this._events[event_name], function(event_fn) { 144 | return event_fn !== fn; 145 | }); 146 | } 147 | 148 | active_publisher.off(event_name, fn); 149 | } 150 | }; 151 | 152 | 153 | 154 | // Clean up anything used by this module 155 | Module.prototype.dispose = function () { 156 | // Call any dispose callbacks 157 | (this._events['dispose'] || []).forEach(function (callback) { 158 | callback(); 159 | }); 160 | 161 | // Remove all bound event listeners 162 | this.off(); 163 | }; 164 | 165 | 166 | 167 | 168 | 169 | 170 | module.exports = { 171 | // Objects 172 | Module: Module, 173 | Publisher: Publisher, 174 | 175 | // Methods 176 | registerPublisher: registerPublisher, 177 | load: loadModule, 178 | unload: unloadModule, 179 | getRegisteredModules: function () { return registered_modules; } 180 | }; -------------------------------------------------------------------------------- /server/rehash.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | events = require('events'), 3 | _ = require('lodash'); 4 | 5 | 6 | 7 | function Rehash() {} 8 | util.inherits(Rehash, events.EventEmitter); 9 | 10 | Rehash.prototype.rehashAll = function () { 11 | var files = [ 12 | './client.js', 13 | './clientcommands.js', 14 | //'./configuration.js', 15 | './httphandler.js', 16 | './irc/commands.js', 17 | './irc/connection.js', 18 | './weblistener.js' 19 | ]; 20 | 21 | _.each(files, function (file) { 22 | delete require.cache[require.resolve(file)]; 23 | require(file); 24 | }); 25 | 26 | this.emit('rehashed', [files]); 27 | }; 28 | 29 | 30 | 31 | module.exports = new Rehash(); -------------------------------------------------------------------------------- /server/stats.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | incr: function incr(stat_name, data) { 3 | global.modules.emit('stat counter', {name: stat_name, data: data}); 4 | }, 5 | 6 | gauge: function gauge(stat_name, value) { 7 | global.modules.emit('stat gauge', {name: stat_name, value: value}); 8 | }, 9 | 10 | 11 | /** 12 | * Send a timer value to the stats 13 | * 14 | * Usage: 15 | * var timer = Stats.startTimer('stat_name', {some_data: 'value'}); 16 | * // Do stuff 17 | * timer.stop({other_data: 'value'}); 18 | * 19 | * The object passed into .startTimer() and .stop(); are optional. If 20 | * given they will be shallow merged with .stop() overridding .startTimer() 21 | */ 22 | startTimer: function statsTimer(stat_name, data_start) { 23 | var timer_started = new Date(); 24 | 25 | var timerStop = function timerStop(data_end) { 26 | var time = (new Date()) - timer_started; 27 | var data = shallowMergeObjects(data_start, data_end); 28 | 29 | global.modules.emit('stat timer', {name: stat_name, time: time, data: data}); 30 | }; 31 | 32 | return { 33 | stop: timerStop 34 | }; 35 | } 36 | }; 37 | 38 | 39 | 40 | function shallowMergeObjects(/** argn, ... **/) { 41 | var arg_idx, arg, 42 | data = {}; 43 | 44 | for(arg_idx=0; arg_idx= ports.length) { 49 | callback_called = true; 50 | callback(false); 51 | } 52 | }; 53 | 54 | var portConnected = function() { 55 | var remote_port = this.remotePort; 56 | 57 | ports_completed++; 58 | this.removeAllListeners(); 59 | this.destroy(); 60 | 61 | if (!callback_called) { 62 | callback_called = true; 63 | callback(true, host, remote_port); 64 | } 65 | }; 66 | 67 | var portTimeout = function() { 68 | ports_completed++; 69 | this.removeAllListeners(); 70 | this.destroy(); 71 | 72 | if (!callback_called && ports_completed >= ports.length) { 73 | callback_called = true; 74 | callback(false); 75 | } 76 | }; 77 | 78 | for (var idx=0; idx< ports.length; idx++) { 79 | net.connect({port: ports[idx], host: host}) 80 | .on('connect', portConnected) 81 | .on('error', portFailed) 82 | .on('close', portFailed) 83 | .on('timeout', portTimeout) 84 | .setTimeout(5000); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /server_modules/stats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stats counter 3 | * 4 | * Retreive stats for internal kiwi events. Handy for graphing 5 | */ 6 | 7 | var kiwiModules = require('../server/modules'), 8 | fs = require('fs'); 9 | 10 | 11 | 12 | var module = new kiwiModules.Module('stats_file'); 13 | 14 | var stats_file = fs.createWriteStream('kiwi_stats.log', {'flags': 'a'}); 15 | 16 | module.on('stat counter', function (event, event_data) { 17 | var stat_name = event_data.name, 18 | timestamp, 19 | ignored_events = []; 20 | 21 | // Some events may want to be ignored 22 | ignored_events.push('http.request'); 23 | 24 | if (ignored_events.indexOf(stat_name) > -1) { 25 | return; 26 | } 27 | 28 | timestamp = Math.floor((new Date()).getTime() / 1000); 29 | stats_file.write(timestamp.toString() + ' ' + stat_name + '\n'); 30 | }); 31 | -------------------------------------------------------------------------------- /server_modules/web_agent_debugger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Server debugging via node-webkit-agent 3 | * https://github.com/c4milo/node-webkit-agent 4 | * 5 | * Requires npm module: webkit-devtools-agent 6 | */ 7 | 8 | 9 | var kiwiModules = require('../server/modules'), 10 | agent = require('webkit-devtools-agent'); 11 | 12 | 13 | var module = new kiwiModules.Module('web_agent_debugger'); 14 | 15 | agent.start({ 16 | port: 9999, 17 | bind_to: '0.0.0.0', 18 | ipc_port: 3333, 19 | verbose: true 20 | }); 21 | 22 | console.log('Debugging can be accessed via http://c4milo.github.io/node-webkit-agent/26.0.1410.65/inspector.html?host=localhost:9999&page=0'); 23 | 24 | module.on('dispose', function() { 25 | agent.stop(); 26 | }); -------------------------------------------------------------------------------- /translations.md: -------------------------------------------------------------------------------- 1 | ### Translations 2 | 3 | Kiwi IRC has been translated to 26 different languages: 4 | * Albanian 5 | * Bosnian 6 | * Catalan 7 | * Čeština 8 | * Deutsch 9 | * English 10 | * Español 11 | * Español (Latino America) 12 | * Français 13 | * Galego 14 | * Greek 15 | * Hebrew 16 | * Italiano 17 | * Korean 18 | * Македонски 19 | * Nederlands 20 | * Norsk 21 | * Polski 22 | * Português (Brasil) 23 | * Română 24 | * русский 25 | * Srpski 26 | * Türkçe 27 | * Українська 28 | * Tiếng Việt 29 | * 中文 (繁體) (Simplified chinese) 30 | 31 | 32 | Current translations are handled by the following (only listing translators willing to be publically mentioned): 33 | 34 | * **Norwegian** - Olav Lindekleiv (https://github.com/oal) 35 | * **Albanian** - Besnik Bleta (besnik@programeshqip.org) 36 | * **Ukrainian** - Artem Polivanchuk (polivanchuk@outlook.com) 37 | * **Catalan** - NeoMahler (https://github.com/NeoMahler) 38 | * **Bosnian** - Predsjednik (predsjednik@live.com) 39 | * **Polish** - Piotr Doroszewski (piotrekd at vivaldi dot net) 40 | * **Serbian** - Stefan C. (admin@xshellz.com) 41 | * **Galicia** - Chema Casanova (https://github.com/txenoo) 42 | * **Greek** - Nikos Papakonstantinou (https://github.com/NikosPapakonstantinou) 43 | --------------------------------------------------------------------------------