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