#kiwi.theme_relaxed .messages .msg { border-bottom: 1px solid #DEDEDE; padding: 1px; font-family:arial; font-size:0.9em; }
#kiwi.theme_relaxed .messages .msg .time { width:6em; float:left; color:#777; display:none; }
-#kiwi.theme_relaxed .messages .msg .nick { width:11em; float:left; font-size:12px; font-family:Arial; text-align:left; padding: 5px; }
+#kiwi.theme_relaxed .messages .msg .nick { width:11em; float:left; font-size:12px; font-family:Arial; text-align:left; padding: 5px; overflow:hidden; }
#kiwi.theme_relaxed .messages .msg .text { display:block; margin-left:12em; border-left: 1px solid #DEDEDE; white-space:pre-wrap; word-wrap:break-word; font-family:arial; padding:5px; }
#kiwi.theme_relaxed .messages .msg.action .nick { }
#kiwi.theme_relaxed .messages .msg.highlight { background:#D9D9D9; }
+#kiwi.theme_relaxed .messages .msg .media { margin-left:0.5em; }
+#kiwi.theme_relaxed .messages .msg .media .media_close { font-size:0.9em; }
+#kiwi.theme_relaxed .messages .msg .media .media_content { margin:10px 0 0 10px; overflow:hidden; }
+#kiwi.theme_relaxed .messages .msg .media .media_content img { padding:3px; border:1px solid gray; }
+#kiwi.theme_relaxed .messages .msg .media .media_content > .content {
+ background: white;
+ overflow: hidden;
+ padding: 10px;
+ border: #DDD 1px solid;
+ border-top-color: #EEE;
+ border-bottom-color: #BBB;
+ -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
+ -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.15);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
+ border-radius: 5px;
+ float: left;
+}
+
+#kiwi.theme_relaxed .messages .msg .media.twitter .media_content > .content {
+ background: transparent;
+ border:none;
+ overflow: hidden;
+ box-shadow: none;
+ padding: 0;
+}
+#kiwi.theme_relaxed .messages .msg .media.reddit .thumbnail_nsfw {
+ display: inline-block;
+ float: left;
+}
+#kiwi.theme_relaxed .messages .msg .media.reddit .thumbnail { float:left; margin-right: 0.5em; }
+
#kiwi.theme_relaxed #memberlists {
background-color: #DADADA;
-webkit-border-radius:5px;
-khtml-border-radius:5px;
}
-#kiwi.theme_relaxed #controlbox .input .nick { text-align: right; width:11em; left:0px; position:absolute; padding:2px; }
+#kiwi.theme_relaxed #controlbox .input .nick { text-align: right; width:11em; left:0px; position:absolute; padding:2px; overflow:hidden; }
#kiwi.theme_relaxed #controlbox .input .nick a { text-decoration:none; color:black; }
#kiwi.theme_relaxed #controlbox .input .input_wrap {
position:absolute;
#kiwi.theme_cli #controlbox { background:#111111; border-top:1px solid #444444; color:#909090; font-size:1.3em; line-height:2em; margin:3px; }
#kiwi.theme_cli #controlbox .input_wrap:before { content:"> " }
#kiwi.theme_cli #controlbox .input { background:none; border:none;}
-#kiwi.theme_cli #controlbox .input .nick { line-height:1.7em; padding:0; text-align: right; width:11em; left:0px; position:absolute; }
+#kiwi.theme_cli #controlbox .input .nick { line-height:1.7em; padding:0; text-align: right; width:11em; left:0px; position:absolute; overflow:hidden; }
#kiwi.theme_cli #controlbox .input .input_wrap {
position:absolute;
right:7px; left: 12.2em;
height:1.7em;
}
#kiwi.theme_cli #controlbox .input .inp {
- line-height:1.7em;
+ line-height:1.4em;
font-size:1.3em;
background:transparent; color:#909090;
border: medium none;
outline:none; resize:none;
overflow:hidden;
position:absolute;
- height:100%; width:98%;
+ height:99%; width:98%;
display: inline;
padding-left:0.5em;
}
#kiwi.theme_cli .messages .msg.global_nick_highlight { background:#111111; }
+
+#kiwi.theme_cli .messages .msg .media { margin-left:0.5em; }
+#kiwi.theme_cli .messages .msg .media .media_close { font-size:0.9em; }
+#kiwi.theme_cli .messages .msg .media .media_content { margin:10px 0 0 6em; overflow:hidden; }
+#kiwi.theme_cli .messages .msg .media .media_content img { padding:3px; border:1px solid gray; }
+#kiwi.theme_cli .messages .msg .media .media_content > .content {
+ background: white;
+ overflow: hidden;
+ padding: 10px;
+ border: #DDD 1px solid;
+ border-top-color: #EEE;
+ border-bottom-color: #BBB;
+ -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
+ -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.15);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
+ border-radius: 5px;
+ float: left;
+}
+
+#kiwi.theme_cli .messages .msg .media.twitter .media_content > .content {
+ background: transparent;
+ border:none;
+ overflow: hidden;
+ box-shadow: none;
+ padding: 0;
+}
+#kiwi.theme_cli .messages .msg .media.reddit .thumbnail_nsfw {
+ display: inline-block;
+ float: left;
+}
+#kiwi.theme_cli .messages .msg .media.reddit .thumbnail { float:left; margin-right: 0.5em; }
+
+
+
/* The server select dialog */
#kiwi.theme_cli .server_select { width:730px; padding:3em 0 2em 0; margin: 0 auto; }
#kiwi.theme_cli .server_select .more { display: none; width:270px; margin:0 auto; }
\r
// Now actually show the current settings\r
this.loadSettings();\r
+\r
+\r
},\r
\r
\r
\r
\r
\r
- _kiwi.applets.Settings = Backbone.Model.extend({\r
+ var Applet = Backbone.Model.extend({\r
initialize: function () {\r
this.set('title', 'Settings');\r
this.view = new View();\r
}\r
});\r
+\r
+\r
+ _kiwi.model.Applet.register('kiwi_settings', Applet);\r
})();
\ No newline at end of file
this.loaded_applet = null;\r
},\r
\r
+\r
// Load an applet within this panel\r
load: function (applet_object, applet_name) {\r
if (typeof applet_object === 'object') {\r
return this;\r
},\r
\r
+\r
loadFromUrl: function(applet_url, applet_name) {\r
var that = this;\r
\r
});\r
},\r
\r
+\r
close: function () {\r
this.view.$el.remove();\r
this.destroy();\r
\r
this.closePanel();\r
}\r
+},\r
+\r
+\r
+{\r
+ // Load an applet type once only. If it already exists, return that\r
+ loadOnce: function (applet_name) {\r
+\r
+ // See if we have an instance loaded already\r
+ var applet = _.find(_kiwi.app.panels.models, function(panel) {\r
+ // Ignore if it's not an applet\r
+ if (!panel.isApplet()) return;\r
+\r
+ // Ignore if it doesn't have an applet loaded\r
+ if (!panel.loaded_applet) return;\r
+\r
+ if (panel.loaded_applet.get('_applet_name') === applet_name) {\r
+ return true;\r
+ }\r
+ });\r
+\r
+ if (applet) return applet;\r
+\r
+\r
+ // If we didn't find an instance, load a new one up\r
+ return this.load(applet_name);\r
+ },\r
+\r
+\r
+ load: function (applet_name) {\r
+ var applet;\r
+\r
+ // Find the applet within the registered applets\r
+ if (!_kiwi.applets[applet_name]) return;\r
+\r
+ // Create the applet and load the content\r
+ applet = new _kiwi.model.Applet();\r
+ applet.load(new _kiwi.applets[applet_name]({_applet_name: applet_name}));\r
+\r
+ // Add it into the tab list\r
+ _kiwi.app.panels.add(applet);\r
+\r
+\r
+ return applet;\r
+ },\r
+\r
+\r
+ register: function (applet_name, applet) {\r
+ _kiwi.applets[applet_name] = applet;\r
+ }\r
});
\ No newline at end of file
member.set('away', !(!event.trailing));\r
}\r
});\r
- });
+ });\r
\r
\r
gw.on('onlist_start', function (data) {\r
}\r
\r
function settingsCommand (ev) {\r
- var panel = new _kiwi.model.Applet();\r
- panel.load(new _kiwi.applets.Settings());\r
- \r
- _kiwi.app.panels.add(panel);\r
- panel.view.show();\r
+ var settings = _kiwi.model.Applet.loadOnce('kiwi_settings');\r
+ settings.view.show();\r
}\r
\r
function appletCommand (ev) {\r
className: "messages",\r
events: {\r
"click .chan": "chanClick",\r
+ 'click .media .open': 'mediaClick',\r
'mouseenter .msg .nick': 'msgEnter',\r
'mouseleave .msg .nick': 'msgLeave'\r
},\r
msg.msg = $('<div />').text(msg.msg).html();\r
\r
// Make the channels clickable\r
- re = new RegExp('\\B([' + _kiwi.gateway.get('channel_prefix') + '][^ ,.\\007]+)', 'g');\r
+ re = new RegExp('(?:^|\\s)([' + _kiwi.gateway.get('channel_prefix') + '][^ ,.\\007]+)', 'g');\r
msg.msg = msg.msg.replace(re, function (match) {\r
- return '<a class="chan">' + match + '</a>';\r
+ return '<a class="chan" data-channel="' + match.trim() + '">' + match + '</a>';\r
});\r
\r
\r
- // Make links clickable\r
- msg.msg = msg.msg.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]*))?/gi, function (url) {\r
- var nice;\r
+ // Parse any links found\r
+ msg.msg = msg.msg.replace(/(([A-Za-z0-9\-]+\:\/\/)|(www\.))([\w.]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w#!:.?$'()[\]*,;~+=&%@!\-\/]*)?/gi, function (url) {\r
+ var nice, extra_html = '';\r
\r
- // Add the http is no protoocol was found\r
+ // Add the http if no protoocol was found\r
if (url.match(/^www\./)) {\r
url = 'http://' + url;\r
}\r
\r
+ // Shorten the displayed URL if it's going to be too long\r
nice = url;\r
if (nice.length > 100) {\r
nice = nice.substr(0, 100) + '...';\r
}\r
\r
- return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a>';\r
+ // Get any media HTML if supported\r
+ extra_html = _kiwi.view.MediaMessage.buildHtml(url);\r
+\r
+ // Make the link clickable\r
+ return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a> ' + extra_html;\r
});\r
\r
\r
},\r
chanClick: function (event) {\r
if (event.target) {\r
- _kiwi.gateway.join($(event.target).text());\r
+ _kiwi.gateway.join($(event.target).data('channel'));\r
} else {\r
// IE...\r
- _kiwi.gateway.join($(event.srcElement).text());\r
+ _kiwi.gateway.join($(event.srcElement).data('channel'));\r
}\r
},\r
\r
+ mediaClick: function (event) {\r
+ var $media = $(event.target).parents('.media');\r
+ var media_message;\r
+\r
+ if ($media.data('media')) {\r
+ media_message = $media.data('media');\r
+ } else {\r
+ media_message = new _kiwi.view.MediaMessage({el: $media[0]});\r
+ $media.data('media', media_message);\r
+ }\r
+\r
+ $media.data('media', media_message);\r
+\r
+ media_message.open();\r
+ },\r
+\r
msgEnter: function (event) {\r
var nick_class;\r
\r
\r
// Set the panels width depending on the memberlist visibility\r
if (el_memberlists.css('display') != 'none') {\r
- // Handle + panels to the side of the memberlist\r
- el_panels.css('right', el_memberlists.outerWidth(true) + el_resize_handle.outerWidth(true));\r
- el_resize_handle.css('left', el_memberlists.position().left - el_resize_handle.outerWidth(true));\r
+ // Panels to the side of the memberlist\r
+ el_panels.css('right', el_memberlists.outerWidth(true));\r
+ // The resize handle sits overlapping the panels and memberlist\r
+ el_resize_handle.css('left', el_memberlists.position().left - (el_resize_handle.outerWidth(true) / 2));\r
} else {\r
- // Memberlist is hidden so handle + panels to the right edge\r
- el_panels.css('right', el_resize_handle.outerWidth(true));\r
+ // Memberlist is hidden so panels to the right edge\r
+ el_panels.css('right', 0);\r
+ // And move the handle just out of sight to the right\r
el_resize_handle.css('left', el_panels.outerWidth(true));\r
}\r
},\r
this.doLayout();\r
}\r
}\r
+});\r
+\r
+\r
+\r
+\r
+\r
+\r
+\r
+\r
+\r
+_kiwi.view.MediaMessage = Backbone.View.extend({\r
+ events: {\r
+ 'click .media_close': 'close'\r
+ },\r
+\r
+ initialize: function () {\r
+ // Get the URL from the data\r
+ this.url = this.$el.data('url');\r
+ },\r
+\r
+ // Close the media content and remove it from display\r
+ close: function () {\r
+ var that = this;\r
+ this.$content.slideUp('fast', function () {\r
+ that.$content.remove();\r
+ });\r
+ },\r
+\r
+ // Open the media content within its wrapper\r
+ open: function () {\r
+ // Create the content div if we haven't already\r
+ if (!this.$content) {\r
+ this.$content = $('<div class="media_content"><a class="media_close"><i class="icon-chevron-up"></i> Close media</a><br /><div class="content"></div></div>');\r
+ this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || 'Not found :(');\r
+ }\r
+\r
+ // Now show the content if not already\r
+ if (!this.$content.is(':visible')) {\r
+ // Hide it first so the slideDown always plays\r
+ this.$content.hide();\r
+\r
+ // Add the media content and slide it into view\r
+ this.$el.append(this.$content);\r
+ this.$content.slideDown();\r
+ }\r
+ },\r
+\r
+\r
+\r
+ // Generate the media content for each recognised type\r
+ mediaTypes: {\r
+ twitter: function () {\r
+ var tweet_id = this.$el.data('tweetid');\r
+ var that = this;\r
+\r
+ $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id + '&callback=?', function (data) {\r
+ that.$content.find('.content').html(data.html);\r
+ });\r
+\r
+ return $('<div>Loading tweet..</div>');\r
+ },\r
+\r
+\r
+ image: function () {\r
+ return $('<a href="' + this.url + '" target="_blank"><img height="100" src="' + this.url + '" /></a>');\r
+ },\r
+\r
+\r
+ reddit: function () {\r
+ var that = this;\r
+ var matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url);\r
+\r
+ $.getJSON('http://www.' + matches[0] + '.json?jsonp=?', function (data) {\r
+ console.log('Loaded reddit data', data);\r
+ var post = data[0].data.children[0].data;\r
+ var thumb = '';\r
+\r
+ // Show a thumbnail if there is one\r
+ if (post.thumbnail) {\r
+ //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';\r
+\r
+ // Hide the thumbnail if an over_18 image\r
+ if (post.over_18) {\r
+ thumb = '<span class="thumbnail_nsfw" onclick="$(this).find(\'p\').remove(); $(this).find(\'img\').css(\'visibility\', \'visible\');">';\r
+ thumb += '<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>';\r
+ thumb += '<img src="' + post.thumbnail + '" class="thumbnail" style="visibility:hidden;" />';\r
+ thumb += '</span>';\r
+ } else {\r
+ thumb = '<img src="' + post.thumbnail + '" class="thumbnail" />';\r
+ }\r
+ }\r
+\r
+ // Build the template string up\r
+ var tmpl = '<div>' + thumb + '<b><%- title %></b><br />Posted by <%- author %>. ';\r
+ tmpl += '<i class="icon-arrow-up"></i> <%- ups %> <i class="icon-arrow-down"></i> <%- downs %><br />';\r
+ tmpl += '<%- num_comments %> comments made. <a href="http://www.reddit.com<%- permalink %>">View post</a></div>';\r
+\r
+ that.$content.find('.content').html(_.template(tmpl, post));\r
+ });\r
+\r
+ return $('<div>Loading Reddit thread..</div>');\r
+ }\r
+ }\r
+\r
+}, {\r
+\r
+ // Build the closed media HTML from a URL\r
+ buildHtml: function (url) {\r
+ var html = '', matches;\r
+\r
+ // Is it an image?\r
+ if (url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {\r
+ html += '<span class="media image" data-type="image" data-url="' + url + '" title="Open Image"><a class="open"><i class="icon-chevron-right"></i></a></span>';\r
+ }\r
+\r
+ // Is it a tweet?\r
+ matches = (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url);\r
+ if (matches) {\r
+ html += '<span class="media twitter" data-type="twitter" data-url="' + url + '" data-tweetid="' + matches[2] + '" title="Show tweet information"><a class="open"><i class="icon-chevron-right"></i></a></span>';\r
+ }\r
+\r
+ // Is reddit?\r
+ matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url);\r
+ if (matches) {\r
+ html += '<span class="media reddit" data-type="reddit" data-url="' + url + '" title="Reddit thread"><a class="open"><i class="icon-chevron-right"></i></a></span>';\r
+ }\r
+\r
+ return html;\r
+ }\r
});
\ No newline at end of file
conf.module_dir = "./kiwi_modules/";
// Which modules to load
-conf.modules = ["spamfilter", "statistics"];
+conf.modules = [];
\r
var listeners = {\r
PRIVMSG: function (args, irc_connection, callback) {\r
- if (args.target && (args.msg)) {\r
- // TODO: Enable plugin support here again\r
- //obj = kiwi.kiwi_mod.run('msgsend', args, {websocket: websocket});\r
- //if (obj !== null) {\r
- irc_connection.write('PRIVMSG ' + args.target + ' :' + args.msg, callback);\r
- //}\r
+ if (args.target && (args.msg)) {\r
+ irc_connection.write('PRIVMSG ' + args.target + ' :' + args.msg, callback);\r
}\r
},\r
\r
_ = require('lodash'),
WebListener = require('./weblistener.js'),
config = require('./configuration.js'),
- rehash = require('./rehash.js');
+ rehash = require('./rehash.js'),
+ modules = require('./modules.js');
+// Create a plugin interface
+global.modules = new modules.Publisher();
+
+// Register as the active interface
+modules.registerPublisher(global.modules);
+
+// Load any modules in the config
+(global.config.modules || []).forEach(function (module_name) {
+ if (modules.load('../server_modules/' + module_name + '.js')) {
+ console.log('Module ' + module_name + ' loaded successfuly');
+ } else {
+ console.log('Module ' + module_name + ' failed to load');
+ }
+});
+
+
+
+
// Holder for all the connected clients
global.clients = {
--- /dev/null
+var events = require('events'),
+ util = require('util'),
+ _ = require('lodash');
+
+
+/**
+ * Publisher
+ * The main point in which events are fired and bound to
+ */
+
+// Where events are bound to
+var active_publisher;
+
+
+// Create a publisher to allow event subscribing
+function Publisher (obj) {
+ var EventPublisher = function modulePublisher() {};
+ util.inherits(EventPublisher, events.EventEmitter);
+
+ return new EventPublisher();
+}
+
+
+// Register an already created Publisher() as the active instance
+function registerPublisher (obj) {
+ active_publisher = obj;
+}
+
+
+
+
+
+
+/**
+ * Keeping track of modules
+ */
+
+// Hold the loaded modules
+var registered_modules = {};
+
+function loadModule (module_file) {
+ var module;
+
+ // Get an instance of the module and remove it from the cache
+ try {
+ module = require(module_file);
+ delete require.cache[require.resolve(module_file)];
+ } catch (err) {
+ // Module was not found
+ return false;
+ }
+
+ return module;
+}
+
+
+// Find a registered collection, .dispose() of it and remove it
+function unloadModule (module) {
+ registered_modules = _.reject(registered_modules, function (registered_module) {
+ if (module === registered_module) {
+ module.dispose();
+ return true;
+ }
+ });
+}
+
+
+
+
+
+
+/**
+ * Module object
+ * To be created by modules to bind to server events
+ */
+function Module (module_name) {
+ registered_modules[module_name] = this;
+}
+
+
+// Holder for all the bound events by this module
+Module.prototype._events = {};
+
+
+// Keep track of this modules events and bind
+Module.prototype.on = function (event_name, fn) {
+ var internal_events = ['dispose'];
+
+ this._events[event_name] = this._events[event_name] || [];
+ this._events[event_name].push(fn);
+
+ // If this is an internal event, do not propogate the event
+ if (internal_events.indexOf(event_name) !== -1) {
+ active_publisher.on(event_name, fn);
+ }
+};
+
+
+// Keep track of this modules events and bind once
+Module.prototype.once = function (event_name, fn) {
+ this._events[event_name] = this._events[event_name] || [];
+ this._events[event_name].push(fn);
+
+ active_publisher.once(event_name, fn);
+};
+
+
+// Remove any events by this module only
+Module.prototype.off = function (event_name, fn) {
+ var idx;
+
+ if (typeof event_name === 'undefined') {
+ // Remove all events
+ this._events = [];
+
+ } else if (typeof fn === 'undefined') {
+ // Remove all of 1 event type
+ delete this._events[event_name];
+
+ } else {
+ // Remove a single event + callback
+ for (idx in (this._events[event_name] || [])) {
+ if (this._events[event_name][idx] === fn) {
+ delete this._events[event_name][idx];
+ }
+ }
+ }
+
+ active_publisher.removeListener(event_name, fn);
+};
+
+
+
+// Clean up anything used by this module
+Module.prototype.dispose = function () {
+ // Call any dispose callbacks
+ (this._events['dispose'] || []).forEach(function (callback) {
+ callback();
+ });
+
+ // Remove all bound event listeners
+ this.off();
+};
+
+
+
+
+
+
+module.exports = {
+ // Objects
+ Module: Module,
+ Publisher: Publisher,
+
+ // Methods
+ registerPublisher: registerPublisher,
+ load: loadModule,
+ unload: unloadModule,
+ getRegisteredModules: function () { return registered_modules; }
+};
\ No newline at end of file
--- /dev/null
+var kiwiModules = require('../server/modules');
+
+var module = new kiwiModules.Module('Example Module');
+
+
+module.subscribe('client:connected', function(data) {
+ console.log('Client connection:', data);
+});
+
+
+module.subscribe('client:commands:msg', function(data) {
+ console.log('Client msg:', data.args.target, ': ', data.args.msg);
+ data.args.msg += ' - modified!';
+});
\ No newline at end of file