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