]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/plugins.rb
lib/rbot/plugins.rb: remove executable bit
[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       m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
345     end
346
347     # Define the priority of the module.  During event delegation, lower
348     # priority modules will be called first.  Default priority is 1
349     def priority=(prio)
350       if @priority != prio
351         @priority = prio
352         @bot.plugins.mark_priorities_dirty
353       end
354     end
355
356     # Directory name to be joined to the botclass to access data files. By
357     # default this is the plugin name itself, but may be overridden, for
358     # example by plugins that share their datafiles or for backwards
359     # compatibilty
360     def dirname
361       name
362     end
363
364     # Filename for a datafile built joining the botclass, plugin dirname and
365     # actual file name
366     def datafile(*fname)
367       @bot.path dirname, *fname
368     end
369   end
370
371   # A CoreBotModule is a BotModule that provides core functionality.
372   #
373   # This class should not be used by user plugins, as it's reserved for system
374   # plugins such as the ones that handle authentication, configuration and basic
375   # functionality.
376   #
377   class CoreBotModule < BotModule
378     def botmodule_class
379       :CoreBotModule
380     end
381   end
382
383   # A Plugin is a BotModule that provides additional functionality.
384   #
385   # A user-defined plugin should subclass this, and then define any of the
386   # methods described in the documentation for BotModule to handle interaction
387   # with Irc events.
388   #
389   class Plugin < BotModule
390     def botmodule_class
391       :Plugin
392     end
393   end
394
395   # Singleton to manage multiple plugins and delegate messages to them for
396   # handling
397   class PluginManagerClass
398     include Singleton
399     attr_reader :bot
400     attr_reader :botmodules
401     attr_reader :maps
402
403     # This is the list of patterns commonly delegated to plugins.
404     # A fast delegation lookup is enabled for them.
405     DEFAULT_DELEGATE_PATTERNS = %r{^(?:
406       connect|names|nick|
407       listen|ctcp_listen|privmsg|unreplied|
408       kick|join|part|quit|
409       save|cleanup|flush_registry|
410       set_.*|event_.*
411     )$}x
412
413     def initialize
414       @botmodules = {
415         :CoreBotModule => [],
416         :Plugin => []
417       }
418
419       @names_hash = Hash.new
420       @commandmappers = Hash.new
421       @maps = Hash.new
422
423       # modules will be sorted on first delegate call
424       @sorted_modules = nil
425
426       @delegate_list = Hash.new { |h, k|
427         h[k] = Array.new
428       }
429
430       @core_module_dirs = []
431       @plugin_dirs = []
432
433       @failed = Array.new
434       @ignored = Array.new
435
436       bot_associate(nil)
437     end
438
439     def inspect
440       ret = self.to_s[0..-2]
441       ret << ' corebotmodules='
442       ret << @botmodules[:CoreBotModule].map { |m|
443         m.name
444       }.inspect
445       ret << ' plugins='
446       ret << @botmodules[:Plugin].map { |m|
447         m.name
448       }.inspect
449       ret << ">"
450     end
451
452     # Reset lists of botmodules
453     def reset_botmodule_lists
454       @botmodules[:CoreBotModule].clear
455       @botmodules[:Plugin].clear
456       @names_hash.clear
457       @commandmappers.clear
458       @maps.clear
459       @failures_shown = false
460       mark_priorities_dirty
461     end
462
463     # Associate with bot _bot_
464     def bot_associate(bot)
465       reset_botmodule_lists
466       @bot = bot
467     end
468
469     # Returns the botmodule with the given _name_
470     def [](name)
471       @names_hash[name.to_sym]
472     end
473
474     # Returns +true+ if _cmd_ has already been registered as a command
475     def who_handles?(cmd)
476       return nil unless @commandmappers.has_key?(cmd.to_sym)
477       return @commandmappers[cmd.to_sym][:botmodule]
478     end
479
480     # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
481     def register(botmodule, cmd, auth_path)
482       raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
483       @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
484     end
485
486     # Registers botmodule _botmodule_ with map _map_. This adds the map to the #maps hash
487     # which has three keys:
488     #
489     # botmodule:: the associated botmodule
490     # auth:: an array of auth keys checked by the map; the first is the full_auth_path of the map
491     # map:: the actual MessageTemplate object
492     #
493     #
494     def register_map(botmodule, map)
495       raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
496       @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map }
497     end
498
499     def add_botmodule(botmodule)
500       raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
501       kl = botmodule.botmodule_class
502       if @names_hash.has_key?(botmodule.to_sym)
503         case self[botmodule].botmodule_class
504         when kl
505           raise "#{kl} #{botmodule} already registered!"
506         else
507           raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
508         end
509       end
510       @botmodules[kl] << botmodule
511       @names_hash[botmodule.to_sym] = botmodule
512       mark_priorities_dirty
513     end
514
515     # Returns an array of the loaded plugins
516     def core_modules
517       @botmodules[:CoreBotModule]
518     end
519
520     # Returns an array of the loaded plugins
521     def plugins
522       @botmodules[:Plugin]
523     end
524
525     # Returns a hash of the registered message prefixes and associated
526     # plugins
527     def commands
528       @commandmappers
529     end
530
531     # Tells the PluginManager that the next time it delegates an event, it
532     # should sort the modules by priority
533     def mark_priorities_dirty
534       @sorted_modules = nil
535     end
536
537     # Makes a string of error _err_ by adding text _str_
538     def report_error(str, err)
539       ([str, err.inspect] + err.backtrace).join("\n")
540     end
541
542     # This method is the one that actually loads a module from the
543     # file _fname_
544     #
545     # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
546     #
547     # It returns the Symbol :loaded on success, and an Exception
548     # on failure
549     #
550     def load_botmodule_file(fname, desc=nil)
551       # create a new, anonymous module to "house" the plugin
552       # the idea here is to prevent namespace pollution. perhaps there
553       # is another way?
554       plugin_module = Module.new
555       # each plugin uses its own textdomain, we bind it automatically here
556       bindtextdomain_to(plugin_module, "rbot-#{File.basename(fname, '.rb')}")
557
558       desc = desc.to_s + " " if desc
559
560       begin
561         plugin_string = IO.read(fname)
562         debug "loading #{desc}#{fname}"
563         plugin_module.module_eval(plugin_string, fname)
564         return :loaded
565       rescue Exception => err
566         # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
567         error report_error("#{desc}#{fname} load failed", err)
568         bt = err.backtrace.select { |line|
569           line.match(/^(\(eval\)|#{fname}):\d+/)
570         }
571         bt.map! { |el|
572           el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
573             "#{fname}#{$1}#{$3}"
574           }
575         }
576         msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
577           "#{fname}#{$1}#{$3}"
578         }
579         newerr = err.class.new(msg)
580         newerr.set_backtrace(bt)
581         return newerr
582       end
583     end
584     private :load_botmodule_file
585
586     # add one or more directories to the list of directories to
587     # load core modules from
588     def add_core_module_dir(*dirlist)
589       @core_module_dirs += dirlist
590       debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
591     end
592
593     # add one or more directories to the list of directories to
594     # load plugins from
595     def add_plugin_dir(*dirlist)
596       @plugin_dirs += dirlist
597       debug "Plugin loading paths: #{@plugin_dirs.join(', ')}"
598     end
599
600     def clear_botmodule_dirs
601       @core_module_dirs.clear
602       @plugin_dirs.clear
603       debug "Core module and plugin loading paths cleared"
604     end
605
606     def scan_botmodules(opts={})
607       type = opts[:type]
608       processed = Hash.new
609
610       case type
611       when :core
612         dirs = @core_module_dirs
613       when :plugins
614         dirs = @plugin_dirs
615
616         @bot.config['plugins.blacklist'].each { |p|
617           pn = p + ".rb"
618           processed[pn.intern] = :blacklisted
619         }
620
621         whitelist = @bot.config['plugins.whitelist'].map { |p|
622           p + ".rb"
623         }
624       end
625
626       dirs.each do |dir|
627         next unless FileTest.directory?(dir)
628         d = Dir.new(dir)
629         d.sort.each do |file|
630           next unless file =~ /\.rb$/
631           next if file =~ /^\./
632
633           case type
634           when :plugins
635             if !whitelist.empty? && !whitelist.include?(file)
636               @ignored << {:name => file, :dir => dir, :reason => :"not whitelisted" }
637               next
638             elsif processed.has_key?(file.intern)
639               @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
640               next
641             end
642
643             if(file =~ /^(.+\.rb)\.disabled$/)
644               # GB: Do we want to do this? This means that a disabled plugin in a directory
645               #     will disable in all subsequent directories. This was probably meant
646               #     to be used before plugins.blacklist was implemented, so I think
647               #     we don't need this anymore
648               processed[$1.intern] = :disabled
649               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
650               next
651             end
652           end
653
654           did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
655           case did_it
656           when Symbol
657             processed[file.intern] = did_it
658           when Exception
659             @failed << { :name => file, :dir => dir, :reason => did_it }
660           end
661         end
662       end
663     end
664
665     # load plugins from pre-assigned list of directories
666     def scan
667       @failed.clear
668       @ignored.clear
669       @delegate_list.clear
670
671       scan_botmodules(:type => :core)
672       scan_botmodules(:type => :plugins)
673
674       debug "finished loading plugins: #{status(true)}"
675       (core_modules + plugins).each { |p|
676        p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
677          @delegate_list[m.intern] << p
678        }
679       }
680       mark_priorities_dirty
681     end
682
683     # call the save method for each active plugin
684     def save
685       delegate 'flush_registry'
686       delegate 'save'
687     end
688
689     # call the cleanup method for each active plugin
690     def cleanup
691       delegate 'cleanup'
692       reset_botmodule_lists
693     end
694
695     # drop all plugins and rescan plugins on disk
696     # calls save and cleanup for each plugin before dropping them
697     def rescan
698       save
699       cleanup
700       scan
701     end
702
703     def status(short=false)
704       output = []
705       if self.core_length > 0
706         if short
707           output << n_("%{count} core module loaded", "%{count} core modules loaded",
708                     self.core_length) % {:count => self.core_length}
709         else
710           output <<  n_("%{count} core module: %{list}",
711                      "%{count} core modules: %{list}", self.core_length) %
712                      { :count => self.core_length,
713                        :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
714         end
715       else
716         output << _("no core botmodules loaded")
717       end
718       # Active plugins first
719       if(self.length > 0)
720         if short
721           output << n_("%{count} plugin loaded", "%{count} plugins loaded",
722                        self.length) % {:count => self.length}
723         else
724           output << n_("%{count} plugin: %{list}",
725                        "%{count} plugins: %{list}", self.length) %
726                    { :count => self.length,
727                      :list => plugins.collect{ |p| p.name}.sort.join(", ") }
728         end
729       else
730         output << "no plugins active"
731       end
732       # Ignored plugins next
733       unless @ignored.empty? or @failures_shown
734         if short
735           output << n_("%{highlight}%{count} plugin ignored%{highlight}",
736                        "%{highlight}%{count} plugins ignored%{highlight}",
737                        @ignored.length) %
738                     { :count => @ignored.length, :highlight => Underline }
739         else
740           output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
741                        "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
742                        @ignored.length) %
743                     { :count => @ignored.length, :highlight => Underline,
744                       :bold => Bold, :command => "help ignored plugins"}
745         end
746       end
747       # Failed plugins next
748       unless @failed.empty? or @failures_shown
749         if short
750           output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
751                        "%{highlight}%{count} plugins failed to load%{highlight}",
752                        @failed.length) %
753                     { :count => @failed.length, :highlight => Reverse }
754         else
755           output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
756                        "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
757                        @failed.length) %
758                     { :count => @failed.length, :highlight => Reverse,
759                       :bold => Bold, :command => "help failed plugins"}
760         end
761       end
762       output.join '; '
763     end
764
765     # return list of help topics (plugin names)
766     def helptopics
767       rv = status
768       @failures_shown = true
769       rv
770     end
771
772     def length
773       plugins.length
774     end
775
776     def core_length
777       core_modules.length
778     end
779
780     # return help for +topic+ (call associated plugin's help method)
781     def help(topic="")
782       case topic
783       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
784         # debug "Failures: #{@failed.inspect}"
785         return _("no plugins failed to load") if @failed.empty?
786         return @failed.collect { |p|
787           _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
788               :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
789               :exception => p[:reason].class, :reason => p[:reason],
790           } + if $1 && !p[:reason].backtrace.empty?
791                 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
792               else
793                 ''
794               end
795         }.join("\n")
796       when /ignored?\s*plugins?/
797         return _('no plugins were ignored') if @ignored.empty?
798
799         tmp = Hash.new
800         @ignored.each do |p|
801           reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
802           ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
803         end
804
805         return tmp.map do |dir, reasons|
806           # FIXME get rid of these string concatenations to make gettext easier
807           s = reasons.map { |r, list|
808             list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
809           }.join('; ')
810           "in #{dir}: #{s}"
811         end.join('; ')
812       when /^(\S+)\s*(.*)$/
813         key = $1
814         params = $2
815
816         # Let's see if we can match a plugin by the given name
817         (core_modules + plugins).each { |p|
818           next unless p.name == key
819           begin
820             return p.help(key, params)
821           rescue Exception => err
822             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
823             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
824           end
825         }
826
827         # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
828         k = key.to_sym
829         if commands.has_key?(k)
830           p = commands[k][:botmodule]
831           begin
832             return p.help(key, params)
833           rescue Exception => err
834             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
835             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
836           end
837         end
838       end
839       return false
840     end
841
842     def sort_modules
843       @sorted_modules = (core_modules + plugins).sort do |a, b|
844         a.priority <=> b.priority
845       end || []
846
847       @delegate_list.each_value do |list|
848         list.sort! {|a,b| a.priority <=> b.priority}
849       end
850     end
851
852     # call-seq: delegate</span><span class="method-args">(method, m, opts={})</span>
853     # <span class="method-name">delegate</span><span class="method-args">(method, opts={})
854     #
855     # see if each plugin handles _method_, and if so, call it, passing
856     # _m_ as a parameter (if present). BotModules are called in order of
857     # priority from lowest to highest.
858     #
859     # If the passed _m_ is a BasicUserMessage and is marked as #ignored?, it
860     # will only be delegated to plugins with negative priority. Conversely, if
861     # it's a fake message (see BotModule#fake_message), it will only be
862     # delegated to plugins with positive priority.
863     #
864     # Note that _m_ can also be an exploded Array, but in this case the last
865     # element of it cannot be a Hash, or it will be interpreted as the options
866     # Hash for delegate itself. The last element can be a subclass of a Hash, though.
867     # To be on the safe side, you can add an empty Hash as last parameter for delegate
868     # when calling it with an exploded Array:
869     #   @bot.plugins.delegate(method, *(args.push Hash.new))
870     #
871     # Currently supported options are the following:
872     # :above ::
873     #   if specified, the delegation will only consider plugins with a priority
874     #   higher than the specified value
875     # :below ::
876     #   if specified, the delegation will only consider plugins with a priority
877     #   lower than the specified value
878     #
879     def delegate(method, *args)
880       # if the priorities order of the delegate list is dirty,
881       # meaning some modules have been added or priorities have been
882       # changed, then the delegate list will need to be sorted before
883       # delegation.  This should always be true for the first delegation.
884       sort_modules unless @sorted_modules
885
886       opts = {}
887       opts.merge(args.pop) if args.last.class == Hash
888
889       m = args.first
890       if BasicUserMessage === m
891         # ignored messages should not be delegated
892         # to plugins with positive priority
893         opts[:below] ||= 0 if m.ignored?
894         # fake messages should not be delegated
895         # to plugins with negative priority
896         opts[:above] ||= 0 if m.recurse_depth > 0
897       end
898
899       above = opts[:above]
900       below = opts[:below]
901
902       # debug "Delegating #{method.inspect}"
903       ret = Array.new
904       if method.match(DEFAULT_DELEGATE_PATTERNS)
905         debug "fast-delegating #{method}"
906         m = method.to_sym
907         debug "no-one to delegate to" unless @delegate_list.has_key?(m)
908         return [] unless @delegate_list.has_key?(m)
909         @delegate_list[m].each { |p|
910           begin
911             prio = p.priority
912             unless (above and above >= prio) or (below and below <= prio)
913               ret.push p.send(method, *args)
914             end
915           rescue Exception => err
916             raise if err.kind_of?(SystemExit)
917             error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
918             raise if err.kind_of?(BDB::Fatal)
919           end
920         }
921       else
922         debug "slow-delegating #{method}"
923         @sorted_modules.each { |p|
924           if(p.respond_to? method)
925             begin
926               # debug "#{p.botmodule_class} #{p.name} responds"
927               prio = p.priority
928               unless (above and above >= prio) or (below and below <= prio)
929                 ret.push p.send(method, *args)
930               end
931             rescue Exception => err
932               raise if err.kind_of?(SystemExit)
933               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
934               raise if err.kind_of?(BDB::Fatal)
935             end
936           end
937         }
938       end
939       return ret
940       # debug "Finished delegating #{method.inspect}"
941     end
942
943     # see if we have a plugin that wants to handle this message, if so, pass
944     # it to the plugin and return true, otherwise false
945     def privmsg(m)
946       debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
947       return unless m.plugin
948       k = m.plugin.to_sym
949       if commands.has_key?(k)
950         p = commands[k][:botmodule]
951         a = commands[k][:auth]
952         # We check here for things that don't check themselves
953         # (e.g. mapped things)
954         debug "Checking auth ..."
955         if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
956           debug "Checking response ..."
957           if p.respond_to?("privmsg")
958             begin
959               debug "#{p.botmodule_class} #{p.name} responds"
960               p.privmsg(m)
961             rescue Exception => err
962               raise if err.kind_of?(SystemExit)
963               error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
964               raise if err.kind_of?(BDB::Fatal)
965             end
966             debug "Successfully delegated #{m.inspect}"
967             return true
968           else
969             debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
970           end
971         else
972           debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
973         end
974       else
975         debug "Command #{k} isn't handled"
976       end
977       return false
978     end
979
980     # delegate IRC messages, by delegating 'listen' first, and the actual method
981     # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message
982     # as appropriate.
983     def irc_delegate(method, m)
984       delegate('listen', m)
985       if method.to_sym == :privmsg
986         delegate('ctcp_listen', m) if m.ctcp
987         delegate('message', m)
988         privmsg(m) if m.address? and not m.ignored?
989         delegate('unreplied', m) unless m.replied
990       else
991         delegate(method, m)
992       end
993     end
994   end
995
996   # Returns the only PluginManagerClass instance
997   def Plugins.manager
998     return PluginManagerClass.instance
999   end
1000
1001 end
1002 end
1003 end