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