]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/plugins.rb
core/config: remove leftover heavy-load debug line
[user/henk/code/ruby/rbot.git] / lib / rbot / plugins.rb
index 9fa7ad88ecbe5c5297a5e339c07eefdc7af804ce..99ae31b6f03db8ddd2d7ed32493879ca3ec409cb 100644 (file)
@@ -1,6 +1,8 @@
+require 'singleton'
+
 module Irc
     BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
 module Irc
     BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
-      :default => [], :wizard => false, :requires_restart => true,
+      :default => [], :wizard => false, :requires_rescan => true,
       :desc => "Plugins that should not be loaded")
 module Plugins
   require 'rbot/messagemapper'
       :desc => "Plugins that should not be loaded")
 module Plugins
   require 'rbot/messagemapper'
@@ -59,11 +61,14 @@ module Plugins
                          etc.
 
   privmsg(PrivMessage)::
                          etc.
 
   privmsg(PrivMessage)::
-                         called for a PRIVMSG if the first word matches one
+                         Called for a PRIVMSG if the first word matches one
                          the plugin register()d for. Use m.plugin to get
                          that word and m.params for the rest of the message,
                          if applicable.
 
                          the plugin register()d for. Use m.plugin to get
                          that word and m.params for the rest of the message,
                          if applicable.
 
+  unreplied(PrivMessage)::
+                         Called for a PRIVMSG which has not been 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.
@@ -86,6 +91,10 @@ module Plugins
   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)
 
+  set_language(String)::
+                         Called when the user sets a new language
+                         whose name is the given String
+
   save::                 Called when you are required to save your plugin's
                          state, if you maintain data between sessions
 
   save::                 Called when you are required to save your plugin's
                          state, if you maintain data between sessions
 
@@ -94,15 +103,28 @@ module Plugins
                          files/connections or flush caches here
 =end
 
                          files/connections or flush caches here
 =end
 
-  class Plugin
+  class BotModule
     attr_reader :bot   # the associated bot
     attr_reader :bot   # the associated bot
-    # initialise your plugin. Always call super if you override this method,
+
+    # initialise your bot module. Always call super if you override this method,
     # as important variables are set up for you
     def initialize
     # as important variables are set up for you
     def initialize
-      @bot = Plugins.bot
-      @names = Array.new
+      @manager = Plugins::manager
+      @bot = @manager.bot
+
+      @botmodule_triggers = Array.new
+
       @handler = MessageMapper.new(self)
       @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
       @handler = MessageMapper.new(self)
       @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
+
+      @manager.add_botmodule(self)
+      if self.respond_to?('set_language')
+        self.set_language(@bot.lang.language)
+      end
+    end
+
+    def botmodule_class
+      :BotModule
     end
 
     def flush_registry
     end
 
     def flush_registry
@@ -119,11 +141,15 @@ module Plugins
       @handler.handle(m)
     end
 
       @handler.handle(m)
     end
 
+    def call_event(ev, *args)
+      @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *args)
+    end
+
     def map(*args)
     def map(*args)
-      @handler.map(*args)
+      @handler.map(self, *args)
       # register this map
       name = @handler.last.items[0]
       # register this map
       name = @handler.last.items[0]
-      self.register name
+      self.register name, :auth => nil
       unless self.respond_to?('privmsg')
         def self.privmsg(m)
           handle(m)
       unless self.respond_to?('privmsg')
         def self.privmsg(m)
           handle(m)
@@ -132,10 +158,10 @@ module Plugins
     end
 
     def map!(*args)
     end
 
     def map!(*args)
-      @handler.map(*args)
+      @handler.map(self, *args)
       # register this map
       name = @handler.last.items[0]
       # register this map
       name = @handler.last.items[0]
-      self.register name, {:hidden => true}
+      self.register name, :auth => nil, :hidden => true
       unless self.respond_to?('privmsg')
         def self.privmsg(m)
           handle(m)
       unless self.respond_to?('privmsg')
         def self.privmsg(m)
           handle(m)
@@ -143,10 +169,39 @@ module Plugins
       end
     end
 
       end
     end
 
+    # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
+    # usually _chan_ is either "*" for everywhere, public and private (in which
+    # case it can be omitted) or "?" for private communications
+    #
+    def default_auth(cmd, val, chan="*")
+      case cmd
+      when "*", ""
+        c = nil
+      else
+        c = cmd
+      end
+      Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
+    end
+
+    # Gets the default command path which would be given to command _cmd_
+    def propose_default_path(cmd)
+      [name, cmd].compact.join("::")
+    end
+
     # return an identifier for this plugin, defaults to a list of the message
     # prefixes handled (used for error messages etc)
     def name
     # return an identifier for this plugin, defaults to a list of the message
     # prefixes handled (used for error messages etc)
     def name
-      @names.join("|")
+      self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
+    end
+
+    # just calls name
+    def to_s
+      name
+    end
+
+    # intern the name
+    def to_sym
+      self.name.to_sym
     end
 
     # return a help string for your module. for complex modules, you may wish
     end
 
     # return a help string for your module. for complex modules, you may wish
@@ -161,64 +216,203 @@ module Plugins
     # register the plugin as a handler for messages prefixed +name+
     # this can be called multiple times for a plugin to handle multiple
     # message prefixes
     # register the plugin as a handler for messages prefixed +name+
     # this can be called multiple times for a plugin to handle multiple
     # message prefixes
-    def register(name,opts={})
+    def register(cmd, opts={})
       raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
       raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
-      return if Plugins.plugins.has_key?(name)
-      Plugins.plugins[name] = self
-      @names << name unless opts.fetch(:hidden, false)
+      who = @manager.who_handles?(cmd)
+      if who
+        raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
+        return
+      end
+      if opts.has_key?(:auth)
+        @manager.register(self, cmd, opts[:auth])
+      else
+        @manager.register(self, cmd, propose_default_path(cmd))
+      end
+      @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
     end
 
     # default usage method provided as a utility for simple plugins. The
     # MessageMapper uses 'usage' as its default fallback method.
     def usage(m, params = {})
     end
 
     # 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}'"
+      m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
+    end
+
+  end
+
+  class CoreBotModule < BotModule
+    def botmodule_class
+      :CoreBotModule
     end
     end
+  end
 
 
+  class Plugin < BotModule
+    def botmodule_class
+      :Plugin
+    end
   end
 
   end
 
-  # class to manage multiple plugins and delegate messages to them for
+  # Singleton to manage multiple plugins and delegate messages to them for
   # handling
   # handling
-  class Plugins
-    # hash of registered message prefixes and associated plugins
-    @@plugins = Hash.new
-    # associated IrcBot class
-    @@bot = nil
-
-    # bot::     associated IrcBot class
-    # dirlist:: array of directories to scan (in order) for plugins
+  class PluginManagerClass
+    include Singleton
+    attr_reader :bot
+    attr_reader :botmodules
+
+    def initialize
+      @botmodules = {
+        :CoreBotModule => [],
+        :Plugin => []
+      }
+
+      @names_hash = Hash.new
+      @commandmappers = Hash.new
+
+      @dirs = []
+
+      @failed = Array.new
+      @ignored = Array.new
+
+      bot_associate(nil)
+    end
+
+    # Reset lists of botmodules
+    def reset_botmodule_lists
+      @botmodules[:CoreBotModule].clear
+      @botmodules[:Plugin].clear
+      @names_hash.clear
+      @commandmappers.clear
+      @failures_shown = false
+    end
+
+    # Associate with bot _bot_
+    def bot_associate(bot)
+      reset_botmodule_lists
+      @bot = bot
+    end
+
+    # Returns the botmodule with the given _name_
+    def [](name)
+      @names_hash[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)
+      return @commandmappers[cmd.to_sym][:botmodule]
+    end
+
+    # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
+    def register(botmodule, cmd, auth_path)
+      raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
+      @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
+    end
+
+    def add_botmodule(botmodule)
+      raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
+      kl = botmodule.botmodule_class
+      if @names_hash.has_key?(botmodule.to_sym)
+        case self[botmodule].botmodule_class
+        when kl
+          raise "#{kl} #{botmodule} already registered!"
+        else
+          raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
+        end
+      end
+      @botmodules[kl] << botmodule
+      @names_hash[botmodule.to_sym] = botmodule
+    end
+
+    # Returns an array of the loaded plugins
+    def core_modules
+      @botmodules[:CoreBotModule]
+    end
+
+    # Returns an array of the loaded plugins
+    def plugins
+      @botmodules[:Plugin]
+    end
+
+    # Returns a hash of the registered message prefixes and associated
+    # plugins
+    def commands
+      @commandmappers
+    end
+
+    # Makes a string of error _err_ by adding text _str_
+    def report_error(str, err)
+      ([str, err.inspect] + err.backtrace).join("\n")
+    end
+
+    # This method is the one that actually loads a module from the
+    # file _fname_
     #
     #
-    # create a new plugin handler, scanning for plugins in +dirlist+
-    def initialize(bot, dirlist)
-      @@bot = bot
-      @dirs = dirlist
-      scan
+    # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
+    #
+    # It returns the Symbol :loaded on success, and an Exception
+    # on failure
+    #
+    def load_botmodule_file(fname, desc=nil)
+      # create a new, anonymous module to "house" the plugin
+      # the idea here is to prevent namespace pollution. perhaps there
+      # is another way?
+      plugin_module = Module.new
+
+      desc = desc.to_s + " " if desc
+
+      begin
+        plugin_string = IO.readlines(fname).join("")
+        debug "loading #{desc}#{fname}"
+        plugin_module.module_eval(plugin_string, fname)
+        return :loaded
+      rescue Exception => err
+        # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
+        error report_error("#{desc}#{fname} load failed", err)
+        bt = err.backtrace.select { |line|
+          line.match(/^(\(eval\)|#{fname}):\d+/)
+        }
+        bt.map! { |el|
+          el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
+            "#{fname}#{$1}#{$3}"
+          }
+        }
+        msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
+          "#{fname}#{$1}#{$3}"
+        }
+        newerr = err.class.new(msg)
+        newerr.set_backtrace(bt)
+        return newerr
+      end
     end
     end
+    private :load_botmodule_file
 
 
-    # access to associated bot
-    def Plugins.bot
-      @@bot
+    # 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(', ')}"
     end
 
     end
 
-    # access to list of plugins
-    def Plugins.plugins
-      @@plugins
+    def clear_botmodule_dirs
+      @dirs.clear
+      debug "Botmodule loading path cleared"
     end
 
     # load plugins from pre-assigned list of directories
     def scan
     end
 
     # load plugins from pre-assigned list of directories
     def scan
-      @failed = Array.new
-      @ignored = Array.new
+      @failed.clear
+      @ignored.clear
       processed = Hash.new
 
       processed = Hash.new
 
-      @@bot.config['plugins.blacklist'].each { |p|
+      @bot.config['plugins.blacklist'].each { |p|
         pn = p + ".rb"
         processed[pn.intern] = :blacklisted
       }
 
         pn = p + ".rb"
         processed[pn.intern] = :blacklisted
       }
 
-      dirs = Array.new
-      dirs << Config::datadir + "/plugins"
-      dirs += @dirs
-      dirs.reverse.each {|dir|
+      dirs = @dirs
+      dirs.each {|dir|
         if(FileTest.directory?(dir))
           d = Dir.new(dir)
           d.sort.each {|file|
         if(FileTest.directory?(dir))
           d = Dir.new(dir)
           d.sort.each {|file|
@@ -242,43 +436,18 @@ module Plugins
 
             next unless(file =~ /\.rb$/)
 
 
             next unless(file =~ /\.rb$/)
 
-            tmpfilename = "#{dir}/#{file}"
-
-            # create a new, anonymous module to "house" the plugin
-            # the idea here is to prevent namespace pollution. perhaps there
-            # is another way?
-            plugin_module = Module.new
-
-            begin
-              plugin_string = IO.readlines(tmpfilename).join("")
-              debug "loading plugin #{tmpfilename}"
-              plugin_module.module_eval(plugin_string, tmpfilename)
-              processed[file.intern] = :loaded
-            rescue Exception => err
-              # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
-              warning "plugin #{tmpfilename} load failed\n" + err.inspect
-              debug err.backtrace.join("\n")
-              bt = err.backtrace.select { |line|
-                line.match(/^(\(eval\)|#{tmpfilename}):\d+/)
-              }
-              bt.map! { |el|
-                el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
-                  "#{tmpfilename}#{$1}#{$3}"
-                }
-              }
-              msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
-                "#{tmpfilename}#{$1}#{$3}"
-              }
-              newerr = err.class.new(msg)
-              newerr.set_backtrace(bt)
-              # debug "Simplified error: " << newerr.inspect
-              # debug newerr.backtrace.join("\n")
-              @failed << { :name => file, :dir => dir, :reason => newerr }
-              # debug "Failures: #{@failed.inspect}"
+            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
             end
+
           }
         end
       }
           }
         end
       }
+      debug "finished loading plugins: #{status(true)}"
     end
 
     # call the save method for each active plugin
     end
 
     # call the save method for each active plugin
@@ -290,6 +459,7 @@ module Plugins
     # call the cleanup method for each active plugin
     def cleanup
       delegate 'cleanup'
     # call the cleanup method for each active plugin
     def cleanup
       delegate 'cleanup'
+      reset_botmodule_lists
     end
 
     # drop all plugins and rescan plugins on disk
     end
 
     # drop all plugins and rescan plugins on disk
@@ -297,28 +467,84 @@ module Plugins
     def rescan
       save
       cleanup
     def rescan
       save
       cleanup
-      @@plugins = Hash.new
       scan
     end
 
       scan
     end
 
-    # return list of help topics (plugin names)
-    def helptopics
+    def status(short=false)
+      output = []
+      if self.core_length > 0
+        if short
+          output << n_("%{count} core module loaded", "%{count} core modules loaded",
+                    self.core_length) % {:count => self.core_length}
+        else
+          output <<  n_("%{count} core module: %{list}",
+                     "%{count} core modules: %{list}", self.core_length) %
+                     { :count => self.core_length,
+                       :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
+        end
+      else
+        output << _("no core botmodules loaded")
+      end
       # Active plugins first
       # Active plugins first
-      if(@@plugins.length > 0)
-        list = " [#{length} plugin#{'s' if length > 1}: " + @@plugins.values.uniq.collect{|p| p.name}.sort.join(", ")
+      if(self.length > 0)
+        if short
+          output << n_("%{count} plugin loaded", "%{count} plugins loaded",
+                       self.length) % {:count => self.length}
+        else
+          output << n_("%{count} plugin: %{list}",
+                       "%{count} plugins: %{list}", self.length) %
+                   { :count => self.length,
+                     :list => plugins.collect{ |p| p.name}.sort.join(", ") }
+        end
       else
       else
-        list = " [no plugins active"
+        output << "no plugins active"
       end
       # Ignored plugins next
       end
       # Ignored plugins next
-      list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}: use #{Bold}help ignored plugins#{Bold} to see why" unless @ignored.empty?
+      unless @ignored.empty? or @failures_shown
+        if short
+          output << n_("%{highlight}%{count} plugin ignored%{highlight}",
+                       "%{highlight}%{count} plugins ignored%{highlight}",
+                       @ignored.length) %
+                    { :count => @ignored.length, :highlight => Underline }
+        else
+          output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
+                       "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
+                       @ignored.length) %
+                    { :count => @ignored.length, :highlight => Underline,
+                      :bold => Bold, :command => "help ignored plugins"}
+        end
+      end
       # Failed plugins next
       # Failed plugins next
-      list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}: use #{Bold}help failed plugins#{Bold} to see why" unless @failed.empty?
-      list << "]"
-      return list
+      unless @failed.empty? or @failures_shown
+        if short
+          output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
+                       "%{highlight}%{count} plugins failed to load%{highlight}",
+                       @failed.length) %
+                    { :count => @failed.length, :highlight => Reverse }
+        else
+          output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
+                       "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
+                       @failed.length) %
+                    { :count => @failed.length, :highlight => Reverse,
+                      :bold => Bold, :command => "help failed plugins"}
+        end
+      end
+      output.join '; '
+    end
+
+    # return list of help topics (plugin names)
+    def helptopics
+      rv = status
+      @failures_shown = true
+      rv
     end
 
     def length
     end
 
     def length
-      @@plugins.values.uniq.length
+      plugins.length
+    end
+
+    def core_length
+      core_modules.length
     end
 
     # return help for +topic+ (call associated plugin's help method)
     end
 
     # return help for +topic+ (call associated plugin's help method)
@@ -326,76 +552,128 @@ module Plugins
       case topic
       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
         # debug "Failures: #{@failed.inspect}"
       case topic
       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
         # debug "Failures: #{@failed.inspect}"
-        return "no plugins failed to load" if @failed.empty?
-        return (@failed.inject(Array.new) { |list, p|
-          list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
-          list << "with error #{p[:reason].class}: #{p[:reason]}"
-          list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
-          list
-        }).join("\n")
+        return _("no plugins failed to load") if @failed.empty?
+        return @failed.collect { |p|
+          _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
+              :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
+              :exception => p[:reason].class, :reason => p[:reason],
+          } + if $1 && !p[:reason].backtrace.empty?
+                _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
+              else
+                ''
+              end
+        }.join("\n")
       when /ignored?\s*plugins?/
       when /ignored?\s*plugins?/
-        return "no plugins were ignored" if @ignored.empty?
-        return (@ignored.inject(Array.new) { |list, p|
-          case p[:reason]
-          when :loaded
-            list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
-          else
-            list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
-          end
-          list
-        }).join(", ")
+        return _('no plugins were ignored') if @ignored.empty?
+
+        tmp = Hash.new
+        @ignored.each do |p|
+          reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
+          ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
+        end
+
+        return tmp.map do |dir, reasons|
+          # FIXME get rid of these string concatenations to make gettext easier
+          s = reasons.map { |r, list|
+            list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
+          }.join('; ')
+          "in #{dir}: #{s}"
+        end.join('; ')
       when /^(\S+)\s*(.*)$/
         key = $1
         params = $2
       when /^(\S+)\s*(.*)$/
         key = $1
         params = $2
-        if(@@plugins.has_key?(key))
+
+       # Let's see if we can match a plugin by the given name
+        (core_modules + plugins).each { |p|
+         next unless p.name == key
           begin
           begin
-            return @@plugins[key].help(key, params)
+            return p.help(key, params)
           rescue Exception => err
             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
           rescue Exception => err
             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
-            error "plugin #{@@plugins[key].name} help() failed: #{err.class}: #{err}"
-            error err.backtrace.join("\n")
+            error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
+          end
+        }
+
+       # 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]
+          begin
+            return p.help(key, params)
+          rescue Exception => err
+            #rescue TimeoutError, StandardError, NameError, SyntaxError => err
+            error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
           end
           end
-        else
-          return false
         end
       end
         end
       end
+      return false
     end
 
     # see if each plugin handles +method+, and if so, call it, passing
     # +message+ as a parameter
     def delegate(method, *args)
     end
 
     # see if each plugin handles +method+, and if so, call it, passing
     # +message+ as a parameter
     def delegate(method, *args)
-      @@plugins.values.uniq.each {|p|
-        if(p.respond_to? method)
-          begin
-            p.send method, *args
-          rescue Exception => err
-            #rescue TimeoutError, StandardError, NameError, SyntaxError => err
-            error "plugin #{p.name} #{method}() failed: #{err.class}: #{err}"
-            error err.backtrace.join("\n")
+      # debug "Delegating #{method.inspect}"
+      ret = Array.new
+      [core_modules, plugins].each { |pl|
+        pl.each {|p|
+          if(p.respond_to? method)
+            begin
+              # debug "#{p.botmodule_class} #{p.name} responds"
+              ret.push p.send(method, *args)
+            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
+        }
       }
       }
+      return ret
+      # debug "Finished delegating #{method.inspect}"
     end
 
     # 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)
     end
 
     # 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)
-      return unless(m.plugin)
-      if (@@plugins.has_key?(m.plugin) &&
-          @@plugins[m.plugin].respond_to?("privmsg") &&
-          @@bot.auth.allow?(m.plugin, m.source, m.replyto))
-        begin
-          @@plugins[m.plugin].privmsg(m)
-        rescue Exception => err
-          #rescue TimeoutError, StandardError, NameError, SyntaxError => err
-          error "plugin #{@@plugins[m.plugin].name} privmsg() failed: #{err.class}: #{err}"
-          error err.backtrace.join("\n")
+      # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
+      return unless m.plugin
+      k = m.plugin.to_sym
+      if commands.has_key?(k)
+        p = commands[k][:botmodule]
+        a = commands[k][:auth]
+        # We check here for things that don't check themselves
+        # (e.g. mapped things)
+        # debug "Checking auth ..."
+        if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
+          # debug "Checking response ..."
+          if p.respond_to?("privmsg")
+            begin
+              # 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)
+              raise if err.kind_of?(BDB::Fatal)
+            end
+            # debug "Successfully delegated #{m.message}"
+            return true
+          else
+            # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
+          end
+        else
+          # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
         end
         end
-        return true
       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
   end
 
     end
   end
 
+  # Returns the only PluginManagerClass instance
+  def Plugins.manager
+    return PluginManagerClass.instance
+  end
+
 end
 end
 end
 end