]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/plugins.rb
fix: stderr vs stdout write override
[user/henk/code/ruby/rbot.git] / lib / rbot / plugins.rb
index 96f61efc24a54d00dc068d8b205b6a59a05604b4..06cc09e7527b8a3ad14feaea6ac144b7215b33e8 100644 (file)
@@ -4,11 +4,16 @@
 # :title: rbot plugin management
 
 require 'singleton'
 # :title: rbot plugin management
 
 require 'singleton'
+require_relative './core/utils/where_is.rb'
 
 module Irc
 
 module Irc
-    BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
+class Bot
+    Config.register Config::ArrayValue.new('plugins.blacklist',
       :default => [], :wizard => false, :requires_rescan => true,
       :desc => "Plugins that should not be loaded")
       :default => [], :wizard => false, :requires_rescan => true,
       :desc => "Plugins that should not be loaded")
+    Config.register Config::ArrayValue.new('plugins.whitelist',
+      :default => [], :wizard => false, :requires_rescan => true,
+      :desc => "Only whitelisted plugins will be loaded unless the list is empty")
 module Plugins
   require 'rbot/messagemapper'
 
 module Plugins
   require 'rbot/messagemapper'
 
@@ -17,7 +22,7 @@ module Plugins
   functionality. Rather than subclassing BotModule, however, one should
   subclass either CoreBotModule (reserved for system modules) or Plugin
   (for user plugins).
   functionality. Rather than subclassing BotModule, however, one should
   subclass either CoreBotModule (reserved for system modules) or Plugin
   (for user plugins).
-  
+
   A BotModule interacts with Irc events by defining one or more of the following
   methods, which get called as appropriate when the corresponding Irc event
   happens.
   A BotModule interacts with Irc events by defining one or more of the following
   methods, which get called as appropriate when the corresponding Irc event
   happens.
@@ -27,42 +32,42 @@ module Plugins
      map is the new, cleaner way to respond to specific message formats without
      littering your plugin code with regexps, and should be used instead of
      #register() and #privmsg() (see below) when possible.
      map is the new, cleaner way to respond to specific message formats without
      littering your plugin code with regexps, and should be used instead of
      #register() and #privmsg() (see below) when possible.
-     
+
      The difference between map and map! is that map! will not register the new
      command as an alternative name for the plugin.
 
      Examples:
 
      The difference between map and map! is that map! will not register the new
      command as an alternative name for the plugin.
 
      Examples:
 
-       plugin.map 'karmastats', :action => 'karma_stats'
+       plugin.map 'pointstats', :action => 'point_stats'
 
        # while in the plugin...
 
        # while in the plugin...
-       def karma_stats(m, params)
+       def point_stats(m, params)
          m.reply "..."
        end
 
        # the default action is the first component
          m.reply "..."
        end
 
        # the default action is the first component
-       plugin.map 'karma'
+       plugin.map 'points'
 
        # attributes can be pulled out of the match string
 
        # attributes can be pulled out of the match string
-       plugin.map 'karma for :key'
-       plugin.map 'karma :key'
+       plugin.map 'points for :key'
+       plugin.map 'points :key'
 
        # while in the plugin...
 
        # while in the plugin...
-       def karma(m, params)
+       def points(m, params)
          item = params[:key]
          item = params[:key]
-         m.reply 'karma for #{item}'
+         m.reply 'points for #{item}'
        end
 
        # you can setup defaults, to make parameters optional
        end
 
        # you can setup defaults, to make parameters optional
-       plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
+       plugin.map 'points :key', :defaults => {:key => 'defaultvalue'}
 
        # the default auth check is also against the first component
        # but that can be changed
 
        # the default auth check is also against the first component
        # but that can be changed
-       plugin.map 'karmastats', :auth => 'karma'
+       plugin.map 'pointstats', :auth => 'points'
 
        # maps can be restricted to public or private message:
 
        # maps can be restricted to public or private message:
-       plugin.map 'karmastats', :private => false
-       plugin.map 'karmastats', :public => false
+       plugin.map 'pointstats', :private => false
+       plugin.map 'pointstats', :public => false
 
      See MessageMapper#map for more information on the template format and the
      allowed options.
 
      See MessageMapper#map for more information on the template format and the
      allowed options.
@@ -81,6 +86,11 @@ module Plugins
                          use message.ctcp_reply, which sends a private NOTICE
                          to the sender.
 
                          use message.ctcp_reply, which sends a private NOTICE
                          to the sender.
 
+  message(PrivMessage)::
+                         Called for all PRIVMSG. Hook on this method if you
+                         need to handle PRIVMSGs regardless of whether they are
+                         addressed to the bot or not, and regardless of
+
   privmsg(PrivMessage)::
                          Called for a PRIVMSG if the first word matches one
                          the plugin #register()ed for. Use m.plugin to get
   privmsg(PrivMessage)::
                          Called for a PRIVMSG if the first word matches one
                          the plugin #register()ed for. Use m.plugin to get
@@ -90,10 +100,17 @@ module Plugins
   unreplied(PrivMessage)::
                          Called for a PRIVMSG which has not been replied to.
 
   unreplied(PrivMessage)::
                          Called for a PRIVMSG which has not been replied to.
 
+  notice(NoticeMessage)::
+                         Called for all Notices. Please notice that in general
+                         should not be replied to.
+
   kick(KickMessage)::
                          Called when a user (or the bot) is kicked from a
                          channel the bot is in.
 
   kick(KickMessage)::
                          Called when a user (or the bot) is kicked from a
                          channel the bot is in.
 
+  invite(InviteMessage)::
+                         Called when the bot is invited to a channel.
+
   join(JoinMessage)::
                          Called when a user (or the bot) joins a channel
 
   join(JoinMessage)::
                          Called when a user (or the bot) joins a channel
 
@@ -105,10 +122,20 @@ module Plugins
 
   nick(NickMessage)::
                          Called when a user (or the bot) changes Nick
 
   nick(NickMessage)::
                          Called when a user (or the bot) changes Nick
+  modechange(ModeChangeMessage)::
+                         Called when a User or Channel mode is changed
   topic(TopicMessage)::
                          Called when a user (or the bot) changes a channel
                          topic
 
   topic(TopicMessage)::
                          Called when a user (or the bot) changes a channel
                          topic
 
+  welcome(WelcomeMessage)::
+                         Called when the welcome message is received on
+                         joining a server succesfully.
+
+  motd(MotdMessage)::
+                         Called when the Message Of The Day is fully
+                         recevied from the server.
+
   connect::              Called when a server is joined successfully, but
                          before autojoin channels are joined (no params)
 
   connect::              Called when a server is joined successfully, but
                          before autojoin channels are joined (no params)
 
@@ -125,7 +152,17 @@ module Plugins
 =end
 
   class BotModule
 =end
 
   class BotModule
-    attr_reader :bot   # the associated bot
+    # the associated bot
+    attr_reader :bot
+
+    # the plugin registry
+    attr_reader :registry
+
+    # the message map handler
+    attr_reader :handler
+
+    # the directory in which the plugin is located
+    attr_reader :plugin_path
 
     # Initialise your bot module. Always call super if you override this method,
     # as important variables are set up for you:
 
     # Initialise your bot module. Always call super if you override this method,
     # as important variables are set up for you:
@@ -134,7 +171,7 @@ module Plugins
     #   the rbot instance
     # @registry::
     #   the botmodule's registry, which can be used to store permanent data
     #   the rbot instance
     # @registry::
     #   the botmodule's registry, which can be used to store permanent data
-    #   (see BotRegistryAccessor for additional documentation)
+    #   (see Registry::Accessor for additional documentation)
     #
     # Other instance variables which are defined and should not be overwritten
     # byt the user, but aren't usually accessed directly, are:
     #
     # Other instance variables which are defined and should not be overwritten
     # byt the user, but aren't usually accessed directly, are:
@@ -149,19 +186,27 @@ module Plugins
     def initialize
       @manager = Plugins::manager
       @bot = @manager.bot
     def initialize
       @manager = Plugins::manager
       @bot = @manager.bot
+      @priority = nil
 
       @botmodule_triggers = Array.new
 
       @handler = MessageMapper.new(self)
 
       @botmodule_triggers = Array.new
 
       @handler = MessageMapper.new(self)
-      @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
+      @registry = @bot.registry_factory.create(@bot.path, self.class.to_s.gsub(/^.*::/, ''))
 
       @manager.add_botmodule(self)
 
       @manager.add_botmodule(self)
+      @plugin_path = @manager.next_plugin_path
       if self.respond_to?('set_language')
         self.set_language(@bot.lang.language)
       end
     end
 
       if self.respond_to?('set_language')
         self.set_language(@bot.lang.language)
       end
     end
 
-    # Returns the symbol :BotModule 
+    # Changing the value of @priority directly will cause problems,
+    # Please use priority=.
+    def priority
+      @priority ||= 1
+    end
+
+    # Returns the symbol :BotModule
     def botmodule_class
       :BotModule
     end
     def botmodule_class
       :BotModule
     end
@@ -174,6 +219,7 @@ module Plugins
       @registry.flush
     end
 
       @registry.flush
     end
 
+
     # Method called to cleanup before the plugin is unloaded. If you overload
     # this method to handle additional cleanup tasks, remember to call super()
     # so that the default cleanup actions are taken care of as well.
     # Method called to cleanup before the plugin is unloaded. If you overload
     # this method to handle additional cleanup tasks, remember to call super()
     # so that the default cleanup actions are taken care of as well.
@@ -194,7 +240,7 @@ module Plugins
     # Signal to other BotModules that an even happened.
     #
     def call_event(ev, *args)
     # Signal to other BotModules that an even happened.
     #
     def call_event(ev, *args)
-      @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *args)
+      @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *(args.push Hash.new))
     end
 
     # call-seq: map(template, options)
     end
 
     # call-seq: map(template, options)
@@ -203,15 +249,7 @@ module Plugins
     # responds to appropriately-formed messages on Irc.
     #
     def map(*args)
     # responds to appropriately-formed messages on Irc.
     #
     def map(*args)
-      @handler.map(self, *args)
-      # register this map
-      name = @handler.last.items[0]
-      self.register name, :auth => nil
-      unless self.respond_to?('privmsg')
-        def self.privmsg(m) #:nodoc:
-          handle(m)
-        end
-      end
+      do_map(false, *args)
     end
 
     # call-seq: map!(template, options)
     end
 
     # call-seq: map!(template, options)
@@ -220,10 +258,17 @@ module Plugins
     # as an alternative name for the plugin.
     #
     def map!(*args)
     # as an alternative name for the plugin.
     #
     def map!(*args)
+      do_map(true, *args)
+    end
+
+    # Auxiliary method called by #map and #map!
+    def do_map(silent, *args)
       @handler.map(self, *args)
       # register this map
       @handler.map(self, *args)
       # register this map
-      name = @handler.last.items[0]
-      self.register name, :auth => nil, :hidden => true
+      map = @handler.last
+      name = map.items[0]
+      self.register name, :auth => nil, :hidden => silent
+      @manager.register_map(self, map)
       unless self.respond_to?('privmsg')
         def self.privmsg(m) #:nodoc:
           handle(m)
       unless self.respond_to?('privmsg')
         def self.privmsg(m) #:nodoc:
           handle(m)
@@ -282,7 +327,7 @@ module Plugins
     #
     # This command is now superceded by the #map() command, which should be used
     # instead whenever possible.
     #
     # This command is now superceded by the #map() command, which should be used
     # instead whenever possible.
-    # 
+    #
     def register(cmd, opts={})
       raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
       who = @manager.who_handles?(cmd)
     def register(cmd, opts={})
       raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
       who = @manager.who_handles?(cmd)
@@ -302,9 +347,40 @@ module Plugins
     # MessageMapper uses 'usage' as its default fallback method.
     #
     def usage(m, params = {})
     # MessageMapper uses 'usage' as its default fallback method.
     #
     def usage(m, params = {})
+      if params[:failures].respond_to? :find
+        friendly = params[:failures].find do |f|
+          f.kind_of? MessageMapper::FriendlyFailure
+        end
+        if friendly
+          m.reply friendly.friendly
+          return
+        end
+      end
       m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
     end
 
       m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
     end
 
+    # Define the priority of the module.  During event delegation, lower
+    # priority modules will be called first.  Default priority is 1
+    def priority=(prio)
+      if @priority != prio
+        @priority = prio
+        @bot.plugins.mark_priorities_dirty
+      end
+    end
+
+    # Directory name to be joined to the botclass to access data files. By
+    # default this is the plugin name itself, but may be overridden, for
+    # example by plugins that share their datafiles or for backwards
+    # compatibilty
+    def dirname
+      name
+    end
+
+    # Filename for a datafile built joining the botclass, plugin dirname and
+    # actual file name
+    def datafile(*fname)
+      @bot.path dirname, *fname
+    end
   end
 
   # A CoreBotModule is a BotModule that provides core functionality.
   end
 
   # A CoreBotModule is a BotModule that provides core functionality.
@@ -337,6 +413,11 @@ module Plugins
     include Singleton
     attr_reader :bot
     attr_reader :botmodules
     include Singleton
     attr_reader :bot
     attr_reader :botmodules
+    attr_reader :maps
+
+    attr_reader :core_module_dirs
+    attr_reader :plugin_dirs
+    attr_reader :next_plugin_path
 
     # This is the list of patterns commonly delegated to plugins.
     # A fast delegation lookup is enabled for them.
 
     # This is the list of patterns commonly delegated to plugins.
     # A fast delegation lookup is enabled for them.
@@ -356,11 +437,17 @@ module Plugins
 
       @names_hash = Hash.new
       @commandmappers = Hash.new
 
       @names_hash = Hash.new
       @commandmappers = Hash.new
+      @maps = Hash.new
+
+      # modules will be sorted on first delegate call
+      @sorted_modules = nil
+
       @delegate_list = Hash.new { |h, k|
         h[k] = Array.new
       }
 
       @delegate_list = Hash.new { |h, k|
         h[k] = Array.new
       }
 
-      @dirs = []
+      @core_module_dirs = []
+      @plugin_dirs = []
 
       @failed = Array.new
       @ignored = Array.new
 
       @failed = Array.new
       @ignored = Array.new
@@ -368,13 +455,46 @@ module Plugins
       bot_associate(nil)
     end
 
       bot_associate(nil)
     end
 
+    def inspect
+      ret = self.to_s[0..-2]
+      ret << ' corebotmodules='
+      ret << @botmodules[:CoreBotModule].map { |m|
+        m.name
+      }.inspect
+      ret << ' plugins='
+      ret << @botmodules[:Plugin].map { |m|
+        m.name
+      }.inspect
+      ret << ">"
+    end
+
     # Reset lists of botmodules
     # Reset lists of botmodules
-    def reset_botmodule_lists
-      @botmodules[:CoreBotModule].clear
-      @botmodules[:Plugin].clear
-      @names_hash.clear
-      @commandmappers.clear
-      @failures_shown = false
+    #
+    # :botmodule ::
+    #   optional instance of a botmodule to remove from the lists
+    def reset_botmodule_lists(botmodule=nil)
+      if botmodule
+        # deletes only references of the botmodule
+        @botmodules[:CoreBotModule].delete botmodule
+        @botmodules[:Plugin].delete botmodule
+        @names_hash.delete_if {|key, value| value == botmodule}
+        @commandmappers.delete_if {|key, value| value[:botmodule] == botmodule }
+        @delegate_list.each_pair { |cmd, list|
+          list.delete botmodule
+        }
+        @delegate_list.delete_if {|key, value| value.empty?}
+        @maps.delete_if {|key, value| value[:botmodule] == botmodule }
+        @failures_shown = false
+      else
+        @botmodules[:CoreBotModule].clear
+        @botmodules[:Plugin].clear
+        @names_hash.clear
+        @commandmappers.clear
+        @delegate_list.clear
+        @maps.clear
+        @failures_shown = false
+      end
+      mark_priorities_dirty
     end
 
     # Associate with bot _bot_
     end
 
     # Associate with bot _bot_
@@ -385,9 +505,16 @@ module Plugins
 
     # Returns the botmodule with the given _name_
     def [](name)
 
     # Returns the botmodule with the given _name_
     def [](name)
+      return if not name
       @names_hash[name.to_sym]
     end
 
       @names_hash[name.to_sym]
     end
 
+    # Returns +true+ if a botmodule named _name_ exists.
+    def has_key?(name)
+      return if not name
+      @names_hash.has_key?(name.to_sym)
+    end
+
     # Returns +true+ if _cmd_ has already been registered as a command
     def who_handles?(cmd)
       return nil unless @commandmappers.has_key?(cmd.to_sym)
     # Returns +true+ if _cmd_ has already been registered as a command
     def who_handles?(cmd)
       return nil unless @commandmappers.has_key?(cmd.to_sym)
@@ -400,6 +527,19 @@ module Plugins
       @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
     end
 
       @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
     end
 
+    # Registers botmodule _botmodule_ with map _map_. This adds the map to the #maps hash
+    # which has three keys:
+    #
+    # botmodule:: the associated botmodule
+    # auth:: an array of auth keys checked by the map; the first is the full_auth_path of the map
+    # map:: the actual MessageTemplate object
+    #
+    #
+    def register_map(botmodule, map)
+      raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
+      @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map }
+    end
+
     def add_botmodule(botmodule)
       raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
       kl = botmodule.botmodule_class
     def add_botmodule(botmodule)
       raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
       kl = botmodule.botmodule_class
@@ -413,6 +553,12 @@ module Plugins
       end
       @botmodules[kl] << botmodule
       @names_hash[botmodule.to_sym] = botmodule
       end
       @botmodules[kl] << botmodule
       @names_hash[botmodule.to_sym] = botmodule
+      # add itself to the delegate list for the fast-delegation
+      # of methods like cleanup or privmsg, etc..
+      botmodule.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
+        @delegate_list[m.intern] << botmodule
+      }
+      mark_priorities_dirty
     end
 
     # Returns an array of the loaded plugins
     end
 
     # Returns an array of the loaded plugins
@@ -431,15 +577,26 @@ module Plugins
       @commandmappers
     end
 
       @commandmappers
     end
 
+    # Tells the PluginManager that the next time it delegates an event, it
+    # should sort the modules by priority
+    def mark_priorities_dirty
+      @sorted_modules = nil
+    end
+
     # Makes a string of error _err_ by adding text _str_
     def report_error(str, err)
       ([str, err.inspect] + err.backtrace).join("\n")
     end
 
     # Makes a string of error _err_ by adding text _str_
     def report_error(str, err)
       ([str, err.inspect] + err.backtrace).join("\n")
     end
 
+    def get_plugin(name)
+      plugins.find { |plugin| plugin.name == name }
+    end
+
     # This method is the one that actually loads a module from the
     # file _fname_
     #
     # This method is the one that actually loads a module from the
     # file _fname_
     #
-    # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
+    # _desc_ is a simple description of what we are loading
+    # (plugin/botmodule/whatever) for error reporting
     #
     # It returns the Symbol :loaded on success, and an Exception
     # on failure
     #
     # It returns the Symbol :loaded on success, and an Exception
     # on failure
@@ -449,13 +606,23 @@ module Plugins
       # the idea here is to prevent namespace pollution. perhaps there
       # is another way?
       plugin_module = Module.new
       # the idea here is to prevent namespace pollution. perhaps there
       # is another way?
       plugin_module = Module.new
+      
+      # each plugin uses its own textdomain, we bind it automatically here
+      bindtextdomain_to(plugin_module, "rbot-#{File.basename(fname, '.rb')}")
 
       desc = desc.to_s + " " if desc
 
       begin
 
       desc = desc.to_s + " " if desc
 
       begin
-        plugin_string = IO.readlines(fname).join("")
+        plugin_string = IO.read(fname)
         debug "loading #{desc}#{fname}"
         debug "loading #{desc}#{fname}"
+
+        # set path of the plugin that will be loaded next (see BotModule#initialize)
+        @next_plugin_path = File.dirname fname
+
         plugin_module.module_eval(plugin_string, fname)
         plugin_module.module_eval(plugin_string, fname)
+
+        @next_plugin_path = nil
+
         return :loaded
       rescue Exception => err
         # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
         return :loaded
       rescue Exception => err
         # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
@@ -468,53 +635,98 @@ module Plugins
             "#{fname}#{$1}#{$3}"
           }
         }
             "#{fname}#{$1}#{$3}"
           }
         }
-        msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
+        msg = err.to_s.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
           "#{fname}#{$1}#{$3}"
         }
           "#{fname}#{$1}#{$3}"
         }
-        newerr = err.class.new(msg)
+        msg.gsub!(fname, File.basename(fname))
+        begin
+          newerr = err.class.new(msg)
+        rescue ArgumentError => aerr_in_err
+          # Somebody should hang the ActiveSupport developers by their balls
+          # with barbed wire. Their MissingSourceFile extension to LoadError
+          # _expects_ a second argument, breaking the usual Exception interface
+          # (instead, the smart thing to do would have been to make the second
+          # parameter optional and run the code in the from_message method if
+          # it was missing).
+          # Anyway, we try to cope with this in the simplest possible way. On
+          # the upside, this new block can be extended to handle other similar
+          # idiotic approaches
+          if err.class.respond_to? :from_message
+            newerr = err.class.from_message(msg)
+          elsif ([:file, :line, :column, :offset, :problem, :context] & err.methods).length == 6
+            # Another â€˜brillian’ overload, this time from Psych::SyntaxError
+            # In this case we'll just leave the message as-is
+            newerr = err.dup
+          else
+            raise aerr_in_err
+          end
+        rescue NoMethodError => nmerr_in_err
+          # Another braindead extension to StandardError, OAuth2::Error,
+          # doesn't get a string as message, but a response
+          if err.respond_to? :response
+            newerr = err.class.new(err.response)
+          else
+            raise nmerr_in_err
+          end
+        end
         newerr.set_backtrace(bt)
         return newerr
       end
     end
         newerr.set_backtrace(bt)
         return newerr
       end
     end
-    private :load_botmodule_file
 
     # add one or more directories to the list of directories to
 
     # add one or more directories to the list of directories to
-    # load botmodules from
-    #
-    # TODO find a way to specify necessary plugins which _must_ be loaded
-    #
-    def add_botmodule_dir(*dirlist)
-      @dirs += dirlist
-      debug "Botmodule loading path: #{@dirs.join(', ')}"
+    # load core modules from
+    def add_core_module_dir(*dirlist)
+      @core_module_dirs += dirlist
+      debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
     end
 
     end
 
-    def clear_botmodule_dirs
-      @dirs.clear
-      debug "Botmodule loading path cleared"
+    # add one or more directories to the list of directories to
+    # load plugins from
+    def add_plugin_dir(*dirlist)
+      @plugin_dirs += dirlist
+      debug "Plugin loading paths: #{@plugin_dirs.join(', ')}"
     end
 
     end
 
-    # load plugins from pre-assigned list of directories
-    def scan
-      @failed.clear
-      @ignored.clear
-      @delegate_list.clear
+    def clear_botmodule_dirs
+      @core_module_dirs.clear
+      @plugin_dirs.clear
+      debug "Core module and plugin loading paths cleared"
+    end
 
 
+    def scan_botmodules(opts={})
+      type = opts[:type]
       processed = Hash.new
 
       processed = Hash.new
 
-      @bot.config['plugins.blacklist'].each { |p|
-        pn = p + ".rb"
-        processed[pn.intern] = :blacklisted
-      }
+      case type
+      when :core
+        dirs = @core_module_dirs
+      when :plugins
+        dirs = @plugin_dirs
 
 
-      dirs = @dirs
-      dirs.each {|dir|
-        if(FileTest.directory?(dir))
-          d = Dir.new(dir)
-          d.sort.each {|file|
+        @bot.config['plugins.blacklist'].each { |p|
+          pn = p + ".rb"
+          processed[pn.intern] = :blacklisted
+        }
 
 
-            next if(file =~ /^\./)
+        whitelist = @bot.config['plugins.whitelist'].map { |p|
+          p + ".rb"
+        }
+      end
 
 
-            if processed.has_key?(file.intern)
+      dirs.each do |dir|
+        next unless FileTest.directory?(dir)
+        d = Dir.new(dir)
+        d.sort.each do |file|
+          next unless file =~ /\.rb$/
+          next if file =~ /^\./
+
+          case type
+          when :plugins
+            if !whitelist.empty? && !whitelist.include?(file)
+              @ignored << {:name => file, :dir => dir, :reason => :"not whitelisted" }
+              next
+            elsif processed.has_key?(file.intern)
               @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
               next
             end
               @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
               next
             end
@@ -528,46 +740,87 @@ module Plugins
               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
               next
             end
               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
               next
             end
+          end
 
 
-            next unless(file =~ /\.rb$/)
-
+          begin
             did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
             did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
-            case did_it
-            when Symbol
-              processed[file.intern] = did_it
-            when Exception
-              @failed <<  { :name => file, :dir => dir, :reason => did_it }
-            end
+          rescue Exception => e
+            error e
+            did_it = e
+          end
 
 
-          }
+          case did_it
+          when Symbol
+            processed[file.intern] = did_it
+          when Exception
+            @failed << { :name => file, :dir => dir, :reason => did_it }
+          end
         end
         end
-      }
+      end
+    end
+
+    # load plugins from pre-assigned list of directories
+    def scan
+      @failed.clear
+      @ignored.clear
+      @delegate_list.clear
+
+      scan_botmodules(:type => :core)
+      scan_botmodules(:type => :plugins)
+
       debug "finished loading plugins: #{status(true)}"
       debug "finished loading plugins: #{status(true)}"
-      (core_modules + plugins).each { |p|
-       p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
-         @delegate_list[m.intern] << p
-       }
-      }
+      mark_priorities_dirty
     end
 
     # call the save method for each active plugin
     end
 
     # call the save method for each active plugin
-    def save
-      delegate 'flush_registry'
-      delegate 'save'
+    #
+    # :botmodule ::
+    #   optional instance of a botmodule to save
+    def save(botmodule=nil)
+      if botmodule
+        botmodule.flush_registry
+        botmodule.save if botmodule.respond_to? 'save'
+      else
+        delegate 'flush_registry'
+        delegate 'save'
+      end
     end
 
     # call the cleanup method for each active plugin
     end
 
     # call the cleanup method for each active plugin
-    def cleanup
-      delegate 'cleanup'
-      reset_botmodule_lists
+    #
+    # :botmodule ::
+    #   optional instance of a botmodule to cleanup
+    def cleanup(botmodule=nil)
+      if botmodule
+        botmodule.cleanup
+      else
+        delegate 'cleanup'
+      end
+      reset_botmodule_lists(botmodule)
     end
 
     end
 
-    # drop all plugins and rescan plugins on disk
-    # calls save and cleanup for each plugin before dropping them
-    def rescan
-      save
-      cleanup
-      scan
+    # drops botmodules and rescan botmodules on disk
+    # calls save and cleanup for each botmodule before dropping them
+    # a optional _botmodule_ argument might specify a botmodule 
+    # instance that should be reloaded
+    #
+    # :botmodule ::
+    #   instance of the botmodule to rescan
+    def rescan(botmodule=nil)
+      save(botmodule)
+      cleanup(botmodule)
+      if botmodule
+        @failed.clear
+        @ignored.clear
+        filename = where_is(botmodule.class)
+        err = load_botmodule_file(filename, "plugin")
+        if err.is_a? Exception
+          @failed << { :name => botmodule.to_s,
+                       :dir => File.dirname(filename), :reason => err }
+        end
+      else
+        scan
+      end
     end
 
     def status(short=false)
     end
 
     def status(short=false)
@@ -632,6 +885,20 @@ module Plugins
       output.join '; '
     end
 
       output.join '; '
     end
 
+    # returns the last logged failure (if present) of a botmodule
+    #
+    # :name ::
+    #   name of the botmodule
+    def botmodule_failure(name)
+      failure = @failed.find { |f| f[:name] == name }
+      if failure
+        "%{exception}: %{reason}" % {
+          :exception => failure[:reason].class,
+          :reason => failure[:reason]
+        }
+      end
+    end
+
     # return list of help topics (plugin names)
     def helptopics
       rv = status
     # return list of help topics (plugin names)
     def helptopics
       rv = status
@@ -683,9 +950,9 @@ module Plugins
         key = $1
         params = $2
 
         key = $1
         params = $2
 
-       # Let's see if we can match a plugin by the given name
+        # Let's see if we can match a plugin by the given name
         (core_modules + plugins).each { |p|
         (core_modules + plugins).each { |p|
-         next unless p.name == key
+          next unless p.name == key
           begin
             return p.help(key, params)
           rescue Exception => err
           begin
             return p.help(key, params)
           rescue Exception => err
@@ -694,7 +961,7 @@ module Plugins
           end
         }
 
           end
         }
 
-       # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
+        # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
         k = key.to_sym
         if commands.has_key?(k)
           p = commands[k][:botmodule]
         k = key.to_sym
         if commands.has_key?(k)
           p = commands[k][:botmodule]
@@ -709,9 +976,65 @@ module Plugins
       return false
     end
 
       return false
     end
 
-    # see if each plugin handles +method+, and if so, call it, passing
-    # +message+ as a parameter
+    def sort_modules
+      @sorted_modules = (core_modules + plugins).sort do |a, b|
+        a.priority <=> b.priority
+      end || []
+
+      @delegate_list.each_value do |list|
+        list.sort! {|a,b| a.priority <=> b.priority}
+      end
+    end
+
+    # delegate(method, [m,] opts={})
+    #
+    # see if each plugin handles _method_, and if so, call it, passing
+    # _m_ as a parameter (if present). BotModules are called in order of
+    # priority from lowest to highest.
+    #
+    # If the passed _m_ is a BasicUserMessage and is marked as #ignored?, it
+    # will only be delegated to plugins with negative priority. Conversely, if
+    # it's a fake message (see BotModule#fake_message), it will only be
+    # delegated to plugins with positive priority.
+    #
+    # Note that _m_ can also be an exploded Array, but in this case the last
+    # element of it cannot be a Hash, or it will be interpreted as the options
+    # Hash for delegate itself. The last element can be a subclass of a Hash, though.
+    # To be on the safe side, you can add an empty Hash as last parameter for delegate
+    # when calling it with an exploded Array:
+    #   @bot.plugins.delegate(method, *(args.push Hash.new))
+    #
+    # Currently supported options are the following:
+    # :above ::
+    #   if specified, the delegation will only consider plugins with a priority
+    #   higher than the specified value
+    # :below ::
+    #   if specified, the delegation will only consider plugins with a priority
+    #   lower than the specified value
+    #
     def delegate(method, *args)
     def delegate(method, *args)
+      # if the priorities order of the delegate list is dirty,
+      # meaning some modules have been added or priorities have been
+      # changed, then the delegate list will need to be sorted before
+      # delegation.  This should always be true for the first delegation.
+      sort_modules unless @sorted_modules
+
+      opts = {}
+      opts.merge!(args.pop) if args.last.class == Hash
+
+      m = args.first
+      if BasicUserMessage === m
+        # ignored messages should not be delegated
+        # to plugins with positive priority
+        opts[:below] ||= 0 if m.ignored?
+        # fake messages should not be delegated
+        # to plugins with negative priority
+        opts[:above] ||= 0 if m.recurse_depth > 0
+      end
+
+      above = opts[:above]
+      below = opts[:below]
+
       # debug "Delegating #{method.inspect}"
       ret = Array.new
       if method.match(DEFAULT_DELEGATE_PATTERNS)
       # debug "Delegating #{method.inspect}"
       ret = Array.new
       if method.match(DEFAULT_DELEGATE_PATTERNS)
@@ -721,24 +1044,28 @@ module Plugins
         return [] unless @delegate_list.has_key?(m)
         @delegate_list[m].each { |p|
           begin
         return [] unless @delegate_list.has_key?(m)
         @delegate_list[m].each { |p|
           begin
-            ret.push p.send(method, *args)
+            prio = p.priority
+            unless (above and above >= prio) or (below and below <= prio)
+              ret.push p.send(method, *args)
+            end
           rescue Exception => err
             raise if err.kind_of?(SystemExit)
             error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
           rescue Exception => err
             raise if err.kind_of?(SystemExit)
             error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
-            raise if err.kind_of?(BDB::Fatal)
           end
         }
       else
         debug "slow-delegating #{method}"
           end
         }
       else
         debug "slow-delegating #{method}"
-        (core_modules + plugins).each { |p|
+        @sorted_modules.each { |p|
           if(p.respond_to? method)
             begin
               # debug "#{p.botmodule_class} #{p.name} responds"
           if(p.respond_to? method)
             begin
               # debug "#{p.botmodule_class} #{p.name} responds"
-              ret.push p.send(method, *args)
+              prio = p.priority
+              unless (above and above >= prio) or (below and below <= prio)
+                ret.push p.send(method, *args)
+              end
             rescue Exception => err
               raise if err.kind_of?(SystemExit)
               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
             rescue Exception => err
               raise if err.kind_of?(SystemExit)
               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
-              raise if err.kind_of?(BDB::Fatal)
             end
           end
         }
             end
           end
         }
@@ -750,7 +1077,7 @@ module Plugins
     # see if we have a plugin that wants to handle this message, if so, pass
     # it to the plugin and return true, otherwise false
     def privmsg(m)
     # see if we have a plugin that wants to handle this message, if so, pass
     # it to the plugin and return true, otherwise false
     def privmsg(m)
-      # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
+      debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
       return unless m.plugin
       k = m.plugin.to_sym
       if commands.has_key?(k)
       return unless m.plugin
       k = m.plugin.to_sym
       if commands.has_key?(k)
@@ -758,30 +1085,44 @@ module Plugins
         a = commands[k][:auth]
         # We check here for things that don't check themselves
         # (e.g. mapped things)
         a = commands[k][:auth]
         # We check here for things that don't check themselves
         # (e.g. mapped things)
-        debug "Checking auth ..."
+        debug "Checking auth ..."
         if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
         if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
-          debug "Checking response ..."
+          debug "Checking response ..."
           if p.respond_to?("privmsg")
             begin
           if p.respond_to?("privmsg")
             begin
-              debug "#{p.botmodule_class} #{p.name} responds"
+              debug "#{p.botmodule_class} #{p.name} responds"
               p.privmsg(m)
             rescue Exception => err
               raise if err.kind_of?(SystemExit)
               error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
               p.privmsg(m)
             rescue Exception => err
               raise if err.kind_of?(SystemExit)
               error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
-              raise if err.kind_of?(BDB::Fatal)
             end
             end
-            # debug "Successfully delegated #{m.message}"
+            debug "Successfully delegated #{m.inspect}"
             return true
           else
             return true
           else
-            debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
+            debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
           end
         else
           end
         else
-          debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
+          debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
         end
         end
+      else
+        debug "Command #{k} isn't handled"
       end
       end
-      # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
       return false
       return false
-      # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
+    end
+
+    # delegate IRC messages, by delegating 'listen' first, and the actual method
+    # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message
+    # as appropriate.
+    def irc_delegate(method, m)
+      delegate('listen', m)
+      if method.to_sym == :privmsg
+        delegate('ctcp_listen', m) if m.ctcp
+        delegate('message', m)
+        privmsg(m) if m.address? and not m.ignored?
+        delegate('unreplied', m) unless m.replied
+      else
+        delegate(method, m)
+      end
     end
   end
 
     end
   end
 
@@ -792,3 +1133,4 @@ module Plugins
 
 end
 end
 
 end
 end
+end