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