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