XMPP4r: A real world example

It’s been a while since I wrote about XMPP and XMPP4r. I’m glad because my introductory posts on this topic were very well received. Today I want to push further and share with you a tutorial-like post explaining step by step how to build a simple application with a XMPP interface. The complete listing is also available on bitbucket

The application

I couldn’t find something better than writing a xmpp weather bot. You type the name of a city and the bot answers with the current temperature and forecast information using the Google Weather API. Simple enough for this tutorial.

The gems you’ll need to install

  • XMPP4r : sudo gem install xmpp4r
  • json_pure, to convert ruby hashes into json: sudo gem install json_pure
  • Crack, a xml and json parser: sudo gem install crack
  • starling, a ruby message queue: sudo gem sources -a http://gems.github.com/ && sudo gem install starling-starling Then, start the starling daemon by typing : sudo starling -d

Why using a message queue like starling?

starling is a message queue library that speaks the memcache protocol. It is my opinion that a message queue is often a very clever and powerful way to establish a direct communication between the various parts of an application or even between different applications. For the sake of this tutorial, I wanted to separate our application into two distinct components and I didn’t want these components to talk to each other via the http protocol.

We won’t install a XMPP server

We won’t go to the pain of installing a XMPP server like ejabberd or openfire because it would be beyond the scope of this article. Instead, we’ll simply use a Google account for our bot

Application architecture

Like I said, I wanted to separate the application into 2 components : The xmpp listener and the backend logic. You probably already guessed that the xmpp listener component job will be to send and receive xmpp stanzas to the user at the other end. To get the weather data, our listener will send the request to the backend component via a starling message queue that we’ll name backend. The backend component will do what is needed to compute the weather data and it will send the result back to the listener component via another starling message queue that we’ll name listener

This setup might be overkill for our simple app but one of my objective was to show you how easy and interesting it can be to work with message queues. Let’s get the ball rolling.

The listener component

create a new file in your code editor and give it the name listener.rb. Here is the complete source code for this file. I will add comments at the bottom of the listing. To copy this code in your clipboard, don’t forget to click on the small “view plain” link at the top or else you will copy the line numbers as well.

[ruby]
require ‘starling’
require ‘json/pure’
require ‘xmpp4r’
require ‘xmpp4r/roster’
require ‘cgi’
class Fleebie
include Jabber
attr_accessor :jid, :password
attr_reader :client, :roster

def initialize
self.jid = ARGV[0]
self.password = ARGV[1]
@client = Client.new(self.jid)
Jabber::debug = true
connect
end

def connect
@client.connect
@client.auth(@password)
@client.send(Presence.new.set_type(:available))

#the "roster" is our bot contact list
@roster = Roster::Helper.new(@client)

#…to accept new subscriptions
start_subscription_callback

#…to do something with the messages we receive
start_message_callback

#When the backend application has done its job, it tells the listener
#via the "listener" message queue.
process_queue
end

private

#Whatever we receive, we send it to our "backend" message queue. It’s
#not our job to parse and decode the actual message
def start_message_callback
@client.add_message_callback do |m|
@starling.set(‘backend’,{:from => m.from, :body => m.body}.to_json)
unless m.composing? || m.body.to_s.strip == ""
end
end

#whenever someone adds the bot to his contact list, it gets here
def start_subscription_callback
@roster.add_subscription_request_callback do |item,pres|
#we accept everyone
@roster.accept_subscription(pres.from)

#Now it’s our turn to send a subscription request
x = Presence.new.set_type(:subscribe).set_to(pres.from)
@client.send(x)

#let’s greet our new user
m=Message::new
m.to = pres.from
m.body = "Welcome! Type a location to get the weather forecast"
@client.send(m)
end
end

#The backend application talks to this XMPP interface via starling.
#in process_queue we process our job list.
def process_queue
@starling = Starling.new(’127.0.0.1:22122′)
th = Thread.new do
Thread.current.abort_on_exception = true
loop do
item = @starling.get(‘listener’)
unless item.nil?
jitem = JSON.parse(item) rescue nil
msg = Message::new(jitem["from"])
msg.type=:chat
if jitem["success"] == true
msg.body = "\n"
msg.body += jitem["message"] + "\n"
msg.body += "Current temp: #{jitem["details"]["current_temperature"]}\n"
msg.body += "Winds: #{jitem["details"]["winds"]}\n\n"
msg.body += "<b>TODAY</b>\n"
msg.body += jitem["details"]["today"]["condition"] + "\n"
msg.body += "Min/Max : #{jitem["details"]["today"]["low_f"]} / "
msg.body += jitem["details"]["today"]["high_f"] + " ("
msg.body += jitem["details"]["today"]["low_c"] + " / "
msg.body += jitem["details"]["today"]["high_c"] + ") \n\n"

msg.body += "<b>TOMORROW</b>\n"
msg.body += jitem["details"]["tomorrow"]["condition"] + "\n"
msg.body += "Min/Max : #{jitem["details"]["tomorrow"]["low_f"]} /"
msg.body += jitem["details"]["tomorrow"]["high_f"] + " ("
msg.body += jitem["details"]["tomorrow"]["low_c"] + " / "
msg.body += jitem["details"]["tomorrow"]["high_c"] + ") \n"

msg.add_element(prepare_html(msg.body))
msg.body = msg.body.gsub(/<.*?>/, ”)
else
msg.body = jitem["message"]
end
@client.send(msg)

end
end
end
end

def prepare_html(text)
h = REXML::Element::new("html")
h.add_namespace(‘http://jabber.org/protocol/xhtml-im’)

# The body part with the correct namespace
b = REXML::Element::new("body")
b.add_namespace(‘http://www.w3.org/1999/xhtml’)

# The html itself
t = REXML::Text.new(text.gsub("\n","<br />"), false, nil, true, nil, %r/.^/ )

# Add the html text to the body, and the body to the html element
b.add(t)
h.add(b)
h
end
end

Fleebie.new
Thread.stop
[/ruby]

Important parts of this listing

I hope that you will find the code above self explaining. However, here are a few important things about the listing. At line 98-99, we set 2 versions of the same message that will be sent to the XMPP user : one in plain text, the other in XHTML. If the client at the other end has support for XHTML messages, the XHTML version will be displayed, otherwise the plain text version will be used. This is a pretty interesting feature of the XMPP protocol.

The process_queue method

We start a new thread, we subscribe to the ‘listener’ message queue and we process our job list until the program ends or gets interrupted (by typing Ctrl-C for example). You can see that by using a message queue, we move away from the traditional “Request / Response” paradigm. In our case the request (line #44) is “disconnected” from the response (line #103).

Finally, at line #129 we stop the main thread (not the same thread that we started in process_queue). We do this for an obvious reason : we don’t want our script to terminate once it will reach the end of the file. We just ask him to wait until the other threads are done executing.

The backend component

It is here that we will fetch, compute and prepare the actual weather data. For the sake of this tutorial I use the google weather API but it could have been anything else. Create a new file in your code editor and call it backend.rb

This code is not very sexy… It just queries the google api, parse the result, convert temperatures from Farenheit to Celcius or the other way around (because the Google API doesn’t do that by itself) and send back the response to the listener component via the corresponding message queue.

Launch the script

Create a shell script and call it launcher.sh (don’t forget to chmod +x). put the following in it :

Now you can type ./launcher.sh and you should be ready to go. If you encounter a problem while following the tutorial, don’t hesitate to post a comment and I’ll do my best to help you resolve it.

  • http://eribium.org Alex MacCaw

    Interesting article. In case you haven’t seen it, I want to point you to blather.
    http://sprsquish.github.com/blather/

    I’ve been working on the MUC part to it: http://github.com/maccman/blather

  • http://www.rubyfleebie.com Frank

    @Alex, yes I learned about blather only recently but didn’t have time to look at it yet. Is it comparable to xmpp4r?

  • Pingback: links for 2010-04-26 « Bloggitation()

  • Jeff

    Blather doesn’t appear to support components. They’re a really nice way to write scalable bots as there is no roster/presence overhead.

  • Airton

    How could I test that?

  • Senthil

    xmpp4r will work for mobile apps using Rhodes

  • Ray Renaldi

    Hi, i’m new to xmpp4r and trying to setup my own chat client, your post has been a great help and i would like to thank you for taking your time to post this.

    However when i tried to run it, i encountered two errors that i can’t seem to fix, it’s my second months learning ruby so bear with me.

    Here’s the errors:
    -`block in cache_get’: unexpected response “[\x00\x00\x00\n” (MemCache::MemCacheError) from backend.rb line 17/18
    -same error message from listener.rb line 80/79

    Thank you in advance.

  • http://Margarette.tumblr.com Natasha

    Finally i quit my day job, now i earn decent money
    on-line you should try too, just type in google – blackhand roulette system

  • http://dunesolarpower.com/Stats/sitemap.html http://dunesolarpower.com/Stats/sitemap.html

    Buy discount replica nfl Jerseys online, wholesale football jerseys for kids/youth, womens and mens. Enjoy fast free shipping and 60-Day return policy.

  • https://cindiriedoenmxt.wordpress.com/2015/02/05/you-make-these-nitric-max-muscle-tablets-mistakes/ nitric max Muscle and anabolic rx24 australia

    Youг style is unique compared to otheг folks
    I’ve reɑd stuff from. I appreciate you for posting when you hɑve thе opportսnity,
    Guess I’ll just bookmark thiѕ site.

  • Steffen

    The connect didn’t work for me. I spied into the test cases of the project.
    It seems the JID hast to be “joe.user@gmail.com/resource”. Otherwise it doesn’t connect.