What we want to achieve?

  • a simple spammer bot – just see how to connect to a jabber server in ruby and send messages to other users – using JabberSimple
  • add a capability to load contacts from a db – ActiveRecord with migrations outside of Rails
  • add callbacks to let the bot answer messages
  • provide ability to chat with a Pandora chatterbot
  • get down to work and create other callbacks!

get the initial ruby bot app skeleton

A simple spammer bot

Launch the irb console to try out JabberSimple:

irb
Now create a jabber client and set the bot’s status:
irb> require 'xmpp4r-simple'
=> true
irb> bot = Jabber::Simple.new('your jid','your pass',:chat,'Ave Ruby Hackers!')
=> Jabber::Simple 

If the first command fails – make sure you have Jabber::Simple installed, otherwise:

sudo gem install xmpp4r-simple

Now that it is connected, we can use the bot object to deliver a message to a custom contact:

irb> bot.deliver('some jid here','hello!')
=> Jabber::Contact 

That’s basically it. Let’s wrap it up in a class:

require 'rubygems'
require 'xmpp4r-simple'

class RubyBot 
  #Initializes the ruby bot with its jid and password.
  def initialize(jid, pass)
    @jid = jid
    @pass = pass
    connect()
  end

  #Connects the bot to the jabber server.
  def connect()
    @bot = Jabber::Simple.new(@jid, @pass, :chat, 'Ave Ruby Hackers!')
  end

  #Sends a message to a single recipient (given by jid).
  def send_msg(user, message)
    @bot.deliver(user,message)
  end

  #Broadcasts a message to recipients (given as an array of jids).
  def send_mass_msg(users, message)
    users.each do |user|
      @bot.deliver(user,message)
    end
  end
end

Save it as lib/ruby_bot.rb.

Several things going on here:
  • a constructor that accepts the bot’s credentials and connects it to the jabber server
  • a send_msg method that wraps up Jabber::Simple’s deliver method
  • an additional send_mass_msg method that allows to broadcast a message to multiple users

Now let’s add a script to run our bot:

$LOAD_PATH.push Dir.pwd+"/lib"
require 'ruby_bot'

#our bot's credentials
bot_jid = "some jid"
bot_pass = "some pass"

#start the bot
bot = RubyBot.new(bot_jid, bot_pass)

#send a message to a single contact
bot.send_msg("some contact","hello!")
Save it as bin/main.rb. To run it, enter the main ruby bot directory and type:
ruby bin/main.rb

To send mass messages we’d like to have a place to store the contact list. Let’s say we’d like to use a relational database for that purpuse, which provides a nice opportunity to demonstrate how to use ActiveRecord outside of rails :)

What we need is:
  • the database itself, in this example a mysql schema called ‘ruby_bot_development’
  • the database configuration file config/database.yml, which specifies the connection parameters (mind the indentation):
    development:
      adapter: mysql
      host: localhost
      port: 3306
      user: root
      password: 
      database: ruby_bot_development
  • a migration file that will create the users table—in db/migrate/001_create_users.rb
    class CreateUsers < ActiveRecord::Migration
      def self.up
        create_table :users do |t|
          t.column :jid, :string, :limit => 256, :null => false
          t.column :bot_jid, :string, :limit => 256, :null => false
        end
      end
    
      def self.down
        drop_table :users
      end
    end
  • a rake task to run the migration, should be specified in the Rakefile as:
    ENV['RAILS_ENV'] ||= 'development'
    require 'active_record'  
    require 'yaml'  
    
    task :default => "db:migrate"
    
    namespace :db do
      desc "Migrate the database through scripts in db/migrate. Target specific version with VERSION=x"  
      task :migrate => :environment do  
         ActiveRecord::Migrator.migrate('db/migrate', ENV['VERSION'] ? ENV['VERSION'].to_i : nil )  
      end  
    
      task :environment do  
         conf = YAML::load(File.open(File.dirname(__FILE__) + '/config/database.yml'))
         ActiveRecord::Base.establish_connection(conf[ENV['RAILS_ENV']])  
         ActiveRecord::Base.logger = Logger.new(File.open('log/active_record.log', 'a'))  
      end 
    end
    Note: the RAILS_ENV appears here just because :)
To run the migration enter the bot’s main directory and:
rake db:migrate

The users table should be now created in the ruby_bot_development database. The two columns are meant to store contact jids along with the bot’s jid, to allow for multiple bots having different contact lists.

Now to use the users data in our app we need a User model, a subclass of ActiveRecord. This one’s pretty straightforward:
class User < ActiveRecord::Base
end
Save it as models/user.rb.

We also need some code to establish a connection to the db and load the models. Let’s wrap it in a DataBase class:

require 'rubygems'
require 'active_record'
require 'yaml'
require 'logger'

class DataBase
  def self.connect(environment="development")
    conf = YAML::load(File.open(File.dirname(__FILE__) + '/../config/database.yml'))
    ActiveRecord::Base.logger = Logger.new(File.open(File.dirname(__FILE__) + '/../log/active_record.log', 1, 50000000))
    ActiveRecord::Base.establish_connection(conf[environment])
  end

  def self.load_models(path=File.dirname(__FILE__) + '/../models/*.rb')
    Dir.glob(path).each do |lib|
      require lib
    end
  end
end

Save it as lib/database.rb.

What it does:
  • loads the db config from config/database.yml and establishes a connection
  • loads all the available models, that is models/*.rb
Now to send mass messages:
require 'data_base'

#connect to db
DataBase.connect
DataBase.load_models

#load our bot's contacts
users = User.find(:all,{:bot_jid => bot_jid},:select => :jid).collect{|u| u.jid}

#send message to multiple users
bot.send_mass_msg(users,"#{'hello! ' * 3}")
This could be appended to bin/main.rb.

Bot callbacks

To be able to respond to callbacks we start with defining a sample behaviour of the bot. This goes into a class called Callbacks, which will include modules responsible for chat callbacks:
module Chat
  def on_message(message)
    p message

    begin
      if message.body
        sender = message.from
        message = message.body

        result = case message
        when /^foo$/
          #do sth about it
        when /^bar$/
          #...
        else 
          #some default behaviour
        end

        if @parent
          @parent.send_msg(sender, result) rescue Exception
        else
          puts result
        end
      end
      rescue Exception =>e
      p e
    end

    result || ""
  end
end

class Callbacks
  include Chat

  attr_accessor :chat_callbacks

  def initialize(parent=nil)
    @parent = parent   
    @chat_callbacks = Chat.instance_methods.collect do |name|
      self.method(name).to_proc if name=~/^on_/
    end.compact
    @status_callbacks = Status.instance_methods.collect do |name|
      self.method(name).to_proc if name=~/^on_/
    end.compact
  end  
end
Looking at the on_message method in the Chat module we can see the body of the received message is inspected and an appropriate action is taken. Let’s provide an action of an intelligent chatterbot, using one from pandora bots. Add a method in the chat module that looks as follows:
  def ask_question(question,custid)
    agent = WWW::Mechanize.new
    page = agent.post('http://www.pandorabots.com/pandora/talk-xml',
     {'botid'=>'bot id', 'custid' => custid, 'input'=>question})
    doc = Hpricot.XML(page.body)
    status = doc.search("//result").first.attributes['status'] 
    case status
    when "0"
      doc.search("//result/that").inner_html
    else
     'Help, this error occurred: ' + xmlDoc.elements["result/message"].get_text.value
    end 
  end

Note: you may use the bot id 8227ebaf7e369437 during the workshop.

What this method does is:
  • contacts the pandora bot, passing its id and the customer id – for which we can use the jid, and the question
  • parses the reply to extract the answer
Since we’re using Mechanize and Hpricot here to fetch the result and parse it, we need appropriate require statements as well:
require 'rubygems'
require 'mechanize'
require 'hpricot'

We can now call this method in on_message, for example to provide the default behaviour.

Now to bind this to our ruby bot, we need to add a method to set the bots callbacks in lib/ruby_bot.rb:
  #Sets up the ruby bot's callbacks.
  def setup_callbacks
    @cb = Callbacks.new(self)
    @cb.chat_callbacks.each do |cb|
      @bot.client.add_message_callback(0,&cb)
    end

    @cb.status_callbacks.each do |cb|
      @bot.client.add_presence_callback(0,&cb)
    end
  end
This new method needs to be called once the bot is instantiated. The bot also needs to sit in a loop waiting for messages. We can just add this bit to our bin/main.rb script:
bot.setup_callbacks

#wait for messages
loop{sleep(10)}

More callbacks!

get the final ruby bot app

Now it’s your turn. Some ideas of callbacks to implement:
  • notifications for new forum posts
  • dictionary lookups
  • currency converter
  • stock indices
  • weather forecast
  • recent bookmarks for a delicious tag
  • ...