view server.js @ 13:161d4ea1c3f8

Migration of the client-side to XMPP.js instead of Strophe.js. Drop BOSH support and add WebSockets support. The server-side is untested, may be broken.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Thu, 03 Nov 2011 14:23:10 -0700
parents 360186772aa3
children 03be0717d3f8
line wrap: on
line source

#!/usr/bin/env node

'use strict';

var config = require('./configuration');

var util = require('util');
var http = require('http');
var parseurl = require('url').parse;
var fs = require('fs');
var xmpp = require('node-xmpp');
var Element = xmpp.Element;
var JID = require('./jid');
var ns = require('./ns');
var forms = require('./forms');
require('./date');

var received = {};

var cl = new xmpp.Client(config);

(function() {
	var send = cl.send;
	cl.send = function(s) {
		util.log('Sent: ' + s + '');
		send.call(cl, s);
	}
})();

cl.on('online', function() {
	util.log('Connected.');
	cl.send(new Element('presence'));
});

var getUniqId = (function() {
	var id = 0;
	return function() {
		return ++id;
	}
})();

var getNodeInfo = function(jid) {
	var iq = new Element('iq', {to: jid.bare, type: 'get', id: getUniqId()})
		.c('query', {xmlns: ns.info, node: jid.resource})
	.up();

	cl.send(iq);
};

var getNodeItems = function(jid) {
	var iq = new Element('iq', {to: jid.bare, type: 'get', id: getUniqId()})
		.c('pubsub', {xmlns: ns.ps})
			.c('items', {node: jid.resource})
		.up()
	.up();

	cl.send(iq);
};

var makeError = function(response) {
	response.attrs.type = 'error';

	response.c('error', {type: 'cancel'})
		.c('feature-not-implemented', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'});

	return cl.send(response);
};

var handleInfo = function(query, from) {
	var x = query.getChild('x', ns.data);
	if (!x)
		return;

	var jid = new JID;
	jid.bare = from;
	jid.resource = query.attrs.node;

	var form = forms.parse(x, true).fields;
	received[jid].form = form;
	generatePage(jid);
};

var handlePubSub = function(pubsub, from) {
	var items = pubsub.getChild('items');
	if (!items)
		return;

	var itemsNS = items.getNS();
	if (itemsNS !== ns.ps && itemsNS !== ns.pse)
		return;

	var jid = new JID;
	jid.bare = from;
	jid.resource = items.attrs.node;

	items = items.getChildren('item', itemsNS);
	if (!items)
		return;

	if (!received[jid])
		received[jid] = {data: null, form: null, wait: {}};

	var data = received[jid].data || {};

	for (var i in items) {
		var item = items[i];
		var id = item.attrs.id;

		var payload = item.children[0];
		delete payload.parent;
		data[id] = payload;
	}

	received[jid].data = data;
	generatePage(jid);
};

cl.on('stanza', function(stanza) {
	util.log('Recv: ' + stanza + '');
	if (stanza.is('iq', ns.j)) {
		var type = stanza.attrs.type;
		if (type === 'error')
			return;

		var result = new Element('iq', {to: stanza.attrs.from, from: stanza.attrs.to, type: 'result'});
		if (type === 'get' || type === 'set')
			return makeError(result);

		var payload = stanza.getChild('query', ns.info);
		if (payload)
			return handleInfo(payload, stanza.attrs.from);

		payload = stanza.getChild('pubsub', ns.ps);
		if (payload)
			return handlePubSub(payload, stanza.attrs.from);

	} else if (stanza.is('message', ns.j)) {
		var type = stanza.attrs.type;
		if (type === 'error')
			return;

		if (type !== 'headline')
			return;

		var payload = stanza.getChild('event', ns.pse);
		if (payload)
			return handlePubSub(payload, stanza.attrs.from);
	}
});

var parseAtom = function(atom, id, jid) {
	var article = new Element('article', {'e:id': id});

	var avatar = article.c('aside').c('img')
	article.up();

	try {
		var title = atom.getChild('title', ns.atom).getText();
		if (title)
			article.c('h2').t(title).up();
	} catch (e) { }

	var footer = article.c('footer');
	article.up();

	var author = atom.getChild('author', ns.atom);
	if (author) {
		footer.t('By ');
		var name = author.getChild('name', ns.atom).getText();

		try{
			var uri = author.getChild('uri', ns.atom).getText();
			footer.c('cite').c('a', {href: uri}).t(name).up();
			avatar.attrs.src = config.avatarRoot + uri.substring(5);
			avatar.attrs.alt = uri.substring(5);
		} catch (e) {
			footer.c('cite').t(name);
		}

		try {
			var email = author.getChild('email', ns.atom).getText();
			footer.t(' (').c('a', {href: 'mailto:' + email}).t('email').up().t(')');
		} catch (e) { }
		footer.up();
	}

	var published = (function() {
		try {
			var elem = atom.getChild('published', ns.atom);
			var iso8601 = elem.getText();
			var d = (new Date).set8601(iso8601);
			var relative = d.getRelative();

			return {iso8601: iso8601, relative: relative};
		} catch (e) {
			var d = new Date;
			return {iso8601: d.to8601(), relative: d.getRelative()};
		}
	})();

	if (author)
		footer.t(', ');
	footer.c('time', {datetime: published.iso8601}).t(published.relative).up();
	article.attrs['e:date'] = published.iso8601;

	try {
		var summary = atom.getChild('summary', ns.atom).getText();
		if (summary)
			article.c('p').t(summary).up();
	} catch (e) { }

	try {
		var links = atom.getChildren('link');
		for (var i in links) {
			var link = links[i];

			if (link.attrs.rel !== 'replies')
				continue;

			if (link.attrs.title !== 'comments')
				continue;

			var href = new JID;
			href.uri = link.attrs.href;

			article.c('a', {href: '?jid=' + href.bare + ';node=' + href.query.node + ';origjid=' + jid.bare + ';orignode=' + jid.resource + ';origitem=' + id}).t('Comments !');
			break;
		}
	} catch (e) { }

	return article;
};

var generatePage = function(jid) {
	var r = received[jid.full];
	var s = r.wait;
	var form = r.form;
	var data = r.data;

	if (!form || !data)
		return;

	for (var i in s) {
		r = s[i];
		delete s[i];
		makePage(r.res, jid, form, data, r.noscript);
	}
};

var makePage = function(res, jid, form, data, noscript) {
	var body = '</div>';

	for (var id in data) {
		var item = data[id];
		var article = parseAtom(item, id, jid);
		body = article + body;
	}
	body = '<div e:jid="' + jid.full + '">' + body;

	home(res, form['pubsub#title'], form['pubsub#description'], body, 'Node created the <time>' + form['pubsub#creation_date'] + '</time> by <cite>' + form['pubsub#creator'] + '</cite> with <a href="http://linkmauve.fr/dev/eldonilo/blog">Eldonilo blog</a>.', noscript);
};

var servePage = function(url, res) {
	util.log(url.href);

	var query = require('querystring').parse(url.query, ';');

	var page = new JID(config.defaultNode);
	if (query.jid)
		page.bare = query.jid;
	if (query.node)
		page.resource = query.node;

	if (query.type === 'atom' && config.atomRoot) {
		res.writeHead(301, {'Location': config.atomRoot + page.resource});
		return res.end();
	}

	res.writeHead(200, {'Content-Type': 'application/xhtml+xml'});

	var noscript = false;
	if (query.no === 'server')
		return fs.readFile('index.xhtml', function(err, data) {
			res.end(data);
		});
	else if (query.no === 'client')
		noscript = true;

	var jid = page.full;
	if (!received[jid]) {
		getNodeInfo(page);
		getNodeItems(page);
		received[jid] = {data: null, form: null, wait: {}};
		received[jid].wait[getUniqId()] = {res: res, noscript: noscript};
	} else {
		received[jid].wait[getUniqId()] = {res: res, noscript: noscript};
		generatePage(page);
	}
}

var home = function(res, title, desc, body, footer, noscript) {
	res.writeHead(200, {'Content-Type': 'application/xhtml+xml'});
	res.write('<?xml version="1.0" encoding="utf-8"?>\n');
	res.write('<?xml-stylesheet type="text/css" href="theme.css" media="screen"?>\n');
	res.write('<!DOCTYPE html>\n');
	res.write('<html xmlns="http://www.w3.org/1999/xhtml">\n');
	res.write('	<head>\n');
	res.write('		<title>' + (title? title: 'Eldonilo blog') + '</title>\n');

	if (!noscript) {
		res.write('\n');
		res.write('		<script type="application/ecmascript" src="configuration.js" defer=""/>\n');
		res.write('		<script type="application/ecmascript" src="xml2json.js" defer=""/>\n');
		res.write('		<script type="application/ecmascript" src="strophe.js" defer=""/>\n');
		res.write('		<script type="application/ecmascript" src="strophe.pubsub.js" defer=""/>\n');
		res.write('		<script type="application/ecmascript" src="date.js" defer=""/>\n');
		res.write('		<script type="application/ecmascript" src="ns.js" defer=""/>\n');
		res.write('		<script type="application/ecmascript" src="jid.js" defer=""/>\n');
		res.write('		<script type="application/ecmascript" src="atom.js" defer=""/>\n');
		res.write('		<script type="application/ecmascript" src="nothing.js" defer=""/>\n');
	}

	if (config.atomRoot) {
		res.write('\n');
		res.write('		<link rel="alternate" type="application/atom+xml" title="Atom feed" href="?type=atom"/>\n');
	}

	res.write('	</head>\n');
	res.write('\n');
	res.write('	<body>\n');
	res.write('		<header>\n');
	res.write('			<h1>' + (title? title: 'Eldonilo blog') + '</h1>\n');
	res.write('			<p>' + (desc? desc: 'Displaying your nodes.') + '</p>\n');
	res.write('\n');
	res.write('			<nav>\n');
	res.write('				<ul>\n');
	res.write('					<li><a href="?no=client">Without client-side</a></li>\n');
	res.write('					<li><a href="?no=server">Without server-side</a></li>\n');
	res.write('					<li><a href="?">Hybrid mode</a></li>\n');
	res.write('				</ul>\n');
	res.write('			</nav>\n');
	res.write('		</header>\n');
	res.write('\n');

	if (!body)
		res.write('		<section id="messages"/>\n');
	else {
		res.write('		<section id="messages" xmlns:e="' + ns.e + '">\n');
		res.write(body);
		res.write('		</section>\n');
	}

	res.write('\n');

	if (!footer)
		res.write('		<footer/>\n');
	else {
		res.write('		<footer>\n');
		res.write(footer);
		res.write('		</footer>\n');
	}

	if (!noscript) {
		res.write('\n');
		res.write('		<script type="application/ecmascript" src="blog.js"/>\n');
	}

	res.write('	</body>\n');
	res.end('</html>\n');
};

http.createServer(function(req, res) {
	util.log('Connection from ' + (req.headers['x-forwarded-for'] || req.client.remoteAddress) + ' (' + req.headers['user-agent'] + ') to ' + req.method.toLocaleLowerCase() + ' “' + req.url + '” from ' + req.headers['referer'] + '.');

	var re = new RegExp('^' + config.webRoot);
	var url = parseurl(req.url.replace(re, ''));
	var pathname = url.pathname || '';
	var ext = pathname.substring(pathname.lastIndexOf('.')+1);

	if (pathname === '')
		return servePage(url, res);

	fs.readFile(pathname, function(err, data) {
		if (err || pathname === 'index.xhtml')
			return servePage(url, res);

		res.writeHead(200, {'Content-Type': config.types[ext] || 'application/octet-stream'});

		res.end(data);
	});
}).listen(config.webPort, config.webHost);