# HG changeset patch # User Emmanuel Gil Peyrot # Date 1296178504 -3600 # Node ID a2dab4544b2d5577e127727316edde7fd9f70d03 Initial commit. diff --git a/config.js b/config.js 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'; diff --git a/forms.js b/forms.js new file mode 100644 --- /dev/null +++ b/forms.js @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2010 Emmanuel Gil Peyrot + * + * 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 . + */ + +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 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(''); + 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); +});