26. September 2010

A Website Chat made easy with XMPP and BOSH


A description of the live chat feature on http://avatar.lupuslabs.de/contact.html

10 years ago we spent weeks to develop a website chat. We implemented a chat server in C++, a PHP library, which talked to the chat server and JavaScript streaming in an iframe. Today it is much simpler.

Today we can use XMPP and BOSH and let the web page talk to my GTalk client, which runs all the time anyway.

Here is the shopping list of technologies:
These components do all the work. There is only some Javascript code and a little bit of plumbing required.

1. Set up ejabberd:

Download ejabberd from http://www.ejabberd.im/. The easiest way is to use the installer from http://www.process-one.net/en/ejabberd/downloads

For XMPP to work we need XMPP users. I prefer to run ejabberd with MySQL storage, because MySQL is the easiest way for me to add users and to manage the user list programatically. But the mnesia database also works.

Here is the config to use MySQL with ejabberd (to be added to ejabberd.cfg):
% {auth_method, internal}. % disabled
{auth_method, odbc}. % enabled
{odbc_server, {mysql, "localhost", "ejabberd", "mysql-user", "mysql-password"}}
Also I comment out XMPP in-band account registration, so that nobody creates users on my server:
{access, register, [{deny, all}]}.
This article explains how to create tables for ejabberd in the MySQL server: https://support.process-one.net/doc/display/MESSENGER/Using+ejabberd+with+MySQL+native+driver

2. Set up Apache as BOSH proxy:

Enable Apache modules "proxy" and "proxy_http". The debian way:
% a2enmod proxy
% a2enmod proxy_http
Add to the proxy configuration (Debian: proxy.conf)
ProxyPass /xmpp-httpbind http://127.0.0.1:5280/http-bind
ProxyPassReverse /xmpp-httpbind http://127.0.0.1:5280/http-bind
By default accessing the proxy is only allowed for localhost. Since Browers will access it, it needs to be accessible from anywhere. Add to the proxy configuration (Debian: proxy.conf)
Allow from all
ProxyRequests Off
3. Create an HTML file and start programming

Download Strophe and jQuery (or use the CDN version http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js). Add references to the HTML-head:
<script type="text/javascript" src="jquery-1.4.2.min.js"></script>
<script type="text/javascript" src='strophe.min.js'></script>
4. Now comes the real fun: coding

We basically create a BOSH connection from Javascript to ejabberd through apache/mod_proxy:
var conn = new Strophe.Connection('/xmpp-httpbind');
Create an XMPP user in MySQL (I am using phpmyadmin) and connect with this user:
conn.connect('test@wolfspelz.de', 'secret', OnConnectionStatus);
The OnConnectionStatus function may look like:

function OnConnectionStatus(nStatus)
{
if (nStatus == Strophe.Status.CONNECTING) {
} else if (nStatus == Strophe.Status.CONNFAIL) {
} else if (nStatus == Strophe.Status.DISCONNECTING) {
} else if (nStatus == Strophe.Status.DISCONNECTED) {
} else if (nStatus == Strophe.Status.CONNECTED) {
OnConnected();
}
}
When the connection is established, register message handlers and send our own presence:
function OnConnected()
{
conn.addHandler(OnPresenceStanza, null, "presence");
conn.addHandler(OnMessageStanza, null, "message");
conn.send($pres());
}
BTW: handlers should always return "true". Otherwise they are removed from the handler list. A message handler may look like:
function OnMessageStanza(stanza)
{
var sFrom = $(stanza).attr('from');
var sType = $(stanza).attr('type');
var sBareJid = Strophe.getBareJidFromJid(sFrom);
var sBody = $(stanza).find('body').text();
// do something, e.g. show sBody with jQuery
return true;
}
A presence handler may be:
function OnPresenceStanza(stanza)
{
var sFrom = $(stanza).attr('from');
var sBareJid = Strophe.getBareJidFromJid(sFrom);
var sType = $(stanza).attr('type');
var sShow = $(stanza).find('show').text();
// do something, e.g. show status icon with jQuery
return true;
}
The connection should be closed when the page unloads. Unfortunately strophe.js (at least up to version 1.0.2) disconnects asynchronously, which does not work when the page is destroyed. After some time the XMPP server will notice, that the page disappeared and will close the connection. But if you do not want to wait, then we have to force strophe to close immediately.

There is a patch, which allows for synchronous connection closing. The patch must be applied to the strophe.js file:
diff --git a/src/core.js b/src/core.js
index 5aeb06a..f79ae29 100644
--- a/src/core.js
+++ b/src/core.js
@@ -2161,7 +2161,8 @@ Strophe.Connection.prototype = {

             req.date = new Date();
             try {
-                req.xhr.open("POST", this.service, true);
+               var async = !('sync' in this && this.sync === true);
+                req.xhr.open("POST", this.service, async);
             } catch (e2) {
                 Strophe.error("XHR open failed.");
                 if (!this.connected) {
How to use the patch:
    this.conn.flush();
    this.conn.sync = true; // Set sync flag before calling disconnect()
    this.conn.disconnect();

5. Summary

Of course, all these functions and callbacks should be prototype based and bind the instance to the closure. We should also use a model-view architecture and handle the protocol stuff in the model, while notifying the view of really important events.

Here are the files:
contact.html - the "driver" which loads everything and produces the GUI
model.js - the model does it, classes: Model, Room, Participant
view.js - the view shows it, the view registers listeners with the model
utils.js - utility classes, logging, unit test, oberver pattern
config.js - configurations for test and production
setup.js - selects the appropriate configuration
style.css
lib/strophe.js - including the above patch
lib/jquery-1.4.2.min.js
lib/jquery-ui-1.8.5.custom.min.js
_happy_chatting()

1 Kommentar:

Aj Dave hat gesagt…

Thank you,, very nice