diff --git a/README.md b/README.md index 0c45e08..cf84fab 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ A web client for WeeChat ======================== +Required Weechat version: 0.4.2 + To use the web interface you first need to set a relay and a password: /relay add weechat 9001 diff --git a/css/glowingbear.css b/css/glowingbear.css index 1902717..682a729 100644 --- a/css/glowingbear.css +++ b/css/glowingbear.css @@ -1,5 +1,3 @@ -html { -} body { color: white; background-color: #222; @@ -8,6 +6,11 @@ body { padding-bottom:70px; padding-top: 70px; } + +input#sendMessage { + border: 0; + width: 100%; +} .content { height: 100%; min-height: 100%; diff --git a/index.html b/index.html index a750669..3eb0594 100644 --- a/index.html +++ b/index.html @@ -1,126 +1,126 @@ - - - - - + + + + - + + + - -
-
-

- - glowing bear - - WeeChat web frontend - -

-
To start using, please enable relay in your WeeChat client: -
+  
+    
+    
+

+ + glowing bear + + WeeChat web frontend + +

+
To start using, please enable relay in your WeeChat client: +
 /set relay.network.password yourpassword
 /relay add weechat 9001
- Note: The communication goes directly between your browser and your weechat in clear text. - Connection settings are saved between sessions, including password, in your own browser. -

Encryption

- If you want to use encrypted session you first have to set up the relay using SSL -
+      Note: The communication goes directly between your browser and your weechat in clear text.
+      Connection settings are saved between sessions, including password, in your own browser.
+      

Encryption

+ If you want to use encrypted session you first have to set up the relay using SSL +
 $ mkdir -p ~/.weechat/ssl
 $ cd ~/.weechat/ssl
 $ openssl req -nodes -newkey rsa:2048 -keyout relay.pem -x509 -days 365 -out relay.pem
 
- If WeeChat is already running, you can reload the certificate and private key with command: -
+      If WeeChat is already running, you can reload the certificate and private key with command:
+      
 /relay sslcertkey
 /relay add ssl.weechat 8000
 
-
-

Connection settings

-
-
- Oh no! We cannot connect! -
-
- - -

Enter the hostname and the port to the WeeChat relay, separated by a :

-
-
- - -

Password will be stored in your browser session

-
-
- - -

Check the box if you want to encrypt communication between browser and WeeChat. Note: Due to a bug encryption will not work in Firefox. You must also first visit the URL https://weechathost:relayport/ to accept the certificate

-
- -
-
- -
-
- - {{ bufferline.date | date:'HH:mm' }} - +

Connection settings

+
+
+ Oh no! We cannot connect! +
+
+ + +

Enter the hostname and the port to the WeeChat relay, separated by a :

+
+
+ + +

Password will be stored in your browser session

+
+
+ + +

Check the box if you want to encrypt communication between browser and WeeChat. Note: Due to a bug encryption will not work in Firefox. You must also first visit the URL https://weechathost:relayport/ to accept the certificate

+
+ +
+
+
+
+
+ + {{ bufferline.date | date:'HH:mm' }} + - - {{ part.text }} - + + {{ part.text }} + - - - +
+
+ diff --git a/js/models.js b/js/models.js new file mode 100644 index 0000000..8dc36d5 --- /dev/null +++ b/js/models.js @@ -0,0 +1,183 @@ +/* + * This file contains the weechat models and various + * helper methods to work with them. + */ +var models = angular.module('weechatModels', []); + +models.service('models', ['colors', function(colors) { + /* + * Buffer class + */ + this.Buffer = function(message) { + // weechat properties + var fullName = message['full_name'] + var shortName = message['short_name'] + var title = message['title'] + var number = message['number'] + var pointer = message['pointers'][0] + var lines = [] + var active = false; + var notification = false; + var unread = ''; + + /* + * Adds a line to this buffer + * + * @param line the BufferLine object + * @return undefined + */ + var addLine = function(line) { + lines.push(line); + } + + return { + id: pointer, + fullName: fullName, + shortName: shortName, + number: number, + title: title, + lines: lines, + addLine: addLine + } + + } + + /* + * BufferLine class + */ + this.BufferLine = function(message) { + + /* + * Parse the text elements from the buffer line added + * + * @param message weechat message + */ + function parseLineAddedTextElements(message) { + var prefix = colors.parse(message['prefix']); + + var buffer = message['buffer']; + text_elements = _.union(prefix, text); + text_elements =_.map(text_elements, function(text_element) { + if (text_element && ('fg' in text_element)) { + text_element['fg'] = colors.prepareCss(text_element['fg']); + } + // TODO: parse background as well + + return text_element; + }); + return text_elements; + } + + + var buffer = message['buffer']; + var date = message['date']; + var text = colors.parse(message['message']); + var tags_array = message['tags_array']; + var displayed = message['displayed']; + var highlight = message['highlight']; + var content = parseLineAddedTextElements(message); + var text = ""; + if(text[0] != undefined) { + text = text[0]['text']; + } + + return { + content: content, + date: date, + buffer: buffer, + tags: tags_array, + highlight: highlight, + displayed: displayed, + text: text, + } + + } + + + var BufferList = [] + activeBuffer = null; + + this.model = { 'buffers': {} } + + /* + * Adds a buffer to the list + * + * @param buffer buffer object + * @return undefined + */ + this.addBuffer = function(buffer) { + BufferList[buffer.id] = buffer; + if (BufferList.length == 1) { + activeBuffer = buffer.id; + } + this.model.buffers[buffer.id] = buffer; + } + + /* + * Returns the current active buffer + * + * @return active buffer object + */ + this.getActiveBuffer = function() { + return activeBuffer; + } + + /* + * Sets the buffer specifiee by bufferId as active. + * Deactivates the previous current buffer. + * + * @param bufferId id of the new active buffer + * @return undefined + */ + this.setActiveBuffer = function(bufferId) { + + if (this.getActiveBuffer()) { + this.getActiveBuffer().active = false; + } + + activeBuffer = _.find(this.model['buffers'], function(buffer) { + if (buffer['id'] == bufferId) { + return buffer; + } + }); + activeBuffer.notification = false; + activeBuffer.active = true; + activeBuffer.unread = ''; + + } + + /* + * Returns the buffer list + */ + this.getBuffers = function() { + return BufferList; + } + + /* + * Returns a specific buffer object + * + * @param bufferId id of the buffer + * @return the buffer object + */ + this.getBuffer = function(bufferId) { + return _.find(this.model['buffers'], function(buffer) { + if (buffer['id'] == bufferId) { + return buffer; + } + }); + } + + /* + * Closes a weechat buffer. Sets the first buffer + * as active. + * + * @param bufferId id of the buffer to close + * @return undefined + */ + this.closeBuffer = function(bufferId) { + + delete(this.model['buffers'][bufferId.id]); + var firstBuffer = _.keys(this.model['buffers'])[0]; + this.setActiveBuffer(firstBuffer); + } +}]); diff --git a/js/plugins.js b/js/plugins.js new file mode 100644 index 0000000..64ad843 --- /dev/null +++ b/js/plugins.js @@ -0,0 +1,132 @@ +/* + * This file contains the plugin definitions + */ + +plugins = angular.module('plugins', []); + +/* + * Definition of a user provided plugin with sensible default values + * + * User plugins are created by providing a contentForMessage function + * that parses a string and return any additional content. + */ +var Plugin = function(contentForMessage) { + + return { + contentForMessage: contentForMessage, + exclusive: false, + } +} + +/* + * This service provides access to the plugin manager + * + * The plugin manager is where the various user provided plugins + * are registered. It is responsible for finding additional content + * to display when messages are received. + * + */ +plugins.service('plugins', ['userPlugins', function(userPlugins) { + + /* + * Defines the plugin manager object + */ + var PluginManagerObject = function() { + + var plugins = []; + + /* + * Register the user provides plugins + * + * @param userPlugins user provided plugins + */ + var registerPlugins = function(userPlugins) { + for (var i = 0; i < userPlugins.length; i++) { + plugins.push(userPlugins[i]); + }; + } + + /* + * Iterates through all the registered plugins + * and run their contentForMessage function. + */ + var contentForMessage = function(message) { + + var content = []; + for (var i = 0; i < plugins.length; i++) { + var pluginContent = plugins[i].contentForMessage(message); + if (pluginContent) { + var pluginContent = {'visible': false, + 'content': pluginContent } + content.push(pluginContent); + + if (plugins[i].exclusive) { + break; + } + } + } + return content; + } + + return { + registerPlugins: registerPlugins, + contentForMessage: contentForMessage + } + } + + // Instanciates and registers the plugin manager. + this.PluginManager = new PluginManagerObject(); + this.PluginManager.registerPlugins(userPlugins.plugins); + +}]); + +/* + * This factory exposes the collection of user provided plugins. + * + * To create your own plugin, you need to: + * + * 1. Define it's contentForMessage function. The contentForMessage + * function takes a string as a parameter and returns a HTML string. + * + * 2. Instanciate a Plugin object with contentForMessage function as it's + * argument. + * + * 3. Add it to the plugins array. + * + */ +plugins.factory('userPlugins', function() { + + var youtubePlugin = new Plugin(function(message) { + + if (message.indexOf('youtube.com') != -1) { + var index = message.indexOf("?v="); + var token = message.substr(index+3); + return '' + } + + return null; + }); + + var urlPlugin = new Plugin(function(message) { + var urlPattern = /(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?/; + var url = message.match(urlPattern); + if (url) { + return '' + message + ''; + } + return null; + + }); + + var imagePlugin = new Plugin(function(message) { + var urls = message.match(/https?:\/\/[^\s]*\.(jpg|png|gif)\b/) + if (urls != null) { + var url = urls[0]; /* Actually parse one url per message */ + return ''; + } + return null; + }); + + return { + plugins: [youtubePlugin, urlPlugin, imagePlugin] + } +}); diff --git a/js/protocol.js b/js/protocol.js deleted file mode 100644 index ea6dda8..0000000 --- a/js/protocol.js +++ /dev/null @@ -1,180 +0,0 @@ -var Protocol = function() { - var self = this; - - var getInfo = function() { - var info = {}; - info.key = getString(); - info.value = getString(); - return info; - }; - - var getHdata = function() { - var paths; - var count; - var objs = []; - var hpath = getString(); - - - - keys = getString().split(','); - paths = hpath.split('/'); - count = getInt(); - - keys = keys.map(function(key) { - return key.split(':'); - }); - var i; - for (i = 0; i < count; i++) { - var tmp = {}; - - tmp.pointers = paths.map(function(path) { - return getPointer(); - }); - - keys.forEach(function(key) { - tmp[key[0]] = runType(key[1]); - }); - objs.push(tmp); - }; - return objs; - }; - - function getPointer() { - var l = getChar(); - - var pointer = getSlice(l) - var parsed_data = new Uint8Array(pointer); - return _uiatos(parsed_data); - - }; - - var _uiatos =function(uia) { - var _str = []; - for (var c = 0; c < uia.length; c++) { - _str[c] = String.fromCharCode(uia[c]); - } - return decodeURIComponent(escape(_str.join(""))); - }; - - var getInt = function() { - var parsed_data = new Uint8Array(getSlice(4)); - var i = ((parsed_data[0] & 0xff) << 24) | ((parsed_data[1] & 0xff) << 16) | ((parsed_data[2] & 0xff) << 8) | (parsed_data[3] & 0xff); - return i; - - }; - - var getChar = function() { - var parsed_data = new Uint8Array(getSlice(1)); - return parsed_data[0]; - }; - - var getString = function() { - var l = getInt(); - if (l > 0) { - var s = getSlice(l); - var parsed_data = new Uint8Array(s); - return _uiatos(parsed_data); - } - return ""; - }; - - var getSlice = function(length) { - var slice = self.data.slice(0,length); - self.data = self.data.slice(length); - return slice; - }; - - var getType = function() { - var t = getSlice(3); - return _uiatos(new Uint8Array(t)); - }; - - var runType = function(type) { - if (type in types) { - return types[type](); - } - 0; - }; - - var getHeader = function() { - return { - length: getInt(), - compression: getChar(), - } - }; - - var getId = function() { - return getString(); - } - - var getObject = function() { - var type = getType(); - if (type) { - return object = { - type: type, - content: runType(type), - } - } - } - - self.parse = function(data) { - self.setData(data); - var header = getHeader(); - var id = getId(); - var objects = []; - var object = getObject(); - while(object) { - objects.push(object); - object = getObject(); - } - return { - header: header, - id: id, - objects: objects, - } - } - - self.setData = function (data) { - self.data = data; - }; - - function array() { - var type; - var count; - var values; - - type = getType(); - count = getInt(); - values = []; - var i; - for (i = 0; i < count; i++) { - values.push(runType(type)); - }; - return values; - } - - var types = { - chr: getChar, - "int": getInt, - str: getString, - inf: getInfo, - hda: getHdata, - ptr: getPointer, - lon: getPointer, - tim: getPointer, - buf: getString, - arr: array - }; -//TODO: IMPLEMENT THIS STUFF -// chr: this.getChar, -// 'int': getInt, - // hacks - - // hacks -// htb: getHashtable, -// inf: Protocol.getInfo, -// inl: getInfolist, - -// }, - -} diff --git a/js/websockets.js b/js/websockets.js index b10cde9..4d473c4 100644 --- a/js/websockets.js +++ b/js/websockets.js @@ -1,4 +1,5 @@ -var weechat = angular.module('weechat', ['localStorage']); +var weechat = angular.module('weechat', ['localStorage', 'weechatModels', 'plugins']); + weechat.filter('toArray', function () { 'use strict'; @@ -167,7 +168,7 @@ weechat.factory('colors', [function($scope) { return { - + setAttrs: setAttrs, getColor: getColor, prepareCss: prepareCss, @@ -177,142 +178,39 @@ weechat.factory('colors', [function($scope) { }]); -weechat.factory('pluginManager', ['youtubePlugin', 'urlPlugin', 'imagePlugin', function(youtubePlugin, urlPlugin, imagePlugin) { - - var plugins = [youtubePlugin, urlPlugin, imagePlugin] - - var hookPlugin = function(plugin) { - plugins.push(plugin); - } - - var contentForMessage = function(message) { - - console.log('Message: ', message); - var content = []; - for (var i = 0; i < plugins.length; i++) { - var pluginContent = plugins[i].contentForMessage(message); - if (pluginContent) { - var pluginContent = {'visible': false, 'content': pluginContent } - content.push(pluginContent); - - if (plugins[i].exclusive) { - break; - } - } - } - - console.log('Content: ', content); - return content; - } - - return { - hookPlugin: hookPlugin, - contentForMessage: contentForMessage - } - -}]); - -weechat.factory('youtubePlugin', [function() { - - var contentForMessage = function(message) { - if (message.indexOf('youtube.com') != -1) { - var index = message.indexOf("?v="); - var token = message.substr(index+3); - return '' - } - return null; - } - - return { - contentForMessage: contentForMessage, - exclusive: true - } - -}]); - -weechat.factory('urlPlugin', [function() { - var contentForMessage = function(message) { - var urlPattern = /(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?/; - var url = message.match(urlPattern); - if (url) { - return '' + message + ''; - } - return null; - } - - return { - contentForMessage: contentForMessage, - exclusive: false - } -}]); - -weechat.factory('imagePlugin', [function() { - var contentForMessage = function(message) { - var urls = message.match(/https?:\/\/[^\s]*\.(jpg|png|gif)\b/) - if (urls != null) { - var url = urls[0]; /* Actually parse one url per message */ - return ''; - } - return null; - } - - return { - contentForMessage: contentForMessage - } -}]); - -weechat.factory('handlers', ['$rootScope', 'colors', 'pluginManager', function($rootScope, colors, pluginManager) { +weechat.factory('handlers', ['$rootScope', 'colors', 'models', 'plugins', function($rootScope, colors, models, plugins) { var handleBufferClosing = function(message) { - var buffer_pointer = message['objects'][0]['content'][0]['pointers'][0]; - $rootScope.closeBuffer(buffer_pointer); + var bufferMessage = message['objects'][0]['content'][0]; + var buffer = new models.Buffer(bufferMessage); + models.closeBuffer(buffer); } var handleLine = function(line, initial) { - var buffer_line = {} - var date = line['date']*1000; - var prefix = colors.parse(line['prefix']); - var text = colors.parse(line['message']); - var buffer = line['buffer']; - var tags_array = line['tags_array']; - var displayed = line['displayed']; - var highlight = line['highlight']; - var message = _.union(prefix, text); - message =_.map(message, function(message) { - if (message != "" && 'fg' in message) { - message['fg'] = colors.prepareCss(message['fg']); - } - return message; - }); + var message = new models.BufferLine(line); // Only react to line if its displayed - if (displayed) { - buffer_line['message'] = message; + if(message.displayed) { + var buffer = models.getBuffer(message.buffer); + message.metadata = plugins.PluginManager.contentForMessage(message.text); + buffer.addLine(message); + if (buffer.active) { + $rootScope.scrollToBottom(); + } - if (!_isActiveBuffer(buffer) && !initial && !_.contains(tags_array, 'notify_none')) { - - if ($rootScope.buffers[buffer]['unread'] == '') { - $rootScope.buffers[buffer]['unread'] = 1; - }else { - $rootScope.buffers[buffer]['unread'] = parseInt($rootScope.buffers[buffer]['unread']) + 1; + if (!initial) { + if (!buffer.active && _.contains(message.tags, 'notify_message') && !_.contains(message.tags, 'notify_none')) { + if (buffer.unread == '' || buffer.unread == undefined) { + buffer.unread = 1; + }else { + buffer.unread++; + } } - } - if (text[0] != undefined) { - var additionalContent = pluginManager.contentForMessage(text[0]['text']); - - if (additionalContent) { - buffer_line['metadata'] = additionalContent; - } - } - - $rootScope.addLine(buffer, buffer_line); - - buffer_line['date'] = date; - - if(!initial && (highlight || _.contains(tags_array, 'notify_private')) ) { - $rootScope.createHighlight(prefix, text, message, buffer, additionalContent); - $rootScope.buffers[buffer]['highlight'] = true; + if(message.highlight || _.contains(message.tags, 'notify_private') ) { + $rootScope.createHighlight(buffer, message); + buffer.notification = true; + } } } } @@ -323,26 +221,10 @@ weechat.factory('handlers', ['$rootScope', 'colors', 'pluginManager', function($ }); } - /* - * Returns whether or not this buffer is the active buffer - */ - var _isActiveBuffer = function(buffer) { - if ($rootScope.activeBuffer['id'] == buffer) { - return true; - } else { - return false; - } - } - var handleBufferOpened = function(message) { - var obj = message['objects'][0]['content'][0]; - var fullName = obj['full_name']; - var buffer = obj['pointers'][0]; - var short_name = obj['short_name']; - var title = obj['title']; - - $rootScope.buffers[buffer] = { 'id': buffer, 'lines':[], 'full_name':fullName, 'short_name':short_name, 'title':title, 'unread':'' } - + var bufferMessage = message['objects'][0]['content'][0]; + var buffer = new models.Buffer(bufferMessage); + models.addBuffer(buffer); } var handleBufferTitleChanged = function(message) { @@ -371,21 +253,10 @@ weechat.factory('handlers', ['$rootScope', 'colors', 'pluginManager', function($ // buffer info from message var bufferInfos = message['objects'][0]['content']; // buffers objects - var buffers = {}; for (var i = 0; i < bufferInfos.length ; i++) { - var bufferInfo = bufferInfos[i]; - var pointer = bufferInfo['pointers'][0]; - bufferInfo['id'] = pointer; - bufferInfo['lines'] = []; - bufferInfo['unread'] = ''; - buffers[pointer] = bufferInfo - if (i == 0) { - // first buffer is active buffer by default - $rootScope.activeBuffer = buffers[pointer]; - $rootScope.activeBuffer['active'] = true; - } + var buffer = new models.Buffer(bufferInfos[i]); + models.addBuffer(buffer); } - $rootScope.buffers = buffers; // Request latest buffer lines for each buffer $rootScope.getLines(); @@ -403,7 +274,7 @@ weechat.factory('handlers', ['$rootScope', 'colors', 'pluginManager', function($ handleLine(l, true); }); } - + var handleEvent = function(event) { if (_.has(eventHandlers, event['id'])) { eventHandlers[event['id']](event); @@ -411,15 +282,6 @@ weechat.factory('handlers', ['$rootScope', 'colors', 'pluginManager', function($ } - var findMetaData = function(message) { - if (message.indexOf('youtube.com') != -1) { - var index = message.indexOf("?v="); - var token = message.substr(index+3); - return '' - } - return null; - } - var eventHandlers = { bufinfo: handleBufferInfo, lineinfo: handleLineInfo, @@ -437,12 +299,12 @@ weechat.factory('handlers', ['$rootScope', 'colors', 'pluginManager', function($ }]); -weechat.factory('connection', ['$rootScope', '$log', 'handlers', 'colors', function($rootScope, $log, handlers, colors) { - protocol = new Protocol(); +weechat.factory('connection', ['$rootScope', '$log', 'handlers', 'colors', 'models', function($rootScope, $log, handlers, colors, models) { + protocol = new WeeChatProtocol(); var websocket = null; - // Sanitizes messages to be sent to the weechat relay + // Sanitizes messages to be sent to the weechat relay var doSend = function(message) { msgs = message.replace(/[\r\n]+$/g, "").split("\n"); for (var i = 0; i < msgs.length; i++) { @@ -451,23 +313,26 @@ weechat.factory('connection', ['$rootScope', '$log', 'handlers', 'colors', funct } websocket.send(message); } - + // Takes care of the connection and websocket hooks - var connect = function (hostport, password, ssl) { + var connect = function (hostport, passwd, ssl) { var proto = ssl ? 'wss':'ws'; websocket = new WebSocket(proto+"://" + hostport + "/weechat"); websocket.binaryType = "arraybuffer" websocket.onopen = function (evt) { - var send = ""; - if (password) { - send += "init compression=off,password=" + password + "\n"; - } + doSend(WeeChatProtocol.formatInit({ + password: passwd, + compression: 'off' + })); + doSend(WeeChatProtocol.formatHdata({ + id: 'bufinfo', + path: 'buffer:gui_buffers(*)', + keys: ['number,full_name,short_name,title'] + })); + doSend(WeeChatProtocol.formatSync({})); - send += "(bufinfo) hdata buffer:gui_buffers(*) number,full_name,short_name,title\n"; - send += "sync\n"; $log.info("Connected to relay"); - doSend(send); $rootScope.connected = true; $rootScope.$apply(); } @@ -496,13 +361,18 @@ weechat.factory('connection', ['$rootScope', '$log', 'handlers', 'colors', funct } var sendMessage = function(message) { - message = "input " + $rootScope.activeBuffer['full_name'] + " " + message + "\n" - doSend(message); + doSend(WeeChatProtocol.formatInput({ + buffer: models.getActiveBuffer()['fullName'], + data: message + })); } var getLines = function(count) { - var message = "(lineinfo) hdata buffer:gui_buffers(*)/own_lines/last_line(-"+count+")/data\n"; - doSend(message) + doSend(WeeChatProtocol.formatHdata({ + id: 'lineinfo', + path: "buffer:gui_buffers(*)/own_lines/last_line(-"+count+")/data", + keys: [] + })); } return { @@ -513,8 +383,8 @@ weechat.factory('connection', ['$rootScope', '$log', 'handlers', 'colors', funct } }]); -weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', 'connection', function ($rootScope, $scope, $store, connection) { - +weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', 'models', 'connection', function ($rootScope, $scope, $store, models, connection, testService) { + // Request notification permission Notification.requestPermission(function (status) { console.log('Notification permission status:',status); @@ -529,30 +399,38 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', 'connection } } + + $scope.buffers = models.model.buffers; + $scope.activeBuffer = models.getActiveBuffer + + $scope.incrementAge = function () { + models.model.age++; + models.model.cats.push('nouveau chat'); + } + + + $scope.clickS = function () { + $scope.countS = testService.incrementCount(); + }; + $rootScope.commands = [] + + $rootScope.models = models; $rootScope.buffer = [] - $rootScope.buffers = {} - $rootScope.activeBuffer = null; + $store.bind($scope, "hostport", "localhost:9001"); $store.bind($scope, "proto", "weechat"); $store.bind($scope, "password", ""); + $store.bind($scope, "ssl", false); // TODO checkbox for saving password or not? // $scope.password = ""; - $rootScope.closeBuffer = function(buffer_pointer) { - delete($rootScope.buffers[buffer_pointer]); - var first_buffer = _.keys($rootScope.buffers)[0]; - $scope.setActiveBuffer(first_buffer); - } $scope.setActiveBuffer = function(key) { - $rootScope.activeBuffer['active'] = false; - $rootScope.buffers[key]['active'] = true; - $rootScope.buffers[key]['highlight'] = false; - $rootScope.buffers[key]['unread'] = ''; - $rootScope.activeBuffer = $rootScope.buffers[key]; - $rootScope.pageTitle = $rootScope.activeBuffer['short_name'] + ' | ' + $rootScope.activeBuffer['title']; + models.setActiveBuffer(key); + var ab = models.getActiveBuffer(); + $rootScope.pageTitle = ab.shortName + ' | ' + ab.title; $rootScope.scrollToBottom(); }; @@ -562,14 +440,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', 'connection } }); - $rootScope.addLine = function(buffer, line) { - $rootScope.buffers[buffer]['lines'].push(line); - // Scroll if needed - if ($rootScope.activeBuffer['id'] == buffer) { - $rootScope.scrollToBottom(); - } - } - $rootScope.scrollToBottom = function() { setTimeout(function() { window.scrollTo(0, window.scrollMaxY); @@ -590,22 +460,18 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', 'connection } /* Function gets called from bufferLineAdded code if user should be notified */ - $rootScope.createHighlight = function(prefix, text, message, buffer, additionalContent) { - var prefixs = ""; - prefixs += prefix[0].text; - if(prefix[1] != undefined) { - prefixs += prefix[1].text; - } + $rootScope.createHighlight = function(buffer, message) { var messages = ""; - messages += text[0].text; + message.content.forEach(function(part) { + if (part.text != undefined) + messages += part.text + " "; + }); - var buffers = $rootScope.buffers[buffer]; - - var title = buffers.full_name; - var content = "<"+prefixs+">"+messages; + var title = buffer.fullName; + var content = messages; var timeout = 15*1000; - console.log('Displaying notification:',title,',with timeout:',timeout); + console.log('Displaying notification:buffer:',buffer,',message:',message,',with timeout:',timeout); var notification = new Notification(title, {body:content, icon:'img/favicon.png'}); // Cancel notification automatically notification.onshow = function() { diff --git a/js/weechat-protocol.js b/js/weechat-protocol.js new file mode 100644 index 0000000..05b3096 --- /dev/null +++ b/js/weechat-protocol.js @@ -0,0 +1,655 @@ +/** + * WeeChat protocol handling. + * + * This object parses messages and formats commands for the WeeChat + * protocol. It's independent from the communication layer and thus + * may be used with any network mechanism. + */ +var WeeChatProtocol = function() { + // specific parsing for each message type + this._types = { + 'chr': this._getChar, + 'int': this._getInt, + 'str': this._getString, + 'inf': this._getInfo, + 'hda': this._getHdata, + 'ptr': this._getPointer, + 'lon': this._getStrNumber, + 'tim': this._getTime, + 'buf': this._getString, + 'arr': this._getArray, + 'htb': this._getHashTable, + 'inl': function() { + this._warnUnimplemented('infolist'); + } + }; + + // string value for some message types + this._typesStr = { + 'chr': this._strDirect, + 'str': this._strDirect, + 'int': this._strToString, + 'tim': this._strToString, + 'ptr': this._strDirect + }; +}; + +/** + * Unsigned integer array to string. + * + * @param uia Unsigned integer array + * @return Decoded string + */ +WeeChatProtocol._uia2s = function(uia) { + var str = []; + + for (var c = 0; c < uia.length; c++) { + str.push(String.fromCharCode(uia[c])); + } + + return decodeURIComponent(escape(str.join(''))); +}; + +/** + * Merges default parameters with overriding parameters. + * + * @param defaults Default parameters + * @param override Overriding parameters + * @return Merged parameters + */ +WeeChatProtocol._mergeParams = function(defaults, override) { + for (var v in override) { + defaults[v] = override[v]; + } + + return defaults; +} + +/** + * Formats a command. + * + * @param id Command ID (null for no ID) + * @param name Command name + * @param parts Command parts + * @return Formatted command string + */ +WeeChatProtocol._formatCmd = function(id, name, parts) { + var cmdIdName; + var cmd; + + cmdIdName = (id !== null) ? '(' + id + ') ' : ''; + cmdIdName += name; + parts.unshift(cmdIdName); + cmd = parts.join(' '); + cmd += '\n'; + + return cmd; +}; + +/** + * Formats an init command. + * + * @param params Parameters: + * password: password (optional) + * compression: compression ('off' or 'zlib') (optional) + * @return Formatted init command string + */ +WeeChatProtocol.formatInit = function(params) { + var defaultParams = { + password: null, + compression: 'off' + }; + var keys = []; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + keys.push('compression=' + params.compression); + if (params.password !== null) { + keys.push('password=' + params.password); + } + parts.push(keys.join(',')); + + return WeeChatProtocol._formatCmd(null, 'init', parts); +}; + +/** + * Formats an hdata command. + * + * @param params Parameters: + * id: command ID (optional) + * path: hdata path (mandatory) + * keys: array of keys (optional) + * @return Formatted hdata command string + */ +WeeChatProtocol.formatHdata = function(params) { + var defaultParams = { + id: null, + keys: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + parts.push(params.path); + if (params.keys !== null) { + parts.push(params.keys.join(',')); + } + + return WeeChatProtocol._formatCmd(params.id, 'hdata', parts); +}; + +/** + * Formats an info command. + * + * @param params Parameters: + * id: command ID (optional) + * name: info name (mandatory) + * @return Formatted info command string + */ +WeeChatProtocol.formatInfo = function(params) { + var defaultParams = { + id: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + parts.push(params.name); + + return WeeChatProtocol._formatCmd(params.id, 'info', parts); +}; + +/** + * Formats a nicklist command. + * + * @param params Parameters: + * id: command ID (optional) + * buffer: buffer name (optional) + * @return Formatted nicklist command string + */ +WeeChatProtocol.formatNicklist = function(params) { + var defaultParams = { + id: null, + buffer: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + if (params.buffer !== null) { + parts.push(params.buffer); + } + + return WeeChatProtocol._formatCmd(params.id, 'nicklist', parts); +}; + +/** + * Formats an input command. + * + * @param params Parameters: + * id: command ID (optional) + * buffer: target buffer (mandatory) + * data: input data (mandatory) + * @return Formatted input command string + */ +WeeChatProtocol.formatInput = function(params) { + var defaultParams = { + id: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + parts.push(params.buffer); + parts.push(params.data); + + return WeeChatProtocol._formatCmd(params.id, 'input', parts); +}; + +/** + * Formats a sync or a desync command. + * + * @param params Parameters (see _formatSync and _formatDesync) + * @return Formatted sync/desync command string + */ +WeeChatProtocol._formatSyncDesync = function(cmdName, params) { + var defaultParams = { + id: null, + buffers: null, + options: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + if (params.buffers !== null) { + parts.push(params.buffers.join(',')); + if (params.options !== null) { + parts.push(params.options.join(',')); + } + } + + return WeeChatProtocol._formatCmd(params.id, cmdName, parts); +} + +/** + * Formats a sync command. + * + * @param params Parameters: + * id: command ID (optional) + * buffers: array of buffers to sync (optional) + * options: array of options (optional) + * @return Formatted sync command string + */ +WeeChatProtocol.formatSync = function(params) { + return WeeChatProtocol._formatSyncDesync('sync', params); +}; + +/** + * Formats a desync command. + * + * @param params Parameters: + * id: command ID (optional) + * buffers: array of buffers to desync (optional) + * options: array of options (optional) + * @return Formatted desync command string + */ +WeeChatProtocol.formatDesync = function(params) { + return WeeChatProtocol._formatSyncDesync('desync', params); +}; + +/** + * Formats a test command. + * + * @param params Parameters: + * id: command ID (optional) + * @return Formatted test command string + */ +WeeChatProtocol.formatTest = function(params) { + var defaultParams = { + id: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + + return WeeChatProtocol._formatCmd(params.id, 'test', parts); +}; + +/** + * Formats a quit command. + * + * @return Formatted quit command string + */ +WeeChatProtocol.formatQuit = function() { + return WeeChatProtocol._formatCmd(null, 'quit', []); +}; + +/** + * Formats a ping command. + * + * @param params Parameters: + * id: command ID (optional) + * args: array of custom arguments (optional) + * @return Formatted ping command string + */ +WeeChatProtocol.formatPing = function(params) { + var defaultParams = { + id: null, + args: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + if (params.args !== null) { + parts.push(params.args.join(' ')); + } + + return WeeChatProtocol._formatCmd(params.id, 'ping', parts); +}; + +WeeChatProtocol.prototype = { + /** + * Warns that message parsing is not implemented for a + * specific type. + * + * @param type Message type to display + */ + _warnUnimplemented: function(type) { + console.log('Warning: ' + type + ' message parsing is not implemented'); + }, + + /** + * Reads a 3-character message type token value from current + * set data. + * + * @return Type + */ + _getType: function() { + var t = this._getSlice(3); + + if (!t) { + return null; + } + + return WeeChatProtocol._uia2s(new Uint8Array(t)); + }, + + /** + * Runs the appropriate read routine for the specified message type. + * + * @param type Message type + * @return Data value + */ + _runType: function(type) { + var cb = this._types[type]; + var boundCb = cb.bind(this); + + return boundCb(); + }, + + /** + * Reads a "number as a string" token value from current set data. + * + * @return Number as a string + */ + _getStrNumber: function() { + var len = this._getByte(); + var str = this._getSlice(len); + + return WeeChatProtocol._uia2s(new Uint8Array(str)); + }, + + /** + * Returns the passed object. + * + * @param obj Object + * @return Passed object + */ + _strDirect: function(obj) { + return obj; + }, + + /** + * Calls toString() on the passed object and returns the value. + * + * @param obj Object to call toString() on + * @return String value of object + */ + _strToString: function(obj) { + return obj.toString(); + }, + + /** + * Gets the string value of an object representing the message + * value for a specified type. + * + * @param obj Object for which to get the string value + * @param type Message type + * @return String value of object + */ + _objToString: function(obj, type) { + var cb = this._typesStr[type]; + var boundCb = cb.bind(this); + + return boundCb(obj); + }, + + /** + * Reads an info token value from current set data. + * + * @return Info object + */ + _getInfo: function() { + var info = {}; + info.key = this._getString(); + info.value = this._getString(); + + return info; + }, + + /** + * Reads an hdata token value from current set data. + * + * @return Hdata object + */ + _getHdata: function() { + var self = this; + var paths; + var count; + var objs = []; + var hpath = this._getString(); + + keys = this._getString().split(','); + paths = hpath.split('/'); + count = this._getInt(); + + keys = keys.map(function(key) { + return key.split(':'); + }); + + for (var i = 0; i < count; i++) { + var tmp = {}; + + tmp.pointers = paths.map(function(path) { + return self._getPointer(); + }); + keys.forEach(function(key) { + tmp[key[0]] = self._runType(key[1]); + }); + objs.push(tmp); + }; + + return objs; + }, + + /** + * Reads a pointer token value from current set data. + * + * @return Pointer value + */ + _getPointer: function() { + return this._getStrNumber(); + }, + + /** + * Reads a time token value from current set data. + * + * @return Time value (Date) + */ + _getTime: function() { + var str = this._getStrNumber(); + + return new Date(parseInt(str) * 1000); + }, + + /** + * Reads an integer token value from current set data. + * + * @return Integer value + */ + _getInt: function() { + var parsedData = new Uint8Array(this._getSlice(4)); + + return ((parsedData[0] & 0xff) << 24) | + ((parsedData[1] & 0xff) << 16) | + ((parsedData[2] & 0xff) << 8) | + (parsedData[3] & 0xff); + }, + + /** + * Reads a byte from current set data. + * + * @return Byte value (integer) + */ + _getByte: function() { + var parsedData = new Uint8Array(this._getSlice(1)); + + return parsedData[0]; + }, + + /** + * Reads a character token value from current set data. + * + * @return Character (string) + */ + _getChar: function() { + return this._getByte(); + }, + + /** + * Reads a string token value from current set data. + * + * @return String value + */ + _getString: function() { + var l = this._getInt(); + + if (l > 0) { + var s = this._getSlice(l); + var parsedData = new Uint8Array(s); + + return WeeChatProtocol._uia2s(parsedData); + } + + return ""; + }, + + /** + * Reads a message header from current set data. + * + * @return Header object + */ + _getHeader: function() { + var len = this._getInt(); + var comp = this._getByte(); + + return { + length: len, + compression: comp, + }; + }, + + /** + * Reads a message header ID from current set data. + * + * @return Message ID (string) + */ + _getId: function() { + return this._getString(); + }, + + /** + * Reads an arbitrary object token from current set data. + * + * @return Object value + */ + _getObject: function() { + var self = this; + var type = this._getType(); + + if (type) { + return { + type: type, + content: self._runType(type), + }; + } + }, + + /** + * Reads an hash table token from current set data. + * + * @return Hash table + */ + _getHashTable: function() { + var self = this; + var typeKeys, typeValues, count; + var dict = {}; + + typeKeys = this._getType(); + typeValues = this._getType(); + count = this._getInt(); + + for (var i = 0; i < count; ++i) { + var key = self._runType(typeKeys); + var keyStr = self._objToString(key, typeKeys); + var value = self._runType(typeValues); + dict[keyStr] = value; + } + + return dict; + }, + + /** + * Reads an array token from current set data. + * + * @return Array + */ + _getArray: function() { + var self = this; + var type; + var count; + var values; + + type = this._getType(); + count = this._getInt(); + values = []; + + for (var i = 0; i < count; i++) { + values.push(self._runType(type)); + }; + + return values; + }, + + /** + * Reads a specified number of bytes from current set data. + * + * @param length Number of bytes to read + * @return Sliced array + */ + _getSlice: function(length) { + if (this.dataAt + length > this._data.byteLength) { + return null; + } + + var slice = this._data.slice(this._dataAt, this._dataAt + length); + + this._dataAt += length; + + return slice; + }, + + /** + * Sets the current data. + * + * @param data Current data + */ + _setData: function (data) { + this._data = data; + }, + + /** + * Parses a WeeChat message. + * + * @param data Message data (ArrayBuffer) + * @return Message value + */ + parse: function(data) { + var self = this; + + this._setData(data); + this._dataAt = 0; + + var header = this._getHeader(); + var id = this._getId(); + var objects = []; + var object = this._getObject(); + + while (object) { + objects.push(object); + object = self._getObject(); + } + + return { + header: header, + id: id, + objects: objects, + }; + } +};