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                               =  ' //close quote 
		var password                               = ' //close single quote
		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

About these ads

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

  1. hello,

    Good Turorial. But I want to know how to make video conference using this xmpp protocol . Is there any tutorial regarding this.

  2. Hi Sid,

    I am new to rails world and I am using your code to check how your chat service works because I have to create a simple chat client using Ejabberd and Xmpp4r in rails 3. But I am not able to find a good example for it. It would be very helpful if you help me out with some example links.
    Thank you for your help.

    • I would suggest first going through François Lamontagne’s two part tutorial on using Jabber with xmpp4r. http://www.rubyfleebie.com/im-integration-with-xmpp4r/ and http://www.rubyfleebie.com/im-integration-with-xmpp4r-part-2/ to get a basic understanding of the Ruby API for XMPP. I would also recommend having a look at the Peepcode tutorial on XMPP and Ruby. http://peepcode.com/products/xmpp.

      For the client side I would suggest you look at Websockets or SocketIO. I’m not sure how long orbited.org will be down and whether support for it will continue. If you are having trouble setting up your servers please let me know Ill send you some instructions on how to get a basic setup going on your dev environment. Checkout processOne website too, they have some setup instructions there. I have some basic code available on github under the project called FireDemon which you could try as a start.

      If this is not something you want to do only with Rails then I would recommend trying NodeJS too.

  3. Hi Sid,

    Thanks for your reply. I used strophe.js and it was pretty effective for creating a chat client. Not sure how much it will scale, but to stand up some thing really quickly it is a great library. I will also look into the other libraries which you have mentioned.
    Thank you.

    • I would need to look into the code to be of any help. If you could provide me with the github link or some link where its being used maybe I could give it a shot.

  4. Could you possibly put the source code for this app on github? I am looking for a working multiuser chat and would greatly appreciate it if you could put this on github

    Thanks!

  5. Hi I have a question: when I create a new room (e.g at http://localhost:3000/users/15/rooms/4) and try to post a message, I get the message ‘Wats going on’ that you wrote in _xmpp_client.html.erb line 101. I then get redirected to http://localhost:3000/users/15/rooms/4 (the same page where I just was), but receive the routing error “No route matches “/users/15/rooms/4″” etc even though that was the address from where I posted the message. I then reload the page, which takes me back to http://localhost:3000/users/15/rooms/4, wait for about a minute without doing anything then receive the two messages “Login Successful” and “string” from localhost:3000. My status then changes to available but I still cant post any messages and if I try to I get the same routing error.

    Do you know what’s happening?

    • My first guess would be some javascript error. Check if the app finds Orbited.js and all the js files it needs. Some more info would be handy.

  6. Yes, the app finds orbited.js and xmpp.js. My code is exactly the same as the github code except that I changed all references to ‘Siddharth@siddharth-ravichandrans-macbook-pro.local’ to ‘macpro.local’ (my local address). Could it be a problem with one of the servers? Do I have to do anything in the ejabberd console at localhost:5280/admin? Or something to do with orbited? Thanks for all your help.

    • I suppose what I’m trying to ask is if you cloned the github code to a different computer what would you change in the code to get the site to work?

      Your help is greatly appreciated as I am new to XMPP and fairly new to rails.

      • Yes I would only change the domain in the config or if its hardcoded like in the example on the _xmpp_client file. Also I would strongly recommend checking if there are any Javascript errors that popup on the console. The whats up is a simple alert message just to test if the dom is loaded. Have you tried the http://www.rubyfleebie.com/im-integration-with-xmpp4r/ examples once in the console just to check if your code can access the Ejabberd Server. I had problems connecting to Ejabberd when I first began and I couldn’t figure out why. I realized that its best to let Ejabberd use the defaults, i.e. your computer name as the domain. I tried it with custom domains on my local machine but the connection failed. You shouldn’t have this issue in production. Then I would recommend going through the Orbited.cfg file once again to check the domain/port are correct.

        Once all this is verified add this block to your chat show page and comment everything else

        
        
        	
        	
        	
        	
        		if((Orbited.util.browser=="opera")||(Orbited.util.browser=="ie")){
        	     Orbited.settings.streaming = false;
        	  };
        		document.domain                = document.domain;
        		Orbited.settings.port          = 8000;
        		Orbited.settings.hostname      = 'localhost';
        		TCPSocket                      = Orbited.TCPSocket;		
        	
        	
        
        	
        	$(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'); }
        
        	});
        
        	
        
        
        
        

        And try sending a message using the console with one of the RubyFlee tutorials. It’ll help you figure out if the connection between Orbited.js and Ejabberd is working. I hope this helps. Sorry about the lousy code formatting.

  7. I tried that but still no success. This may seem like a basic question but in the tutorial you give the instructions below. Which files contain the two pieces of code shown below, becayse I cannot find them? And second, how do I perform actions like muc = Jabber::MUC::MUCClient.new(client) or muc.send(Jabber::Message.new(‘chatroom@conference.siddharth-ravichandrans-macbook-pro.local’, ‘Pink Floyd is the greatest band ever’))? Can you enter muc = ……… from the terminal? If so, how? Thanks once again.

    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

    • You can. You must require ‘xmpp4r’ and its subfiles in the console.

      The application is just a working example. The blog post was intended more as a tutorial so I thought illustrating it in that fashion would be best.

      The files are located in the lib directory in your Rails application.

  8. Nice tutorial.

    Just an update that on line

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

    it should be

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

  9. hi there im looking 4 an xmpp client to connect to eyeball networks i cna connect using other jabber clients but only to IM’s not in to the chat rooms them selves im very new to this but interested to learn so im just wondering if its possible to write a simple program to do so if any 1 can help or guide me in the right direction id appreciate it thank you

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s