changeset 0:a2dab4544b2d default tip

Initial commit.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Fri, 28 Jan 2011 02:35:04 +0100
parents
children
files config.js forms.js namespaces.js xmpp.js
diffstat 4 files changed, 1000 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/config.js
@@ -0,0 +1,6 @@
+var config = exports;
+
+config.jid = 'you@example.org';
+config.password = 'hellohello';
+
+config.server = 'pubsub.example.com';
new file mode 100644
--- /dev/null
+++ b/forms.js
@@ -0,0 +1,217 @@
+/*
+ *  Copyright (C) 2010  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ *
+ *  This file is part of PSĜS, a PubSub server written in JavaScript.
+ *
+ *  PSĜS is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as
+ *  published by the Free Software Foundation, either version 3 of the
+ *  License.
+ *
+ *  PSĜS is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with PSĜS.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var xmpp = require('clxmpp')
+
+var parseBoolean = function(b) {
+	if (b == 'true' || b == 'True' || b == 'TRUE' || b == '1')
+		return true;
+	return false;
+}
+
+exports.build = function(type, desc, content, labels, title, instructions) {
+	var x = xmpp.stanza('x', {xmlns: 'jabber:x:data', type: type});
+
+	if (desc._TITLE)
+		x.s('title').t(desc._TITLE);
+	else if (title)
+		x.s('title').t(title);
+
+	if (desc._INSTRUCTIONS)
+		x.s('instructions').t(desc._INSTRUCTIONS);
+	else if (instructions)
+		x.s('instructions').t(instructions);
+
+	if (content == 'default') {
+		content = {};
+		for (var i in desc)
+			content[i] = desc[i].value;
+	}
+
+	for (var i in desc) {
+		if (i[0] == '_')
+			continue;
+
+		var fieldAttr = {'var': i};
+
+		if (labels) {
+			if (desc[i].type)
+				fieldAttr.type = desc[i].type;
+			if (desc[i].label)
+				fieldAttr.label = desc[i].label;
+		}
+		var field = xmpp.stanza('field', fieldAttr);
+
+		if (labels &&
+		    (desc[i].type == 'list-multi' ||
+		     desc[i].type == 'list-single')) {
+			for (var j in desc[i].options) {
+				var optAttr = {};
+				if (desc[i].options[j].label)
+					optAttr.label = desc[i].options[j].label;
+				field.s('option', optAttr).c('value').t(j);
+			}
+		}
+
+		if (i == 'FORM_TYPE')
+			field.s('value').t(desc[i].value);
+		else if (typeof content[i] != 'undefined') {
+			var md = content[i];
+			if (desc[i].type == 'jid-multi' ||
+			    desc[i].type == 'list-multi' ||
+			    desc[i].type == 'text-multi') {
+				for (var j=0; j<md.length; j++)
+					field.s('value')
+						.t(md[j].toString());
+			} else
+				field.s('value').t(md.toString());
+		}
+
+		x.cnode(field);
+	}
+	if (field)
+		return x;
+	return -1; //FIXME
+}
+
+exports.parse = function(x, params) {
+	var form = {};
+
+	if (!params) {
+		var type = x.getAttribute('type');
+		if (type)
+			form.type = type;
+
+		var title = x.getChild('title');
+		if (title)
+			form.title = title;
+
+		var instructions = x.getChild('instructions');
+		if (instructions)
+			form.instructions = instructions;
+	}
+
+	form.fields = {};
+	for (var i in x.tags) {
+		var field = x.tags[i];
+		var name = field.getAttribute('var');
+		if (params && name == 'FORM_TYPE')
+			continue;
+
+		if (params) {
+			var type = field.getAttribute('type');
+			if (type == 'jid-multi' || type == 'list-multi' || type == 'text-multi') {
+				form.fields[name] = [];
+				for (var j in field.tags) {
+					var elem = field.tags[j];
+					if (elem.name == 'value')
+						form.fields[name].push(elem.getText());
+				}
+			} else if (type == 'boolean') {
+				var value = field.getChild('value');
+				if (value)
+					form.fields[name] = parseBoolean(value.getText());
+			} else {
+				var value = field.getChild('value');
+				if (value)
+					form.fields[name] = value.getText();
+			}
+		} else {
+			form.fields[name] = {};
+
+			var label = field.getAttribute('label');
+			if (label)
+				form.fields[name].label = label;
+
+			var type = field.getAttribute('type');
+			if (type)
+				form.fields[name].type = type;
+
+			if (field.tags.length) {
+				if (field.tags.length == 1) {
+					if (field.tags[0].name == 'value') {
+						var value = field.tags[0];
+						if (type == 'boolean')
+							form.fields[name].value = parseBoolean(value.getText());
+						else
+							form.fields[name].value = value.getText();
+					}
+				} else {
+					form.fields[name].options = {};
+					form.fields[name].values = [];
+					for (var j in field.tags) {
+						var elem = field.tags[j];
+						if (elem.name == 'option') {
+							var value = elem.getChild('value');
+							if (!value)
+								continue;
+
+							value = value.getText();
+							if (!value)
+								continue;
+
+							form.fields[name].options[value] = {};
+
+							label = elem.getAttribute('label')
+							if (label)
+								form.fields[name].options[value].label = label;
+						} else if (elem.name == 'value')
+							form.fields[name].values.push(elem.getText());
+					}
+					if (form.fields[name].values.length == 1) {
+						form.fields[name].value = form.fields[name].values[0];
+						delete form.fields[name].values;
+					}
+				}
+			}
+		}
+	}
+	return form;
+}
+
+exports.toString = function(form) {
+	var text = '';
+
+	var inspect = require('sys').inspect;
+
+	for (var i in form.fields) {
+		if (i == 'FORM_TYPE')
+			continue;
+
+		var field = form.fields[i];
+		text += '\n\t' + i;
+		if (field.label)
+			text += ' (' + field.label + ')';
+		if (field.type)
+			text += ', ' + field.type;
+
+		text += ': ';
+		if (typeof field.value != 'undefined')
+			text += inspect(field.value);
+		else if (field.values)
+			text += inspect(field.values);
+		else
+			text += 'empty';
+
+		if (typeof field.options == 'object')
+			for (var j in field.options)
+				text += '\n\t\t' + j + ': ' + field.options[j].label;
+	}
+	return text;
+}
new file mode 100644
--- /dev/null
+++ b/namespaces.js
@@ -0,0 +1,16 @@
+var NS = exports;
+
+NS.DISCO_INFO = 'http://jabber.org/protocol/disco#info';
+NS.DISCO_ITEMS = 'http://jabber.org/protocol/disco#items';
+
+NS.PUBSUB = 'http://jabber.org/protocol/pubsub';
+NS.PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner';
+NS.PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event';
+NS.PUBSUB_ERROR = 'http://jabber.org/protocol/pubsub#error';
+NS.PUBSUB_METADATA = 'http://jabber.org/protocol/pubsub#meta-data';
+NS.PUBSUB_NODE_CONFIG = 'http://jabber.org/protocol/pubsub#node_config';
+NS.PUBSUB_PUBLISH_OPTIONS = 'http://jabber.org/protocol/pubsub#publish-options';
+
+NS.DATAFORMS = 'jabber:x:data';
+NS.DELAY = 'urn:xmpp:delay';
+NS.VERSION = 'jabber:iq:version';
new file mode 100755
--- /dev/null
+++ b/xmpp.js
@@ -0,0 +1,761 @@
+#!/usr/bin/env node
+
+var nc = require('ncurses')
+var consts = require('ncconsts')
+var sys = require('sys');
+
+var fs = require('fs');
+
+var xmpp = require('clxmpp');
+var config = require('./config');
+var forms = require('./forms');
+var NS = require('./namespaces');
+
+var options = {
+	xml: false,
+	command_parsing: false,
+	chr: false
+}
+
+var st = [
+	'Error',
+	'Connecting',
+	'Connfail',
+	'Authenticating',
+	'Authfail',
+	'Connected',
+	'Disconnected',
+	'Disconnecting'
+];
+
+var jid = config.jid.split('@');
+var user = jid[0];
+var server = jid[1];
+
+var history = [];
+
+process.addListener('SIGWINCH', function (err) {
+	win.resize(win.lines, win.cols);
+	win.hline(win.height-2, 0, win.width);
+	win.setscrreg(0, win.height-3);
+	win.cursor(win.height-1, 0);
+
+	win.clearok(true);
+	win.refresh();
+	appendLine('Window size changed to ' + win.lines + ' x ' + win.cols + '!');
+});
+
+process.addListener('uncaughtException', function (err) {
+	appendLine('Uncaught exception (' + err + '), this should never happen:\n' + err.stack + '.');
+});
+
+// ============================================================================
+// 				   FUNCTIONS
+// ============================================================================
+
+var getTimestamp = function() {
+	var time = new Date();
+	var hours = time.getHours();
+	var mins = time.getMinutes();
+	return '' + (hours < 10 ? '0' : '') + hours + ':' + (mins < 10 ? '0' : '') + mins;
+};
+
+var appendLine = function(message) {
+	var curx=win.curx;
+	win.scroll(1);
+	win.cursor(win.height-3, 0);
+	win.print('[' + getTimestamp() + '] ');
+	if (arguments.length > 1 && arguments[1]) {
+		if (win.hasColors)
+			win.attrset(win.colorPair(1));
+		win.print('== ');
+	}
+	if (arguments.length > 2 && arguments[2] && win.hasColors)
+		win.attrset(arguments[2]);
+	win.print('' + message);
+	if (arguments.length > 2 && arguments[2] && win.hasColors)
+		win.attrset(win.colorPair(0));
+	if (arguments.length > 3) {
+		win.cursor(win.height-1, 0);
+		win.clrtoeol();
+	} else
+		win.cursor(win.height-1, curx);
+	win.refresh();
+};
+
+var cleanup = function() {
+	win.clear();
+	win.refresh();
+	win.close();
+	process.exit(0);
+};
+
+
+// ============================================================================
+// 				    HANDLERS
+// ============================================================================
+
+var on = {
+	configure: function(stanza) {
+		var pubsub = stanza.getChild('pubsub', NS.PUBSUB_OWNER);
+		var configure = pubsub.getChild('configure', NS.PUBSUB_OWNER);
+
+		var nodeID = configure.getAttribute('node');
+		if (nodeID)
+			var text = 'Configuration of node “' + nodeID + '”:';
+		else
+			var text = 'Configuration of a new node:';
+
+		var x = configure.getChild('x', 'jabber:x:data');
+		var form = forms.parse(x);
+		text += forms.toString(form);
+
+		next.func = function() {
+			var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'set'})
+				.c('pubsub', {xmlns: NS.PUBSUB_OWNER})
+					.c('configure', {node: nodeID});
+
+			var form = forms.build();
+			stanza.cnode(form);
+
+			return stanza;
+		};
+
+		appendLine(text);
+	},
+
+	default: function(stanza) {
+		var pubsub = stanza.getChild('pubsub', NS.PUBSUB);
+		var def = pubsub.getChild('default', NS.PUBSUB);
+
+		var nodeID = def.getAttribute('node');
+		if (nodeID)
+			var text = 'Options of a new subscription on ' + nodeID + ':';
+		else
+			var text = 'Subscription options on a new node:';
+
+		var x = def.getChild('x', 'jabber:x:data');
+		var form = forms.parse(x);
+		text += '\nOptions:' + forms.toString(form);
+
+		appendLine(text);
+	},
+
+	'delete': function(stanza) {
+		appendLine('Node correctly deleted.');
+	},
+
+	purge: function(stanza) {
+		appendLine('Node correctly purged.');
+	},
+
+	info: function(stanza) {
+		var query = stanza.getChild('query', NS.DISCO_INFO);
+		var list = query.tags;
+
+		var nodeID = query.getAttribute('node');
+
+		var text = '';
+		for (var i in list) {
+			if (list[i].name == 'identity') {
+				if (nodeID)
+					text += '"' + nodeID + '"';
+				else
+					text += '"' + list[i].getAttribute('name') + '"';
+
+				text += ', ' + list[i].getAttribute('category')
+					+ ' ' + list[i].getAttribute('type')
+					+ ', features:';
+			} else if (list[i].name == 'feature')
+				text += '\n\t' + list[i].getAttribute('var');
+		}
+
+		var x = query.getChild('x', 'jabber:x:data');
+		if (x) {
+			var form = forms.parse(x);
+			text += '\nMetadata:' + forms.toString(form);
+		}
+		appendLine(text);
+	},
+
+	list: function(stanza) {
+		var query = stanza.getChild('query', NS.DISCO_ITEMS);
+		var list = query.tags;
+
+		var nodeID = query.getAttribute('node');
+		if (nodeID)
+			var text = 'Nodes in ' + nodeID + ':';
+		else
+			var text = 'Nodes:';
+
+		for (var i in list) {
+			var node = list[i].getAttribute('node');
+			if (node) {
+				text += '\n\t' + list[i].getAttribute('node');
+
+				var name = list[i].getAttribute('name');
+				if (name)
+					text += ' (' + name + ')';
+			}
+		}
+
+		if (nodeID)
+			text += '\nItems in ' + nodeID + ':';
+		else
+			text += '\nItems:';
+
+		for (var i in list) {
+			var node = list[i].getAttribute('node');
+			if (!node)
+				text += '\n\t' + list[i].getAttribute('name');
+		}
+
+		appendLine(text);
+	},
+
+	manage: function(stanza) {
+		var pubsub = stanza.getChild('pubsub', NS.PUBSUB_OWNER);
+		var tag = pubsub.tags[0];
+
+		if (tag.attr.xmlns != NS.PUBSUB_OWNER)
+			return;
+
+		var action = tag.name;
+		if (action != 'subscriptions' && action != 'affiliations')
+			return;
+
+		var nodeID = tag.getAttribute('node');
+		if (!nodeID)
+			return;
+
+		var text = action[0].toUpperCase() + action.substr(1) + ' in ' + nodeID + ':';
+
+		for (var i in tag.tags) {
+			var sub = tag.tags[i];
+
+			var jid = sub.getAttribute('jid');
+			text += '\n\t' + jid;
+
+			if (action == 'subscriptions') {
+				var subscription = sub.getAttribute('subscription');
+				var subid = sub.getAttribute('subid');
+
+				text += ' (' + subid + '): ' + subscription;
+			} else {
+				var affiliation = sub.getAttribute('affiliation');
+
+				text += ': ' + affiliation;
+			}
+		}
+
+		next.func = function() {
+			var tag = this.tag;
+			var name = tag.substr(0, tag.length-1);
+
+			var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'set'})
+				.c('pubsub', {xmlns: NS.PUBSUB_OWNER})
+					.c(tag, {node: nodeID});
+
+			for (var i in this) {
+				if (!(this[i] instanceof Array))
+					continue;
+
+				if (this[i].length != 2)
+					continue;
+
+				var obj = {jid: this[i][0]};
+				obj[name] = this[i][1];
+				stanza.c(name, obj).up();
+			}
+
+			return stanza;
+		};
+
+		next.tag = action;
+
+		appendLine(text);
+	},
+
+	options: function(stanza) {
+		var pubsub = stanza.getChild('pubsub', NS.PUBSUB);
+		var options = pubsub.getChild('options', NS.PUBSUB);
+
+		var nodeID = options.getAttribute('node');
+		var jid = options.getAttribute('jid');
+		var text = 'Options of ' + jid + ' on ' + nodeID + ':';
+
+		var x = options.getChild('x', 'jabber:x:data');
+		var form = forms.parse(x);
+		text += '\nOptions:' + forms.toString(form);
+
+		appendLine(text);
+	},
+
+	error: function(stanza) {
+		var error = stanza.getChild('error');
+		var type = error.getAttribute('type');
+		var name = error.tags[0].name;
+
+		var text = 'Got error of type ' + type + ': ' + name;
+
+		if (error.tags[1]) {
+			var name2 = error.tags[1].name;
+
+			text += ', ';
+
+			var feature = error.tags[1].getAttribute('feature');
+			if (feature)
+				text += 'feature ' + feature + ' ';
+
+			text += name2;
+		}
+		text += '.';
+
+		appendLine(text, false, win.colorPair(1));
+	}
+};
+
+// ============================================================================
+// 				    COMMANDS
+// ============================================================================
+
+var next = {};
+
+var commands = {
+	create: function(args) {
+		var nodeID = args[0];
+		var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'set'})
+			.c('pubsub', {xmlns: NS.PUBSUB})
+				.c('create', {node: nodeID});
+		conn.sendIQ(stanza, function(stanza) {
+			appendLine('Node correctly created.');
+		}, on.error);
+	},
+
+	configure: function(args) {
+		if (args < 1)
+			return;
+
+		var nodeID = args[0];
+
+		var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+			.c('pubsub', {xmlns: NS.PUBSUB_OWNER})
+				.c('configure', {node: nodeID});
+
+		conn.sendIQ(stanza, on.configure, on.error);
+	},
+
+	default: function(args) {
+		var nodeID = args[0];
+
+		if (nodeID)
+			var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+				.c('pubsub', {xmlns: NS.PUBSUB})
+					.c('default', {node: nodeID});
+		else
+			var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+				.c('pubsub', {xmlns: NS.PUBSUB})
+					.c('default');
+
+		conn.sendIQ(stanza, on.default, on.error);
+	},
+
+	'delete': function(args) {
+		var nodeID = args[0];
+		var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'set'})
+			.c('pubsub', {xmlns: NS.PUBSUB_OWNER})
+				.c('delete', {node: nodeID});
+
+		conn.sendIQ(stanza, on.delete, on.error);
+	},
+
+	purge: function(args) {
+		var nodeID = args[0];
+		var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'set'})
+			.c('pubsub', {xmlns: NS.PUBSUB_OWNER})
+				.c('purge', {node: nodeID});
+
+		conn.sendIQ(stanza, on.purge, on.error);
+	},
+
+	info: function(args) {
+		if (args.length)
+			var disco = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+				.c('query', {xmlns: NS.DISCO_INFO, node: args[0]});
+		else
+			var disco = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+				.c('query', {xmlns: NS.DISCO_INFO});
+
+		conn.sendIQ(disco, on.info, on.error);
+	},
+
+	help: function(args) {
+		if (args.length == 0) {
+			var text = 'Available commands:';
+			for (var i in commands)
+				text += '\n\t' + i;
+		} else {
+			var text = 'Commands:';
+			for (var i in commands)
+				for (var j in args)
+					if (i == args[j])
+						text += '\n\t' + i;
+		}
+		appendLine(text);
+	},
+
+	list: function(args) {
+		if (args.length)
+			var disco = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+				.c('query', {xmlns: NS.DISCO_ITEMS, node: args[0]});
+		else
+			var disco = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+				.c('query', {xmlns: NS.DISCO_ITEMS});
+
+		conn.sendIQ(disco, on.list, on.error);
+	},
+
+	manage: function(args) {
+		if (args.length < 2)
+			return;
+
+		var action = args[0];
+		var nodeID = args[1];
+
+		if (!nodeID)
+			return;
+
+		if (action != 'subscriptions' && action != 'affiliations')
+			return;
+
+		var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+			.c('pubsub', {xmlns: NS.PUBSUB_OWNER})
+				.c(action, {node: nodeID});
+
+		conn.sendIQ(stanza, on.manage, on.error);
+	},
+
+	options: function(args) {
+		if (args.length < 1)
+			return;
+
+		var nodeID = args[0];
+
+		var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+			.c('pubsub', {xmlns: NS.PUBSUB})
+				.c('options', {node: nodeID, jid: config.jid});
+
+		conn.sendIQ(stanza, on.opions, on.error);
+	},
+
+	quit: function() {
+		conn.send('</stream:stream>');
+		cleanup();
+	},
+
+	refresh: function() {
+		win.clearok(true);
+		appendLine('Refresh, done!');
+	},
+
+	retrieve: function(args) {
+		if (args.length < 1)
+			return;
+
+		var action = args[0];
+		var nodeID = args[1];
+
+		if (action == 'subscriptions') {
+			if (nodeID)
+				var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+					.c('pubsub', {xmlns: NS.PUBSUB})
+						.c('subscriptions', {node: nodeID});
+			else
+				var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+					.c('pubsub', {xmlns: NS.PUBSUB})
+						.c('subscriptions');
+
+			conn.sendIQ(stanza, function(stanza) {
+				var pubsub = stanza.getChild('pubsub', NS.PUBSUB);
+				var subscriptions = pubsub.getChild('subscriptions', NS.PUBSUB);
+
+				var nodeID = subscriptions.getAttribute('node');
+				if (nodeID)
+					var text = 'Subscriptions in ' + nodeID + ':';
+				else
+					var text = 'Subscriptions:';
+
+				for (var i in subscriptions.tags) {
+					var sub = subscriptions.tags[i];
+					text += '\n\t' + sub.getAttribute('jid') + ', ' + sub.getAttribute('subscription') + ' to node ' + sub.getAttribute('node') + ' (' + sub.getAttribute('subid') + ')';
+				}
+
+				appendLine(text);
+			}, on.error);
+		} else if (action == 'affiliations') {
+			if (nodeID)
+				var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+					.c('pubsub', {xmlns: NS.PUBSUB})
+						.c('affiliations', {node: nodeID});
+			else
+				var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'get'})
+					.c('pubsub', {xmlns: NS.PUBSUB})
+						.c('affiliations');
+
+			conn.sendIQ(stanza, function(stanza) {
+				var pubsub = stanza.getChild('pubsub', NS.PUBSUB);
+				var affiliations = pubsub.getChild('affiliations', NS.PUBSUB);
+
+				var text = 'Affiliations:';
+
+				for (var i in affiliations.tags) {
+					var affil = affiliations.tags[i];
+					text += '\n\t' + affil.getAttribute('affiliation') + ' of node ' + affil.getAttribute('node') + '.';
+				}
+
+				appendLine(text);
+			}, on.error);
+		}
+	},
+
+	send: function() {
+		var stanza = next.func();
+		appendLine(sys.inspect(stanza, false, null));
+		conn.sendIQ(stanza, function() {
+			next = {};
+		}, on.error);
+	},
+
+	sendFile: function(args) {
+		if (args.length < 1)
+			return;
+
+		fs.readFile(args[0], function(err, data) {
+			if (err)
+				return;
+
+			conn.send(data.toString());
+		});
+	},
+
+	set: function(args) {
+		if (args.length < 1)
+			return;
+
+		var key = args.shift();
+
+		if (!args.length)
+			delete next[key];
+		else
+			next[key] = args;
+
+		appendLine(sys.inspect(next));
+	},
+
+	subscribe: function(args) {
+		if (args.length < 1)
+			return;
+
+		var nodeID = args[0];
+		var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'set'})
+			.c('pubsub', {xmlns: NS.PUBSUB})
+				.c('subscribe', {node: nodeID, jid: config.jid});
+
+		conn.sendIQ(stanza, function(stanza) {
+			var pubsub = stanza.getChild('pubsub', NS.PUBSUB);
+			var subscription = pubsub.getChild('subscription', NS.PUBSUB);
+
+			var text = subscription.getAttribute('jid') + ' ' + subscription.getAttribute('subscription') + ' to ' + subscription.getAttribute('node') + ': ' + subscription.getAttribute('subid') + '.';
+
+			var subOpt = subscription.getChild('subscribe-options', NS.PUBSUB);
+			if (subOpt && subOpt.getChild('required', NS.PUBSUB))
+				text += ' Warning: you should configure your subscription!';
+
+			appendLine(text);
+		}, on.error);
+	},
+
+	unsubscribe: function(args) {
+		if (args.length < 1)
+			return;
+
+		var nodeID = args[0];
+		var stanza = xmpp.iq({to: config.server, id: conn.getUniqueId(), type: 'set'})
+			.c('pubsub', {xmlns: NS.PUBSUB})
+				.c('unsubscribe', {node: nodeID, jid: config.jid});
+
+		conn.sendIQ(stanza, function(stanza) {
+			appendLine('Node correctly unsubscribed.');
+		}, on.error);
+	},
+
+	activate: function(args) {
+		if (args[0] == 'xml')
+			conn.log = function(_, m) {
+				if (m.indexOf('--> ') == 0)
+					appendLine(m.substr(4), false, COLOR_IN);
+				else if (m.indexOf('<== ') == 0)
+					appendLine(m.substr(4), false, COLOR_OUT);
+				else
+					appendLine(m);
+			};
+	},
+
+	deactivate: function(args) {
+		if (args[0] == 'xml')
+			conn.log = function(_, m) {};
+	},
+
+	toggle: function(args) {
+		if (args[0] == 'xml') {
+			if (options.xml)
+				conn.log = function(_, m) {};
+			else
+				conn.log = function(_, m) {
+					if (m.indexOf('--> ') == 0)
+						appendLine(m.substr(4), false, COLOR_IN);
+					else if (m.indexOf('<== ') == 0)
+						appendLine(m.substr(4), false, COLOR_OUT);
+					else
+						appendLine(m);
+				};
+			options.xml = !options.xml;
+		}
+		if (args[0] in options)
+			options[args[0]] = !options[args[0]];
+		appendLine(args[0] + ' ' + options[args[0]]);
+	},
+};
+
+// ============================================================================
+
+parseCommand = function(line) {
+	var all = line.split(' ');
+	var command = all.shift().substr(1);
+	var args = [];
+	var multi = '';
+	for (var i in all) {
+		if (all[i][0] == '"')
+			multi += all[i].substr(1)+' ';
+		else {
+			if (multi != '') {
+				multi += all[i];
+				if (all[i][all[i].length-1] == '"') {
+					multi = multi.substr(0, multi.length-1);
+					args.push(multi);
+					multi = '';
+				}
+			} else
+				args.push(all[i]);
+		}
+	}
+	return {command: command, args: args};
+}
+
+// ============================================================================
+
+var win = new nc.ncWindow();
+var inputLine = '';
+var hline = 0;
+var pos = 0;
+win.addListener('inputChar', function (chr, intval) {
+	if (options.chr)
+		appendLine(chr + ' ' + intval);
+	if (intval == consts.keys['BACKSPACE'] || intval == 7 || intval == 127) {
+		win.delch();
+		if (inputLine.length) {
+			inputLine = inputLine.substr(0, inputLine.length-1);
+			if (pos > 0)
+				pos--;
+		}
+	} else if (chr == consts.DOWN || intval == 2) {
+		if (!history.length)
+			return;
+		if (hline > history.length-1);
+		else if (hline == history.length-1) {
+			inputLine = '';
+			hline++;
+		} else
+			inputLine = history[++hline];
+	} else if (chr == consts.UP || intval == 3) {
+		if (!history.length)
+			return;
+		if (hline > 0)
+			inputLine = history[--hline];
+	} else if (chr == consts.LEFT || intval == 4) {
+		if (inputLine.length && pos > 0)
+			pos--;
+	} else if (chr == consts.RIGHT || intval == 5) {
+		if (inputLine.length && pos < inputLine.length)
+			pos++;
+	} else if (chr == '\n') {
+		if (inputLine[0] == '/') {
+			var command = parseCommand(inputLine);
+			if (options.command_parsing)
+				appendLine(sys.inspect(command));
+			var notcommand = true;
+			for (var i in commands) {
+				if (i == command.command) {
+					commands[i](command.args);
+					notcommand = false;
+					break;
+				}
+			}
+			if (notcommand)
+				appendLine('Command unknown.');
+		} else if (inputLine[0] == '<') {
+			conn.send(inputLine);
+		} else if (!inputLine.length);
+		else
+			appendLine('Only commands and XML are allowed.');
+		history.push(inputLine);
+		hline = history.length;
+		inputLine = '';
+		pos = 0;
+	} else {
+		if (inputLine.length == pos)
+			inputLine += chr;
+		else {
+			var tmp = inputLine.substr(0, pos);
+			tmp += chr;
+			tmp += inputLine.substr(pos, inputLine.length);
+			inputLine = tmp;
+		}
+		pos++;
+	}
+	win.deleteln();
+	win.cursor(win.height-1, 0);
+	win.print(inputLine);
+	win.cursor(win.height-1, pos);
+	win.refresh();
+});
+
+// Setup colors
+var COLOR_IN = win.colorPair(2);
+var COLOR_OUT = win.colorPair(3);
+var COLOR_QUIT = win.colorPair(6);
+var COLOR_ACTION = win.colorPair(5);
+
+// Setup window
+win.clear();
+win.echo = false;
+win.scrollok(true);
+win.hline(win.height-2, 0, win.width);
+win.setscrreg(0, win.height-3); // Leave one line at the top for the channel name and optional topic
+win.cursor(win.height-1, 0);
+win.refresh();
+
+var conn = new xmpp.Connection(server);
+
+conn.log = function(_, m) {
+	if (m.indexOf('--> ') == 0)
+		appendLine(m.substr(4), false, COLOR_IN);
+	else if (m.indexOf('<== ') == 0)
+		appendLine(m.substr(4), false, COLOR_OUT);
+	else
+		appendLine(m);
+};
+
+conn.connect(user, server, config.password, function (status, condition) {
+	appendLine(st[status] + (condition?(' ('+condition+')'):''), true, COLOR_ACTION);
+});