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