changeset 24:b80ab94da447

Add new modules files.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Mon, 01 Nov 2010 00:02:27 +0100
parents 5fc4ee90c1bc
children c774f2ffb271
files modules.js modules/mod_adhoc.js modules/mod_configure.js modules/mod_disco.js modules/mod_manage.js modules/mod_options.js modules/mod_owner.js modules/mod_publish.js modules/mod_retrieve.js modules/mod_subscribe.js modules/mod_version.js namespaces.js notifs.js
diffstat 13 files changed, 1248 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/modules.js
@@ -0,0 +1,14 @@
+var modules = exports;
+
+var files = require('fs').readdirSync('modules');
+var regex = /^mod_.*\.js/;
+
+for (var i in files) {
+	var file = files[i];
+	if (!regex.test(file))
+		continue;
+
+	var module = require('./modules/' + files[i]);
+	for (var j in module)
+		modules[j] = module[j];
+}
new file mode 100644
--- /dev/null
+++ b/modules/mod_adhoc.js
@@ -0,0 +1,38 @@
+var config = require('../configuration');
+var storage = require('../storage');
+var forms = require('../forms');
+var errors = require('../errors');
+var makeError = errors.makeError;
+var toBareJID = require('../util').toBareJID;
+var NS = require('../namespaces');
+
+// XEP-0050: Ad-Hoc Commands
+exports.configureSub = {
+	type: 'set',
+	child: 'command',
+	ns: NS.COMMANDS,
+	func: function(response, stanza, request, to) {
+		var action = request.getAttribute('action');
+		if (action != 'execute')
+			return makeError(response, errors.bad_request.n);
+
+		var node = request.getAttribute('node');
+		if (node == 'ping') {
+			var cmd = xmpp.stanza('command', {xmlns: NS.COMMANDS,
+	//				      sessionid: 'list:20020923T213616Z-700',
+					      node: node,
+					      'status': 'completed'})
+				.c('note', {type: 'info'}).t('pong');
+			response.cnode(cmd);
+		} else if (node == 'reload') {
+			storage.load();
+			response.c('command', {xmlns: NS.COMMANDS,
+					       node: node,
+					       'status': 'completed'})
+				.c('note', {type: 'info'}).t('The server has correctly reloaded.');
+		} else
+			return makeError(response, errors.bad_request.n);
+
+		return response;
+	}
+}
new file mode 100644
--- /dev/null
+++ b/modules/mod_configure.js
@@ -0,0 +1,103 @@
+var config = require('../configuration');
+var storage = require('../storage');
+var forms = require('../forms');
+var errors = require('../errors');
+var makeError = errors.makeError;
+var toBareJID = require('../util').toBareJID;
+var NS = require('../namespaces');
+
+// SECTION 8.2: Configure a Node
+exports.getConfigure = {
+	type: 'get',
+	child: 'pubsub',
+	ns: NS.PUBSUB_OWNER,
+	pschild: 'configure',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('config-node'))
+			return makeError(response, errors.owner.configure.node_configuration_not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID || nodeID == '')
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var affil = storage.getAffiliation(toBareJID(to), nodeID);
+		if (affil != 'super-owner' && affil != 'owner' && affil != 'publish-only')
+			return makeError(response, errors.pub.publish.insufficient_privileges.n);
+
+		response.c('pubsub', {xmlns: NS.PUBSUB_OWNER});
+		response.c('configure', {node: nodeID});
+
+		var form = forms.build('form', 'node_config', 'default', true);
+		response.cnode(form);
+
+		return response;
+	}
+}
+
+// SECTION 8.3: Request Default Node Configuration Options
+exports.default = {
+	type: 'get',
+	child: 'pubsub',
+	ns: NS.PUBSUB_OWNER,
+	pschild: 'default',
+	func: function(response) {
+		if (!config.enabled('config-node'))
+			return makeError(response, errors.owner.default_options.node_configuration_not_supported.n);
+
+		response.c('pubsub', {xmlns: NS.PUBSUB_OWNER});
+		response.c('default');
+
+		var form = forms.build('node_config', service_configuration.node_config, null, true);
+		response.cnode(form);
+
+		return response;
+	}
+}
+
+// SECTION 8.2.4: Form Submission
+exports.setConfigure = {
+	type: 'set',
+	child: 'pubsub',
+	ns: NS.PUBSUB_OWNER,
+	pschild: 'configure',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('config-node'))
+			return makeError(response, errors.owner.configure.node_configuration_not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID)
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var affil = storage.getAffiliation(toBareJID(to), nodeID);
+		if (affil != 'super-owner' && affil != 'owner' && affil != 'publish-only')
+			return makeError(response, errors.forbidden.n);
+
+		var x = request.getChild('x', 'jabber:x:data');
+		if (!x)
+			return makeError(response, errors.bad_request.n);
+
+		var type = x.getAttribute('type');
+		if (type == 'cancel') {
+			conn.send(response);
+			return;
+		}
+		if (type != 'submit')
+			return makeError(response, errors.bad_request.n);
+
+		var form = forms.parse(x, true);
+		if (typeof form == 'number')
+			return makeError(response, form);
+
+		var conf = form;
+
+		var set = storage.configure(nodeID, conf);
+		if (typeof set == 'number')
+			return makeError(response, set);
+
+		return response;
+	}
+}
new file mode 100644
--- /dev/null
+++ b/modules/mod_disco.js
@@ -0,0 +1,113 @@
+var config = require('../configuration');
+var storage = require('../storage');
+var forms = require('../forms');
+var errors = require('../errors');
+var makeError = errors.makeError;
+var NS = require('../namespaces');
+
+// SECTION 5.1: Discover Features
+exports.disco_info = {
+	type: 'get',
+	child: 'query',
+	ns: NS.DISCO_INFO,
+	func: function(response, stanza, request) {
+		var nodeID = request.getAttribute('node');
+
+		// SECTION 5.3: Discover Node Information
+		if (nodeID && nodeID != '') {
+			if (!storage.existsNode(nodeID))
+				return makeError(response, errors.node_does_not_exist.n);
+
+			var conf = storage.getConfiguration(nodeID);
+			if (typeof conf == 'number')
+				return makeError(response, conf);
+
+			var type = 'leaf'
+			if (conf['pubsub#node_type'])
+				type = conf['pubsub#node_type'];
+
+			response.c('query', {xmlns: NS.DISCO_INFO, node: nodeID})
+				.c('identity', {category: 'pubsub', type: type}).up()
+				.c('feature', {'var': NS.PUBSUB}).up();
+
+			// SECTION 5.4
+			if (config.enabled('meta-data')) {
+				var x = forms.build('result', 'node_metadata', storage.getMetadata(nodeID), true);
+				if (x)
+					response.cnode(x);
+			}
+
+		// SECTION 5.1: Discover Features
+		} else {
+			response.c('query', {xmlns: NS.DISCO_INFO})
+				.c('identity', {category: 'pubsub', type: 'service', name: 'PubSub JavaScript Server'}).up()
+				.c('feature', {'var': NS.DISCO_INFO}).up()
+				.c('feature', {'var': NS.DISCO_ITEMS}).up()
+				.c('feature', {'var': NS.PUBSUB}).up()
+				.c('feature', {'var': 'jabber:iq:version'}).up()
+				.c('feature', {'var': NS.COMMANDS}).up();
+
+			for (var i in config.activated)
+				if (typeof i == 'string')
+					response.c('feature', {'var': 'http://jabber.org/protocol/pubsub#' + config.activated[i]}).up();
+		}
+
+		return response;
+	}
+}
+
+// SECTION 5.2: Discover Nodes
+exports.disco_items = {
+	type: 'get',
+	child: 'query',
+	ns: NS.DISCO_ITEMS,
+	func: function(response, stanza, request) {
+		var children;
+		var nodeID = request.getAttribute('node');
+		if (nodeID && nodeID != '') {
+			if (nodeID == NS.COMMANDS) {
+				// XEP-0050: Ad-Hoc Commands
+
+				response.c('query', {xmlns: NS.DISCO_ITEMS, node: nodeID})
+					.c('item', {jid: config.jid, node: 'ping', name: 'Ping'}).up()
+					.c('item', {jid: config.jid, node: 'reload', name: 'Reload'}).up();
+
+				return response;
+			} else {
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				response.c('query', {xmlns: NS.DISCO_ITEMS, node: nodeID});
+
+				children = storage.getChildren(nodeID);
+				if (typeof children == 'number')
+					return makeError(response, children);
+			}
+		} else {
+			response.c('query', {xmlns: NS.DISCO_ITEMS});
+
+			children = storage.getChildren();
+			if (typeof children == 'number')
+				return makeError(response, children);
+		}
+
+		for (var i in children) {
+			var attr = {jid: config.jid};
+			if (children[i] == 'node') {
+				if (config.enabled('meta-data')) {
+					var metadata = storage.getMetadata(i);
+					if (metadata['pubsub#title'])
+						attr.name = metadata['pubsub#title'];
+				}
+				attr.node = i;
+
+			// SECTION 5.5: Discover Items for a Node
+			} else
+				attr.name = i;
+
+			response.c('item', attr).up();
+		}
+
+		return response;
+	}
+}
new file mode 100644
--- /dev/null
+++ b/modules/mod_manage.js
@@ -0,0 +1,162 @@
+var config = require('../configuration');
+var storage = require('../storage');
+var errors = require('../errors');
+var makeError = errors.makeError;
+var toBareJID = require('../util').toBareJID;
+var NS = require('../namespaces');
+
+// SECTION 8.8.1: Retrieve Subscriptions List
+exports.manageRetrieveSub = {
+	type: 'get',
+	child: 'pubsub',
+	ns: NS.PUBSUB_OWNER,
+	pschild: 'subscriptions',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('manage-subscriptions'))
+			return makeError(response, errors.owner.manage_subscriptions.not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID || nodeID == '')
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var affil = storage.getAffiliation(toBareJID(to), nodeID);
+		if (affil != 'super-owner' && affil != 'owner')
+			return makeError(response, errors.forbidden.n);
+
+		response.c('pubsub', {xmlns: NS.PUBSUB_OWNER});
+		response.c('subscriptions', {node: nodeID});
+
+		var subs = storage.getSubscriptionsFromNodeID(nodeID)
+		for (var jid in subs)
+			response.c('subscription', {jid: jid, subscription: subs[jid].type, subid: subs[jid].subid}).up();
+
+		return response;
+	}
+}
+
+// SECTION 8.9.1: Retrieve Affiliations List
+exports.manageRetrieveAff = {
+	type: 'get',
+	child: 'pubsub',
+	ns: NS.PUBSUB_OWNER,
+	pschild: 'affiliations',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('modify-affiliations'))
+			return makeError(response, errors.owner.manage_affiliations.not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID || nodeID == '')
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var affils = storage.getAffiliationsFromNodeID(nodeID);
+		var affil = affils[toBareJID(to)];
+		if (affil != 'super-owner' && affil != 'owner')
+			return makeError(response, errors.owner.manage_affiliations.retrieve_list.entity_is_not_an_owner.n);
+
+		response.c('pubsub', {xmlns: NS.PUBSUB_OWNER});
+		response.c('affiliations', {node: nodeID});
+
+		for (var jid in affils)
+			response.c('affiliation', {jid: jid, affiliation: affils[jid]}).up();
+
+		return response;
+	}
+}
+
+// SECTION 8.8.2: Modify Subscriptions
+exports.modifySub = {
+	type: 'set',
+	child: 'pubsub',
+	ns: NS.PUBSUB_OWNER,
+	pschild: 'subscriptions',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('manage-subscriptions'))
+			return makeError(response, errors.owner.manage_subscriptions.not_supported.n); //XXX
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID)
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var affil = storage.getAffiliation(toBareJID(to), nodeID);
+		if (affil != 'super-owner' && affil != 'owner')
+			return makeError(response, errors.forbidden.n);
+
+		var e = false;
+		var tags2 = [];
+		for (i in request.tags) {
+			var tag = request.tags[i];
+			var jid = tag.getAttribute('jid');
+			var sub = tag.getAttribute('subscription');
+
+			if (sub == 'none' || sub == 'pending' || sub == 'subscribed' || sub == 'unconfigured') {
+				var set = storage.subscribe(nodeID, jid, sub);
+
+				if (typeof set == 'number') {
+					e = true;
+					tags2.push(tag);
+				} else {
+					// SECTION 8.8.4
+					notifs.send(jid, 'subscription', nodeID, {jid: jid, subscription: sub});
+				}
+			} else {
+				e = true;
+				tags2.push(tag);
+			}
+		}
+
+		request.tags = tags2;
+
+		if (e)
+			return makeError(response, errors.owner.manage_subscriptions.modify.multiple_simultaneous_modifications.n, pubsub);
+
+		return response;
+	}
+}
+
+// SECTION 8.9.2: Modify Affiliation
+exports.modifyAff = {
+	type: 'set',
+	child: 'pubsub',
+	ns: NS.PUBSUB_OWNER,
+	pschild: 'affiliations',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('modify-affiliations'))
+			return makeError(response, errors.owner.manage_affiliations.not_supported.n); //XXX
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID)
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var affil = storage.getAffiliation(toBareJID(to), nodeID);
+		if (affil != 'super-owner' && affil != 'owner')
+			return makeError(response, errors.forbidden.n);
+
+		var e = false;
+		for (i in request.children) {
+			var jid = request.children[i].getAttribute('jid');
+			var affiliation = request.children[i].getAttribute('affiliation');
+
+			var set = storage.setAffiliation(nodeID, jid, affiliation);
+			if (typeof set == 'number')
+				e = true;
+			else {
+				// SECTION 8.9.4
+				notifs.send(jid, 'affiliations', nodeID, {jid: jid, affiliation: affiliation});
+				request.children.splice(i, 1);
+			}
+		}
+
+		if (e)
+			return makeError(response, errors.owner.manage_affiliations.modify.multiple_simultaneous_modifications.n, pubsub);
+
+		return response;
+	}
+}
new file mode 100644
--- /dev/null
+++ b/modules/mod_options.js
@@ -0,0 +1,107 @@
+var config = require('../configuration');
+var storage = require('../storage');
+var forms = require('../forms');
+var errors = require('../errors');
+var makeError = errors.makeError;
+var toBareJID = require('../util').toBareJID;
+var NS = require('../namespaces');
+
+// SECTION 6.3.2: Configure Subscription Options (Request)
+exports.configureSub = {
+	type: 'get',
+	child: 'pubsub',
+	ns: NS.PUBSUB,
+	pschild: 'options',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('subscription-options'))
+			return makeError(response, errors.sub.configure.subscription_options_not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID || nodeID == '')
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var jid = request.getAttribute('jid');
+		if (!jid)
+			return makeError(response, errors.sub.configure.subscriber_jid_required.n);
+		if (toBareJID(jid) != toBareJID(to))
+			return makeError(response, errors.sub.configure.insufficient_privileges.n);
+
+		var subs = storage.getSubscription(jid, nodeID);
+		if (!subs.subid) // FIXME: better test for empty object.
+			return makeError(response, errors.sub.configure.no_such_subscriber.n);
+
+		response.c('pubsub', {xmlns: NS.PUBSUB});
+		response.c('options', {node: nodeID, jid: jid});
+
+		var form = forms.build('form', 'subscribe_options', subs.options, true);
+		response.cnode(form);
+
+		return response;
+	}
+}
+
+// SECTIONS 6.3.5: Form Submission
+exports.configureSub = {
+	type: 'set',
+	child: 'pubsub',
+	ns: NS.PUBSUB,
+	pschild: 'options',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('subscription-options'))
+			return makeError(response, errors.sub.subscribe.not_supported.n);
+
+		var nodeID = unsubscribe.getAttribute('node');
+		if (!nodeID || nodeID == '')
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var jid = unsubscribe.getAttribute('jid');
+		if (!jid || toBareJID(jid) != toBareJID(to))
+			return makeError(response, errors.sub.unsubscribe.insufficient_privileges.n);
+
+		var x = request.getChild('x', 'jabber:x:data');
+		if (!x || x.getAttribute(type) != 'submit')
+			return makeError(response, errors.bad_request);
+
+		var form = forms.parse(x, true);
+		if (typeof form == 'number')
+			return makeError(response, form);
+
+		var set = storage.configureSubscription(nodeID, jid, form);
+		if (typeof form == 'number')
+			return makeError(response, form);
+
+		return response;
+	}
+}
+
+// SECTION 6.4: Request Default Subscription Configuration Options
+exports.defaultSub = {
+	type: 'get',
+	child: 'pubsub',
+	ns: NS.PUBSUB,
+	pschild: 'default',
+	func: function(response, stanza, request) {
+		if (!config.enabled('retrieve-default-sub'))
+			return makeError(response, errors.sub.default_options.default_subscription_configuration_retrieval_not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (nodeID && !storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		response.c('pubsub', {xmlns: NS.PUBSUB});
+		var s;
+		if (nodeID)
+			response.c('default', {node: nodeID});
+		else
+			response.c('default');
+
+		var form = forms.build('form', 'subscribe_options', 'default', false);
+		response.cnode(form);
+
+		return response;
+	}
+}
new file mode 100644
--- /dev/null
+++ b/modules/mod_owner.js
@@ -0,0 +1,121 @@
+var config = require('../configuration');
+var storage = require('../storage');
+var forms = require('../forms');
+var notifs = require('../notifs');
+var errors = require('../errors');
+var makeError = errors.makeError;
+var toBareJID = require('../util').toBareJID;
+var NS = require('../namespaces');
+
+// SECTION 8.1: Create a Node
+exports.getConfigure = {
+	type: 'set',
+	child: 'pubsub',
+	ns: NS.PUBSUB,
+	pschild: 'create',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('create-nodes'))
+			return makeError(response, errors.owner.create.node_creation_not_supported.n);
+
+		var instant = false;
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID || nodeID == '') {
+			if (!config.enabled('instant-nodes'))
+				return makeError(response, errors.owner.create.instant_nodes_not_supported.n);
+			nodeID = utils.makeRandomId();
+			instant = true;
+		}
+		if (storage.existsNode(nodeID))
+			return makeError(response, errors.owner.create.nodeid_already_exists.n);
+
+		var bare = toBareJID(to);
+		var right = false;
+
+		// Check for super-owner
+		for (var i in config.owner)
+			if (config.owner[i] == bare)
+				right = true;
+
+		// Check for authorized user
+		for (var i in config.allowCreateNode)
+			if (config.allowCreateNode[i].exec(bare))
+				right = true;
+
+		if (!right)
+			return makeError(response, errors.forbidden.n);
+
+		var r = storage.createNode(nodeID);
+		if (typeof r == 'number')
+			return makeError(response, r);
+
+		if (instant)
+			response.c('pubsub', {xmlns: NS.PUBSUB})
+				.c('create', {node: nodeID});
+
+		return response;
+	}
+}
+
+// SECTION 8.4: Delete a Node
+exports['delete'] = {
+	type: 'set',
+	child: 'pubsub',
+	ns: NS.PUBSUB_OWNER,
+	pschild: 'delete',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('delete-nodes'))
+			return makeError(response, errors.feature_not_implemented.n); //XXX
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID)
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var affil = storage.getAffiliation(toBareJID(to), nodeID);
+		if (affil != 'super-owner' && affil != 'owner')
+			return makeError(response, errors.forbidden.n);
+
+		var notifs = storage.deleteNode(nodeID);
+		if (typeof notifs == 'number')
+			return makeError(response, r);
+
+		notifs.send(notifs, 'delete', nodeID);
+
+		return response;
+	}
+}
+
+// SECTION 8.5: Purge All Node Items
+exports.purge = {
+	type: 'set',
+	child: 'pubsub',
+	ns: NS.PUBSUB_OWNER,
+	pschild: 'purge',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('purge-nodes'))
+			return makeError(response, errors.owner.purge.node_purging_not_supported.n); //XXX
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID)
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var affil = storage.getAffiliation(toBareJID(to), nodeID);
+		if (affil != 'super-owner' && affil != 'owner')
+			return makeError(response, errors.forbidden.n);
+
+		if (!config.enabled('persistent-items')) //FIXME: autre condition, supporté par le node
+			return makeError(response, errors.owner.purge.node_does_not_persist_items.n);
+
+		var notifs = storage.purgeNode(nodeID);
+		if (typeof notifs == 'number')
+			return makeError(response, r);
+
+		notifs.send(notifs, 'purge', nodeID);
+
+		return response;
+	}
+}
new file mode 100644
--- /dev/null
+++ b/modules/mod_publish.js
@@ -0,0 +1,130 @@
+var config = require('../configuration');
+var storage = require('../storage');
+var toBareJID = require('../util').toBareJID;
+var NS = require('../namespaces');
+
+// SECTION 7.1: Publish an Item to a Node
+exports.retrieveSub = {
+	type: 'set',
+	child: 'pubsub',
+	ns: NS.PUBSUB,
+	pschild: 'publish',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('publish'))
+			return makeError(response, errors.pub.publish.item_publication_not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID || nodeID == '')
+			return makeError(response, errors.nodeid_required.n);
+
+		var autocreate = false;
+		if (!storage.existsNode(nodeID)) {
+			if (config.enabled('auto-create'))
+				autocreate = true;
+			else
+				return makeError(response, errors.node_does_not_exist.n);
+		}
+
+		var affil = storage.getAffiliation(toBareJID(to), nodeID);
+		if (typeof affil == 'number' && affil != errors.node_does_not_exist.n)
+			return makeError(response, affil);
+		if (!autocreate && affil != 'super-owner' && affil != 'owner' && affil != 'publisher' && affil != 'publish-only')
+			return makeError(response, errors.forbidden.n);
+
+		var item = request.getChild('item');
+		var itemID = item.getAttribute('id');
+		if (!config.enabled('item-ids') && itemID)
+			return makeError(response, errors.itemid_required.n);
+		itemID = itemID? itemID: utils.makeRandomId();
+
+		if (item.tags.length != 1)
+			return makeError(response, errors.pub.publish.bad_payload.n);
+
+		var conf = storage.getConfiguration(nodeID);
+		var publishOptions = pubsub.getChild('publish-options');
+		if (publishOptions && config.enabled('publish-options')) {
+			var x = publishOptions.getChild('x', 'jabber:x:data');
+			if (!x || x.getAttribute('type') != 'submit')
+				return makeError(response, errors.bad_request.n);
+
+			var form = forms.parse(x, true);
+			if (form.access_model != conf['pubsub#access_model'] && !autocreate)
+				return makeError(response, errors.pub.configuration.precondition.n);
+		}
+
+		if (!config.enabled('persistent-items')) {
+			var notifs = storage.purgeNode(nodeID);
+			if (typeof notifs == 'number')
+				return makeError(response, r);
+		}
+
+		if (autocreate) {
+			if (!form)
+				form = {};
+			form['pubsub#creator'] = toBareJID(to);
+
+			var r = storage.createNode(nodeID, form);
+			if (typeof r == 'number')
+				return makeError(response, r);
+		}
+
+		var content = item.getChild();
+
+		subscribers = storage.setItem(nodeID, itemID, content);
+		if (typeof subscribers == 'number')
+			return makeError(response, subscribers);
+
+		var attrs = {};
+		if (content)
+			attrs[itemID] = {content: content};
+		else
+			attrs[itemID] = {};
+		notifs.send(subscribers, 'items', nodeID, attrs);
+
+		response.c('pubsub', {xmlns: NS.PUBSUB})
+			.c('publish', {node: nodeID})
+			.c('item', {id: itemID});
+
+		return response;
+	}
+}
+
+// SECTION 7.2: Delete an Item from a Node
+exports.retrieveSub = {
+	type: 'set',
+	child: 'pubsub',
+	ns: NS.PUBSUB,
+	pschild: 'retract',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('retract-items'))
+			return makeError(response, errors.pub.retract.item_deletion_not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID || nodeID == '')
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var item = request.getChild('item');
+		if (!item)
+			return makeError(response, errors.pub.retract.item_or_itemid_required.n);
+
+		var itemID = item.getAttribute('id')
+		if (!itemID || itemID == '')
+			return makeError(response, errors.pub.retract.item_or_itemid_required.n);
+
+		var affil = storage.getAffiliation(toBareJID(to), nodeID);
+		if (affil != 'super-owner' && affil != 'owner' && affil != 'publish-only')
+			return makeError(response, errors.forbidden.n);
+
+		var subscribers = storage.deleteItem(nodeID, itemID);
+		if (typeof subscribers == 'number')
+			return makeError(response, subscribers);
+
+		var attrs = {};
+		attrs[itemID] = {};
+		notifs.send(subscribers, 'items', nodeID, attrs, 'retract')
+
+		return response;
+	}
+}
new file mode 100644
--- /dev/null
+++ b/modules/mod_retrieve.js
@@ -0,0 +1,132 @@
+var config = require('../configuration');
+var storage = require('../storage');
+var errors = require('../errors');
+var makeError = errors.makeError;
+var toBareJID = require('../util').toBareJID;
+var NS = require('../namespaces');
+
+// SECTION 5.6: Retrieve Subscriptions
+exports.retrieveSubscriptions = {
+	type: 'get',
+	child: 'pubsub',
+	ns: NS.PUBSUB,
+	pschild: 'subscriptions',
+	func: function(response, stanza, request) {
+		if (!config.enabled('retrieve-subscriptions'))
+			return makeError(response, errors.subscriptions_retrieval_not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (nodeID && nodeID != '') {
+			if (!storage.existsNode(nodeID))
+				return makeError(response, errors.node_does_not_exist.n);
+			var subs = storage.getSubscription(toBareJID(to), node);
+		} else
+			var subs = storage.getSubscription(toBareJID(to));
+
+		response.c('pubsub', {xmlns: NS.PUBSUB});
+		response.c('subscriptions');
+
+		for (i in subs)
+			response.c('subscription', {node: i, jid: to, subscription: subs[i].type, subid: subs[i].subid}).up();
+
+		return response;
+	}
+}
+
+// SECTION 5.7: Retrieve Affiliations
+exports.retrieveAffiliations = {
+	type: 'get',
+	child: 'pubsub',
+	ns: NS.PUBSUB,
+	pschild: 'affiliations',
+	func: function(response, stanza, request) {
+		if (!config.enabled('retrieve-affiliations'))
+			return makeError(response, errors.affiliations_retrieval_not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (nodeID && nodeID != '') {
+			if (!storage.existsNode(nodeID))
+				return makeError(response, errors.node_does_not_exist.n);
+			var affils = {};
+			affils[nodeID] = storage.getAffiliation(toBareJID(to), nodeID);
+		} else
+			var affils = storage.getAffiliationsFromJID(toBareJID(to));
+
+		response.c('pubsub', {xmlns: NS.PUBSUB});
+		response.c('affiliations');
+
+		for (i in affils)
+			response.c('affiliation', {node: i, affiliation: affils[i]}).up();
+
+		return response;
+	}
+}
+
+// SECTION 6.5: Retrieve Items from a Node
+exports.retrieveItems = {
+	type: 'get',
+	child: 'pubsub',
+	ns: NS.PUBSUB,
+	pschild: 'items',
+	func: function(response, stanza, request) {
+		if (!config.enabled('retrieve-items'))
+			return makeError(response, errors.sub.default_options.node_configuration_not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID || nodeID == '')
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var configuration = storage.getConfiguration(nodeID);
+		if (configuration['pubsub#access_model'] == 'whitelist') {
+			var affil = storage.getAffiliation(toBareJID(to), nodeID);
+			if (affil != 'super-owner' && affil != 'owner' && affil != 'publisher' && affil != 'member')
+				return makeError(response, errors.pub.publish.insufficient_privileges.n);
+		}
+
+		var item = [];
+		for (var i=0; i<request.children.length; i++) {
+			var j = request.children[i];
+			if (j.name == 'item' && j.attr['id'] && j.attr['id'] != '')
+				item.push(j.attr['id']);
+		}
+
+		response.c('pubsub', {xmlns: NS.PUBSUB});
+
+		var max_items = request.getAttribute('max_items');
+		if (max_items)
+			max_items = Number (max_items);
+
+		if (item.length) {
+			response.c('items', {node: nodeID});
+
+			for (var i=0; i<item.length; i++) {
+				var j = storage.getItem(nodeID, item[i]);
+				if (typeof j == 'number')
+					return makeError(response, j);
+				if (j == errors.success)
+					continue;
+
+				response.c('item', {id: item[i]})
+				response.cnode(j).up().up();
+			}
+		} else {
+			response.c('items', {node: nodeID});
+
+			var j;
+			if (max_items)
+				j = storage.getLastItem(nodeID, max_items);
+			else
+				j = storage.getItems(nodeID);
+			if (typeof j == 'number')
+				return makeError(response, j);
+
+			var k = 0;
+			for (var i in j)
+				response.c('item', {id: i}).t(j[i].content).up();
+		}
+
+		return response;
+	}
+}
new file mode 100644
--- /dev/null
+++ b/modules/mod_subscribe.js
@@ -0,0 +1,128 @@
+var config = require('../configuration');
+var storage = require('../storage');
+var errors = require('../errors');
+var makeError = errors.makeError;
+var toBareJID = require('../util').toBareJID;
+var NS = require('../namespaces');
+
+// SECTION 6.1: Subscribe to a Node
+exports.subscribe = {
+	type: 'set',
+	child: 'pubsub',
+	ns: NS.PUBSUB,
+	pschild: 'subscribe',
+	func: function(response, stanza, request, to) {
+		if (!config.enabled('subscribe'))
+			return makeError(response, errors.sub.subscribe.not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID || nodeID == '')
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var configuration = storage.getConfiguration(nodeID);
+		if (!configuration['pubsub#subscribe'])
+			return makeError(response, errors.sub.subscribe.not_supported.n);
+
+		var affil = storage.getAffiliation(toBareJID(to), nodeID);
+		if (affil == 'publish-only' || affil == 'outcast')
+			return makeError(response, errors.pub.publish.insufficient_privileges.n);
+
+		var jid = request.getAttribute('jid');
+		if (!jid || toBareJID(jid) != toBareJID(to))
+			return makeError(response, errors.sub.subscribe.jids_do_not_match.n);
+
+		/*
+		// SECTION 6.3.7
+		var options = pubsub.getChild('options');
+		if (options && config.enabled('subscription-options')) {
+			if (options.getAttribute('node') || options.getAttribute('jid'))
+				return makeError(response, errors.bad_request.n);
+
+			var x = options.getChild('x', 'jabber:x:data');
+			if (!x || x.getAttribute('type') != 'submit')
+				return makeError(response, errors.bad_request.n);
+
+			var form = forms.parse(x, true);
+			if (typeof form == 'number')
+				return makeError(response, form);
+
+			var conf = form;
+		}*/
+
+		var subID;
+		if (configuration['pubsub#access_model'] == 'open') {
+			subID = storage.subscribe(nodeID, jid, 'subscribe');
+			if (typeof subID == 'number')
+				return makeError(response, subID);
+		} else if (configuration['pubsub#access_model'] == 'authorize') {
+			subID = storage.subscribe(nodeID, jid, 'pending');
+			if (typeof subID == 'number')
+				return makeError(response, subID);
+
+			var affiliates = storage.getAffiliationsFromNodeID(nodeID);
+			var form = forms.build('form', 'subscribe_authorization', {allow: false, node: nodeID, subscriber_jid: jid}, true); //168
+
+			for (var i in affiliates) {
+				if (affiliates[i] == 'super-owner' || affiliates[i] == 'owner') {
+					var message = xmpp.message({to: i}).cnode(form);
+					conn.send(message); // FIXME: impossible à faire d’ici
+				}
+			}
+		} else if (configuration['pubsub#access_model'] == 'whitelist') {
+			var affil = storage.getAffiliation(jid, nodeID);
+			if (affil != 'super-owner' && affil != 'owner' && affil != 'publisher' && affil != 'member')
+				return makeError(response, errors.sub.subscribe.not_on_whitelist.n);
+
+			subID = storage.subscribe(nodeID, jid);
+			if (typeof subID == 'number')
+				return makeError(response, subID);
+		}
+
+		response.c('pubsub', {xmlns: NS.PUBSUB})
+			.c('subscription', {node: nodeID, jid: jid, subid: subID.subid, subscription: subID.type});
+
+		if (config.enabled('last-published')) {
+			var last = storage.getLastItem(nodeID);
+			if (typeof last != 'number') {
+				var item = storage.getItem(nodeID, last);
+				if (typeof item != 'number') {
+					var attr = {};
+					attr[last] = {content: item};
+					notifs.send(jid, 'items', nodeID, attr);
+				}
+			}
+		}
+
+		return response;
+	}
+}
+
+// SECTION 6.2: Unsubscribe from a Node
+exports.unsubscribe = {
+	type: 'set',
+	child: 'pubsub',
+	ns: NS.PUBSUB,
+	pschild: 'unsubscribe',
+	func: function(response, stanza, request) {
+		if (!config.enabled('subscribe'))
+			return makeError(response, errors.sub.subscribe.not_supported.n);
+
+		var nodeID = request.getAttribute('node');
+		if (!nodeID || nodeID == '')
+			return makeError(response, errors.nodeid_required.n);
+		if (!storage.existsNode(nodeID))
+			return makeError(response, errors.node_does_not_exist.n);
+
+		var jid = request.getAttribute('jid');
+		if (!jid || toBareJID(jid) != toBareJID(to))
+			return makeError(response, errors.sub.unsubscribe.insufficient_privileges.n);
+
+		var subID = storage.subscribe(nodeID, jid, 'none');
+		if (typeof subID == 'number')
+			return makeError(response, subID);
+
+		return response;
+	}
+}
new file mode 100644
--- /dev/null
+++ b/modules/mod_version.js
@@ -0,0 +1,17 @@
+// XEP-0092: Software Version
+
+var config = require('../configuration');
+var NS = require('../namespaces');
+
+exports.version = {
+	type: 'get',
+	child: 'query',
+	ns: NS.VERSION,
+	func: function(response) {
+		response.c('query', {xmlns: NS.VERSION})
+			.s('name').t('PSĜS')
+			.s('version').t(config.version)
+			.s('os').t(config.os);
+		return response;
+	}
+}
new file mode 100644
--- /dev/null
+++ b/namespaces.js
@@ -0,0 +1,17 @@
+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';
+NS.COMMANDS = 'http://jabber.org/protocol/commands';
new file mode 100644
--- /dev/null
+++ b/notifs.js
@@ -0,0 +1,166 @@
+var xmpp = require('xmpp');
+var storage = require('./storage');
+var config = require('./configuration');
+var conn;
+
+function _(obj, color) {
+	var str = require('sys').inspect(obj, false, null);
+	if (color)
+		console.log('\033['+c+';1m' + str + '\033[0m');
+	else
+		console.log(str);
+};
+
+exports.send = function(notifs, type, nodeID, a1, a2) {
+	var ev = xmpp.stanza('event', {xmlns: 'http://jabber.org/protocol/pubsub#event'});
+
+	if (type == 'affiliations') {
+		ev.attr.xmlns = 'http://jabber.org/protocol/pubsub';
+
+		var args = {};
+		for (i in a1) {
+			var attr = a1[i];
+			if (i == 'affiliation')
+				args.affiliation = attr;
+			else if (i == 'jid')
+				args.jid = attr;
+		}
+		var affiliations = xmpp.stanza('affiliations', {node: nodeID})
+			.c('affiliation', args);
+		ev.cnode(affiliations);
+	} else if (type == 'collection') {
+		var collection = xmpp.stanza('collection', {node: nodeID});
+		if (a1 == 'associate')
+			collection.cnode('associate', {node: nodeID});
+		else
+			collection.cnode('disassociate', {node: nodeID});
+		ev.cnode(collection);
+	} else if (type == 'configuration') {
+		if (!config.enabled('config-node')) {
+			_('Error #4', 41)
+			return;
+		}
+
+		var configuration = xmpp.stanza('configuration', {node: nodeID});
+		if (a1) {
+			var x = forms.build('node_config', service_configuration.node_config, storage.getConfiguration(nodeID));
+			if (x)
+				configuration.cnode(x); //TODO: voir exemple 150
+		}
+		ev.cnode(configuration);
+	} else if (type == 'delete') {
+		var del = xmpp.stanza('delete', {node: nodeID});
+		if (a1)
+			del.c('redirect', {uri: a1});
+		ev.cnode(del);
+	} else if (type == 'items') {
+		var items = xmpp.stanza(type, {node: nodeID});
+		if (a2 == 'retract')
+			for (var i in a1)
+				items.s('retract', {id: i});
+		else {
+			for (var i in a1) {
+				var item = a1[i];
+				var args = {};
+				if (i != '')
+					args.id = i;
+				if (item.node)
+					args.node = item.node;
+				if (item.publisher)
+					args.publisher = item.publisher;
+				var it = xmpp.stanza('item', args);
+				if (item.content)
+					it.cnode(item.content);
+				items.cnode(it);
+			}
+		}
+		ev.cnode(items);
+	} else if (type == 'purge') {
+		ev.c('purge', {node: nodeID});
+	} else if (type == 'subscription') {
+		if (!config.enabled('subscription-notifications'))
+			return;
+
+		var args = {node: nodeID};
+		for (i in a1) {
+			var attr = a1[i];
+			if (i == 'subscription') {
+				if (attr == 'none' || attr == 'pending' || attr == 'subscribed' || attr == 'unconfigured')
+					args[i] = attr;
+				else {
+					_('Error #3', 41)
+					return;
+				}
+			} else if (i == 'jid' || i == 'subid')
+				args[i] = attr;
+			else if (i == 'expiry')
+				args[i] = attr.toString();
+		}
+		if (!args.jid || args.jid == '') {
+			_('Error #2', 41)
+			return;
+		}
+		var sub = xmpp.stanza('subscription', args);
+		ev.cnode(sub);
+	} else {
+		_('Error #1', 41)
+		return;
+	}
+
+	var subs;
+	if (typeof notifs == 'string') {
+		subs = {};
+		subs[notifs] = storage.getSubscription(notifs, nodeID);
+	} else
+		subs = notifs;
+
+	for (var i in subs) {
+		var sub = subs[i];
+
+		if (sub.options) {
+			if (typeof sub.options['pubsub#deliver'] != 'undefined' && !sub.options['pubsub#deliver'])
+				continue;
+
+			if (typeof sub.options['pubsub#digest'] != 'undefined' && sub.options['pubsub#digest']) {
+				if (!sub.digest)
+					sub.digest = [];
+				sub.digest.push(ev)
+
+				if (sub.digestTimeout)
+					continue;
+
+				var freq;
+				if (typeof sub.options['pubsub#digest_frequency'] == 'undefined')
+					freq = 0;
+				else
+					freq = parseInt(sub.options['pubsub#digest_frequency']);
+
+				if (freq == 0)
+					freq = 24*60*60*1000;
+
+				setTimeout(sendDigest, freq, notifs[i], nodeID);
+				sub.digestTimeout = true;
+				continue;
+			}
+		}
+
+		var message = xmpp.message({to: i, from: config.jid, id: conn.getUniqueId()});
+		message.cnode(ev);
+		conn.send(message);
+	}
+}
+
+exports.sendDigest = function(jid, nodeID) {
+	var sub = storage.getSubscription(jid, nodeID);
+	if (sub.digestTimeout)
+		sub.digestTimeout = false;
+
+	var message = xmpp.message({to: jid, from: config.jid, id: conn.getUniqueId()});
+	for (var i in sub.digest)
+		message.cnode(sub.digest[i]);
+	conn.send(message);
+}
+
+exports.setConnection = function(c) {
+	conn = c;
+}