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