XMPP4r: A real world example

by Frank

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.

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

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

require 'starling'
require 'crack'
require 'net/http'
require 'cgi'
require 'iconv'
require 'json'
class Backend
  def run
    process_queue
  end

  private
  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('backend')
        unless item.nil?
          jitem = Crack::JSON.parse(item) rescue nil

          google_api_url = "http://www.google.com/ig/api?weather=#{CGI::escape(jitem["body"])}"

          ig_weather = Crack::XML.parse(Iconv.conv('UTF-8',
          'ISO-8859-1',
          Net::HTTP.get(URI.parse(google_api_url))
          )
          ) rescue nil

          if jitem && ig_weather
            process_job(jitem,ig_weather)
          else
            puts jitem["from"]
            @starling.set('listener',
            {
              :from => jitem["from"],
              :success => false,
              :message => "An error occured while accessing Google Weather API. You may try again later"
            }.to_json) unless jitem.nil?
          end

        end
      end
    end

  end

  def process_job(jitem,ig_weather)
    if ig_weather["xml_api_reply"]["weather"]["problem_cause"]
      @starling.set('listener',{
        :from => jitem["from"],
        :success => false,
        :message => "Data not available. Try being more precise when typing your location (ex. trois-rivières, québec)"
      }.to_json)
    else
      weather = ig_weather["xml_api_reply"]["weather"]
      data = {
        :forecast_obj => weather["forecast_conditions"],
        :city => weather["forecast_information"]["city"]["data"],
        :winds => weather["current_conditions"]["wind_condition"]["data"],
        :unit_system => weather["forecast_information"]["unit_system"]["data"],
        :temp_f => weather["current_conditions"]["temp_f"]["data"],
        :temp_c => weather["current_conditions"]["temp_c"]["data"],
      }      

      @starling.set('listener',
      {
        :from => jitem["from"],
        :success => true,
        :message => "Weather data for #{data[:city]}:",
        :details => {
          :current_temperature => "#{data[:temp_f]} ° F / #{data[:temp_c]} ° C",
          :unit => data[:unit_system],
          :winds => data[:winds],
          :today => temperatures(
                      data[:unit_system],
                      data[:forecast_obj][0]
                    ).merge(:condition => data[:forecast_obj][0]["condition"]["data"]),

          :tomorrow => temperatures(
                        data[:unit_system],
                        data[:forecast_obj][1]
                      ).merge(:condition => data[:forecast_obj][1]["condition"]["data"])
        }
      }.to_json)
    end
  end

  def temperatures(source_unit,obj)
    x = {}

    if(source_unit == "US")
      #we convert to Celcius
      x["low_c"] = obj["low"]["data"].to_i.to_celcius
      x["high_c"] = obj["high"]["data"].to_i.to_celcius
      x["low_f"] = "#{obj["low"]["data"]} ° F"
      x["high_f"] = "#{obj["high"]["data"]} ° F"

    else
      #we convert to farenheit
      x["low_f"] = obj["low"]["data"].to_i.to_farenheit
      x["high_f"] = obj["high"]["data"].to_i.to_farenheit
      x["low_c"] = "#{obj["low"]["data"]} ° C"
      x["high_c"] = "#{obj["high"]["data"]} ° C"
    end
    x
  end
end

class Fixnum
  def to_celcius
    ((self - 32) / 1.8).round.to_s + " °C"
  end

  def to_farenheit
    (self * 1.8 + 32).round.to_s + " °F"
  end
end

Backend.new.run

#Always remember to pause the main thread at the end since it does not contain any
#blocking call.
Thread.stop

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 :

#!/bin/bash
export RUBYOPT=rubygems
d=`dirname $0`
ruby $d/backend.rb &
ruby $d/listener.rb yourbot@gmail.com somepassword

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.

Bookmark this post : These icons link to social bookmarking sites where readers can share and discover new web pages.
  • DZone
  • Reddit
  • del.icio.us
  • Digg
  • Furl
  • Technorati
  • StumbleUpon