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
========================
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

View file

@ -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%;

View file

@ -1,126 +1,126 @@
<!DOCTYPE html>
<html ng-app="weechat" ng-cloak>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title ng-bind-template="WeeChat {{ pageTitle}}"></title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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 rel="shortcut icon" type="image/png" href="img/favicon.png" >
<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/underscore.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/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="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
</head>
<body>
<div ng-controller="WeechatCtrl">
<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>
<body ng-controller="WeechatCtrl">
<nav ng-show="connected" class="navbar navbar-default navbar-inverse navbar-fixed-top" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="navbar-collapse collapse">
<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
/relay add weechat 9001</pre>
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.
<h4>Encryption</h4>
If you want to use encrypted session you first have to set up the relay using SSL
<pre>
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.
<h4>Encryption</h4>
If you want to use encrypted session you first have to set up the relay using SSL
<pre>
$ mkdir -p ~/.weechat/ssl
$ cd ~/.weechat/ssl
$ openssl req -nodes -newkey rsa:2048 -keyout relay.pem -x509 -days 365 -out relay.pem
</pre>
If WeeChat is already running, you can reload the certificate and private key with command:
<pre>
If WeeChat is already running, you can reload the certificate and private key with command:
<pre>
/relay sslcertkey
/relay add ssl.weechat 8000
</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 class="content" ng-show="connected">
<nav class="navbar navbar-default navbar-inverse navbar-fixed-top" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="navbar-collapse collapse">
<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 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>
</li>
</ul>
</div>
</nav>
<div class="bufferlines">
<div class="bufferline" ng-repeat="bufferline in activeBuffer.lines">
<span class="date text-muted">
{{ bufferline.date | date:'HH:mm' }}
</span>
<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 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 }}">
{{ part.text }}
</span>
<span ng-repeat="part in bufferline.content" class="text" style="{{ part.fg }}">
{{ part.text }}
</span>
<div ng-repeat="metadata in bufferline.metadata">
<div ng-show="metadata.visible">
<a ng-click="metadata.visible = false">Hide additional content</a>
<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 ng-repeat="metadata in bufferline.metadata">
<div ng-show="metadata.visible">
<a ng-click="metadata.visible = false">Hide additional content</a>
<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 class="navbar navbar-inverse navbar-fixed-bottom">
<form class="form form-horizontal" ng-submit="sendMessage()">
<div class="input-group">
<input type="text" class="form-control" ng-model="command"></input>
<span class="input-group-btn">
<button class="btn btn-default btn-primary">Send</button>
</span>
</div>
</form>
</div>
</div>
</div>
<div id="footer" ng-show="connected">
<div class="navbar navbar-inverse navbar-fixed-bottom">
<form class="form form-horizontal" ng-submit="sendMessage()">
<div class="input-group">
<input id="sendMessage" type="text" class="form-control" ng-model="command">
<span class="input-group-btn">
<button class="btn btn-default btn-primary">Send</button>
</span>
</div>
</form>
</div>
</div>
</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 () {
'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 '<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) {
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 '<iframe width="560" height="315" src="http://www.youtube.com/embed/' + token + '" frameborder="0" allowfullscreen></iframe>'
}
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() {

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