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