This commit is contained in:
Tor Hveem 2013-10-09 17:53:25 +02:00
parent 0ba0ded728
commit 8028070aaa
8 changed files with 1158 additions and 497 deletions

View File

@ -1,6 +1,8 @@
A web client for WeeChat 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: To use the web interface you first need to set a relay and a password:
/relay add weechat 9001 /relay add weechat 9001

View File

@ -1,5 +1,3 @@
html {
}
body { body {
color: white; color: white;
background-color: #222; background-color: #222;
@ -8,6 +6,11 @@ body {
padding-bottom:70px; padding-bottom:70px;
padding-top: 70px; padding-top: 70px;
} }
input#sendMessage {
border: 0;
width: 100%;
}
.content { .content {
height: 100%; height: 100%;
min-height: 100%; min-height: 100%;

View File

@ -1,126 +1,126 @@
<!DOCTYPE html> <!DOCTYPE html>
<html ng-app="weechat" ng-cloak> <html ng-app="weechat" ng-cloak>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge"> <meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title ng-bind-template="WeeChat {{ pageTitle}}"></title> <title ng-bind-template="WeeChat {{ pageTitle}}"></title>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet" media="screen"> <link href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link rel="shortcut icon" type="image/png" href="img/favicon.png" > <link rel="shortcut icon" type="image/png" href="img/favicon.png" >
<link href="css/glowingbear.css" rel="stylesheet" media="screen"> <link href="css/glowingbear.css" rel="stylesheet" media="screen">
<script type="text/javascript" src="js/angular.min.js"></script> <script type="text/javascript" src="js/angular.min.js"></script>
<script type="text/javascript" src="js/underscore.js"></script> <script type="text/javascript" src="js/underscore.js"></script>
<script type="text/javascript" src="js/localstorage.js"></script> <script type="text/javascript" src="js/localstorage.js"></script>
<script type="text/javascript" src="js/protocol.js"></script> <script type="text/javascript" src="js/weechat-protocol.js"></script>
<script type="text/javascript" src="js/websockets.js"></script> <script type="text/javascript" src="js/websockets.js"></script>
<script type="text/javascript" src="js/models.js"></script>
<script type="text/javascript" src="js/plugins.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
</head> </head>
<body> <body ng-controller="WeechatCtrl">
<div ng-controller="WeechatCtrl"> <nav ng-show="connected" class="navbar navbar-default navbar-inverse navbar-fixed-top" role="navigation">
<div ng-hide="connected" class="container"> <div class="navbar-header">
<h2> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<img src="img/favicon.png"> <span class="sr-only">Toggle navigation</span>
glowing bear <span class="icon-bar"></span>
<small> <span class="icon-bar"></span>
WeeChat web frontend <span class="icon-bar"></span>
</small> </button>
</h2> </div>
<div>To start using, please enable relay in your WeeChat client: <div class="navbar-collapse collapse">
<pre> <ul class="nav navbar-nav ">
<li class="label" ng-class="{'active': content.active }" ng-repeat="(key, content) in buffers | toArray | orderBy:'content.number':true">
<a href="#" ng-click="setActiveBuffer(content.id)" title="{{ content.fullName }}">{{ content.shortName }} <span class="badge" ng-class="{'danger': content.notification }" ng-bind="content.unread"></span></a>
</li>
</ul>
</div>
</nav>
<div ng-hide="connected" class="container">
<h2>
<img src="img/favicon.png">
glowing bear
<small>
WeeChat web frontend
</small>
</h2>
<div>To start using, please enable relay in your WeeChat client:
<pre>
/set relay.network.password yourpassword /set relay.network.password yourpassword
/relay add weechat 9001</pre> /relay add weechat 9001</pre>
Note: The communication goes directly between your browser and your weechat in clear text. 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. Connection settings are saved between sessions, including password, in your own browser.
<h4>Encryption</h4> <h4>Encryption</h4>
If you want to use encrypted session you first have to set up the relay using SSL If you want to use encrypted session you first have to set up the relay using SSL
<pre> <pre>
$ mkdir -p ~/.weechat/ssl $ mkdir -p ~/.weechat/ssl
$ cd ~/.weechat/ssl $ cd ~/.weechat/ssl
$ openssl req -nodes -newkey rsa:2048 -keyout relay.pem -x509 -days 365 -out relay.pem $ openssl req -nodes -newkey rsa:2048 -keyout relay.pem -x509 -days 365 -out relay.pem
</pre> </pre>
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:
<pre> <pre>
/relay sslcertkey /relay sslcertkey
/relay add ssl.weechat 8000 /relay add ssl.weechat 8000
</pre> </pre>
</div>
<h3>Connection settings</h3>
<form role="form">
<div class="alert alert-danger" ng-show="errorMessage">
<strong>Oh no!</strong> We cannot connect!
</div>
<div class="form-group">
<label class="control-label" for="hostport">Hostname and port</label>
<input type="text" class="form-control" id="hostport" ng-model="hostport" placeholder="Hostport">
<p class="help-block">Enter the hostname and the port to the WeeChat relay, separated by a :</p>
</div>
<div class="form-group">
<label class="control-label" for="password">WeeChat relay password</label>
<input type="password"class="form-control" id="password" ng-model="password" placeholder="Password">
<p class="help-block">Password will be stored in your browser session</p>
</div>
<div class="form-group">
<label class="control-label" for="proto">Encryption</label>
<input type="checkbox" class="form-control" id="ssl" ng-model="ssl">
<p class="help-block">Check the box if you want to encrypt communication between browser and WeeChat. <strong>Note</strong>: Due to a <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=594502">bug</a> encryption will not work in Firefox. You must also first visit the URL https://weechathost:relayport/ to accept the certificate</p>
</div>
<button class="btn btn-lg btn-primary" ng-click="connect()">Connect!</button>
</form>
</div> </div>
<div class="content" ng-show="connected"> <h3>Connection settings</h3>
<nav class="navbar navbar-default navbar-inverse navbar-fixed-top" role="navigation"> <form role="form">
<div class="navbar-header"> <div class="alert alert-danger" ng-show="errorMessage">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <strong>Oh no!</strong> We cannot connect!
<span class="sr-only">Toggle navigation</span> </div>
<span class="icon-bar"></span> <div class="form-group">
<span class="icon-bar"></span> <label class="control-label" for="hostport">Hostname and port</label>
<span class="icon-bar"></span> <input type="text" class="form-control" id="hostport" ng-model="hostport" placeholder="Hostport">
</button> <p class="help-block">Enter the hostname and the port to the WeeChat relay, separated by a :</p>
</div> </div>
<div class="navbar-collapse collapse"> <div class="form-group">
<ul class="nav navbar-nav "> <label class="control-label" for="password">WeeChat relay password</label>
<li class="label" ng-class="{'active': content.active }" ng-repeat="(key, content) in buffers | toArray | orderBy:'content.number':true"> <input type="password" class="form-control" id="password" ng-model="password" placeholder="Password">
<a ng-click="setActiveBuffer(content.id)" title="{{ content.full_name }}">{{ content.short_name }} <span class="badge" ng-class="{'danger': content.highlight }" ng-bind="content.unread"></span></a> <p class="help-block">Password will be stored in your browser session</p>
</li> </div>
</ul> <div class="form-group">
</div> <label class="control-label" for="proto">Encryption</label>
</nav> <input type="checkbox" class="form-control" id="ssl" ng-model="ssl">
<div class="bufferlines"> <p class="help-block">Check the box if you want to encrypt communication between browser and WeeChat. <strong>Note</strong>: Due to a <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=594502">bug</a> encryption will not work in Firefox. You must also first visit the URL https://weechathost:relayport/ to accept the certificate</p>
<div class="bufferline" ng-repeat="bufferline in activeBuffer.lines"> </div>
<span class="date text-muted"> <button class="btn btn-lg btn-primary" ng-click="connect()">Connect!</button>
{{ bufferline.date | date:'HH:mm' }} </form>
</span> </div>
<div class="content" ng-show="connected">
<div class="bufferlines">
<div class="bufferline" ng-repeat="bufferline in activeBuffer().lines">
<span class="date text-muted">
{{ bufferline.date | date:'HH:mm' }}
</span>
<span ng-repeat="part in bufferline.message" class="text" style="{{ part.fg }}"> <span ng-repeat="part in bufferline.content" class="text" style="{{ part.fg }}">
{{ part.text }} {{ part.text }}
</span> </span>
<div ng-repeat="metadata in bufferline.metadata"> <div ng-repeat="metadata in bufferline.metadata">
<div ng-show="metadata.visible"> <div ng-show="metadata.visible">
<a ng-click="metadata.visible = false">Hide additional content</a> <a ng-click="metadata.visible = false">Hide additional content</a>
<div ng-bind-html-unsafe="metadata.content"></div> <div ng-bind-html-unsafe="metadata.content"></div>
</div>
<div ng-hide="metadata.visible">
<a ng-click="metadata.visible = true">Show additional content</a>
</div>
</div> </div>
<div ng-hide="metadata.visible">
<a ng-click="metadata.visible = true">Show additional content</a>
</div>
</div> </div>
</div> </div>
</div>
<div class="navbar navbar-inverse navbar-fixed-bottom"> </div>
<form class="form form-horizontal" ng-submit="sendMessage()"> <div id="footer" ng-show="connected">
<div class="input-group"> <div class="navbar navbar-inverse navbar-fixed-bottom">
<input type="text" class="form-control" ng-model="command"></input> <form class="form form-horizontal" ng-submit="sendMessage()">
<span class="input-group-btn"> <div class="input-group">
<button class="btn btn-default btn-primary">Send</button> <input id="sendMessage" type="text" class="form-control" ng-model="command">
</span> <span class="input-group-btn">
</div> <button class="btn btn-default btn-primary">Send</button>
</form> </span>
</div> </div>
</form>
</div> </div>
</div> </div>
</body> </body>

183
js/models.js Normal file
View File

@ -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);
}
}]);

132
js/plugins.js Normal file
View File

@ -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 '<iframe width="560" height="315" src="http://www.youtube.com/embed/' + token + '" frameborder="0" allowfullscreen></iframe>'
}
return null;
});
var urlPlugin = new Plugin(function(message) {
var urlPattern = /(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&amp;:\/~+#-]*[\w@?^=%&amp;\/~+#-])?/;
var url = message.match(urlPattern);
if (url) {
return '<a href="' + url[0] + '">' + message + '</a>';
}
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 '<img src="' + url + '" height="300">';
}
return null;
});
return {
plugins: [youtubePlugin, urlPlugin, imagePlugin]
}
});

View File

@ -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,
// },
}

View File

@ -1,4 +1,5 @@
var weechat = angular.module('weechat', ['localStorage']); var weechat = angular.module('weechat', ['localStorage', 'weechatModels', 'plugins']);
weechat.filter('toArray', function () { weechat.filter('toArray', function () {
'use strict'; 'use strict';
@ -177,142 +178,39 @@ weechat.factory('colors', [function($scope) {
}]); }]);
weechat.factory('pluginManager', ['youtubePlugin', 'urlPlugin', 'imagePlugin', function(youtubePlugin, urlPlugin, imagePlugin) { weechat.factory('handlers', ['$rootScope', 'colors', 'models', 'plugins', function($rootScope, colors, models, plugins) {
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 '<iframe width="560" height="315" src="http://www.youtube.com/embed/' + token + '" frameborder="0" allowfullscreen></iframe>'
}
return null;
}
return {
contentForMessage: contentForMessage,
exclusive: true
}
}]);
weechat.factory('urlPlugin', [function() {
var contentForMessage = function(message) {
var urlPattern = /(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&amp;:\/~+#-]*[\w@?^=%&amp;\/~+#-])?/;
var url = message.match(urlPattern);
if (url) {
return '<a target="_blank" href="' + url[0] + '">' + message + '</a>';
}
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 '<img src="' + url + '" height="300">';
}
return null;
}
return {
contentForMessage: contentForMessage
}
}]);
weechat.factory('handlers', ['$rootScope', 'colors', 'pluginManager', function($rootScope, colors, pluginManager) {
var handleBufferClosing = function(message) { var handleBufferClosing = function(message) {
var buffer_pointer = message['objects'][0]['content'][0]['pointers'][0]; var bufferMessage = message['objects'][0]['content'][0];
$rootScope.closeBuffer(buffer_pointer); var buffer = new models.Buffer(bufferMessage);
models.closeBuffer(buffer);
} }
var handleLine = function(line, initial) { var handleLine = function(line, initial) {
var buffer_line = {} var message = new models.BufferLine(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;
});
// Only react to line if its displayed // Only react to line if its displayed
if (displayed) { if(message.displayed) {
buffer_line['message'] = message; 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 (!initial) {
if (!buffer.active && _.contains(message.tags, 'notify_message') && !_.contains(message.tags, 'notify_none')) {
if ($rootScope.buffers[buffer]['unread'] == '') { if (buffer.unread == '' || buffer.unread == undefined) {
$rootScope.buffers[buffer]['unread'] = 1; buffer.unread = 1;
}else { }else {
$rootScope.buffers[buffer]['unread'] = parseInt($rootScope.buffers[buffer]['unread']) + 1; buffer.unread++;
}
} }
}
if (text[0] != undefined) { if(message.highlight || _.contains(message.tags, 'notify_private') ) {
var additionalContent = pluginManager.contentForMessage(text[0]['text']); $rootScope.createHighlight(buffer, message);
buffer.notification = true;
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;
} }
} }
} }
@ -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 handleBufferOpened = function(message) {
var obj = message['objects'][0]['content'][0]; var bufferMessage = message['objects'][0]['content'][0];
var fullName = obj['full_name']; var buffer = new models.Buffer(bufferMessage);
var buffer = obj['pointers'][0]; models.addBuffer(buffer);
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 handleBufferTitleChanged = function(message) { var handleBufferTitleChanged = function(message) {
@ -371,21 +253,10 @@ weechat.factory('handlers', ['$rootScope', 'colors', 'pluginManager', function($
// buffer info from message // buffer info from message
var bufferInfos = message['objects'][0]['content']; var bufferInfos = message['objects'][0]['content'];
// buffers objects // buffers objects
var buffers = {};
for (var i = 0; i < bufferInfos.length ; i++) { for (var i = 0; i < bufferInfos.length ; i++) {
var bufferInfo = bufferInfos[i]; var buffer = new models.Buffer(bufferInfos[i]);
var pointer = bufferInfo['pointers'][0]; models.addBuffer(buffer);
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;
}
} }
$rootScope.buffers = buffers;
// Request latest buffer lines for each buffer // Request latest buffer lines for each buffer
$rootScope.getLines(); $rootScope.getLines();
@ -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 '<iframe width="560" height="315" src="http://www.youtube.com/embed/' + token + '" frameborder="0" allowfullscreen></iframe>'
}
return null;
}
var eventHandlers = { var eventHandlers = {
bufinfo: handleBufferInfo, bufinfo: handleBufferInfo,
lineinfo: handleLineInfo, lineinfo: handleLineInfo,
@ -437,8 +299,8 @@ weechat.factory('handlers', ['$rootScope', 'colors', 'pluginManager', function($
}]); }]);
weechat.factory('connection', ['$rootScope', '$log', 'handlers', 'colors', function($rootScope, $log, handlers, colors) { weechat.factory('connection', ['$rootScope', '$log', 'handlers', 'colors', 'models', function($rootScope, $log, handlers, colors, models) {
protocol = new Protocol(); protocol = new WeeChatProtocol();
var websocket = null; var websocket = null;
@ -453,21 +315,24 @@ weechat.factory('connection', ['$rootScope', '$log', 'handlers', 'colors', funct
} }
// Takes care of the connection and websocket hooks // 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'; var proto = ssl ? 'wss':'ws';
websocket = new WebSocket(proto+"://" + hostport + "/weechat"); websocket = new WebSocket(proto+"://" + hostport + "/weechat");
websocket.binaryType = "arraybuffer" websocket.binaryType = "arraybuffer"
websocket.onopen = function (evt) { websocket.onopen = function (evt) {
var send = ""; doSend(WeeChatProtocol.formatInit({
if (password) { password: passwd,
send += "init compression=off,password=" + password + "\n"; 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"); $log.info("Connected to relay");
doSend(send);
$rootScope.connected = true; $rootScope.connected = true;
$rootScope.$apply(); $rootScope.$apply();
} }
@ -496,13 +361,18 @@ weechat.factory('connection', ['$rootScope', '$log', 'handlers', 'colors', funct
} }
var sendMessage = function(message) { var sendMessage = function(message) {
message = "input " + $rootScope.activeBuffer['full_name'] + " " + message + "\n" doSend(WeeChatProtocol.formatInput({
doSend(message); buffer: models.getActiveBuffer()['fullName'],
data: message
}));
} }
var getLines = function(count) { var getLines = function(count) {
var message = "(lineinfo) hdata buffer:gui_buffers(*)/own_lines/last_line(-"+count+")/data\n"; doSend(WeeChatProtocol.formatHdata({
doSend(message) id: 'lineinfo',
path: "buffer:gui_buffers(*)/own_lines/last_line(-"+count+")/data",
keys: []
}));
} }
return { return {
@ -513,7 +383,7 @@ 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 // Request notification permission
Notification.requestPermission(function (status) { Notification.requestPermission(function (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.commands = []
$rootScope.models = models;
$rootScope.buffer = [] $rootScope.buffer = []
$rootScope.buffers = {}
$rootScope.activeBuffer = null;
$store.bind($scope, "hostport", "localhost:9001"); $store.bind($scope, "hostport", "localhost:9001");
$store.bind($scope, "proto", "weechat"); $store.bind($scope, "proto", "weechat");
$store.bind($scope, "password", ""); $store.bind($scope, "password", "");
$store.bind($scope, "ssl", false);
// TODO checkbox for saving password or not? // TODO checkbox for saving password or not?
// $scope.password = ""; // $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) { $scope.setActiveBuffer = function(key) {
$rootScope.activeBuffer['active'] = false; models.setActiveBuffer(key);
$rootScope.buffers[key]['active'] = true; var ab = models.getActiveBuffer();
$rootScope.buffers[key]['highlight'] = false; $rootScope.pageTitle = ab.shortName + ' | ' + ab.title;
$rootScope.buffers[key]['unread'] = '';
$rootScope.activeBuffer = $rootScope.buffers[key];
$rootScope.pageTitle = $rootScope.activeBuffer['short_name'] + ' | ' + $rootScope.activeBuffer['title'];
$rootScope.scrollToBottom(); $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() { $rootScope.scrollToBottom = function() {
setTimeout(function() { setTimeout(function() {
window.scrollTo(0, window.scrollMaxY); 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 */ /* Function gets called from bufferLineAdded code if user should be notified */
$rootScope.createHighlight = function(prefix, text, message, buffer, additionalContent) { $rootScope.createHighlight = function(buffer, message) {
var prefixs = "";
prefixs += prefix[0].text;
if(prefix[1] != undefined) {
prefixs += prefix[1].text;
}
var messages = ""; 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 = buffer.fullName;
var content = messages;
var title = buffers.full_name;
var content = "<"+prefixs+">"+messages;
var timeout = 15*1000; 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'}); var notification = new Notification(title, {body:content, icon:'img/favicon.png'});
// Cancel notification automatically // Cancel notification automatically
notification.onshow = function() { notification.onshow = function() {

655
js/weechat-protocol.js Normal file
View File

@ -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,
};
}
};