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

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 [email protected] 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.

RESTful admin controllers and views with Rails

You want a RESTful Rails app with a backend administration? The worst thing to do in my opinion is to use the same controllers for the public and the admin side. At first, it might look wise to do this. I mean, if you have a “books” resource, it would be logical that all methods related to books go in the same controller, right? Well, logical or not… I suggest you never do this because your application will become a real mess in no time. Personally, I did it once and will never get caught again!
Exemple of a mess in a controller :

def index
  if administrator?
    @books = Book.all
  else
    @books = Book.published
  end
end

Example of a mess in a view :

<%if administrator?%>
  <%=render partial: "admin_index"%>
<%else%>
  

Published books

bla bla bla <%end%>

If you go that route, be prepared to make your fingers bleed because you will write tons of confusing and ugly “if” statements everywhere. Most of the time anyway, what you need to do with the resources is completely different depending if you’re on the admin or public side, so you’re better to separate them.
Step #1 : Generating the controllers
To generate a public controller, you do like you always do : ./script/generate controller books
To generate its admin counterpart, you simply do this : ./script/generate controller admin/books
Rails will generate the controller in controllers/admin/books_controller.rb and a folder for the views in views/admin/books
Step #2 : Configuring the routes
One route for the public side :

map.resources :books, :only => [:index, :show]

One route for the admin side

map.namespace :admin do |admin|
  admin.resources :books
  admin.resources :some_other_resource
end

Now your namespaced controller has its own named urls as well : admin_books_url, edit_admin_book_url and so on…
Step #3 : Get the “form_for” url right

<%form_for [:admin, @book] do |f|%>
   <%=f.text_field :title%>
   <%=f.submit "save"%>
<%end%>

That way Rails will correctly call the update/create method in controllers/admin/books_controller.rb instead of the one in controllers/books_controller.rb
A final note
The controllers and the views are best kept separated but NOT the model which should always remain unique in your app.