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