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:
irbirb> 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-simpleNow 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_msgmethod that wraps up Jabber::Simple’s deliver method - an additional
send_mass_msgmethod 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!")ruby bin/main.rbTo 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
userstable—in db/migrate/001_create_users.rbclass 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:
Note: the RAILS_ENV appears here just because :)
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
rake db:migrateThe 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
endWe 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
endSave 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
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}")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
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
endNote: 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
require 'rubygems'
require 'mechanize'
require 'hpricot'We can now call this method in on_message, for example to provide the default behaviour.
#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
endbot.setup_callbacks
#wait for messages
loop{sleep(10)}More callbacks!
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
- ...
