summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Gilbert <tom@linuxbrit.co.uk>2005-07-26 21:50:00 +0000
committerTom Gilbert <tom@linuxbrit.co.uk>2005-07-26 21:50:00 +0000
commit5d5d9df1a4825fad5ef045cfc0b21b16e5e2bcc7 (patch)
treef3814abafc8f1d6c589a29f4ddc89f55e510d493
parent3ba6917c904f5e664ae78b146f4e394ad805eb96 (diff)
* Prevent multiple plugin registrations of the same name
* reworking the config system to use yaml for persistence * reworking the config system key names * on first startup, the bot will prompt for the essential startup config * new config module for configuring the bot at runtime * new config module includes new configurables, for example changing the bot's language at runtime. * various other fixes * New way of mapping plugins to strings, using maps. These may be familiar to rails users. This is to reduce the amount of regexps plugins currently need to do to parse arguments. The old method (privmsg) is still supported, of course. Example plugin now: def MyPlugin < Plugin def foo(m, params) m.reply "bar" end def complexfoo(m, params) m.reply "qux! (#{params[:bar]} #{params[:baz]})" end end plugin = MyPlugin.new # simple map plugin.map 'foo' # this will match "rbot: foo somestring otherstring" and pass the # parameters as a hash using the names in the map. plugin.map 'foo :bar :baz', :action => 'complexfoo' # this means :foo is an optional parameter plugin.map 'foo :foo', :defaults => {:foo => 'bar'} # you can also gobble up into an array plugin.map 'foo *bar' # params[:bar] will be an array of string elements # and you can validate, here the first param must be a number plugin.map 'foo :bar', :requirements => {:foo => /^\d+$/}
-rw-r--r--ChangeLog45
-rwxr-xr-xrbot.rb4
-rw-r--r--rbot/auth.rb2
-rw-r--r--rbot/config.rb205
-rw-r--r--rbot/ircbot.rb64
-rw-r--r--rbot/ircsocket.rb56
-rw-r--r--rbot/keywords.rb4
-rw-r--r--rbot/message.rb9
-rw-r--r--rbot/messagemapper.rb158
-rw-r--r--rbot/plugins.rb66
-rw-r--r--rbot/plugins/autoop.rb4
-rw-r--r--rbot/plugins/cal.rb9
-rw-r--r--rbot/plugins/eightball.rb5
-rw-r--r--rbot/plugins/karma.rb93
-rw-r--r--rbot/plugins/lart.rb8
-rw-r--r--rbot/plugins/nickserv.rb10
-rw-r--r--rbot/plugins/opmeh.rb1
-rw-r--r--rbot/plugins/quotes.rb4
-rw-r--r--rbot/plugins/remind.rb2
-rw-r--r--rbot/plugins/roulette.rb2
20 files changed, 596 insertions, 155 deletions
diff --git a/ChangeLog b/ChangeLog
index 1c4dcd58..30d2390c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,48 @@
+Tue Jul 26 14:41:34 BST 2005 Tom Gilbert <tom@linuxbrit.co.uk>
+
+ * Prevent multiple plugin registrations of the same name
+ * reworking the config system to use yaml for persistence
+ * reworking the config system key names
+ * on first startup, the bot will prompt for the essential startup config
+ * new config module for configuring the bot at runtime
+ * new config module includes new configurables, for example changing the
+ bot's language at runtime.
+ * various other fixes
+ * New way of mapping plugins to strings, using maps. These may be
+ familiar to rails users. This is to reduce the amount of regexps plugins
+ currently need to do to parse arguments. The old method (privmsg) is still
+ supported, of course. Example plugin now:
+ def MyPlugin < Plugin
+ def foo(m, params)
+ m.reply "bar"
+ end
+
+ def complexfoo(m, params)
+ m.reply "qux! (#{params[:bar]} #{params[:baz]})"
+ end
+ end
+ plugin = MyPlugin.new
+ # simple map
+ plugin.map 'foo'
+
+ # this will match "rbot: foo somestring otherstring" and pass the
+ # parameters as a hash using the names in the map.
+ plugin.map 'foo :bar :baz', :action => 'complexfoo'
+ # this means :foo is an optional parameter
+ plugin.map 'foo :foo', :defaults => {:foo => 'bar'}
+ # you can also gobble up into an array
+ plugin.map 'foo *bar' # params[:bar] will be an array of string elements
+ # and you can validate, here the first param must be a number
+ plugin.map 'foo :bar', :requirements => {:foo => /^\d+$/}
+
+
+Sat Jul 23 01:39:08 BST 2005 Tom Gilbert <tom@linuxbrit.co.uk>
+
+ * Changed BotConfig to use yaml storage, method syntax instead of hash for
+ get/set, to allow more flexibility and encapsulation
+ * Added convenience method Message.okay (m.okay is the same as the
+ old-style @bot.okay m.replyto)
+
Wed Jul 20 23:30:01 BST 2005 Tom Gilbert <tom@linuxbrit.co.uk>
* Move some core plugins to use the new httputil
diff --git a/rbot.rb b/rbot.rb
index 4d13a0d0..60867daf 100755
--- a/rbot.rb
+++ b/rbot.rb
@@ -49,6 +49,10 @@ opts.each {|opt, arg|
botclass = ARGV.shift
botclass = "rbotconf" unless(botclass);
+unless FileTest.directory? botclass
+ # TODO copy in samples/templates from install directory
+end
+
if(bot = Irc::IrcBot.new(botclass))
if($opts["help"])
puts bot.help($opts["help"])
diff --git a/rbot/auth.rb b/rbot/auth.rb
index 017745ab..7811d9e4 100644
--- a/rbot/auth.rb
+++ b/rbot/auth.rb
@@ -182,7 +182,7 @@ module Irc
m.reply "user #$1 is gone"
end
when (/^auth\s+(\S+)/)
- if($1 == @bot.config["PASSWD"])
+ if($1 == @bot.config["auth.password"])
@bot.auth.useradd(Regexp.escape(m.source), 1000)
m.reply "Identified, security level maxed out"
else
diff --git a/rbot/config.rb b/rbot/config.rb
index 52899205..971a413c 100644
--- a/rbot/config.rb
+++ b/rbot/config.rb
@@ -1,40 +1,205 @@
module Irc
+ require 'yaml'
+
# container for bot configuration
- # just treat it like a hash
- class BotConfig < Hash
+ class BotConfig
+
+ # currently we store values in a hash but this could be changed in the
+ # future. We use hash semantics, however.
+ def method_missing(method, *args, &block)
+ return @config.send(method, *args, &block)
+ end
# bot:: parent bot class
# create a new config hash from #{botclass}/conf.rbot
def initialize(bot)
- super(false)
@bot = bot
# some defaults
- self["SERVER"] = "localhost"
- self["PORT"] = "6667"
- self["NICK"] = "rbot"
- self["USER"] = "gilbertt"
- self["LANGUAGE"] = "english"
- self["SAVE_EVERY"] = "60"
- self["KEYWORD_LISTEN"] = false
- if(File.exist?("#{@bot.botclass}/conf.rbot"))
- IO.foreach("#{@bot.botclass}/conf.rbot") do |line|
- next if(line =~ /^\s*#/)
- if(line =~ /(\S+)\s+=\s+(.*)$/)
- self[$1] = $2 if($2)
- end
- end
+ @config = Hash.new(false)
+
+ @config['server.name'] = "localhost"
+ @config['server.port'] = 6667
+ @config['server.password'] = false
+ @config['server.bindhost'] = false
+ @config['irc.nick'] = "rbot"
+ @config['irc.user'] = "rbot"
+ @config['irc.join_channels'] = ""
+ @config['core.language'] = "english"
+ @config['core.save_every'] = 60
+ @config['keyword.listen'] = false
+ @config['auth.password'] = ""
+ @config['server.sendq_delay'] = 2.0
+ @config['server.sendq_burst'] = 4
+ @config['keyword.address'] = true
+ @config['keyword.listen'] = false
+
+ # TODO
+ # have this class persist key/values in hash using yaml as it kinda
+ # already does.
+ # have other users of the class describe config to it on init, like:
+ # @config.add(:key => 'server.name', :type => 'string',
+ # :default => 'localhost', :restart => true,
+ # :help => 'irc server to connect to')
+ # that way the config module doesn't have to know about all the other
+ # classes but can still provide help and defaults.
+ # Classes don't have to add keys, they can just use config as a
+ # persistent hash, but then they won't be presented by the config
+ # module for runtime display/changes.
+ # (:restart, if true, makes the bot reply to changes with "this change
+ # will take effect after the next restart)
+ # :proc => Proc.new {|newvalue| ...}
+ # (:proc, proc to run on change of setting)
+ # or maybe, @config.add_key(...) do |newvalue| .... end
+ # :validate => /regex/
+ # (operates on received string before conversion)
+ # Special handling for arrays so the config module can be used to
+ # add/remove elements as well as changing the whole thing
+ # Allow config options to list possible valid values (if type is enum,
+ # for example). Then things like the language module can list the
+ # available languages for choosing.
+
+ if(File.exist?("#{@bot.botclass}/conf.yaml"))
+ newconfig = YAML::load_file("#{@bot.botclass}/conf.yaml")
+ @config.update(newconfig)
+ else
+ # first-run wizard!
+ wiz = BotConfigWizard.new(@bot)
+ newconfig = wiz.run(@config)
+ @config.update(newconfig)
end
end
# write current configuration to #{botclass}/conf.rbot
def save
Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}"))
- File.open("#{@bot.botclass}/conf.rbot", "w") do |file|
- self.each do |key, value|
- file.puts "#{key} = #{value}"
+ File.open("#{@bot.botclass}/conf.yaml", "w") do |file|
+ file.puts @config.to_yaml
+ end
+ end
+ end
+
+ # I don't see a nice way to avoid the first start wizard knowing way too
+ # much about other modules etc, because it runs early and stuff it
+ # configures is used to initialise the other modules...
+ # To minimise this we'll do as little as possible and leave the rest to
+ # online modification
+ class BotConfigWizard
+
+ # TODO things to configure..
+ # config directory (botclass) - people don't realise they should set
+ # this. The default... isn't good.
+ # users? - default *!*@* to 10
+ # levels? - need a way to specify a default level, methinks, for
+ # unconfigured items.
+ #
+ def initialize(bot)
+ @bot = bot
+ @questions = [
+ {
+ :question => "What server should the bot connect to?",
+ :prompt => "Hostname",
+ :key => "server.name",
+ :type => :string,
+ },
+ {
+ :question => "What port should the bot connect to?",
+ :prompt => "Port",
+ :key => "server.port",
+ :type => :number,
+ },
+ {
+ :question => "Does this IRC server require a password for access? Leave blank if not.",
+ :prompt => "Password",
+ :key => "server.password",
+ :type => :password,
+ },
+ {
+ :question => "Would you like rbot to bind to a specific local host or IP? Leave blank if not.",
+ :prompt => "Local bind",
+ :key => "server.bindhost",
+ :type => :string,
+ },
+ {
+ :question => "What IRC nickname should the bot attempt to use?",
+ :prompt => "Nick",
+ :key => "irc.nick",
+ :type => :string,
+ },
+ {
+ :question => "What local user should the bot appear to be?",
+ :prompt => "User",
+ :key => "irc.user",
+ :type => :string,
+ },
+ {
+ :question => "What channels should the bot always join at startup? List multiple channels using commas to separate. If a channel requires a password, use a space after the channel name. e.g: '#chan1, #chan2, #secretchan secritpass, #chan3'",
+ :prompt => "Channels",
+ :key => "irc.join_channels",
+ :type => :string,
+ },
+ {
+ :question => "Which language file should the bot use?",
+ :prompt => "Language",
+ :key => "core.language",
+ :type => :enum,
+ :items => Dir.new(File.dirname(__FILE__) + "/languages/").collect {|f|
+ f =~ /\.lang$/ ? f.gsub(/\.lang$/, "") : nil
+ }.compact
+ },
+ {
+ :question => "Enter your password for maxing your auth with the bot (used to associate new hostmasks with your owner-status etc)",
+ :prompt => "Password",
+ :key => "auth.password",
+ :type => :password,
+ },
+ ]
+ end
+
+ def run(defaults)
+ config = defaults.clone
+ puts "First time rbot configuration wizard"
+ puts "===================================="
+ puts "This is the first time you have run rbot with a config directory of:"
+ puts @bot.botclass
+ puts "This wizard will ask you a few questions to get you started."
+ puts "The rest of rbot's configuration can be manipulated via IRC once"
+ puts "rbot is connected and you are auth'd."
+ puts "-----------------------------------"
+
+ @questions.each do |q|
+ puts q[:question]
+ begin
+ key = q[:key]
+ if q[:type] == :enum
+ puts "valid values are: " + q[:items].join(", ")
+ end
+ if (defaults.has_key?(key))
+ print q[:prompt] + " [#{defaults[key]}]: "
+ else
+ print q[:prompt] + " []: "
+ end
+ response = STDIN.gets
+ response.chop!
+ response = defaults[key] if response == "" && defaults.has_key?(key)
+ case q[:type]
+ when :string
+ when :number
+ raise "value '#{response}' is not a number" unless (response.class == Fixnum || response =~ /^\d+$/)
+ response = response.to_i
+ when :password
+ when :enum
+ raise "selected value '#{response}' is not one of the valid values" unless q[:items].include?(response)
+ end
+ config[key] = response
+ puts "configured #{key} => #{config[key]}"
+ puts "-----------------------------------"
+ rescue RuntimeError => e
+ puts e.message
+ retry
end
end
+ return config
end
end
end
diff --git a/rbot/ircbot.rb b/rbot/ircbot.rb
index 7129c105..844231dd 100644
--- a/rbot/ircbot.rb
+++ b/rbot/ircbot.rb
@@ -86,20 +86,20 @@ class IrcBot
@config = Irc::BotConfig.new(self)
@timer = Timer::Timer.new
@registry = BotRegistry.new self
- @timer.add(@config["SAVE_EVERY"].to_i) { save }
+ @timer.add(@config['core.save_every']) { save } if @config['core.save_every']
@channels = Hash.new
@logs = Hash.new
@httputil = Irc::HttpUtil.new(self)
- @lang = Irc::Language.new(@config["LANGUAGE"])
+ @lang = Irc::Language.new(@config['core.language'])
@keywords = Irc::Keywords.new(self)
@auth = Irc::IrcAuth.new(self)
@plugins = Irc::Plugins.new(self, ["#{botclass}/plugins"])
- @socket = Irc::IrcSocket.new(@config["SERVER"], @config["PORT"], @config["HOST"], @config["SENDQ_DELAY"], @config["SENDQ_BURST"])
- @nick = @config["NICK"]
- @server_password = @config["SERVER_PASSWORD"]
- if @config["ADDRESS_PREFIX"]
- @addressing_prefixes = @config["ADDRESS_PREFIX"].split(" ")
+
+ @socket = Irc::IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst'])
+ @nick = @config['irc.nick']
+ if @config['core.address_prefix']
+ @addressing_prefixes = @config['core.address_prefix'].split(" ")
else
@addressing_prefixes = Array.new
end
@@ -179,13 +179,13 @@ class IrcBot
if data['NICK'] && data['NICK'].length > 0
@nick = data['NICK']
end
- if(@config["QUSER"])
- puts "authing with Q using #{@config["QUSER"]} #{@config["QAUTH"]}"
- @socket.puts "PRIVMSG Q@CServe.quakenet.org :auth #{@config["QUSER"]} #{@config["QAUTH"]}"
+ if(@config['irc.quser'])
+ puts "authing with Q using #{@config['quakenet.user']} #{@config['quakenet.auth']}"
+ @socket.puts "PRIVMSG Q@CServe.quakenet.org :auth #{@config['quakenet.user']} #{@config['quakenet.auth']}"
end
- if(@config["JOIN_CHANNELS"])
- @config["JOIN_CHANNELS"].split(", ").each {|c|
+ if(@config['irc.join_channels'])
+ @config['irc.join_channels'].split(", ").each {|c|
puts "autojoining channel #{c}"
if(c =~ /^(\S+)\s+(\S+)$/i)
join $1, $2
@@ -225,8 +225,8 @@ class IrcBot
m = TopicMessage.new(self, data["SOURCE"], data["CHANNEL"], timestamp, data["TOPIC"])
ontopic(m)
- @plugins.delegate("topic", m)
@plugins.delegate("listen", m)
+ @plugins.delegate("topic", m)
}
@client["TOPIC"] = @client["TOPICINFO"] = proc {|data|
channel = data["CHANNEL"]
@@ -258,10 +258,10 @@ class IrcBot
begin
@socket.connect
rescue => e
- raise "failed to connect to IRC server at #{@config['SERVER']} #{@config['PORT']}: " + e
+ raise "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e
end
- @socket.puts "PASS " + @server_password if @server_password
- @socket.puts "NICK #{@nick}\nUSER #{@config['USER']} 4 #{@config['SERVER']} :Ruby bot. (c) Tom Gilbert"
+ @socket.puts "PASS " + @config['server.password'] if @config['server.password']
+ @socket.puts "NICK #{@nick}\nUSER #{@config['server.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert"
end
# begin event handling loop
@@ -563,8 +563,8 @@ class IrcBot
join 0 if(@auth.allow?("join", m.source, m.replyto))
when (/^save$/i)
if(@auth.allow?("config", m.source, m.replyto))
- okay m.replyto
save
+ m.okay
end
when (/^nick\s+(\S+)$/i)
nickchg($1) if(@auth.allow?("nick", m.source, m.replyto))
@@ -580,55 +580,55 @@ class IrcBot
say m.replyto, "pong"
when (/^rescan$/i)
if(@auth.allow?("config", m.source, m.replyto))
- okay m.replyto
+ m.okay
rescan
end
when (/^quiet$/i)
if(auth.allow?("talk", m.source, m.replyto))
- say m.replyto, @lang.get("okay")
+ m.okay
@channels.each_value {|c| c.quiet = true }
end
when (/^quiet in (\S+)$/i)
where = $1
if(auth.allow?("talk", m.source, m.replyto))
- say m.replyto, @lang.get("okay")
+ m.okay
where.gsub!(/^here$/, m.target) if m.public?
@channels[where].quiet = true if(@channels.has_key?(where))
end
when (/^talk$/i)
if(auth.allow?("talk", m.source, m.replyto))
@channels.each_value {|c| c.quiet = false }
- okay m.replyto
+ m.okay
end
when (/^talk in (\S+)$/i)
where = $1
if(auth.allow?("talk", m.source, m.replyto))
where.gsub!(/^here$/, m.target) if m.public?
@channels[where].quiet = false if(@channels.has_key?(where))
- okay m.replyto
+ m.okay
end
- # TODO break this out into an options module
+ # TODO break this out into a config module
when (/^options get sendq_delay$/i)
if auth.allow?("config", m.source, m.replyto)
- m.reply "options->sendq_delay = #{@socket.get_sendq}"
+ m.reply "options->sendq_delay = #{@socket.sendq_delay}"
end
when (/^options get sendq_burst$/i)
if auth.allow?("config", m.source, m.replyto)
- m.reply "options->sendq_burst = #{@socket.get_maxburst}"
+ m.reply "options->sendq_burst = #{@socket.sendq_burst}"
end
when (/^options set sendq_burst (.*)$/i)
num = $1.to_i
if auth.allow?("config", m.source, m.replyto)
- @socket.set_maxburst(num)
- @config["SENDQ_BURST"] = num
- okay m.replyto
+ @socket.sendq_burst = num
+ @config['irc.sendq_burst'] = num
+ m.okay
end
when (/^options set sendq_delay (.*)$/i)
freq = $1.to_f
if auth.allow?("config", m.source, m.replyto)
- @socket.set_sendq(freq)
- @config["SENDQ_DELAY"] = freq
- okay m.replyto
+ @socket.sendq_delay = freq
+ @config['irc.sendq_delay'] = freq
+ m.okay
end
when (/^status$/i)
m.reply status if auth.allow?("status", m.source, m.replyto)
@@ -735,7 +735,7 @@ class IrcBot
@channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil?
@channels[m.channel].topic.by = m.source if !m.source.nil?
- puts @channels[m.channel].topic
+ debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}"
end
# delegate a privmsg to auth, keyword or plugin handlers
diff --git a/rbot/ircsocket.rb b/rbot/ircsocket.rb
index 25895644..35857736 100644
--- a/rbot/ircsocket.rb
+++ b/rbot/ircsocket.rb
@@ -8,29 +8,37 @@ module Irc
class IrcSocket
# total number of lines sent to the irc server
attr_reader :lines_sent
+
# total number of lines received from the irc server
attr_reader :lines_received
+
+ # delay between lines sent
+ attr_reader :sendq_delay
+
+ # max lines to burst
+ attr_reader :sendq_burst
+
# server:: server to connect to
# port:: IRCd port
# host:: optional local host to bind to (ruby 1.7+ required)
# create a new IrcSocket
- def initialize(server, port, host, sendfreq=2, maxburst=4)
+ def initialize(server, port, host, sendq_delay=2, sendq_burst=4)
@server = server.dup
@port = port.to_i
@host = host
@lines_sent = 0
@lines_received = 0
- if sendfreq
- @sendfreq = sendfreq.to_f
+ if sendq_delay
+ @sendq_delay = sendq_delay.to_f
else
- @sendfreq = 2
+ @sendq_delay = 2
end
- @last_send = Time.new - @sendfreq
+ @last_send = Time.new - @sendq_delay
@burst = 0
- if maxburst
- @maxburst = maxburst.to_i
+ if sendq_burst
+ @sendq_burst = sendq_burst.to_i
else
- @maxburst = 4
+ @sendq_burst = 4
end
end
@@ -52,15 +60,15 @@ module Irc
@qthread = false
@qmutex = Mutex.new
@sendq = Array.new
- if (@sendfreq > 0)
+ if (@sendq_delay > 0)
@qthread = Thread.new { spooler }
end
end
- def set_sendq(newfreq)
+ def sendq_delay=(newfreq)
debug "changing sendq frequency to #{newfreq}"
@qmutex.synchronize do
- @sendfreq = newfreq
+ @sendq_delay = newfreq
if newfreq == 0 && @qthread
clearq
Thread.kill(@qthread)
@@ -71,20 +79,12 @@ module Irc
end
end
- def set_maxburst(newburst)
+ def sendq_burst=(newburst)
@qmutex.synchronize do
- @maxburst = newburst
+ @sendq_burst = newburst
end
end
- def get_maxburst
- return @maxburst
- end
-
- def get_sendq
- return @sendfreq
- end
-
# used to send lines to the remote IRCd
# message: IRC message to send
def puts(message)
@@ -106,7 +106,7 @@ module Irc
end
def queue(msg)
- if @sendfreq > 0
+ if @sendq_delay > 0
@qmutex.synchronize do
# debug "QUEUEING: #{msg}"
@sendq.push msg
@@ -120,7 +120,7 @@ module Irc
def spooler
while true
spool
- sleep 0.1
+ sleep 0.2
end
end
@@ -128,17 +128,17 @@ module Irc
def spool
unless @sendq.empty?
now = Time.new
- if (now >= (@last_send + @sendfreq))
- # reset burst counter after @sendfreq has passed
+ if (now >= (@last_send + @sendq_delay))
+ # reset burst counter after @sendq_delay has passed
@burst = 0
debug "in spool, resetting @burst"
- elsif (@burst >= @maxburst)
+ elsif (@burst >= @sendq_burst)
# nope. can't send anything
return
end
@qmutex.synchronize do
- debug "(can send #{@maxburst - @burst} lines, there are #{@sendq.length} to send)"
- (@maxburst - @burst).times do
+ debug "(can send #{@sendq_burst - @burst} lines, there are #{@sendq.length} to send)"
+ (@sendq_burst - @burst).times do
break if @sendq.empty?
puts_critical(@sendq.shift)
end
diff --git a/rbot/keywords.rb b/rbot/keywords.rb
index f1997829..3305af29 100644
--- a/rbot/keywords.rb
+++ b/rbot/keywords.rb
@@ -415,9 +415,9 @@ module Irc
end
else
# in channel message, not to me
- if(m.message =~ /^'(.*)$/ || (@bot.config["NO_KEYWORD_ADDRESS"] == "true" && m.message =~ /^(.*\S)\s*\?\s*$/))
+ if(m.message =~ /^'(.*)$/ || (!@bot.config["keyword.noaddress"] && m.message =~ /^(.*\S)\s*\?\s*$/))
keyword m, $1, false if(@bot.auth.allow?("keyword", m.source))
- elsif(@bot.config["KEYWORD_LISTEN"] == "true" && (m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/))
+ elsif(@bot.config["keyword.listen"] == true && (m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/))
# TODO MUCH more selective on what's allowed here
keyword_command(m.sourcenick, m.replyto, $1, $2, $3, true) if(@bot.auth.allow?("keycmd", m.source))
end
diff --git a/rbot/message.rb b/rbot/message.rb
index c217b1da..d7f614ab 100644
--- a/rbot/message.rb
+++ b/rbot/message.rb
@@ -5,6 +5,9 @@ module Irc
# nick/channel and a message part)
class BasicUserMessage
+ # associated bot
+ attr_reader :bot
+
# when the message was received
attr_reader :time
@@ -175,6 +178,12 @@ module Irc
@replied = true
end
+ # convenience method to reply "okay" in the current language to the
+ # message
+ def okay
+ @bot.say @replyto, @bot.lang.get("okay")
+ end
+
end
# class to manage IRC PRIVMSGs
diff --git a/rbot/messagemapper.rb b/rbot/messagemapper.rb
new file mode 100644
index 00000000..d03721c6
--- /dev/null
+++ b/rbot/messagemapper.rb
@@ -0,0 +1,158 @@
+module Irc
+ class MessageMapper
+ attr_writer :fallback
+
+ def initialize(parent)
+ @parent = parent
+ @routes = Array.new
+ @fallback = 'usage'
+ end
+
+ def map(*args)
+ @routes << Route.new(*args)
+ end
+
+ def each
+ @routes.each {|route| yield route}
+ end
+ def last
+ @routes.last
+ end
+
+ def handle(m)
+ return false if @routes.empty?
+ failures = []
+ @routes.each do |route|
+ options, failure = route.recognize(m)
+ if options.nil?
+ failures << [route, failure]
+ else
+ action = route.options[:action] ? route.options[:action] : route.items[0]
+ next unless @parent.respond_to?(action)
+ auth = route.options[:auth] ? route.options[:auth] : action
+ if m.bot.auth.allow?(auth, m.source, m.replyto)
+ debug "route found and auth'd: #{action.inspect} #{options.inspect}"
+ @parent.send(action, m, options)
+ return true
+ end
+ # if it's just an auth failure but otherwise the match is good,
+ # don't try any more handlers
+ break
+ end
+ end
+ debug failures.inspect
+ debug "no handler found, trying fallback"
+ if @fallback != nil && @parent.respond_to?(@fallback)
+ if m.bot.auth.allow?(@fallback, m.source, m.replyto)
+ @parent.send(@fallback, m, {})
+ return true
+ end
+ end
+ return false
+ end
+
+ end
+
+ class Route
+ attr_reader :defaults # The defaults hash
+ attr_reader :options # The options hash
+ attr_reader :items
+ def initialize(template, hash={})
+ raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
+ @defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {}
+ @requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {}
+ self.items = template
+ @options = hash
+ end
+ def items=(str)
+ items = str.split(/\s+/).collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if str.kind_of?(String) # split and convert ':xyz' to symbols
+ items.shift if items.first == ""
+ items.pop if items.last == ""
+ @items = items
+
+ if @items.first.kind_of? Symbol
+ raise ArgumentError, "Illegal template -- first component cannot be dynamic\n #{str.inspect}"
+ end
+
+ # Verify uniqueness of each component.
+ @items.inject({}) do |seen, item|
+ if item.kind_of? Symbol
+ raise ArgumentError, "Illegal template -- duplicate item #{item}\n #{str.inspect}" if seen.key? item
+ seen[item] = true
+ end
+ seen
+ end
+ end
+
+ # Recognize the provided string components, returning a hash of
+ # recognized values, or [nil, reason] if the string isn't recognized.
+ def recognize(m)
+ components = m.message.split(/\s+/)
+ options = {}
+
+ @items.each do |item|
+ if /^\*/ =~ item.to_s
+ if components.empty?
+ value = @defaults.has_key?(item) ? @defaults[item].clone : []
+ else
+ value = components.clone
+ end
+ components = []
+ def value.to_s() self.join(' ') end
+ options[item.to_s.sub(/^\*/,"").intern] = value
+ elsif item.kind_of? Symbol
+ value = components.shift || @defaults[item]
+ return nil, requirements_for(item) unless passes_requirements?(item, value)
+ options[item] = value
+ else
+ return nil, "No value available for component #{item.inspect}" if components.empty?
+ component = components.shift
+ return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item
+ end
+ end
+
+ return nil, "Unused components were left: #{components.join '/'}" unless components.empty?
+
+ return nil, "route is not configured for private messages" if @options.has_key?(:private) && !@options[:private] && m.private?
+ return nil, "route is not configured for public messages" if @options.has_key?(:public) && !@options[:public] && !m.private?
+
+ options.delete_if {|k, v| v.nil?} # Remove nil values.
+ return options, nil
+ end
+
+ def inspect
+ when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
+ default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}"
+ "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join('/').inspect}#{default_str}#{when_str}>"
+ end
+
+ # Verify that the given value passes this route's requirements
+ def passes_requirements?(name, value)
+ return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be
+
+ case @requirements[name]
+ when nil then true
+ when Regexp then
+ value = value.to_s
+ match = @requirements[name].match(value)
+ match && match[0].length == value.length
+ else
+ @requirements[name] == value.to_s
+ end
+ end
+
+ def requirements_for(name)
+ name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect)
+ presence = (@defaults.key?(name) && @defaults[name].nil?)
+ requirement = case @requirements[name]
+ when nil then nil
+ when Regexp then "match #{@requirements[name].inspect}"
+ else "be equal to #{@requirements[name].inspect}"
+ end
+ if presence && requirement then "#{name} must be present and #{requirement}"
+ elsif presence || requirement then "#{name} must #{requirement || 'be present'}"
+ else "#{name} has no requirements"
+ end
+ end
+ end
+end
diff --git a/rbot/plugins.rb b/rbot/plugins.rb
index b99fe562..5db047fb 100644
--- a/rbot/plugins.rb
+++ b/rbot/plugins.rb
@@ -1,8 +1,50 @@
module Irc
+ require 'rbot/messagemapper'
# base class for all rbot plugins
# certain methods will be called if they are provided, if you define one of
# the following methods, it will be called as appropriate:
+ #
+ # map(template, options)::
+ # map is the new, cleaner way to respond to specific message formats
+ # without littering your plugin code with regexps
+ # examples:
+ # plugin.map 'karmastats', :action => 'karma_stats'
+ #
+ # # while in the plugin...
+ # def karma_stats(m, params)
+ # m.reply "..."
+ # end
+ #
+ # # the default action is the first component
+ # plugin.map 'karma'
+ #
+ # # attributes can be pulled out of the match string
+ # plugin.map 'karma for :key'
+ # plugin.map 'karma :key'
+ #
+ # # while in the plugin...
+ # def karma(m, params)
+ # item = params[:key]
+ # m.reply 'karma for #{item}'
+ # end
+ #
+ # # you can setup defaults, to make parameters optional
+ # plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
+ #
+ # # the default auth check is also against the first component
+ # # but that can be changed
+ # plugin.map 'karmastats', :auth => 'karma'
+ #
+ # # maps can be restricted to public or private message:
+ # plugin.map 'karmastats', :private false,
+ # plugin.map 'karmastats', :public false,
+ # end
+ #
+ # To activate your maps, you simply register them
+ # plugin.register_maps
+ # This also sets the privmsg handler to use the map lookups for
+ # handling messages. You can still use listen(), kick() etc methods
#
# listen(UserMessage)::
# Called for all messages of any type. To
@@ -43,14 +85,28 @@ module Irc
# plugin reload or bot quit - close any open
# files/connections or flush caches here
class Plugin
+ attr_reader :bot # the associated bot
# initialise your plugin. Always call super if you override this method,
# as important variables are set up for you
def initialize
@bot = Plugins.bot
@names = Array.new
+ @handler = MessageMapper.new(self)
@registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
end
+ def map(*args)
+ @handler.map(*args)
+ # register this map
+ name = @handler.last.items[0]
+ self.register name
+ unless self.respond_to?('privmsg')
+ def self.privmsg(m)
+ @handler.handle(m)
+ end
+ end
+ end
+
# return an identifier for this plugin, defaults to a list of the message
# prefixes handled (used for error messages etc)
def name
@@ -70,15 +126,17 @@ module Irc
# this can be called multiple times for a plugin to handle multiple
# message prefixes
def register(name)
+ return if Plugins.plugins.has_key?(name)
Plugins.plugins[name] = self
@names << name
end
- # is this plugin listening to all messages?
- def listen?
- @listen
+ # default usage method provided as a utility for simple plugins. The
+ # MessageMapper uses 'usage' as its default fallback method.
+ def usage(m, params)
+ m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
end
-
+
end
# class to manage multiple plugins and delegate messages to them for
diff --git a/rbot/plugins/autoop.rb b/rbot/plugins/autoop.rb
index 094ee343..fdbcf6e0 100644
--- a/rbot/plugins/autoop.rb
+++ b/rbot/plugins/autoop.rb
@@ -38,7 +38,7 @@ class AutoOP < Plugin
@registry[ma[1]] = channels.split(/,\s*/).collect { |x|
x.strip
}
- @bot.okay m.replyto
+ m.okay
else
m.reply @bot.lang.get('dunno')
end
@@ -48,7 +48,7 @@ class AutoOP < Plugin
if(!@registry.delete(params))
m.reply @bot.lang.get('dunno')
else
- @bot.okay m.replyto
+ m.okay
end
end
diff --git a/rbot/plugins/cal.rb b/rbot/plugins/cal.rb
index 1e823194..4f28310b 100644
--- a/rbot/plugins/cal.rb
+++ b/rbot/plugins/cal.rb
@@ -2,13 +2,14 @@ class CalPlugin < Plugin
def help(plugin, topic="")
"cal [options] => show current calendar [unix cal options]"
end
- def privmsg(m)
- if m.params && m.params.length > 0
- m.reply Utils.safe_exec("cal", m.params)
+ def cal(m, params)
+ if params.has_key?(:month)
+ m.reply Utils.safe_exec("cal", params[:month], params[:year])
else
m.reply Utils.safe_exec("cal")
end
end
end
plugin = CalPlugin.new
-plugin.register("cal")
+plugin.map 'cal :month :year', :requirements => {:month => /^\d+$/, :year => /^\d+$/}
+plugin.map 'cal'
diff --git a/rbot/plugins/eightball.rb b/rbot/plugins/eightball.rb
index 6d123b34..64748490 100644
--- a/rbot/plugins/eightball.rb
+++ b/rbot/plugins/eightball.rb
@@ -8,11 +8,12 @@ class EightBallPlugin < Plugin
def help(plugin, topic="")
"magic 8-ball ruby bot module written by novex for nvinfo on #dumber@quakenet, usage:<botname> 8ball will i ever beat this cancer?"
end
- def privmsg(m)
+ def eightball(m, params)
answers = @answers[rand(@answers.length)]
action = "shakes the magic 8-ball... #{answers}"
@bot.action m.replyto, action
end
end
plugin = EightBallPlugin.new
-plugin.register("8ball")
+plugin.map '8ball', :action => 'usage'
+plugin.map '8ball *params', :action => 'eightball'
diff --git a/rbot/plugins/karma.rb b/rbot/plugins/karma.rb
index 1bed175a..148427a5 100644
--- a/rbot/plugins/karma.rb
+++ b/rbot/plugins/karma.rb
@@ -27,60 +27,59 @@ class KarmaPlugin < Plugin
end
end
+
+ def stats(m, params)
+ if (@registry.length)
+ max = @registry.values.max
+ min = @registry.values.min
+ best = @registry.to_hash.index(max)
+ worst = @registry.to_hash.index(min)
+ m.reply "#{@registry.length} items. Best: #{best} (#{max}); Worst: #{worst} (#{min})"
+ end
+ end
+
+ def karma(m, params)
+ thing = params[:key]
+ thing = m.sourcenick unless thing
+ thing = thing.to_s
+ karma = @registry[thing]
+ if(karma != 0)
+ m.reply "karma for #{thing}: #{@registry[thing]}"
+ else
+ m.reply "#{thing} has neutral karma"
+ end
+ end
+
+
def help(plugin, topic="")
- "karma module: <thing>++/<thing>-- => increase/decrease karma for <thing>, karma for <thing>? => show karma for <thing>. Karma is a community rating system - only in-channel messages can affect karma and you cannot adjust your own."
+ "karma module: <thing>++/<thing>-- => increase/decrease karma for <thing>, karma for <thing>? => show karma for <thing>, karmastats => show stats. Karma is a community rating system - only in-channel messages can affect karma and you cannot adjust your own."
end
def listen(m)
- if(m.kind_of?(PrivMessage) && m.public?)
- # in channel message, the kind we are interested in
- if(m.message =~ /(\+\+|--)/)
- string = m.message.sub(/\W(--|\+\+)(\(.*?\)|[^(++)(\-\-)\s]+)/, "\2\1")
- seen = Hash.new
- while(string.sub!(/(\(.*?\)|[^(++)(\-\-)\s]+)(\+\+|--)/, ""))
- key = $1
- change = $2
- next if seen[key]
- seen[key] = true
+ return unless m.kind_of?(PrivMessage) && m.public?
+ # in channel message, the kind we are interested in
+ if(m.message =~ /(\+\+|--)/)
+ string = m.message.sub(/\W(--|\+\+)(\(.*?\)|[^(++)(\-\-)\s]+)/, "\2\1")
+ seen = Hash.new
+ while(string.sub!(/(\(.*?\)|[^(++)(\-\-)\s]+)(\+\+|--)/, ""))
+ key = $1
+ change = $2
+ next if seen[key]
+ seen[key] = true
- key.sub!(/^\((.*)\)$/, "\1")
- key.gsub!(/\s+/, " ")
- next unless(key.length > 0)
- next if(key == m.sourcenick)
- if(change == "++")
- @registry[key] += 1
- elsif(change == "--")
- @registry[key] -= 1
- end
+ key.sub!(/^\((.*)\)$/, "\1")
+ key.gsub!(/\s+/, " ")
+ next unless(key.length > 0)
+ next if(key == m.sourcenick)
+ if(change == "++")
+ @registry[key] += 1
+ elsif(change == "--")
+ @registry[key] -= 1
end
end
end
end
- def privmsg(m)
- if (m.plugin == "karmastats")
- if (@registry.length)
- max = @registry.values.max
- min = @registry.values.min
- best = @registry.to_hash.index(max)
- worst = @registry.to_hash.index(min)
- m.reply "#{@registry.length} votes. Best: #{best} (#{max}); Worst: #{worst} (#{min})"
- return
- end
- end
- unless(m.params)
- m.reply "incorrect usage: " + m.plugin
- return
- end
- if(m.params =~ /^(?:for\s+)?(\S+?)\??$/)
- thing = $1
- karma = @registry[thing]
- if(karma != 0)
- m.reply "karma for #{thing}: #{@registry[thing]}"
- else
- m.reply "#{thing} has neutral karma"
- end
- end
- end
end
plugin = KarmaPlugin.new
-plugin.register("karma")
-plugin.register("karmastats")
+plugin.map 'karmastats', :action => 'stats'
+plugin.map 'karma :key', :defaults => {:key => false}
+plugin.map 'karma for :key'
diff --git a/rbot/plugins/lart.rb b/rbot/plugins/lart.rb
index 385b17c3..1c72c648 100644
--- a/rbot/plugins/lart.rb
+++ b/rbot/plugins/lart.rb
@@ -130,25 +130,25 @@ class LartPlugin < Plugin
#{{{
def handle_addlart(m)
@larts << m.params
- @bot.okay m.replyto
+ m.okay
end
#}}}
#{{{
def handle_rmlart(m)
@larts.delete m.params
- @bot.okay m.replyto
+ m.okay
end
#}}}
#{{{
def handle_addpraise(m)
@praises << m.params
- @bot.okay m.replyto
+ m.okay
end
#}}}
#{{{
def handle_rmpraise(m)
@praises.delete m.params
- @bot.okay m.replyto
+ m.okay
end
#}}}
#}}}
diff --git a/rbot/plugins/nickserv.rb b/rbot/plugins/nickserv.rb
index 94c57e6d..1ef2baf7 100644
--- a/rbot/plugins/nickserv.rb
+++ b/rbot/plugins/nickserv.rb
@@ -38,23 +38,23 @@ class NickServPlugin < Plugin
nick = $1
passwd = $2
@registry[nick] = passwd
- @bot.okay m.replyto
+ m.okay
when (/^register$/)
passwd = genpasswd
@bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd
@registry[@bot.nick] = passwd
- @bot.okay m.replyto
+ m.okay
when (/^register\s*(\S*)\s*(.*)$/)
passwd = $1
email = $2
@bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd + " " + email
@registry[@bot.nick] = passwd
- @bot.okay m.replyto
+ m.okay
when (/^register\s*(.*)\s*$/)
passwd = $1
@bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd
@registry[@bot.nick] = passwd
- @bot.okay m.replyto
+ m.okay
when (/^listnicks$/)
if @bot.auth.allow?("config", m.source, m.replyto)
if @registry.length > 0
@@ -68,7 +68,7 @@ class NickServPlugin < Plugin
when (/^identify$/)
if @registry.has_key?(@bot.nick)
@bot.sendmsg "PRIVMSG", "NickServ", "IDENTIFY " + @registry[@bot.nick]
- @bot.okay m.replyto
+ m.okay
else
m.reply "I dunno the nickserv password for the nickname #{@bot.nick} :("
end
diff --git a/rbot/plugins/opmeh.rb b/rbot/plugins/opmeh.rb
index eb392513..2776de60 100644
--- a/rbot/plugins/opmeh.rb
+++ b/rbot/plugins/opmeh.rb
@@ -12,6 +12,7 @@ class OpMehPlugin < Plugin
end
target = m.sourcenick
@bot.sendq("MODE #{channel} +o #{target}")
+ m.okay
end
end
plugin = OpMehPlugin.new
diff --git a/rbot/plugins/quotes.rb b/rbot/plugins/quotes.rb
index 0e46b495..674a9ed6 100644
--- a/rbot/plugins/quotes.rb
+++ b/rbot/plugins/quotes.rb
@@ -186,7 +186,7 @@ class QuotePlugin < Plugin
num = $2.to_i
if(@bot.auth.allow?("delquote", m.source, m.replyto))
if(delquote(channel, num))
- @bot.okay m.replyto
+ m.okay
else
m.reply "quote not found!"
end
@@ -288,7 +288,7 @@ class QuotePlugin < Plugin
num = $1.to_i
if(@bot.auth.allow?("delquote", m.source, m.replyto))
if(delquote(m.target, num))
- @bot.okay m.replyto
+ m.okay
else
m.reply "quote not found!"
end
diff --git a/rbot/plugins/remind.rb b/rbot/plugins/remind.rb
index 402e2d08..5ad980ae 100644
--- a/rbot/plugins/remind.rb
+++ b/rbot/plugins/remind.rb
@@ -145,7 +145,7 @@ class RemindPlugin < Plugin
m.reply "incorrect usage: " + help(m.plugin)
return
end
- @bot.okay m.replyto
+ m.okay
end
end
plugin = RemindPlugin.new
diff --git a/rbot/plugins/roulette.rb b/rbot/plugins/roulette.rb
index a3d102f3..c9d585ea 100644
--- a/rbot/plugins/roulette.rb
+++ b/rbot/plugins/roulette.rb
@@ -30,7 +30,7 @@ class RoulettePlugin < Plugin
elsif m.params == "clearstats"
if @bot.auth.allow?("config", m.source, m.replyto)
@registry.clear
- @bot.okay m.replyto
+ m.okay
end
return
elsif m.params