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