]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/plugins.rb
plugins: register maps with full information accessible via @bot.plugins.maps
[user/henk/code/ruby/rbot.git] / lib / rbot / plugins.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: rbot plugin management
5
6 require 'singleton'
7
8 module Irc
9 class Bot
10     Config.register Config::ArrayValue.new('plugins.blacklist',
11       :default => [], :wizard => false, :requires_rescan => true,
12       :desc => "Plugins that should not be loaded")
13 module Plugins
14   require 'rbot/messagemapper'
15
16 =begin rdoc
17   BotModule is the base class for the modules that enhance the rbot
18   functionality. Rather than subclassing BotModule, however, one should
19   subclass either CoreBotModule (reserved for system modules) or Plugin
20   (for user plugins).
21
22   A BotModule interacts with Irc events by defining one or more of the following
23   methods, which get called as appropriate when the corresponding Irc event
24   happens.
25
26   map(template, options)::
27   map!(template, options)::
28      map is the new, cleaner way to respond to specific message formats without
29      littering your plugin code with regexps, and should be used instead of
30      #register() and #privmsg() (see below) when possible.
31
32      The difference between map and map! is that map! will not register the new
33      command as an alternative name for the plugin.
34
35      Examples:
36
37        plugin.map 'karmastats', :action => 'karma_stats'
38
39        # while in the plugin...
40        def karma_stats(m, params)
41          m.reply "..."
42        end
43
44        # the default action is the first component
45        plugin.map 'karma'
46
47        # attributes can be pulled out of the match string
48        plugin.map 'karma for :key'
49        plugin.map 'karma :key'
50
51        # while in the plugin...
52        def karma(m, params)
53          item = params[:key]
54          m.reply 'karma for #{item}'
55        end
56
57        # you can setup defaults, to make parameters optional
58        plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
59
60        # the default auth check is also against the first component
61        # but that can be changed
62        plugin.map 'karmastats', :auth => 'karma'
63
64        # maps can be restricted to public or private message:
65        plugin.map 'karmastats', :private => false
66        plugin.map 'karmastats', :public => false
67
68      See MessageMapper#map for more information on the template format and the
69      allowed options.
70
71   listen(UserMessage)::
72                          Called for all messages of any type. To
73                          differentiate them, use message.kind_of? It'll be
74                          either a PrivMessage, NoticeMessage, KickMessage,
75                          QuitMessage, PartMessage, JoinMessage, NickMessage,
76                          etc.
77
78   ctcp_listen(UserMessage)::
79                          Called for all messages that contain a CTCP command.
80                          Use message.ctcp to get the CTCP command, and
81                          message.message to get the parameter string. To reply,
82                          use message.ctcp_reply, which sends a private NOTICE
83                          to the sender.
84
85   privmsg(PrivMessage)::
86                          Called for a PRIVMSG if the first word matches one
87                          the plugin #register()ed for. Use m.plugin to get
88                          that word and m.params for the rest of the message,
89                          if applicable.
90
91   unreplied(PrivMessage)::
92                          Called for a PRIVMSG which has not been replied to.
93
94   kick(KickMessage)::
95                          Called when a user (or the bot) is kicked from a
96                          channel the bot is in.
97
98   join(JoinMessage)::
99                          Called when a user (or the bot) joins a channel
100
101   part(PartMessage)::
102                          Called when a user (or the bot) parts a channel
103
104   quit(QuitMessage)::
105                          Called when a user (or the bot) quits IRC
106
107   nick(NickMessage)::
108                          Called when a user (or the bot) changes Nick
109   topic(TopicMessage)::
110                          Called when a user (or the bot) changes a channel
111                          topic
112
113   connect::              Called when a server is joined successfully, but
114                          before autojoin channels are joined (no params)
115
116   set_language(String)::
117                          Called when the user sets a new language
118                          whose name is the given String
119
120   save::                 Called when you are required to save your plugin's
121                          state, if you maintain data between sessions
122
123   cleanup::              called before your plugin is "unloaded", prior to a
124                          plugin reload or bot quit - close any open
125                          files/connections or flush caches here
126 =end
127
128   class BotModule
129     attr_reader :bot   # the associated bot
130
131     # Initialise your bot module. Always call super if you override this method,
132     # as important variables are set up for you:
133     #
134     # @bot::
135     #   the rbot instance
136     # @registry::
137     #   the botmodule's registry, which can be used to store permanent data
138     #   (see Registry::Accessor for additional documentation)
139     #
140     # Other instance variables which are defined and should not be overwritten
141     # byt the user, but aren't usually accessed directly, are:
142     #
143     # @manager::
144     #   the plugins manager instance
145     # @botmodule_triggers::
146     #   an Array of words this plugin #register()ed itself for
147     # @handler::
148     #   the MessageMapper that handles this plugin's maps
149     #
150     def initialize
151       @manager = Plugins::manager
152       @bot = @manager.bot
153
154       @botmodule_triggers = Array.new
155
156       @handler = MessageMapper.new(self)
157       @registry = Registry::Accessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
158
159       @manager.add_botmodule(self)
160       if self.respond_to?('set_language')
161         self.set_language(@bot.lang.language)
162       end
163     end
164
165     # Returns the symbol :BotModule 
166     def botmodule_class
167       :BotModule
168     end
169
170     # Method called to flush the registry, thus ensuring that the botmodule's permanent
171     # data is committed to disk
172     #
173     def flush_registry
174       # debug "Flushing #{@registry}"
175       @registry.flush
176     end
177
178     # Method called to cleanup before the plugin is unloaded. If you overload
179     # this method to handle additional cleanup tasks, remember to call super()
180     # so that the default cleanup actions are taken care of as well.
181     #
182     def cleanup
183       # debug "Closing #{@registry}"
184       @registry.close
185     end
186
187     # Handle an Irc::PrivMessage for which this BotModule has a map. The method
188     # is called automatically and there is usually no need to call it
189     # explicitly.
190     #
191     def handle(m)
192       @handler.handle(m)
193     end
194
195     # Signal to other BotModules that an even happened.
196     #
197     def call_event(ev, *args)
198       @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *args)
199     end
200
201     # call-seq: map(template, options)
202     #
203     # This is the preferred way to register the BotModule so that it
204     # responds to appropriately-formed messages on Irc.
205     #
206     def map(*args)
207       do_map(false, *args)
208     end
209
210     # call-seq: map!(template, options)
211     #
212     # This is the same as map but doesn't register the new command
213     # as an alternative name for the plugin.
214     #
215     def map!(*args)
216       do_map(true, *args)
217     end
218
219     # Auxiliary method called by #map and #map!
220     def do_map(silent, *args)
221       @handler.map(self, *args)
222       # register this map
223       map = @handler.last
224       name = map.items[0]
225       self.register name, :auth => nil, :hidden => silent
226       @manager.register_map(self, map)
227       unless self.respond_to?('privmsg')
228         def self.privmsg(m) #:nodoc:
229           handle(m)
230         end
231       end
232     end
233
234     # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
235     # usually _chan_ is either "*" for everywhere, public and private (in which
236     # case it can be omitted) or "?" for private communications
237     #
238     def default_auth(cmd, val, chan="*")
239       case cmd
240       when "*", ""
241         c = nil
242       else
243         c = cmd
244       end
245       Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
246     end
247
248     # Gets the default command path which would be given to command _cmd_
249     def propose_default_path(cmd)
250       [name, cmd].compact.join("::")
251     end
252
253     # Return an identifier for this plugin, defaults to a list of the message
254     # prefixes handled (used for error messages etc)
255     def name
256       self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
257     end
258
259     # Just calls name
260     def to_s
261       name
262     end
263
264     # Intern the name
265     def to_sym
266       self.name.to_sym
267     end
268
269     # Return a help string for your module. For complex modules, you may wish
270     # to break your help into topics, and return a list of available topics if
271     # +topic+ is nil. +plugin+ is passed containing the matching prefix for
272     # this message - if your plugin handles multiple prefixes, make sure you
273     # return the correct help for the prefix requested
274     def help(plugin, topic)
275       "no help"
276     end
277
278     # Register the plugin as a handler for messages prefixed _cmd_.
279     #
280     # This can be called multiple times for a plugin to handle multiple message
281     # prefixes.
282     #
283     # This command is now superceded by the #map() command, which should be used
284     # instead whenever possible.
285     # 
286     def register(cmd, opts={})
287       raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
288       who = @manager.who_handles?(cmd)
289       if who
290         raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
291         return
292       end
293       if opts.has_key?(:auth)
294         @manager.register(self, cmd, opts[:auth])
295       else
296         @manager.register(self, cmd, propose_default_path(cmd))
297       end
298       @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
299     end
300
301     # Default usage method provided as a utility for simple plugins. The
302     # MessageMapper uses 'usage' as its default fallback method.
303     #
304     def usage(m, params = {})
305       m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
306     end
307
308   end
309
310   # A CoreBotModule is a BotModule that provides core functionality.
311   #
312   # This class should not be used by user plugins, as it's reserved for system
313   # plugins such as the ones that handle authentication, configuration and basic
314   # functionality.
315   #
316   class CoreBotModule < BotModule
317     def botmodule_class
318       :CoreBotModule
319     end
320   end
321
322   # A Plugin is a BotModule that provides additional functionality.
323   #
324   # A user-defined plugin should subclass this, and then define any of the
325   # methods described in the documentation for BotModule to handle interaction
326   # with Irc events.
327   #
328   class Plugin < BotModule
329     def botmodule_class
330       :Plugin
331     end
332   end
333
334   # Singleton to manage multiple plugins and delegate messages to them for
335   # handling
336   class PluginManagerClass
337     include Singleton
338     attr_reader :bot
339     attr_reader :botmodules
340     attr_reader :maps
341
342     # This is the list of patterns commonly delegated to plugins.
343     # A fast delegation lookup is enabled for them.
344     DEFAULT_DELEGATE_PATTERNS = %r{^(?:
345       connect|names|nick|
346       listen|ctcp_listen|privmsg|unreplied|
347       kick|join|part|quit|
348       save|cleanup|flush_registry|
349       set_.*|event_.*
350     )$}x
351
352     def initialize
353       @botmodules = {
354         :CoreBotModule => [],
355         :Plugin => []
356       }
357
358       @names_hash = Hash.new
359       @commandmappers = Hash.new
360       @maps = Hash.new
361       @delegate_list = Hash.new { |h, k|
362         h[k] = Array.new
363       }
364
365       @dirs = []
366
367       @failed = Array.new
368       @ignored = Array.new
369
370       bot_associate(nil)
371     end
372
373     def inspect
374       ret = self.to_s[0..-2]
375       ret << ' corebotmodules='
376       ret << @botmodules[:CoreBotModule].map { |m|
377         m.name
378       }.inspect
379       ret << ' plugins='
380       ret << @botmodules[:Plugin].map { |m|
381         m.name
382       }.inspect
383       ret << ">"
384     end
385
386     # Reset lists of botmodules
387     def reset_botmodule_lists
388       @botmodules[:CoreBotModule].clear
389       @botmodules[:Plugin].clear
390       @names_hash.clear
391       @commandmappers.clear
392       @maps.clear
393       @failures_shown = false
394     end
395
396     # Associate with bot _bot_
397     def bot_associate(bot)
398       reset_botmodule_lists
399       @bot = bot
400     end
401
402     # Returns the botmodule with the given _name_
403     def [](name)
404       @names_hash[name.to_sym]
405     end
406
407     # Returns +true+ if _cmd_ has already been registered as a command
408     def who_handles?(cmd)
409       return nil unless @commandmappers.has_key?(cmd.to_sym)
410       return @commandmappers[cmd.to_sym][:botmodule]
411     end
412
413     # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
414     def register(botmodule, cmd, auth_path)
415       raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
416       @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
417     end
418
419     # Registers botmodule _botmodule_ with map _map_. This adds the map to the #maps hash
420     # which has three keys:
421     #
422     # botmodule:: the associated botmodule
423     # auth:: an array of auth keys checked by the map; the first is the full_auth_path of the map
424     # map:: the actual MessageTemplate object
425     #
426     #
427     def register_map(botmodule, map)
428       raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
429       @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map }
430     end
431
432     def add_botmodule(botmodule)
433       raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
434       kl = botmodule.botmodule_class
435       if @names_hash.has_key?(botmodule.to_sym)
436         case self[botmodule].botmodule_class
437         when kl
438           raise "#{kl} #{botmodule} already registered!"
439         else
440           raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
441         end
442       end
443       @botmodules[kl] << botmodule
444       @names_hash[botmodule.to_sym] = botmodule
445     end
446
447     # Returns an array of the loaded plugins
448     def core_modules
449       @botmodules[:CoreBotModule]
450     end
451
452     # Returns an array of the loaded plugins
453     def plugins
454       @botmodules[:Plugin]
455     end
456
457     # Returns a hash of the registered message prefixes and associated
458     # plugins
459     def commands
460       @commandmappers
461     end
462
463     # Makes a string of error _err_ by adding text _str_
464     def report_error(str, err)
465       ([str, err.inspect] + err.backtrace).join("\n")
466     end
467
468     # This method is the one that actually loads a module from the
469     # file _fname_
470     #
471     # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
472     #
473     # It returns the Symbol :loaded on success, and an Exception
474     # on failure
475     #
476     def load_botmodule_file(fname, desc=nil)
477       # create a new, anonymous module to "house" the plugin
478       # the idea here is to prevent namespace pollution. perhaps there
479       # is another way?
480       plugin_module = Module.new
481
482       desc = desc.to_s + " " if desc
483
484       begin
485         plugin_string = IO.readlines(fname).join("")
486         debug "loading #{desc}#{fname}"
487         plugin_module.module_eval(plugin_string, fname)
488         return :loaded
489       rescue Exception => err
490         # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
491         error report_error("#{desc}#{fname} load failed", err)
492         bt = err.backtrace.select { |line|
493           line.match(/^(\(eval\)|#{fname}):\d+/)
494         }
495         bt.map! { |el|
496           el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
497             "#{fname}#{$1}#{$3}"
498           }
499         }
500         msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
501           "#{fname}#{$1}#{$3}"
502         }
503         newerr = err.class.new(msg)
504         newerr.set_backtrace(bt)
505         return newerr
506       end
507     end
508     private :load_botmodule_file
509
510     # add one or more directories to the list of directories to
511     # load botmodules from
512     #
513     # TODO find a way to specify necessary plugins which _must_ be loaded
514     #
515     def add_botmodule_dir(*dirlist)
516       @dirs += dirlist
517       debug "Botmodule loading path: #{@dirs.join(', ')}"
518     end
519
520     def clear_botmodule_dirs
521       @dirs.clear
522       debug "Botmodule loading path cleared"
523     end
524
525     # load plugins from pre-assigned list of directories
526     def scan
527       @failed.clear
528       @ignored.clear
529       @delegate_list.clear
530
531       processed = Hash.new
532
533       @bot.config['plugins.blacklist'].each { |p|
534         pn = p + ".rb"
535         processed[pn.intern] = :blacklisted
536       }
537
538       dirs = @dirs
539       dirs.each {|dir|
540         if(FileTest.directory?(dir))
541           d = Dir.new(dir)
542           d.sort.each {|file|
543
544             next if(file =~ /^\./)
545
546             if processed.has_key?(file.intern)
547               @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
548               next
549             end
550
551             if(file =~ /^(.+\.rb)\.disabled$/)
552               # GB: Do we want to do this? This means that a disabled plugin in a directory
553               #     will disable in all subsequent directories. This was probably meant
554               #     to be used before plugins.blacklist was implemented, so I think
555               #     we don't need this anymore
556               processed[$1.intern] = :disabled
557               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
558               next
559             end
560
561             next unless(file =~ /\.rb$/)
562
563             did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
564             case did_it
565             when Symbol
566               processed[file.intern] = did_it
567             when Exception
568               @failed <<  { :name => file, :dir => dir, :reason => did_it }
569             end
570
571           }
572         end
573       }
574       debug "finished loading plugins: #{status(true)}"
575       (core_modules + plugins).each { |p|
576        p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
577          @delegate_list[m.intern] << p
578        }
579       }
580     end
581
582     # call the save method for each active plugin
583     def save
584       delegate 'flush_registry'
585       delegate 'save'
586     end
587
588     # call the cleanup method for each active plugin
589     def cleanup
590       delegate 'cleanup'
591       reset_botmodule_lists
592     end
593
594     # drop all plugins and rescan plugins on disk
595     # calls save and cleanup for each plugin before dropping them
596     def rescan
597       save
598       cleanup
599       scan
600     end
601
602     def status(short=false)
603       output = []
604       if self.core_length > 0
605         if short
606           output << n_("%{count} core module loaded", "%{count} core modules loaded",
607                     self.core_length) % {:count => self.core_length}
608         else
609           output <<  n_("%{count} core module: %{list}",
610                      "%{count} core modules: %{list}", self.core_length) %
611                      { :count => self.core_length,
612                        :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
613         end
614       else
615         output << _("no core botmodules loaded")
616       end
617       # Active plugins first
618       if(self.length > 0)
619         if short
620           output << n_("%{count} plugin loaded", "%{count} plugins loaded",
621                        self.length) % {:count => self.length}
622         else
623           output << n_("%{count} plugin: %{list}",
624                        "%{count} plugins: %{list}", self.length) %
625                    { :count => self.length,
626                      :list => plugins.collect{ |p| p.name}.sort.join(", ") }
627         end
628       else
629         output << "no plugins active"
630       end
631       # Ignored plugins next
632       unless @ignored.empty? or @failures_shown
633         if short
634           output << n_("%{highlight}%{count} plugin ignored%{highlight}",
635                        "%{highlight}%{count} plugins ignored%{highlight}",
636                        @ignored.length) %
637                     { :count => @ignored.length, :highlight => Underline }
638         else
639           output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
640                        "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
641                        @ignored.length) %
642                     { :count => @ignored.length, :highlight => Underline,
643                       :bold => Bold, :command => "help ignored plugins"}
644         end
645       end
646       # Failed plugins next
647       unless @failed.empty? or @failures_shown
648         if short
649           output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
650                        "%{highlight}%{count} plugins failed to load%{highlight}",
651                        @failed.length) %
652                     { :count => @failed.length, :highlight => Reverse }
653         else
654           output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
655                        "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
656                        @failed.length) %
657                     { :count => @failed.length, :highlight => Reverse,
658                       :bold => Bold, :command => "help failed plugins"}
659         end
660       end
661       output.join '; '
662     end
663
664     # return list of help topics (plugin names)
665     def helptopics
666       rv = status
667       @failures_shown = true
668       rv
669     end
670
671     def length
672       plugins.length
673     end
674
675     def core_length
676       core_modules.length
677     end
678
679     # return help for +topic+ (call associated plugin's help method)
680     def help(topic="")
681       case topic
682       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
683         # debug "Failures: #{@failed.inspect}"
684         return _("no plugins failed to load") if @failed.empty?
685         return @failed.collect { |p|
686           _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
687               :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
688               :exception => p[:reason].class, :reason => p[:reason],
689           } + if $1 && !p[:reason].backtrace.empty?
690                 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
691               else
692                 ''
693               end
694         }.join("\n")
695       when /ignored?\s*plugins?/
696         return _('no plugins were ignored') if @ignored.empty?
697
698         tmp = Hash.new
699         @ignored.each do |p|
700           reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
701           ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
702         end
703
704         return tmp.map do |dir, reasons|
705           # FIXME get rid of these string concatenations to make gettext easier
706           s = reasons.map { |r, list|
707             list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
708           }.join('; ')
709           "in #{dir}: #{s}"
710         end.join('; ')
711       when /^(\S+)\s*(.*)$/
712         key = $1
713         params = $2
714
715         # Let's see if we can match a plugin by the given name
716         (core_modules + plugins).each { |p|
717           next unless p.name == key
718           begin
719             return p.help(key, params)
720           rescue Exception => err
721             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
722             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
723           end
724         }
725
726         # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
727         k = key.to_sym
728         if commands.has_key?(k)
729           p = commands[k][:botmodule]
730           begin
731             return p.help(key, params)
732           rescue Exception => err
733             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
734             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
735           end
736         end
737       end
738       return false
739     end
740
741     # see if each plugin handles +method+, and if so, call it, passing
742     # +message+ as a parameter
743     def delegate(method, *args)
744       # debug "Delegating #{method.inspect}"
745       ret = Array.new
746       if method.match(DEFAULT_DELEGATE_PATTERNS)
747         debug "fast-delegating #{method}"
748         m = method.to_sym
749         debug "no-one to delegate to" unless @delegate_list.has_key?(m)
750         return [] unless @delegate_list.has_key?(m)
751         @delegate_list[m].each { |p|
752           begin
753             ret.push p.send(method, *args)
754           rescue Exception => err
755             raise if err.kind_of?(SystemExit)
756             error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
757             raise if err.kind_of?(BDB::Fatal)
758           end
759         }
760       else
761         debug "slow-delegating #{method}"
762         (core_modules + plugins).each { |p|
763           if(p.respond_to? method)
764             begin
765               # debug "#{p.botmodule_class} #{p.name} responds"
766               ret.push p.send(method, *args)
767             rescue Exception => err
768               raise if err.kind_of?(SystemExit)
769               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
770               raise if err.kind_of?(BDB::Fatal)
771             end
772           end
773         }
774       end
775       return ret
776       # debug "Finished delegating #{method.inspect}"
777     end
778
779     # see if we have a plugin that wants to handle this message, if so, pass
780     # it to the plugin and return true, otherwise false
781     def privmsg(m)
782       # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
783       return unless m.plugin
784       k = m.plugin.to_sym
785       if commands.has_key?(k)
786         p = commands[k][:botmodule]
787         a = commands[k][:auth]
788         # We check here for things that don't check themselves
789         # (e.g. mapped things)
790         # debug "Checking auth ..."
791         if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
792           # debug "Checking response ..."
793           if p.respond_to?("privmsg")
794             begin
795               # debug "#{p.botmodule_class} #{p.name} responds"
796               p.privmsg(m)
797             rescue Exception => err
798               raise if err.kind_of?(SystemExit)
799               error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
800               raise if err.kind_of?(BDB::Fatal)
801             end
802             # debug "Successfully delegated #{m.message}"
803             return true
804           else
805             # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
806           end
807         else
808           # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
809         end
810       end
811       # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
812       return false
813       # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
814     end
815   end
816
817   # Returns the only PluginManagerClass instance
818   def Plugins.manager
819     return PluginManagerClass.instance
820   end
821
822 end
823 end
824 end