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