Topic: HOWTO: Send Instant Messages from Rails
UPDATED for BackgrounDRb 0.2.1
In this tutorial we are going to use a BackgrounDRb worker to hold an IM client. Our Rails app will then use BackgrounDRb to tell the IM client to send instant messages.
To help put this example in the context of a real-world application, let's imagine that we are working on a newspaper website. Whenever a new article is created we want to notify readers by sending them an Instant Message.
Let's roll
Let's get started by creating a small Rails application. I'm assuming you are familiar with using scaffolding to build CRUD functionality so we are going to go through this part quickly.
1) Create a Rails application called dailynews.
saturn:~ bp$ rails dailynews
[...]
saturn:~ bp$ cd dailynews
2) Create a database and modify config/database.yml appropriately.
saturn:~/dailynews bp$ echo "create database dailynews" | mysql -u root -p
3) Build the Article model and edit the migration created in the last line of output.
saturn:~/dailynews bp$ ./script/generate model Article
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/article.rb
create test/unit/article_test.rb
create test/fixtures/articles.yml
create db/migrate
create db/migrate/001_create_articles.rb
Let's keep it simple and only give an Article a title and a body. db/migrate/001_create_articles.rb:
class CreateArticles < ActiveRecord::Migration
def self.up
create_table :articles do |t|
t.column :title, :string
t.column :body, :text
end
enddef self.down
drop_table :articles
end
end
4) Migrate to build the articles table.
saturn:~/dailynews bp$ rake db:migrate
(in /Users/bp/dailynews)
== CreateArticles: migrating==================================================
-- create_table(:articles)
-> 0.1340s
== CreateArticles: migrated (0.1357s)=========================================
6) Generate scaffolding to get some CRUD goodness going.
saturn:~/dailynews bp$ ./script/generate scaffold Article
7) Start the server and check to see that things are working.
saturn:~/dailynews bp$ ./script/server
If everything went as planned we should be able go to http://localhost:3000/articles/new and create our first article. Maybe something about the weather?
Instant Message Me!
Ok, now for some IM fun. XMPP4R is a Ruby XMPP/Jabber library. Grab it and install. I'm using xmpp4r-0.3, which you can grab here: http://download.gna.org/xmpp4r/xmpp4r-0.3.tgz Installation is straightforward. Just unpack the archive and run install.rb. Like this (output removed for brevity):
saturn:~/dailynews bp$ cd /tmpsaturn:/tmp bp$ wget http://download.gna.org/xmpp4r/xmpp4r-0.3.tgz
saturn:/tmp bp$ tar -zxvf xmpp4r-0.3.tgz
saturn:/tmp bp$ cd xmpp4r-0.3
saturn:/tmp/xmpp4r-0.3 bp$ sudo ./setup.rb
A quick test with irb to make sure we have it installed.
saturn:/tmp/xmpp4r-0.3 bp$ irb
irb(main):001:0> require 'XMPP4R'
=> true
irb(main):002:0> quit
The line that says "=> true" means we have it. Back to our Rails app to start sending IMs.
saturn:/tmp/xmpp4r-0.3 bp$ cd ~/dailynews/
We are going to need two IM accounts (that speak XMPP/Jabber) to make this work. You could probably do it with one, but that gets a little confusing. If you have a Gmail account you already have a Google Talk account (http://www.google.com/talk/). I got another by registering an account on xmpp.us. I wasn't able to get iChat to register the new account. For that I used Fire (http://fire.sourceforge.net/). Don't worry about following the same steps I did to get your accounts setup. The important part is that you have two accounts.
The Setup
I'm on OSX using iChat to connect to my Google Talk account. The Rails app will use then use the xmpp.us account to send messages to my Google Talk account. To help keep things clear let's call the account Rails will use
rails@xmpp.us and the other human@google.com (Someone might actually own these accounts so please don't use them when testing your code).
Since we want to send IMs when a new article is created, let's define an after_create method in the Article model. This is article.rb:
require 'XMPP4R'
class Article < ActiveRecord::Base
def after_create
# Connect to the server and set presence to available for chat.
jid = Jabber::JID.new('rails@xmpp.us/dailynews')
client = Jabber::Client.new(jid, false)
client.connect
client.auth('railsxmpppassword')
client.send(Jabber::Presence.new.set_show(:chat).set_status('Rails!'))
# Send an Instant Message.
body = 'Hello from Rails'
to_jid = Jabber::JID.new('human@gmail.com')
message = Jabber::Message::new(to_jid, body).set_type(:normal).set_id('1')
client.send(message)
end
end
The first line imports the XMPP4R library that we then use to send the IM. In after_create we connect to the server, authenticate ourselves, set our presence and then send a message. Now when you create a new article you should get an IM saying "Hello from Rails." Check out the XMPP4R website for more info on the library and rdoc documentation.
Mission Accomplished?
Well, we did send an IM from our Rails application, so yes. But there is something about it that doesn't feel quite right. It seems cumbersome to have to login to the rails@xmpp.us account every time we want to send a message. Instead we ought to login and stay logged in between requests. Then we can fire off IMs easily. Enter BackgrounDRb.
If you haven't heard of BackgrounDRb, it is a plugin that "facilitates running background tasks in a separate process from Rails, thereby decoupling them from the request/response cycle." Awesome! BackgounDRb is typically used for long-running tasks that eventually finish, like uploading a large file. But what about a really really long task? Like one that never finishes? Like an IM client!
First things first. We need to install the BackgrounDRb plugin and get it set up. (NOTE: Requires Slave 1.1.0 or higher and Daemons 1.0.2 or higher.) I've trimmed all of the output except the last line to show which revision I'm using:
saturn:~/dailynews bp$ cd vendor/plugins/
saturn:~/dailynews/vendor/plugins bp$ svn co http://svn.devjavu.com/backgroundrb/tags/release-0.2.1 backgroundrb
[...]
Checked out revision 165.
saturn:~/dailynews/vendor/plugins bp$ cd ~/dailynews/
We set it up with a rake task which copies files where they need to be.
saturn:~/dailynews bp$ rake backgroundrb:setup
(in /Users/bp/dailynews)
Copying backgroundrb.yml config file to /Users/bp/Sites/examples/newsbot/config/../config/backgroundrb.yml
Copying backgroundrb script to /Users/bp/Sites/examples/newsbot/config/../script/backgroundrb
Take a look at config/backgroundrb.yml. I left everything as is. If you are running a firewall be sure that port 2000 is open or change the port to something else that is.
Now we will create a BackgrounDRb worker that will house our IM client. There is a generator that will create the necessary files. Let's call it Alert because it will eventually be sending news alerts.
saturn:~/dailynews bp$ ./script/generate worker Alert
exists lib/workers/
create lib/workers/alert_worker.rb
BackgrounDRb workers come ready-made with a do_work method. This method is called when you instantiate a new worker instance from Rails. We'll use this method to login to the rails@xmpp.us account.
To send the IMs we'll use another method, send_alert, that will take the title of an Article as an argument.
require 'XMPP4R'
class AlertWorker < BackgrounDRb::Worker::RailsBase
def do_work(args)
jid = Jabber::JID.new('rails@xmpp.us/dailynews')
@client = Jabber::Client.new(jid, false)
@client.connect
@client.auth('railsxmpppassword')
@client.send(Jabber::Presence.new.set_show(:chat).set_status('BackgrounDRb!'))
loop do
@client.process
sleep(1)
end
enddef send_alert(title)
to_jid = Jabber::JID.new('human@gmail.com')
message = Jabber::Message::new(to_jid, title).set_type(:normal).set_id('1')
@client.send(message)
endend
AlertWorker.register
This is very similar to the way we sent an IM in the after_create method. The main difference is that we are breaking the connection and sending an IM into two parts. In the do_work method we use an instance variable to hold the client. That allows the send_alert method to use it later on.
We only want one BackgrounDRb worker for our Rails application and we'll do this by setting the job_key when we instantiate the worker for the first time and then using the same job_key to access the worker later. Since we want this worker to be running all the time, we'll instantiate it in app/controllers/application.rb.
class ApplicationController < ActionController::Base
unless MiddleMan[:alerter]
MiddleMan.new_worker(:class => :alert_worker, :job_key => :alerter)
end
end
MiddleMan is the bridge between Rails and BackgounDRb. Here we use it to instantiate a new worker. Later on we will use it to access the same worker.
Start Your Engines
This part is a little tricky. Be sure that you start your Rails server, then BackgrounDRb, and then hit the web page. Like so:
saturn:~/dailynews bp$ ./script/server
Then from a different console:
saturn:~/dailynews bp$ ./script/backgroundrb start
Now hit a page like http://localhost:3000/articles/. The worker should now be running in BackgrounDRb and rails@xmpp.us should be logged in.
If you are having any trouble with BackgrounDRb keep in mind that any change you make to a worker will not take effect until you restart BackgrounDRb. Another thing to do if you are getting strange behavior is stop all ruby processes and start them up again. I had an errant process that caused me some "I swear I changed the right file" confusion.
Send It!
We are so very close now! The final piece is to change the after_create method in the Article model to send an IM via the worker.
def after_create
MiddleMan.worker(:alerter).send_alert(self.title)
end
Now create a new article and, if everything works and the stars are aligned, you should get an IM containing the article title. Yay!
I hope you had fun with this and I encourage you to play around and experiment. What else could we do with this? I'm wondering about communication going the other direction. Possibly send commands to your Rails app from iChat? What else?
References
http://backgroundrb.rubyforge.org/
http://www.infoq.com/articles/BackgrounDRb
http://home.gna.org/xmpp4r/
Last edited by bp (2006-12-11 00:22:34)