]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/plugins.rb
core: sets plugin_path to loaded plugins
[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           else
657             raise aerr_in_err
658           end
659         rescue NoMethodError => nmerr_in_err
660           # Another braindead extension to StandardError, OAuth2::Error,
661           # doesn't get a string as message, but a response
662           if err.respond_to? :response
663             newerr = err.class.new(err.response)
664           else
665             raise nmerr_in_err
666           end
667         end
668         newerr.set_backtrace(bt)
669         return newerr
670       end
671     end
672
673     # add one or more directories to the list of directories to
674     # load core modules from
675     def add_core_module_dir(*dirlist)
676       @core_module_dirs += dirlist
677       debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
678     end
679
680     # add one or more directories to the list of directories to
681     # load plugins from
682     def add_plugin_dir(*dirlist)
683       @plugin_dirs += dirlist
684       debug "Plugin loading paths: #{@plugin_dirs.join(', ')}"
685     end
686
687     def clear_botmodule_dirs
688       @core_module_dirs.clear
689       @plugin_dirs.clear
690       debug "Core module and plugin loading paths cleared"
691     end
692
693     def scan_botmodules(opts={})
694       type = opts[:type]
695       processed = Hash.new
696
697       case type
698       when :core
699         dirs = @core_module_dirs
700       when :plugins
701         dirs = @plugin_dirs
702
703         @bot.config['plugins.blacklist'].each { |p|
704           pn = p + ".rb"
705           processed[pn.intern] = :blacklisted
706         }
707
708         whitelist = @bot.config['plugins.whitelist'].map { |p|
709           p + ".rb"
710         }
711       end
712
713       dirs.each do |dir|
714         next unless FileTest.directory?(dir)
715         d = Dir.new(dir)
716         d.sort.each do |file|
717           next unless file =~ /\.rb$/
718           next if file =~ /^\./
719
720           case type
721           when :plugins
722             if !whitelist.empty? && !whitelist.include?(file)
723               @ignored << {:name => file, :dir => dir, :reason => :"not whitelisted" }
724               next
725             elsif processed.has_key?(file.intern)
726               @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
727               next
728             end
729
730             if(file =~ /^(.+\.rb)\.disabled$/)
731               # GB: Do we want to do this? This means that a disabled plugin in a directory
732               #     will disable in all subsequent directories. This was probably meant
733               #     to be used before plugins.blacklist was implemented, so I think
734               #     we don't need this anymore
735               processed[$1.intern] = :disabled
736               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
737               next
738             end
739           end
740
741           begin
742             did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
743           rescue Exception => e
744             error e
745             did_it = e
746           end
747
748           case did_it
749           when Symbol
750             processed[file.intern] = did_it
751           when Exception
752             @failed << { :name => file, :dir => dir, :reason => did_it }
753           end
754         end
755       end
756     end
757
758     # load plugins from pre-assigned list of directories
759     def scan
760       @failed.clear
761       @ignored.clear
762       @delegate_list.clear
763
764       scan_botmodules(:type => :core)
765       scan_botmodules(:type => :plugins)
766
767       debug "finished loading plugins: #{status(true)}"
768       mark_priorities_dirty
769     end
770
771     # call the save method for each active plugin
772     #
773     # :botmodule ::
774     #   optional instance of a botmodule to save
775     def save(botmodule=nil)
776       if botmodule
777         botmodule.flush_registry
778         botmodule.save if botmodule.respond_to? 'save'
779       else
780         delegate 'flush_registry'
781         delegate 'save'
782       end
783     end
784
785     # call the cleanup method for each active plugin
786     #
787     # :botmodule ::
788     #   optional instance of a botmodule to cleanup
789     def cleanup(botmodule=nil)
790       if botmodule
791         botmodule.cleanup
792       else
793         delegate 'cleanup'
794       end
795       reset_botmodule_lists(botmodule)
796     end
797
798     # drops botmodules and rescan botmodules on disk
799     # calls save and cleanup for each botmodule before dropping them
800     # a optional _botmodule_ argument might specify a botmodule 
801     # instance that should be reloaded
802     #
803     # :botmodule ::
804     #   instance of the botmodule to rescan
805     def rescan(botmodule=nil)
806       save(botmodule)
807       cleanup(botmodule)
808       if botmodule
809         @failed.clear
810         @ignored.clear
811         filename = where_is(botmodule.class)
812         err = load_botmodule_file(filename, "plugin")
813         if err.is_a? Exception
814           @failed << { :name => botmodule.to_s,
815                        :dir => File.dirname(filename), :reason => err }
816         end
817       else
818         scan
819       end
820     end
821
822     def status(short=false)
823       output = []
824       if self.core_length > 0
825         if short
826           output << n_("%{count} core module loaded", "%{count} core modules loaded",
827                     self.core_length) % {:count => self.core_length}
828         else
829           output <<  n_("%{count} core module: %{list}",
830                      "%{count} core modules: %{list}", self.core_length) %
831                      { :count => self.core_length,
832                        :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
833         end
834       else
835         output << _("no core botmodules loaded")
836       end
837       # Active plugins first
838       if(self.length > 0)
839         if short
840           output << n_("%{count} plugin loaded", "%{count} plugins loaded",
841                        self.length) % {:count => self.length}
842         else
843           output << n_("%{count} plugin: %{list}",
844                        "%{count} plugins: %{list}", self.length) %
845                    { :count => self.length,
846                      :list => plugins.collect{ |p| p.name}.sort.join(", ") }
847         end
848       else
849         output << "no plugins active"
850       end
851       # Ignored plugins next
852       unless @ignored.empty? or @failures_shown
853         if short
854           output << n_("%{highlight}%{count} plugin ignored%{highlight}",
855                        "%{highlight}%{count} plugins ignored%{highlight}",
856                        @ignored.length) %
857                     { :count => @ignored.length, :highlight => Underline }
858         else
859           output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
860                        "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
861                        @ignored.length) %
862                     { :count => @ignored.length, :highlight => Underline,
863                       :bold => Bold, :command => "help ignored plugins"}
864         end
865       end
866       # Failed plugins next
867       unless @failed.empty? or @failures_shown
868         if short
869           output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
870                        "%{highlight}%{count} plugins failed to load%{highlight}",
871                        @failed.length) %
872                     { :count => @failed.length, :highlight => Reverse }
873         else
874           output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
875                        "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
876                        @failed.length) %
877                     { :count => @failed.length, :highlight => Reverse,
878                       :bold => Bold, :command => "help failed plugins"}
879         end
880       end
881       output.join '; '
882     end
883
884     # returns the last logged failure (if present) of a botmodule
885     #
886     # :name ::
887     #   name of the botmodule
888     def botmodule_failure(name)
889       failure = @failed.find { |f| f[:name] == name }
890       if failure
891         "%{exception}: %{reason}" % {
892           :exception => failure[:reason].class,
893           :reason => failure[:reason]
894         }
895       end
896     end
897
898     # return list of help topics (plugin names)
899     def helptopics
900       rv = status
901       @failures_shown = true
902       rv
903     end
904
905     def length
906       plugins.length
907     end
908
909     def core_length
910       core_modules.length
911     end
912
913     # return help for +topic+ (call associated plugin's help method)
914     def help(topic="")
915       case topic
916       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
917         # debug "Failures: #{@failed.inspect}"
918         return _("no plugins failed to load") if @failed.empty?
919         return @failed.collect { |p|
920           _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
921               :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
922               :exception => p[:reason].class, :reason => p[:reason],
923           } + if $1 && !p[:reason].backtrace.empty?
924                 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
925               else
926                 ''
927               end
928         }.join("\n")
929       when /ignored?\s*plugins?/
930         return _('no plugins were ignored') if @ignored.empty?
931
932         tmp = Hash.new
933         @ignored.each do |p|
934           reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
935           ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
936         end
937
938         return tmp.map do |dir, reasons|
939           # FIXME get rid of these string concatenations to make gettext easier
940           s = reasons.map { |r, list|
941             list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
942           }.join('; ')
943           "in #{dir}: #{s}"
944         end.join('; ')
945       when /^(\S+)\s*(.*)$/
946         key = $1
947         params = $2
948
949         # Let's see if we can match a plugin by the given name
950         (core_modules + plugins).each { |p|
951           next unless p.name == key
952           begin
953             return p.help(key, params)
954           rescue Exception => err
955             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
956             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
957           end
958         }
959
960         # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
961         k = key.to_sym
962         if commands.has_key?(k)
963           p = commands[k][:botmodule]
964           begin
965             return p.help(key, params)
966           rescue Exception => err
967             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
968             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
969           end
970         end
971       end
972       return false
973     end
974
975     def sort_modules
976       @sorted_modules = (core_modules + plugins).sort do |a, b|
977         a.priority <=> b.priority
978       end || []
979
980       @delegate_list.each_value do |list|
981         list.sort! {|a,b| a.priority <=> b.priority}
982       end
983     end
984
985     # delegate(method, [m,] opts={})
986     #
987     # see if each plugin handles _method_, and if so, call it, passing
988     # _m_ as a parameter (if present). BotModules are called in order of
989     # priority from lowest to highest.
990     #
991     # If the passed _m_ is a BasicUserMessage and is marked as #ignored?, it
992     # will only be delegated to plugins with negative priority. Conversely, if
993     # it's a fake message (see BotModule#fake_message), it will only be
994     # delegated to plugins with positive priority.
995     #
996     # Note that _m_ can also be an exploded Array, but in this case the last
997     # element of it cannot be a Hash, or it will be interpreted as the options
998     # Hash for delegate itself. The last element can be a subclass of a Hash, though.
999     # To be on the safe side, you can add an empty Hash as last parameter for delegate
1000     # when calling it with an exploded Array:
1001     #   @bot.plugins.delegate(method, *(args.push Hash.new))
1002     #
1003     # Currently supported options are the following:
1004     # :above ::
1005     #   if specified, the delegation will only consider plugins with a priority
1006     #   higher than the specified value
1007     # :below ::
1008     #   if specified, the delegation will only consider plugins with a priority
1009     #   lower than the specified value
1010     #
1011     def delegate(method, *args)
1012       # if the priorities order of the delegate list is dirty,
1013       # meaning some modules have been added or priorities have been
1014       # changed, then the delegate list will need to be sorted before
1015       # delegation.  This should always be true for the first delegation.
1016       sort_modules unless @sorted_modules
1017
1018       opts = {}
1019       opts.merge!(args.pop) if args.last.class == Hash
1020
1021       m = args.first
1022       if BasicUserMessage === m
1023         # ignored messages should not be delegated
1024         # to plugins with positive priority
1025         opts[:below] ||= 0 if m.ignored?
1026         # fake messages should not be delegated
1027         # to plugins with negative priority
1028         opts[:above] ||= 0 if m.recurse_depth > 0
1029       end
1030
1031       above = opts[:above]
1032       below = opts[:below]
1033
1034       # debug "Delegating #{method.inspect}"
1035       ret = Array.new
1036       if method.match(DEFAULT_DELEGATE_PATTERNS)
1037         debug "fast-delegating #{method}"
1038         m = method.to_sym
1039         debug "no-one to delegate to" unless @delegate_list.has_key?(m)
1040         return [] unless @delegate_list.has_key?(m)
1041         @delegate_list[m].each { |p|
1042           begin
1043             prio = p.priority
1044             unless (above and above >= prio) or (below and below <= prio)
1045               ret.push p.send(method, *args)
1046             end
1047           rescue Exception => err
1048             raise if err.kind_of?(SystemExit)
1049             error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
1050           end
1051         }
1052       else
1053         debug "slow-delegating #{method}"
1054         @sorted_modules.each { |p|
1055           if(p.respond_to? method)
1056             begin
1057               # debug "#{p.botmodule_class} #{p.name} responds"
1058               prio = p.priority
1059               unless (above and above >= prio) or (below and below <= prio)
1060                 ret.push p.send(method, *args)
1061               end
1062             rescue Exception => err
1063               raise if err.kind_of?(SystemExit)
1064               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
1065             end
1066           end
1067         }
1068       end
1069       return ret
1070       # debug "Finished delegating #{method.inspect}"
1071     end
1072
1073     # see if we have a plugin that wants to handle this message, if so, pass
1074     # it to the plugin and return true, otherwise false
1075     def privmsg(m)
1076       debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
1077       return unless m.plugin
1078       k = m.plugin.to_sym
1079       if commands.has_key?(k)
1080         p = commands[k][:botmodule]
1081         a = commands[k][:auth]
1082         # We check here for things that don't check themselves
1083         # (e.g. mapped things)
1084         debug "Checking auth ..."
1085         if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
1086           debug "Checking response ..."
1087           if p.respond_to?("privmsg")
1088             begin
1089               debug "#{p.botmodule_class} #{p.name} responds"
1090               p.privmsg(m)
1091             rescue Exception => err
1092               raise if err.kind_of?(SystemExit)
1093               error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
1094             end
1095             debug "Successfully delegated #{m.inspect}"
1096             return true
1097           else
1098             debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
1099           end
1100         else
1101           debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
1102         end
1103       else
1104         debug "Command #{k} isn't handled"
1105       end
1106       return false
1107     end
1108
1109     # delegate IRC messages, by delegating 'listen' first, and the actual method
1110     # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message
1111     # as appropriate.
1112     def irc_delegate(method, m)
1113       delegate('listen', m)
1114       if method.to_sym == :privmsg
1115         delegate('ctcp_listen', m) if m.ctcp
1116         delegate('message', m)
1117         privmsg(m) if m.address? and not m.ignored?
1118         delegate('unreplied', m) unless m.replied
1119       else
1120         delegate(method, m)
1121       end
1122     end
1123   end
1124
1125   # Returns the only PluginManagerClass instance
1126   def Plugins.manager
1127     return PluginManagerClass.instance
1128   end
1129
1130 end
1131 end
1132 end