Permalink
Browse files

Support Jabber presence, status and priority. Built-in 'help' command…

… accepts optional command argument. jabber method now a r/o attribute. master method changed to master?(jabber_id). Refactored add_command. More command parsing cleanup. RDoc cleanup.
  • Loading branch information...
1 parent 297ad33 commit 29753e4bbaef7e8a9802a22dcc0e5e5d6bdded0e @brettstimmerman committed Mar 25, 2007
Showing with 302 additions and 196 deletions.
  1. +8 −0 jabber-bot/HISTORY
  2. +12 −10 jabber-bot/README
  3. +9 −9 jabber-bot/Rakefile
  4. +264 −172 jabber-bot/lib/jabber/bot.rb
  5. +9 −5 jabber-bot/sample/sample.rb
View
8 jabber-bot/HISTORY
@@ -1,5 +1,13 @@
Jabber::Bot Release History
+Version 1.1.0 (24 March 2007)
+ * Supports Jabber presence, status message and priority.
+ * Built-in 'help' command now accepts an optional <command> argument.
+ * jabber method now a read-only attribute.
+ * master method changed to master?(jabber_id).
+ * More command parsing regex cleanup.
+ * RDoc cleanup.
+
Version 1.0.1 (21 March 2007)
* Fix debug puts remnant in parse_command.
* Fix message.sub regex during command callback in parse_command.
View
22 jabber-bot/README
@@ -2,13 +2,13 @@
Easily create powerful Jabber bots to do your bidding.
-Jabber::Bot makes it simple to create and command your own Jabber bot with
-little fuss. By adding custom commands powered by regular expressions to your
-bot's repertoire, you and your new bot will be able to accomplish nearly
+Jabber::Bot makes it simple to create and command your own Jabber bot with
+little fuss. By adding custom commands powered by regular expressions to your
+bot's repertoire, you and your new bot will be able to accomplish nearly
anything.
Author:: Brett Stimmerman (mailto:brettstimmerman@gmail.com)
-Version:: 1.0.1
+Version:: 1.1.0
Copyright:: Copyright (c) 2007 Brett Stimmerman. All rights reserved.
License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
Website:: http://socket7.net/software/jabber-bot
@@ -21,14 +21,16 @@ Website:: http://socket7.net/software/jabber-bot
== Basic Usage
# Create a public Jabber::Bot to do your bidding
- bot_config = {
+ config = {
+ :name => 'SampleBot'
:jabber_id => 'bot@example.com',
:password => 'password',
:master => 'master@example.com',
:is_public => true
}
- bot = Jabber::Bot.new(bot_config)
-
+
+ bot = Jabber::Bot.new(config)
+
# Give your bot a private command, 'rand'
bot.add_command(
:syntax => 'rand',
@@ -41,10 +43,10 @@ Website:: http://socket7.net/software/jabber-bot
:syntax => 'puts <string>',
:description => 'Write something to $stdout',
:regex => /^puts\s+.+$/,
- :aliases => [ :alias => 'p <string>', :regex => /^p\s+.+$/ ],
+ :alias => [ :alias => 'p <string>', :regex => /^p\s+.+$/ ],
:is_public => true
- ) do |message|
- puts message
+ ) do |sender, message|
+ puts "#{sender} says '#{message'"
"'#{message}' written to $stdout"
end
View
18 jabber-bot/Rakefile
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2007 Brett Stimmerman <brettstimmerman@socket7.net>
+# Copyright (c) 2007 Brett Stimmerman <brettstimmerman@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
@@ -35,7 +35,7 @@ require 'rake/rdoctask'
spec = Gem::Specification.new do |s|
s.name = 'jabber-bot'
- s.version = '1.0.1'
+ s.version = '1.1.0'
s.author = 'Brett Stimmerman'
s.email = 'brettstimmerman@gmail.com'
s.homepage = 'http://socket7.net/software/jabber-bot'
@@ -44,22 +44,22 @@ spec = Gem::Specification.new do |s|
"Jabber bot with little fuss. By adding custom commands " +
"powered by regular expressions to your bot's repertoire, you " +
"and your new bot will be able to accomplish nearly anything."
-
+
s.rubyforge_project = 'jabber-bot'
-
- s.files = FileList['lib/**/*', 'LICENSE', 'README',
- 'HISTORY'].exclude('rdoc').to_a
+
+ s.files = FileList['lib/**/*', 'LICENSE', 'README',
+ 'HISTORY'].exclude('rdoc').to_a
s.require_path = 'lib'
-
+
s.has_rdoc = true
s.extra_rdoc_files = ['README', 'LICENSE', 'HISTORY']
s.rdoc_options << '--title' << 'Jabber::Bot Documentation' <<
'--main' << 'README' <<
'--line-numbers'
-
+
s.required_ruby_version = '>=1.8.4'
-
+
s.add_dependency('xmpp4r-simple', '>=0.8.7')
end
View
436 jabber-bot/lib/jabber/bot.rb
@@ -30,131 +30,142 @@
require 'xmpp4r-simple'
module Jabber
-
+
# = Jabber::Bot
#
# Jabber::Bot makes it simple to create and command a Jabber bot with little
# fuss. By adding custom commands powered by regular expressions to your bot's
# repertoire, you and your new bot will be able to accomplish nearly anything.
#
# Author:: Brett Stimmerman (mailto:brettstimmerman@gmail.com)
- # Version:: 1.0.1
+ # Version:: 1.1.0
# Copyright:: Copyright (c) 2007 Brett Stimmerman. All rights reserved.
# License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
# Website:: http://socket7.net/software/jabber-bot
#
class Bot
-
- # Creates a new Jabber::Bot object with the specified _name_, _jabber_id_
- # and _password_. If _name_ is omitted, _jabber_id_ is used. The bot will
- # respond to commands from one or more masters specified by _master_.
- # You may choose to restrict a Jabber::Bot to listen only to its master(s),
- # or make it a public bot that will listen to anyone.
+
+ # Direct access to the underlying
+ # Jabber::Simple[http://xmpp4r-simple.rubyforge.org/] object.
+ attr_reader :jabber
+
+ # Creates a new Jabber::Bot object with the specified +config+ Hash, which
+ # must contain +jabber_id+, +password+, and +master+ at a minimum.
+ #
+ # You may optionally give your bot a custom +name+. If +name+ is omitted,
+ # the username portion of +jabber_id+ is used instead.
+ #
+ # You may choose to restrict a Jabber::Bot to listen only to its master(s),
+ # or make it +public+.
#
- # By default, a Jabber::Bot has only a single command, 'help', which will
- # display the full list of commands available in the bot's repertoire.
+ # You may optionally specify a Jabber +presence+, +status+, and +priority+.
+ # If omitted, they each default to +nil+.
#
- # If you choose to make a public bot only the commands you specify as
- # public commands, as well as the default 'help' command, will be publicly
- # executable.
+ # By default, a Jabber::Bot has only a single command, 'help [<command>]',
+ # which displays a help message for the specified command, or all commands
+ # if <command> is omitted.
#
- # # A private bot with a single master
+ # If you choose to make a public bot, only the commands you specify as
+ # public, as well as the default 'help' command, will be public.
+ #
+ # # A minimally confiugured private bot with a single master.
# bot = Jabber::Bot.new(
- # :name => 'PrivateBot',
- # :jabber_id => 'bot@example.com',
- # :password => 'password',
- # :master => 'master@example.com'
+ # :jabber_id => 'bot@example.com',
+ # :password => 'password',
+ # :master => 'master@example.com'
# )
#
- # # A public bot with mutliple masters
- # masters = ['master1@example.com', 'master2@example.com]
+ # # A highly configured public bot with a custom name, mutliple masters,
+ # # Jabber presence, status, and priority.
+ # masters = ['master1@example.com', 'master2@example.com']
+ #
# bot = Jabber::Bot.new(
- # :name => 'PublicBot',
- # :jabber_id => 'bot@example.com',
- # :password => 'password',
- # :master => masters,
- # :is_public => true
+ # :name => 'PublicBot',
+ # :jabber_id => 'bot@example.com',
+ # :password => 'password',
+ # :master => masters,
+ # :is_public => true,
+ # :presence => :chat,
+ # :priority => 5,
+ # :status => 'Hello, I am PublicBot.'
# )
#
- def initialize(bot_config)
-
- if bot_config[:jabber_id].nil?
- abort 'You must specify a :jabber_id'
- elsif bot_config[:password].nil?
- abort 'You must specify a :password'
- elsif bot_config[:master].nil? or bot_config[:master].length == 0
- abort 'You must specify at least one :master'
- end
-
- @bot_config = bot_config
-
- @bot_config[:is_public] = false if @bot_config[:is_public].nil?
+ def initialize(config)
+ @config = config
- if @bot_config[:name].nil? or @bot_config[:name].length == 0
- @bot_config[:name] = @bot_config[:jabber_id].sub(/@.+$/, '')
+ @config[:is_public] ||= false
+
+ if @config[:name].nil? or @config[:name].length == 0
+ @config[:name] = @config[:jabber_id].sub(/@.+$/, '')
end
-
- unless bot_config[:master].is_a?(Array)
- @bot_config[:master] = [bot_config[:master]]
+
+ unless @config[:master].is_a?(Array)
+ @config[:master] = [@config[:master]]
end
-
+
@commands = { :spec => [], :meta => {} }
-
+
add_command(
- :syntax => 'help',
- :description => 'Display this help message',
- :regex => /^help$/,
- :alias => [ :syntax => '?', :regex => /^\?$/ ],
- :is_public => @bot_config[:is_public]
- ) { |sender, message| help_message(sender) }
+ :syntax => 'help [<command>]',
+ :description => 'Display help for the given command, or all commands' +
+ ' if no command is specified',
+ :regex => /^help(\s+?.+?)?$/,
+ :alias => [ :syntax => '? [<command>]', :regex => /^\?(\s+?.+?)?$/ ],
+ :is_public => @config[:is_public]
+ ) { |sender, message| help_message(sender, message) }
end
-
+
# Add a command to the bot's repertoire.
#
# Commands consist of a metadata Hash and a callback block. The metadata
- # Hash *must* contain the command syntax and a description of the command
- # for display with the builtin 'help' command, and a regular expression to
- # detect the presence of the command in an incoming message.
+ # Hash *must* contain the command +syntax+, a +description+ for display with
+ # the builtin 'help' command, and a regular expression (+regex+) to detect
+ # the presence of the command in an incoming message.
#
- # The metadata Hash may optionally contain an array of command aliases. An
- # alias consists of an alias syntax and regex. Aliases allow the bot to
- # understand command shorthands. For example, the default 'help' command has
- # an alias '?'. Saying either 'help' or '?' will trigger the same command
- # callback block.
+ # The metadata Hash may optionally contain an array of command alias Hashes.
+ # An alias consists of an alias +syntax+ and +regex+. Aliases allow the bot
+ # to understand command shorthands. For example, the default 'help' command
+ # has an alias '?'. Saying either 'help' or '?' will trigger the same
+ # command callback block.
#
- # The metadata Hash may optionally contain an is_public flag, indicating
+ # The metadata Hash may optionally contain a +is_public+ flag, indicating
# the bot should respond to *anyone* issuing the command, not just the bot
- # master(s). Public commands are only truly public if the bot itself has
+ # master(s). Public commands are only truly public if the bot itself has
# been made public.
#
- # The specified callback block will be triggered when the bot receives a
- # message that matches the given command regex (or an alias regex). The
- # callback block will have access to the sender and the message text (not
- # including the command), and should either return a String response or
- # _nil_. If a callback block returns a String response, the response will be
- # delivered to the bot master that issued the command.
- #
+ # The specified callback block will be triggered when the bot receives a
+ # message that matches the given command regex (or an alias regex). The
+ # callback block will have access to the sender and the message text (not
+ # including the command itsef), and should either return a String response
+ # or +nil+. If a callback block returns a String response, the response will
+ # be delivered to the Jabber id that issued the command.
+ #
# Examples:
#
- # # Say "puts foo" to the bot and "foo" will be written to $stdout.
+ # # Say 'puts foo' or 'p foo' and 'foo' will be written to $stdout.
# # The bot will also respond with "'foo' written to $stdout."
# add_command(
# :syntax => 'puts <string>',
# :description => 'Write something to $stdout',
- # :regex => /^puts\s+.+$/
- # ) do |message|
- # puts message
+ # :regex => /^puts\s+.+$/,
+ # :alias => [ :syntax => 'p <string>', :regex => /^p\s+.+$/ ]
+ # ) do |sender, message|
+ # puts "#{sender} says #{message}."
# "'#{message}' written to $stdout."
# end
#
- # # "puts!" is a non-responding version of "puts", and has an alias, "p!"
+ # # 'puts!' is a non-responding version of 'puts', and has two aliases,
+ # # 'p!' and '!'
# add_command(
# :syntax => 'puts! <string>',
# :description => 'Write something to $stdout (without response)',
# :regex => /^puts!\s+.+$/,
- # :alias => [ :syntax => 'p! <string>', :regex => /^p!$/ ]
- # ) do |message|
- # puts message
+ # :alias => [
+ # { :syntax => 'p! <string>', :regex => /^p!\s+.+$/ },
+ # { :syntax => '! <string>', :regex => /^!\s+/.+$/ }
+ # ]
+ # ) do |sender, message|
+ # puts "#{sender} says #{message}."
# nil
# end
#
@@ -167,49 +178,32 @@ def initialize(bot_config)
# ) { rand(10).to_s }
#
def add_command(command, &callback)
- syntax = command[:syntax]
- is_public = command[:is_public] || false
-
- # Add the command meta. Using a Hash allows for Hash.sort to list
- # commands aphabetically in the 'help' command response.
- @commands[:meta][syntax] = {
- :syntax => [syntax],
- :description => command[:description],
- :is_public => is_public
- }
-
- # Add the command spec. The command spec is used by parse_command.
- @commands[:spec] << {
- :regex => command[:regex],
- :callback => callback,
- :is_public => is_public
- }
-
+ name = command_name(command[:syntax])
+
+ # Add the command meta - used in the 'help' command response.
+ add_command_meta(name, command)
+
+ # Add the command spec - used for parsing incoming commands.
+ add_command_spec(command, callback)
+
# Add any command aliases to the command meta and spec
unless command[:alias].nil?
- command[:alias].each do |a|
- @commands[:meta][syntax][:syntax] << a[:syntax]
-
- @commands[:spec] << {
- :regex => a[:regex],
- :callback => callback,
- :is_public => is_public
- }
- end
+ command[:alias].each { |a| add_command_alias(name, a, callback) }
end
end
-
- # Connect the bot, making him available to accept commands.
+
+ # Connect the bot, making it available to accept commands.
def connect
- @jabber = Jabber::Simple.new(@bot_config[:jabber_id],
- @bot_config[:password])
-
- deliver(@bot_config[:master], "#{@bot_config[:name]} reporting for duty.")
-
- start_listener_thread
+ @jabber = Jabber::Simple.new(@config[:jabber_id], @config[:password])
+
+ presence(@config[:presence], @config[:status], @config[:priority])
+
+ deliver(@config[:master], "#{@config[:name]} reporting for duty.")
+
+ start_listener_thread
end
-
- # Deliver a message to the specified recipient(s). Accepts a single
+
+ # Deliver a message to the specified recipient(s). Accepts a single
# recipient or an Array of recipients.
def deliver(to, message)
if to.is_a?(Array)
@@ -218,106 +212,204 @@ def deliver(to, message)
@jabber.deliver(to, message)
end
end
-
- # Returns the default help message describing the bot's command repertoire.
- # Commands are sorted alphabetically by name.
- def help_message(sender) #:nodoc:
- help_message = "I understand the following commands:\n\n"
-
- is_master = @bot_config[:master].include?(sender)
-
- @commands[:meta].sort.each do |command|
- if command[1][:is_public] == true || is_master
- command[1][:syntax].each { |syntax| help_message += "#{syntax}\n" }
- help_message += " #{command[1][:description]}\n\n"
- end
+
+ # Disconnect the bot. Once the bot has been disconnected, there is no way
+ # to restart it by issuing a command.
+ def disconnect
+ if @jabber.connected?
+ deliver(@config[:master], "#{@config[:name]} disconnecting...")
+ @jabber.disconnect
end
-
- return help_message
end
-
- # Direct access to the underlying
- # Jabber::Simple[http://xmpp4r-simple.rubyforge.org/] object.
- def jabber
- return @jabber
+
+ # Returns +true+ if the given Jabber id is a master, +false+ otherwise.
+ def master?(jabber_id)
+ @config[:master].include? jabber_id
+ end
+
+ # Sets the bot presence, status message and priority.
+ def presence(presence=nil, status=nil, priority=nil)
+ @config[:presence] = presence
+ @config[:status] = status
+ @config[:priority] = priority
+
+ status_message = Presence.new(presence, status, priority)
+ @jabber.send!(status_message) if @jabber.connected?
+ end
+
+ # Sets the bot presence. If you need to set more than just the presence,
+ # use presence() instead.
+ #
+ # Available values for presence are:
+ #
+ # * nil : online
+ # * :chat : free for chat
+ # * :away : away from the computer
+ # * :dnd : do not disturb
+ # * :xa : extended away
+ #
+ def presence=(presence)
+ presence(presence, @config[:status], @config[:priority])
+ end
+
+ # Set the bot priority. Priority is an integer from -127 to 127. If you need
+ # to set more than just the priority, use presence() instead.
+ def priority=(priority)
+ presence(@config[:presence], @config[:status], priority)
+ end
+
+ # Set the status message. A status message is just a String, e.g. 'I am
+ # here.' or 'Out to lunch.' If you need to set more than just the status
+ # message, use presence() instead.
+ def status=(status)
+ presence(@config[:presence], status, @config[:priority])
+ end
+
+ private
+
+ # Add a command alias for the given original +command_name+
+ def add_command_alias(command_name, alias_command, callback) #:nodoc:
+ original_command = @commands[:meta][command_name]
+ original_command[:syntax] << alias_command[:syntax]
+
+ alias_name = command_name(alias_command[:syntax])
+
+ alias_command[:is_public] = original_command[:is_public]
+
+ add_command_meta(alias_name, original_command, true)
+ add_command_spec(alias_command, callback)
+ end
+
+ # Add a command meta
+ def add_command_meta(name, command, is_alias=false) #:nodoc:
+ syntax = command[:syntax]
+
+ @commands[:meta][name] = {
+ :syntax => syntax.is_a?(Array) ? syntax : [syntax],
+ :description => command[:description],
+ :is_public => command[:is_public] || false,
+ :is_alias => is_alias
+ }
+ end
+
+ # Add a command spec
+ def add_command_spec(command, callback) #:nodoc:
+ @commands[:spec] << {
+ :regex => command[:regex],
+ :callback => callback,
+ :is_public => command[:is_public] || false
+ }
end
-
- # Access the bot master jabber id(s), as an Array
- def master
- return @bot_config[:master]
+
+ # Extract the command name from the given syntax
+ def command_name(syntax) #:nodoc:
+ if syntax.include? ' '
+ name = syntax.sub(/^(\S+).*/, '\1')
+ else
+ name = syntax
+ end
+
+ name
end
-
- # Parses the given command message for the presence of a known command by
+
+ # Returns the default help message describing the bot's command repertoire.
+ # Commands are sorted alphabetically by name, and are displayed according
+ # to the bot's and the commands's _public_ attribute.
+ def help_message(sender, command_name) #:nodoc:
+ if command_name.nil? or command_name.length == 0
+ # Display help for all commands
+ help_message = "I understand the following commands:\n\n"
+
+ @commands[:meta].sort.each do |command|
+ # Thank you, Hash.sort
+ command = command[1]
+
+ if !command[:is_alias] and (command[:is_public] or master? sender)
+ command[:syntax].each { |syntax| help_message += "#{syntax}\n" }
+ help_message += " #{command[:description]}\n\n"
+ end
+ end
+ else
+ # Display help for the given command
+ command = @commands[:meta][command_name]
+
+ if command.nil?
+ help_message = "I don't understand '#{command_name}' Try saying" +
+ " 'help' to see what commands I understand."
+ else
+ help_message = ''
+ command[:syntax].each { |syntax| help_message += "#{syntax}\n" }
+ help_message += " #{command[:description]} "
+ end
+ end
+
+ help_message
+ end
+
+ # Parses the given command message for the presence of a known command by
# testing it against each known command's regex. If a known command is
- # found, the command parameters are passed on to the callback block, minus
+ # found, the command parameters are passed on to the callback block, minus
# the command trigger. If a String result is present it is delivered to the
# sender.
#
# If the bot has not been made public, commands from anyone other than the
# bot master(s) will be silently ignored.
def parse_command(sender, message) #:nodoc:
- is_master = @bot_config[:master].include?(sender)
-
- if @bot_config[:is_public] or is_master
-
+ is_master = master? sender
+
+ if @config[:is_public] or is_master
@commands[:spec].each do |command|
if command[:is_public] or is_master
unless (message.strip =~ command[:regex]).nil?
- response = command[:callback].call(sender,
- message.sub(/^.[^\s]+\s+/, ''))
-
+ params = nil
+
+ if message.include? ' '
+ params = message.sub(/^\S+\s+(.*)$/, '\1')
+ end
+
+ response = command[:callback].call(sender, params)
deliver(sender, response) unless response.nil?
return
end
end
end
-
- response = "I don't understand '#{message.strip}.' Try saying 'help' " +
+
+ response = "I don't understand '#{message.strip}' Try saying 'help' " +
"to see what commands I understand."
- deliver(sender, response)
-
+ deliver(sender, response)
end
end
-
- # Disconnect the bot. Once the bot has been disconnected, there is no way
- # to restart it by issuing a command.
- def disconnect
- if @jabber.connected?
- deliver(@bot_config[:master], "#{@bot_config[:name]} disconnecting...")
- @jabber.disconnect
- end
- end
-
+
# Creates a new Thread dedicated to listening for incoming chat messages.
- # When a chat message is received, the bot checks if the sender is its
- # master. If so, it is tested for the presence commands, and processed
+ # When a chat message is received, the bot checks if the sender is its
+ # master. If so, it is tested for the presence commands, and processed
# accordingly. If the bot itself or the command issued is not made public,
# a message sent by anyone other than the bot's master is silently ignored.
#
- # Only the chat message type is supported. Other message types such as
+ # Only the chat message type is supported. Other message types such as
# error and groupchat are not supported.
def start_listener_thread #:nodoc:
listener_thread = Thread.new do
loop do
@jabber.received_messages do |message|
# Remove the Jabber resourse, if any
sender = message.from.to_s.sub(/\/.+$/, '')
-
- if message.type == :chat
- parse_thread = Thread.new do
+
+ if message.type == :chat
+ parse_thread = Thread.new do
parse_command(sender, message.body)
end
-
+
parse_thread.join
end
end
-
+
sleep 1
end
end
-
+
listener_thread.join
end
-
+
end
end
View
14 jabber-bot/sample/sample.rb
@@ -32,23 +32,27 @@
require 'jabber/bot'
# Configure a public bot
-bot_config = {
+config = {
+ :name =? 'PublicBot',
:jabber_id => 'bot@example.com',
:password => 'password',
:master => 'master@example.com',
- :is_public => true
+ :is_public => true,
+ :status => 'Hello, I am PublicBot.',
+ :presence => :chat,
+ :priority => 10
}
# Create a new bot
-bot = Jabber::Bot.new(bot_config)
+bot = Jabber::Bot.new(config)
# Give the bot a private command, 'puts', with a response message
bot.add_command(
:syntax => 'puts <string>',
:description => 'Write something to $stdout',
:regex => /^puts\s+.+$/
) do |sender, message|
- puts message
+ puts "#{sender} says '#{message}'"
"'#{message}' written to $stdout"
end
@@ -68,7 +72,7 @@
:description => 'Produce a random number from 0 to 10',
:regex => /^rand$/,
:alias => [:syntax => 'r', :regex => /^r$/],
- :is_public => true
+ :is_public => true
) { rand(10).to_s }
# Unleash the bot

0 comments on commit 29753e4

Please sign in to comment.