MultiUser Chat using XMPP and Orbited (Using Ruby-on-Rails)

One of the things that I wanted to understand and build since I first learned to program was to build a chat client. Something that would allow people to communicate and I am extremely thankful to Rishav Rastogi for introducing me to XMPP.

I never really understood all the moving parts very clearly during my first interaction with the technologies but with some time on my hands now I decided to revisit the entire process of building a web chat client. While there are a few well documented resources that cover how to build a simple web chat client the information is mostly directed towards using XMPP and building a one-on-one chat.
Though the requirements for building a multiuser chat aren’t significantly different there are subtle differences that exist.

A brief introduction to XMPP, Ejabberd (our XMPP server) and Orbited

XMPP
Ejabberd is an XMPP server that I used to build my chat client with.

The Extensible Messaging and Presence Protocol (XMPP) is an open technology for real-time communication, which powers a wide range of applications including instant messaging, presence, multi-party chat, voice and video calls, collaboration, lightweight middleware, content syndication, and generalized routing of XML data.Xmpp.org

The technology was initially called Jabber and hence both Jabber and XMPP are used interchangeably on several posts. – Wikipedia

Ejabberd
Ejabberd is a Jabber/XMPP server built using Erlang and is open-source and we would be using Ejabberd in this example. Another popular alternative for Ejabberd is OpenfireEjabberd

Xmpp4r
Since this post would be using Rails we use the xmpp4r gem which is a wrapper over the standard XML that XMPP/Jabber/Ejabberd uses, thus allowing us to work with Ruby rather than generate XML. For those using Ruby 1.9.2 the gem installation may throw up some errors while installing the Rdoc so I’d recommend you either skip the Rdoc installation or ignore the error. The online documentation for Xmpp4r is pretty good and the gem comes with some useful examples that could help you get started.

Orbited
Orbited provides a pure JavaScript/HTML socket in the browser. It is a web router and firewall that allows you to integrate web applications with arbitrary back-end systems.Orbited

Why do we need Orbited?
With our existing arrangement (once we install Ejabberd and xmpp4r gem) we could get a basic messaging system ready. We could have users send messages and receive messages. The problem would be to receive those messages on the browser. There is no way we can display those messages without having to poll our server to fetch this information and we know polling could cause scalability issues. Orbited fills this void by acting as a web router that routes the incoming messages to the appropriate user’s browser using a technique called as long-polling. And long-polling is more scalable than polling.

Long-Polling
Comet is a broad term used for technologies like Long-Polling and streaming. While traditional polling requires periodic requests to be sent to the server and then return with the response, in long-polling a connection is established with the server which persists until a response is provided (or the request times out). Once the response is provided the connection is closed and a new one is step up waiting for the next response from the server. Similarly a new connection is set up on timeout. In Streaming the connection persists between the client and the server while the information is transferred.

According to HTTP 1.1 a browser is allowed to have only 2 connections to the server one of which is used here for real time communication, though I am not fully clear if this is exactly the way the connection is setup. Apparently IE 8 allows 6 connections per host so I shall look forward to any clarifications on this.

Orbited comes with support for technologies such as STOMP, IRC and XMPP so its a handy tool to get started with.

Installation

Installing Xmpp4r
This is the easiest part especially with Rails 3. The following is a snippet of my gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.3'

# Bundle edge Rails instead:
# gem 'rails', :git => 'git://github.com/rails/rails.git'

gem 'mysql2'
gem 'xmpp4r'
gem 'authlogic'
gem 'rails3-generators'

group :development do
 gem 'rspec-rails', '2.3.0'
 gem 'mongrel', '1.2.0.pre2'
 gem 'cgi_multipart_eof_fix'
 gem 'fastthread'
end

group :test do
 gem 'rspec', '2.3.0'
 gem 'webrat', '0.7.1'
end

bundle install and your ready.

Installing Ejabberd

You can download the installer from here. At the time of this tutorial the lastest version was 2.1.6.

The installer guides you on how to setup the xmpp server. Here are some of the questions you would have to provide answers to
Domain: siddharth-ravichandrans-macbook-pro.local This is simply a name (domain name) that you would want your server to be known by. In production this could be chat.example.com or jabber.example.com. For development the default is good. Its important that you note down the domain name somewhere as you will be using this a lot.

Cluster Installation: NO

Admin: siddharth This could be any name that you choose. This provides a way to access the ejabberd web administration interface
Admin password : siddharth

Thats it, you have your ejabberd server installed. Now open the folder you installed it in and navigate to the bin folder.

 
./ejabberdctl start
 
 SIDDHARTH-RAVICHANDRANs-MacBook-Pro:bin SIDDHARTH$ 
./ejabberdctl status
The node ejabberd@localhost is started with status: started
ejabberd 2.1.6 is running in that node

The will let you know if the server is working

Now that we have confirmation that our server is running log onto http://localhost:5280/admin to access you admin interface.

You may log in as [AdminUser]@[domain] followed by the password.
In my case ‘Siddharth@siddharth-ravichandrans-macbook-pro.local’ with the password ‘Siddharth’

You should now be able to see a web console for the administrator

Installing Orbited

The version of Orbited that I used was 0.7.10 which is available here. Ensure that you have python 2.5 or higher installed in your system. Most linux and OS X systems come with Python pre-installed. You can check by

$ python 
Python 2.6.1 (r261:67515, Jun 24 2010, 21:47:49) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> exit()

Twisted is installed as dependency for Orbited 0.7.10 so it need not be installed explicitly but incase you face some errors these are the steps of installation

Ensure that the orbited.cfg file is placed in the /etc folder which is where orbited automatically checks for the configuration file or else it may be supplied as an argument

 sudo orbited --config=/Users/SIDDHARTH/orbited.cfg

Once your done open the configuration file on your favorite editor

[global]
reactor=select
#reactor=kqueue
# reactor=epoll
session.ping_interval = 40
session.ping_timeout = 30
# once the sockets are open, orbited will drop its privileges to this user.
user=SIDDHARTH

For the reactor epoll would the one to select on Linux machines and Kqueue for OS X but I noticed that Kqueue has not been maintained and throws errors so using select is the last resort. Though select has scalability issues its okay to use it for development.

Set the user to the user that you would want orbited to run as.
The access section identifies how orbited will communicate with Ejabberd
Orbited will listen to all incoming requests at port 8000 and communicate with port 5222 with XMPP (Ejabberd uses 5269 for server to server communication)
Therefore

localhost:8000 -> localhost:5222.

In production this could look like

localhost:8000 -> example.com:5222

So our access section would look like

[access]
#localhost:8000 - > irc.freenode.net:6667
localhost:8000 - > localhost:5222
* -> localhost:4747
#* -> localhost:61613

Thats it, orbited is ready. Give it a go by typing ‘orbited’ in the console. You should see the server start.

Beginning with some XMPP programming
I will be working on some basics of using xmpp4r which are explained beautifully in François Lamontagne’s two part tutorial on using Jabber with xmpp4r

Once you’ve conquered the basics of user subscription and sending messages lets take a look at the Multi User Client support provided in Xmpp4r.

Registering our users to the Jabber server. Ideally this would be after a user registers to your site, so an after_create operation.

require 'xmpp4r'
require 'xmpp4r/muc'
require 'xmpp4r/roster'
require 'xmpp4r/client'
# getting done with all the requires so you can try this on the console
 client = 
   Jabber::Client.new(Jabber::JID.new
('first_user@siddharth-ravichandrans-macbook-pro.local'))
 client.connect
 client.register('password')

# do the same for another user with 
#full_jiid = 'second_user@siddharth-ravichandrans-macbook-pro.local'

Logging into the server

require 'xmpp4r'
require 'xmpp4r/muc'
require 'xmpp4r/roster'
require 'xmpp4r/client'
# getting done with all the requires so you can try this on the console
 client = 
 Jabber::Client.new(Jabber::JID.new
('first_user@siddharth-ravichandrans-macbook-pro.local'))
 client.connect
 client.auth('password')

# Don't forget to log both users in

Logging into a room/ creating a room
The MUC Client is a multi User chat Client. The XMPP4R gem provides support for MUC too.

Create a new client

muc = Jabber::MUC::MUCClient.new(client)

The MUC is not to be confused with the room. Its simply a client that serves as an interface for the user in a particular room.

Joining/Creating a room

muc.join(Jabber::JID::new
('chatroom@conference.siddharth-ravichandrans-macbook-pro.local' 
+ client.jid.node))

This lets the user join a room called chatroom and the user is logged in to the room as client.jid.node which evaluates to first_user in our case.

The domain appends the word conference by default to all multi user chat rooms and can be changed by editing the configuration file. The JID for a room can be split as ROOM_NAME + @ + conference.domain_name/user_nick

Setting up callbacks for the client

    
muc.add_join_callback do |m|
      puts "[NEW MEMBER JOINED] " + m.to.jid.node
    end

    muc.add_message_callback do |m|
      puts "[NEW MESSAGE]" + m.body
    end

    muc.add_leave_callback do |m|
      puts "[MEMBER LEFT] " + m.to.jid.node
    end

The callbacks like the one described earlier in François Lamontagne’s two part tutorial get called when a new user joins the chat room, sends a message to the room or leaves the chat room. The MUC chat is actually very similar to the one – on – one chat example described in François Lamontagne’s example except that when a message is directed to the room it relays the message to all of the members in the room. So if you look at the xml you will notice that a message directed to the room is eventually directed to each user in the chatroom. The only difference is the send method which belongs to muc object takes care of the relaying or you may query the roster (I will come to this in a moment) to identify the members in a room and post a message to each member.

Sending a message to the room

muc.send(Jabber::Message.new
('chatroom@conference.siddharth-ravichandrans-macbook-pro.local',
 'Pink Floyd is the greatest band ever'))

The message type for a message sent to a chatroom is automatically set to the type :groupchat. (Jabber::Message is explained here. Lets have a look at the associated xml that is sent to each member

In order to view xml generated set the Jabber::debug to true

 
Jabber::debug = true

The roster describes the subscriptions or the buddies on a one-on-one chat but in a chatroom the muc client has a roster that identifies the number of users in a chatroom along with their presence

muc.roster

would yield something like this

The MUC roster is extremely useful and allows you to set callbacks too.

This pretty much wraps my example using Ejabberd and XMPP4R. The next part of my post will briefly describe how we can use orbited and have this information flow through the browser.

Starting Orbited

orbited

Would get orbited up and running if you placed the orbited.cfg in the /etc folder. Once orbited is running you can log onto http://localhost:8000/static where you would be able to the see the javascript files that Orbited provides you with. You will notice Orbited.js and a static folder. Jump into the static folder -> then protocols -> Xmpp -> to find the xmpp.js file. We will be working primarily with these two files.

So first lets make these two files available to our application by putting them in a layout file.

You will notice two partials at the bottom of my layout file called _tcpsocket and xmpp_client (both poorly named).

Before we begin try running this snippet obtained from Micheal Carter’s Sockets in the Browserarticle on CometDaily.com. Add this snippet to the _tcpsocket.html.erb partial that is included in the layout.

Load a view page (which includes the layout containing this parital). It could be any scaffold generated code block.

 
$(document).ready(function(){
  var conn  = new Orbited.TCPSocket();
   conn.open('localhost', 5222);
   conn.onopen = function(){ alert('connection opened');
   // conn.send('Hello World');
   }
   conn.onread  = function(data){ alert('RECIEVE DATA' + data ); }
   conn.onclose   = function(data){ alert('connection closed'); }

});

This would be a helpful example to understand better what Orbited does. All it does is opens a tcp socket on localhost and connects to port 5222 . The onopen callback is called when the connection is opened and sends a piece of text which is read by the onread callback and the connection close callback is called.

Basically reading the data whenever something is sent by the server while waiting for it with an open socket connection is what we do.

Looking at our _tcpsocket partial

 

  document.domain  = document.domain;
  Orbited.settings.port  = 8000;
  Orbited.settings.hostname   = 'localhost';
  TCPSocket  = Orbited.TCPSocket;

You may ignore the document.domain = document.domain code for now. Here we include the Orbited.js code along with the Xmpp.js javascript files provided by Orbited. We also specify the port we would be listening to and the hostname.

The xmpp.js file is where all the magic (not really) happens.

The xmpp.js contains (yet another) javascript based interface to XMPP methods, thus allowing us to perform all the XMPP operations right from the browser. The existing xmpp.js file comes with partial support for MUC operations. A poorly and hastily hacked xmpp.js file to suit basic MUC operations is available on my github account.

CONNECT = [""];
REGISTER = ["","",""];
LOGIN = ["","","Orbited"];
ROSTER = [""];
MSG = ["",""];
PRESENCE = [""];
EXT_PRESENCE = [];
GROUPCHAT_MSG = ["",""];

....

XMPPClient = function() {
    var self = this;
    var host = null;
    var port = null;
    var conn = null;
    var user = null;
    var domain = null;
    var bare_jid = null;
    var full_jid = null;
    var success = null;
    var failure = null;
    var parser = new XMLReader();
    self.onPresence = function(ntype, from) {}
    self.onMessage = function(jid, username, text) {}
    self.onSocketConnect = function() {}
    self.onUnknownNode = function(node) {}
    self.sendSubscribed = function(jid, me_return) {
        self.send(construct(PRESENCE, [me_return, jid, "subscribed"]));
    }
    self.connect = function(h, p) {
        host = h;
        port = p;
        reconnect();
    }
    self.msg = function(to, content) {
        self.send(construct(MSG, [full_jid, to, content]));
    }
    self.unsubscribe = function(buddy) {
        self.send(construct(PRESENCE, [full_jid, buddy.slice(0, 
        buddy.indexOf('/')), "unsubscribe"]));
    }
    self.subscribe = function(buddy) {
        self.send(construct(PRESENCE, [full_jid, buddy, "subscribe"]));
    }
    self.send = function(s) {.....

If you notice these methods end up generating the exact same XML code (converted to utf8) and sent to the ejabberd. So no real magic there.

Our goal is to now use this API to perform the same operations on the browser. Here is a basic MUC chat javascript. Add this to the _xmpp_client.js on the layout file. The snippet contains some missing text so use the code here

console.log('XmppClient partial loaded');
  var hostname                = 'localhost';
  var domain                  = 'siddharth-ravichandrans-macbook-pro.local';
  var bare_jid                =  ''; 
  var password                = ''; 
  var chatroom_domain         = 'conference.' + domain;
 // var username                = bare_jid + '@' + domain;

  console.log('xmpp client connect request posted');

  function loginSuccess(){
   alert('Login Successful');
  // xmpp_client.set_presence('available');
   alert(typeof ROOM_NICK);
     if(typeof ROOM_NICK != 'undefined'){
     xmpp_client.join_room(ROOM_NICK, chatroom_domain, bare_jid, 
                                 'available', null);   
     console.log('JOIN ROOM Called');
   } 
  }

  function loginFailure(){
   console.log('Login Failed');
  }

  function serverConnectSuccess(){
   alert('Server Connection Success');
   $('.presence-status').html('('+ 'Server Connected'+')');
   xmpp_client.login(bare_jid, password, loginSuccess, loginFailure);

  }

  function serverConnectFailure(){
   alert('Server Connection Failed');
  }

  var xmpp_client                            = new XMPPClient;
  xmpp_client.connect('localhost', 5222);

  xmpp_client.onSocketConnect                = function(){
   $('.presence-status').html('('+ 'On Socket Connected'+')');
   xmpp_client.connectServer(domain, serverConnectSuccess, 
                        serverConnectFailure);
   console.log('After COnnect Server is called'); 
   xmpp_client.login(bare_jid, password, loginSuccess, loginFailure); 

  }

  xmpp_client.onPresence = function(ntype, from) {
   var username  = bare_jid + '@' + domain + '/Orbited'; 
   if(from == username){
    if (ntype == null){
     $('.presence-status').html('(available)');
    }
    else{
     $('.presence-status').html('(' + ntype + ')');
    }
   }    
  }

  xmpp_client.onMessage = function(jid, username, text) {
  $('.conversation-box').append('');
‘ + username.split(“/”)[1] + ‘ says : ‘ + text + ‘
   alert('JID' +  jid.to_s + ' Username ' + username + ' Text' + text);
  }

 $(document).ready(function(){
  $('.send-message-button').click(function(){

   alert('Incoming message');
   var message                               = $('#message').val();   
  // xmpp_client.msg('007@conference.' + domain, message);
   xmpp_client.groupchat_msg(message, chatroom_domain);
   return false;
  });
 });

Note that the xmpp.js file has been modified slightly from what Orbited provides us and the _xmpp_client.html.erb uses this modified api hence the method parameters may appear strange when compared with the original xmpp.js file.

The ROOM_NICK parameter is defined in the view using a content_for :js block and would be available inside a chat room.

I hope this is useful and please let me know of errors or misinformation in my article. In case you are interested in having a detailed write up on the installation of all the software, add a comment and I will send you the write up as soon as possible. I have tried my best to attribute most references to their original authors and sources but in case I have forgotten any I would be glad to update it anytime.

 

Final chat screenshot

  1. Sankalp Singha

    I think that a much better option would have been to use the xmpp4r to get the RID and JID and use strophe .js to prebind to the current session and code the Chat UI in it.

    No need for orbited!

    Check out this project : Converse.js

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

nineteen − nine =

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>