]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/plugins.rb
event delegation thresholds
[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       mark_priorities_dirty
480     end
481
482     # Returns an array of the loaded plugins
483     def core_modules
484       @botmodules[:CoreBotModule]
485     end
486
487     # Returns an array of the loaded plugins
488     def plugins
489       @botmodules[:Plugin]
490     end
491
492     # Returns a hash of the registered message prefixes and associated
493     # plugins
494     def commands
495       @commandmappers
496     end
497
498     # Tells the PluginManager that the next time it delegates an event, it
499     # should sort the modules by priority
500     def mark_priorities_dirty
501       @sorted_modules = nil
502     end
503
504     # Makes a string of error _err_ by adding text _str_
505     def report_error(str, err)
506       ([str, err.inspect] + err.backtrace).join("\n")
507     end
508
509     # This method is the one that actually loads a module from the
510     # file _fname_
511     #
512     # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
513     #
514     # It returns the Symbol :loaded on success, and an Exception
515     # on failure
516     #
517     def load_botmodule_file(fname, desc=nil)
518       # create a new, anonymous module to "house" the plugin
519       # the idea here is to prevent namespace pollution. perhaps there
520       # is another way?
521       plugin_module = Module.new
522
523       desc = desc.to_s + " " if desc
524
525       begin
526         plugin_string = IO.readlines(fname).join("")
527         debug "loading #{desc}#{fname}"
528         plugin_module.module_eval(plugin_string, fname)
529         return :loaded
530       rescue Exception => err
531         # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
532         error report_error("#{desc}#{fname} load failed", err)
533         bt = err.backtrace.select { |line|
534           line.match(/^(\(eval\)|#{fname}):\d+/)
535         }
536         bt.map! { |el|
537           el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
538             "#{fname}#{$1}#{$3}"
539           }
540         }
541         msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
542           "#{fname}#{$1}#{$3}"
543         }
544         newerr = err.class.new(msg)
545         newerr.set_backtrace(bt)
546         return newerr
547       end
548     end
549     private :load_botmodule_file
550
551     # add one or more directories to the list of directories to
552     # load botmodules from
553     #
554     # TODO find a way to specify necessary plugins which _must_ be loaded
555     #
556     def add_botmodule_dir(*dirlist)
557       @dirs += dirlist
558       debug "Botmodule loading path: #{@dirs.join(', ')}"
559     end
560
561     def clear_botmodule_dirs
562       @dirs.clear
563       debug "Botmodule loading path cleared"
564     end
565
566     # load plugins from pre-assigned list of directories
567     def scan
568       @failed.clear
569       @ignored.clear
570       @delegate_list.clear
571
572       processed = Hash.new
573
574       @bot.config['plugins.blacklist'].each { |p|
575         pn = p + ".rb"
576         processed[pn.intern] = :blacklisted
577       }
578
579       dirs = @dirs
580       dirs.each {|dir|
581         if(FileTest.directory?(dir))
582           d = Dir.new(dir)
583           d.sort.each {|file|
584
585             next if(file =~ /^\./)
586
587             if processed.has_key?(file.intern)
588               @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
589               next
590             end
591
592             if(file =~ /^(.+\.rb)\.disabled$/)
593               # GB: Do we want to do this? This means that a disabled plugin in a directory
594               #     will disable in all subsequent directories. This was probably meant
595               #     to be used before plugins.blacklist was implemented, so I think
596               #     we don't need this anymore
597               processed[$1.intern] = :disabled
598               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
599               next
600             end
601
602             next unless(file =~ /\.rb$/)
603
604             did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
605             case did_it
606             when Symbol
607               processed[file.intern] = did_it
608             when Exception
609               @failed <<  { :name => file, :dir => dir, :reason => did_it }
610             end
611
612           }
613         end
614       }
615       debug "finished loading plugins: #{status(true)}"
616       (core_modules + plugins).each { |p|
617        p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
618          @delegate_list[m.intern] << p
619        }
620       }
621       mark_priorities_dirty
622     end
623
624     # call the save method for each active plugin
625     def save
626       delegate 'flush_registry'
627       delegate 'save'
628     end
629
630     # call the cleanup method for each active plugin
631     def cleanup
632       delegate 'cleanup'
633       reset_botmodule_lists
634     end
635
636     # drop all plugins and rescan plugins on disk
637     # calls save and cleanup for each plugin before dropping them
638     def rescan
639       save
640       cleanup
641       scan
642     end
643
644     def status(short=false)
645       output = []
646       if self.core_length > 0
647         if short
648           output << n_("%{count} core module loaded", "%{count} core modules loaded",
649                     self.core_length) % {:count => self.core_length}
650         else
651           output <<  n_("%{count} core module: %{list}",
652                      "%{count} core modules: %{list}", self.core_length) %
653                      { :count => self.core_length,
654                        :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
655         end
656       else
657         output << _("no core botmodules loaded")
658       end
659       # Active plugins first
660       if(self.length > 0)
661         if short
662           output << n_("%{count} plugin loaded", "%{count} plugins loaded",
663                        self.length) % {:count => self.length}
664         else
665           output << n_("%{count} plugin: %{list}",
666                        "%{count} plugins: %{list}", self.length) %
667                    { :count => self.length,
668                      :list => plugins.collect{ |p| p.name}.sort.join(", ") }
669         end
670       else
671         output << "no plugins active"
672       end
673       # Ignored plugins next
674       unless @ignored.empty? or @failures_shown
675         if short
676           output << n_("%{highlight}%{count} plugin ignored%{highlight}",
677                        "%{highlight}%{count} plugins ignored%{highlight}",
678                        @ignored.length) %
679                     { :count => @ignored.length, :highlight => Underline }
680         else
681           output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
682                        "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
683                        @ignored.length) %
684                     { :count => @ignored.length, :highlight => Underline,
685                       :bold => Bold, :command => "help ignored plugins"}
686         end
687       end
688       # Failed plugins next
689       unless @failed.empty? or @failures_shown
690         if short
691           output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
692                        "%{highlight}%{count} plugins failed to load%{highlight}",
693                        @failed.length) %
694                     { :count => @failed.length, :highlight => Reverse }
695         else
696           output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
697                        "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
698                        @failed.length) %
699                     { :count => @failed.length, :highlight => Reverse,
700                       :bold => Bold, :command => "help failed plugins"}
701         end
702       end
703       output.join '; '
704     end
705
706     # return list of help topics (plugin names)
707     def helptopics
708       rv = status
709       @failures_shown = true
710       rv
711     end
712
713     def length
714       plugins.length
715     end
716
717     def core_length
718       core_modules.length
719     end
720
721     # return help for +topic+ (call associated plugin's help method)
722     def help(topic="")
723       case topic
724       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
725         # debug "Failures: #{@failed.inspect}"
726         return _("no plugins failed to load") if @failed.empty?
727         return @failed.collect { |p|
728           _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
729               :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
730               :exception => p[:reason].class, :reason => p[:reason],
731           } + if $1 && !p[:reason].backtrace.empty?
732                 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
733               else
734                 ''
735               end
736         }.join("\n")
737       when /ignored?\s*plugins?/
738         return _('no plugins were ignored') if @ignored.empty?
739
740         tmp = Hash.new
741         @ignored.each do |p|
742           reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
743           ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
744         end
745
746         return tmp.map do |dir, reasons|
747           # FIXME get rid of these string concatenations to make gettext easier
748           s = reasons.map { |r, list|
749             list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
750           }.join('; ')
751           "in #{dir}: #{s}"
752         end.join('; ')
753       when /^(\S+)\s*(.*)$/
754         key = $1
755         params = $2
756
757         # Let's see if we can match a plugin by the given name
758         (core_modules + plugins).each { |p|
759           next unless p.name == key
760           begin
761             return p.help(key, params)
762           rescue Exception => err
763             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
764             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
765           end
766         }
767
768         # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
769         k = key.to_sym
770         if commands.has_key?(k)
771           p = commands[k][:botmodule]
772           begin
773             return p.help(key, params)
774           rescue Exception => err
775             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
776             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
777           end
778         end
779       end
780       return false
781     end
782
783     def sort_modules
784       @sorted_modules = (core_modules + plugins).sort do |a, b| 
785         a.priority <=> b.priority
786       end || []
787
788       @delegate_list.each_value do |list|
789         list.sort! {|a,b| a.priority <=> b.priority}
790       end
791     end
792
793     # see if each plugin handles +method+, and if so, call it, passing
794     # +message+ as a parameter.  botmodules are called in order of priority
795     # from lowest to highest.  +DEPRECATED+ please use delegate_event.
796     def delegate(method, *args)
797       delegate_event(method, :args => args)
798     end
799
800     # see if each plugin handles +method+, and if so, call it, passing
801     # +opts[:args]+ as a parameter.  +opts[:above]+ and +opts[:below]+
802     # are used for a threshold of botmodule priorities that will be called.
803     # If :above is defined, only botmodules with a priority above the value
804     # will be called, for example.  botmodules are called in order of
805     # priority from lowest to hightest.
806     def delegate_event(method, o={})
807       # if the priorities order of the delegate list is dirty,
808       # meaning some modules have been added or priorities have been
809       # changed, then the delegate list will need to be sorted before
810       # delegation.  This should always be true for the first delegation.
811       sort_modules unless @sorted_modules
812
813       # set defaults
814       opts = {:args => []}.merge(o)
815
816       above = opts[:above]
817       below = opts[:below]
818       args = opts[:args]
819
820       # debug "Delegating #{method.inspect}"
821       ret = Array.new
822       if method.match(DEFAULT_DELEGATE_PATTERNS)
823         debug "fast-delegating #{method}"
824         m = method.to_sym
825         debug "no-one to delegate to" unless @delegate_list.has_key?(m)
826         return [] unless @delegate_list.has_key?(m)
827         @delegate_list[m].each { |p|
828           begin
829             prio = p.priority
830             unless (above and above >= prio) or (below and below <= prio)
831               ret.push p.send(method, *(args||[]))
832             end
833           rescue Exception => err
834             raise if err.kind_of?(SystemExit)
835             error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
836             raise if err.kind_of?(BDB::Fatal)
837           end
838         }
839       else
840         debug "slow-delegating #{method}"
841         @sorted_modules.each { |p|
842           if(p.respond_to? method)
843             begin
844               # debug "#{p.botmodule_class} #{p.name} responds"
845               prio = p.priority
846               unless (above and above >= prio) or (below and below <= prio)
847                 ret.push p.send(method, *(args||[]))
848               end
849             rescue Exception => err
850               raise if err.kind_of?(SystemExit)
851               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
852               raise if err.kind_of?(BDB::Fatal)
853             end
854           end
855         }
856       end
857       return ret
858       # debug "Finished delegating #{method.inspect}"
859     end
860
861     # see if we have a plugin that wants to handle this message, if so, pass
862     # it to the plugin and return true, otherwise false
863     def privmsg(m)
864       debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
865       return unless m.plugin
866       k = m.plugin.to_sym
867       if commands.has_key?(k)
868         p = commands[k][:botmodule]
869         a = commands[k][:auth]
870         # We check here for things that don't check themselves
871         # (e.g. mapped things)
872         debug "Checking auth ..."
873         if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
874           debug "Checking response ..."
875           if p.respond_to?("privmsg")
876             begin
877               debug "#{p.botmodule_class} #{p.name} responds"
878               p.privmsg(m)
879             rescue Exception => err
880               raise if err.kind_of?(SystemExit)
881               error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
882               raise if err.kind_of?(BDB::Fatal)
883             end
884             debug "Successfully delegated #{m.inspect}"
885             return true
886           else
887             debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
888           end
889         else
890           debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
891         end
892       else
893         debug "Command #{k} isn't handled"
894       end
895       return false
896     end
897
898     # delegate IRC messages, by delegating 'listen' first, and the actual method
899     # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message
900     # as appropriate.
901     def irc_delegate(method, m)
902       delegate('listen', m)
903       if method.to_sym == :privmsg
904         delegate('ctcp_listen', m) if m.ctcp
905         delegate('message', m)
906         privmsg(m) if m.address?
907         delegate('unreplied', m) unless m.replied
908       else
909         delegate(method, m)
910       end
911     end
912   end
913
914   # Returns the only PluginManagerClass instance
915   def Plugins.manager
916     return PluginManagerClass.instance
917   end
918
919 end
920 end
921 end