diff options
author | Tom Gilbert <tom@linuxbrit.co.uk> | 2005-07-26 21:50:00 +0000 |
---|---|---|
committer | Tom Gilbert <tom@linuxbrit.co.uk> | 2005-07-26 21:50:00 +0000 |
commit | 5d5d9df1a4825fad5ef045cfc0b21b16e5e2bcc7 (patch) | |
tree | f3814abafc8f1d6c589a29f4ddc89f55e510d493 | |
parent | 3ba6917c904f5e664ae78b146f4e394ad805eb96 (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-- | ChangeLog | 45 | ||||
-rwxr-xr-x | rbot.rb | 4 | ||||
-rw-r--r-- | rbot/auth.rb | 2 | ||||
-rw-r--r-- | rbot/config.rb | 205 | ||||
-rw-r--r-- | rbot/ircbot.rb | 64 | ||||
-rw-r--r-- | rbot/ircsocket.rb | 56 | ||||
-rw-r--r-- | rbot/keywords.rb | 4 | ||||
-rw-r--r-- | rbot/message.rb | 9 | ||||
-rw-r--r-- | rbot/messagemapper.rb | 158 | ||||
-rw-r--r-- | rbot/plugins.rb | 66 | ||||
-rw-r--r-- | rbot/plugins/autoop.rb | 4 | ||||
-rw-r--r-- | rbot/plugins/cal.rb | 9 | ||||
-rw-r--r-- | rbot/plugins/eightball.rb | 5 | ||||
-rw-r--r-- | rbot/plugins/karma.rb | 93 | ||||
-rw-r--r-- | rbot/plugins/lart.rb | 8 | ||||
-rw-r--r-- | rbot/plugins/nickserv.rb | 10 | ||||
-rw-r--r-- | rbot/plugins/opmeh.rb | 1 | ||||
-rw-r--r-- | rbot/plugins/quotes.rb | 4 | ||||
-rw-r--r-- | rbot/plugins/remind.rb | 2 | ||||
-rw-r--r-- | rbot/plugins/roulette.rb | 2 |
20 files changed, 596 insertions, 155 deletions
@@ -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 @@ -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 |