]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/commitdiff
* Prevent multiple plugin registrations of the same name
authorTom Gilbert <tom@linuxbrit.co.uk>
Tue, 26 Jul 2005 21:50:00 +0000 (21:50 +0000)
committerTom Gilbert <tom@linuxbrit.co.uk>
Tue, 26 Jul 2005 21:50:00 +0000 (21:50 +0000)
* 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+$/}

20 files changed:
ChangeLog
rbot.rb
rbot/auth.rb
rbot/config.rb
rbot/ircbot.rb
rbot/ircsocket.rb
rbot/keywords.rb
rbot/message.rb
rbot/messagemapper.rb [new file with mode: 0644]
rbot/plugins.rb
rbot/plugins/autoop.rb
rbot/plugins/cal.rb
rbot/plugins/eightball.rb
rbot/plugins/karma.rb
rbot/plugins/lart.rb
rbot/plugins/nickserv.rb
rbot/plugins/opmeh.rb
rbot/plugins/quotes.rb
rbot/plugins/remind.rb
rbot/plugins/roulette.rb

index 1c4dcd581da211a0fc987b6dfdd9430a3386da60..30d2390c95520b188cad26ba45757f67f0794c89 100644 (file)
--- 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 4d13a0d00ea651e0e65a1a913c39eb05427c9ad5..60867daf88527053dd0ed4236c49b7168888bd5e 100755 (executable)
--- 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"])
index 017745ab9755895eb3297dbce105ba20173500da..7811d9e433d3b42af3082c48bd92a9b1da965e3f 100644 (file)
@@ -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
index 5289920549297f51e343f28c7e0c4aaa7ee28a4a..971a413c45f62838fedf28f15a2420dee42ff6ef 100644 (file)
 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
index 7129c10515176b8bc3ae2d484504996df05f1ebf..844231ddeddb19512467a2c1f0df7475f2e9f8fb 100644 (file)
@@ -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
index 258956446031b17bd177ac5bb9c8d87f35456633..358577369a752a253947a3e84d8a050ea6457290 100644 (file)
@@ -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
index f1997829f6814c75d4d12b77338a702a773e2112..3305af299899bab8116e1b3156448a530a75f64b 100644 (file)
@@ -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
index c217b1da0767f724f8285e8b51937390f965d5e2..d7f614ab7b884c12c96fc2070126fb548c389d6f 100644 (file)
@@ -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 (file)
index 0000000..d03721c
--- /dev/null
@@ -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
index b99fe562581862c067f71b6a5fa2ccb9129511cb..5db047fba88b8275e48eec952daea70385a92579 100644 (file)
@@ -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
index 094ee3433315005b975ce5bba1804e0c7b8be10d..fdbcf6e01d1d0536eba1ffd91da657bd01fcac06 100644 (file)
@@ -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
 
index 1e8231946653bf35ecae08851bb68409c5877f52..4f28310b60f640cd721eb0398302fb16e92eed98 100644 (file)
@@ -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'
index 6d123b34f5a4f6a4dddde5233aee7d6f3db68a1a..647484901218ed8f0a96dda2d1548c60dcffbe17 100644 (file)
@@ -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'
index 1bed175a92ff472a746d1ca2c4ebb55c0502a46e..148427a53cd15f36872f8c5abd2ba2ec7fc08bb4 100644 (file)
@@ -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'
index 385b17c3b509acd62afc57346b83e718b1803afb..1c72c648eb0ed98eb9dfb6507bac77cf280b200d 100644 (file)
@@ -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
        #}}}
        #}}}
index 94c57e6de0e5e39d2446bf9a05cee5a6712e1af1..1ef2baf7fdfff8cfcb0e7d596ffff97267557560 100644 (file)
@@ -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
index eb39251341797c4264f22aef9de194812bcb16b7..2776de60fa9fe8dac7dd2c0a76ae9744072ec862 100644 (file)
@@ -12,6 +12,7 @@ class OpMehPlugin < Plugin
     end\r
     target = m.sourcenick\r
     @bot.sendq("MODE #{channel} +o #{target}")\r
+    m.okay\r
   end\r
 end\r
 plugin = OpMehPlugin.new\r
index 0e46b4956cd5f8cd8bcbf293e05bb3191ab12c21..674a9ed6c71bee9e825426f5641f9a7ae0167579 100644 (file)
@@ -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
index 402e2d08bc7b68b08b93f01e526b86a589d726a2..5ad980aeac8d33012da673e0c0594796d8100f32 100644 (file)
@@ -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
index a3d102f33bab7685822e9d25db47de55c741ef81..c9d585ea8e7e576687037ab16ad2b7e66b2f2ca2 100644 (file)
@@ -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