view xmpp.js @ 0:a2dab4544b2d default tip

Initial commit.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Fri, 28 Jan 2011 02:35:04 +0100
parents
children
line wrap: on
line source

#!/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);
});