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