Ejabberd global roster
June 30, 2011 § 2 Comments
Recently we needed a “global roster” for our ejabberd server. By this I mean that we wanted to be able to see all users currently online and allow other users to initiate a chat with them. This isn’t as easy as it sounds— the way XMPP normally deals with who’s online (i.e., who has announced their presence) is through rosters. If you’re not familiar with Jabber, a roster is simply the contact list in a chat program.
You might quite sensibly think “why not make a user that is friends with everyone?” That way every time a user comes online the special user will be notified and will have a list of all online users. Whilst this idea works, you will quickly discover that XMPP doesn’t deal with large rosters very well and if your user base is large this method will seriously slow down your chat server.
Whats the solution then?
As ever there are a few ways to achieve this, I’m going to run through the various different attempts we tried and the eventual optimal one.
Chat component
After ditching the idea of a user with all users in its roster we moved on to try a component. An XMPP component sits on a domain (say bot.chat.com) and accepts all messages sent to it but does nothing with them by default. This is great because you can program a bot to sit here and do something with those messages. For our use case the idea would be that each user just has to send a presence message to the component when they come online and the component then stores this info somehow (but not in a roster— it doesn’t have one).
It’s the fact that a component doesn’t have a roster that makes this idea scale. You just need to store the data in something that can handle a decent size set and that you can access from where you need it (in this case our app). We chose redis to store the data as it is quick to write to and easy to read from our main web application. In terms of code, every time a user sends a presence online message we store their Jabber ID (JID), something like this:
redis> SADD myset "jid"
And when a user logs off:
redis> SREM myset "jid"
Then to get all online users is as simple as:
redis> SMEMBERS myset
This works great, but has a few slight downsides:
* Your users need to send an extra presence
* You need to run a separate bot to handle presence and hence another moving part
Ejabberd module
I’ve been pretty interested in erlang for a few years and recently decided to attend the Erlang Factory in London. I’ve been looking for a little project to get my toes wet and this struck me as the ideal project — why not write a module for ejabberd which automatically updates our redis set when presence is received? We can do away with our bot completely and remove the extra moving part.
With this in mind I started to dive into the ejabberd module architecture. As it turns out this is pretty good, although the docs are brief at best and non-existent at worst. Luckily a few other kind souls have posted some pretty posts which were the most useful resources I could find.
Having read these I got stuck in. The basics of an ejabberd module is that it has to have the gen_mod behavior which requires two methods (start & stop) and thats pretty much it. Ejabberd then provides you with a bunch of hooks (think of them like Rails callbacks or filters) which you can hook onto to insert custom code. For our purposes we can use the set_presence_hook which is fired every time a presence message is received (here’s a ) and then store this info.
Here is a shortened example of the code (this requires a little knowledge of Erlang but should hopefully be pretty understandable):
-module(mod_global_roster).
-behavior(gen_mod).
-include("ejabberd.hrl").
-export([start/2, stop/1, on_presence_joined/4, on_presence_left/4]).
start(Host, _Opts) ->
ejabberd_hooks:add(set_presence_hook, Host, ?MODULE, on_presence_joined, 50),
ejabberd_hooks:add(unset_presence_hook, Host, ?MODULE, on_presence_left, 50),
ok.
stop(Host) ->
ejabberd_hooks:remove(set_presence_hook, Host, ?MODULE, on_presence_joined, 50),
ejabberd_hooks:remove(unset_presence_hook, Host, ?MODULE, on_presence_left, 50),
ok.
on_presence_joined(User, Server, _Resource, _Packet) ->
{ok, Client} = eredis:start_link(),
{ok, <>} = eredis:q(Client, ["SADD", "key", User]),
none.
on_presence_left(User, Server, _Resource, _Status) ->
{ok, Client} = eredis:start_link(),
{ok, <>} = eredis:q(Client, ["SREM", "key", User]),
none.
With just this code every time the server receives a presence joined message it will add the JID of the user to the redis set "key" and then when a left message is received it will remove it. With the code written the final step is to install the module.
To do this first compile it:
erlc -I /path/to/ejabberd/src module_src.erl
Them move the resulting beam to your ejabberd ebin directory and finally add the module to your ejabberd.cfg:
{mod_global_roster, []}
Restart ejabberd and you should be good to go!
This example ignored any custom configuration you might want, please see the full source for details on configuring your redis instance and the key name to use.
Summary
My main takeaway was that it is actually pretty easy to write modules for ejabberd and that these were a great way to get into some real erlang code pretty easily. Ejabberd is a great bit of software and having an easy way to customise it is really useful — if you've used ejabberd for any decent sized project I'm sure you've wanted to do things it couldn't do out of the box and hopefully this has shown you that its not too hard to do.
Was there an architectural reason why you didn’t use a custom MUC?
Using an MUC has slightly the same issue as using a bot (although not the roster one) in that every user would need to join this MUC. This way we just have a list of online users automatically.