]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/plugins.rb
Improvements to the points plugin
[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     # This is the list of patterns commonly delegated to plugins.
414     # A fast delegation lookup is enabled for them.
415     DEFAULT_DELEGATE_PATTERNS = %r{^(?:
416       connect|names|nick|
417       listen|ctcp_listen|privmsg|unreplied|
418       kick|join|part|quit|
419       save|cleanup|flush_registry|
420       set_.*|event_.*
421     )$}x
422
423     def initialize
424       @botmodules = {
425         :CoreBotModule => [],
426         :Plugin => []
427       }
428
429       @names_hash = Hash.new
430       @commandmappers = Hash.new
431       @maps = Hash.new
432
433       # modules will be sorted on first delegate call
434       @sorted_modules = nil
435
436       @delegate_list = Hash.new { |h, k|
437         h[k] = Array.new
438       }
439
440       @core_module_dirs = []
441       @plugin_dirs = []
442
443       @failed = Array.new
444       @ignored = Array.new
445
446       bot_associate(nil)
447     end
448
449     def inspect
450       ret = self.to_s[0..-2]
451       ret << ' corebotmodules='
452       ret << @botmodules[:CoreBotModule].map { |m|
453         m.name
454       }.inspect
455       ret << ' plugins='
456       ret << @botmodules[:Plugin].map { |m|
457         m.name
458       }.inspect
459       ret << ">"
460     end
461
462     # Reset lists of botmodules
463     #
464     # :botmodule ::
465     #   optional instance of a botmodule to remove from the lists
466     def reset_botmodule_lists(botmodule=nil)
467       if botmodule
468         # deletes only references of the botmodule
469         @botmodules[:CoreBotModule].delete botmodule
470         @botmodules[:Plugin].delete botmodule
471         @names_hash.delete_if {|key, value| value == botmodule}
472         @commandmappers.delete_if {|key, value| value[:botmodule] == botmodule }
473         @delegate_list.each_pair { |cmd, list|
474           list.delete botmodule
475         }
476         @delegate_list.delete_if {|key, value| value.empty?}
477         @maps.delete_if {|key, value| value[:botmodule] == botmodule }
478         @failures_shown = false
479       else
480         @botmodules[:CoreBotModule].clear
481         @botmodules[:Plugin].clear
482         @names_hash.clear
483         @commandmappers.clear
484         @delegate_list.clear
485         @maps.clear
486         @failures_shown = false
487       end
488       mark_priorities_dirty
489     end
490
491     # Associate with bot _bot_
492     def bot_associate(bot)
493       reset_botmodule_lists
494       @bot = bot
495     end
496
497     # Returns the botmodule with the given _name_
498     def [](name)
499       return if not name
500       @names_hash[name.to_sym]
501     end
502
503     # Returns +true+ if a botmodule named _name_ exists.
504     def has_key?(name)
505       return if not name
506       @names_hash.has_key?(name.to_sym)
507     end
508
509     # Returns +true+ if _cmd_ has already been registered as a command
510     def who_handles?(cmd)
511       return nil unless @commandmappers.has_key?(cmd.to_sym)
512       return @commandmappers[cmd.to_sym][:botmodule]
513     end
514
515     # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
516     def register(botmodule, cmd, auth_path)
517       raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
518       @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
519     end
520
521     # Registers botmodule _botmodule_ with map _map_. This adds the map to the #maps hash
522     # which has three keys:
523     #
524     # botmodule:: the associated botmodule
525     # auth:: an array of auth keys checked by the map; the first is the full_auth_path of the map
526     # map:: the actual MessageTemplate object
527     #
528     #
529     def register_map(botmodule, map)
530       raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
531       @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map }
532     end
533
534     def add_botmodule(botmodule)
535       raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
536       kl = botmodule.botmodule_class
537       if @names_hash.has_key?(botmodule.to_sym)
538         case self[botmodule].botmodule_class
539         when kl
540           raise "#{kl} #{botmodule} already registered!"
541         else
542           raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
543         end
544       end
545       @botmodules[kl] << botmodule
546       @names_hash[botmodule.to_sym] = botmodule
547       # add itself to the delegate list for the fast-delegation
548       # of methods like cleanup or privmsg, etc..
549       botmodule.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
550         @delegate_list[m.intern] << botmodule
551       }
552       mark_priorities_dirty
553     end
554
555     # Returns an array of the loaded plugins
556     def core_modules
557       @botmodules[:CoreBotModule]
558     end
559
560     # Returns an array of the loaded plugins
561     def plugins
562       @botmodules[:Plugin]
563     end
564
565     # Returns a hash of the registered message prefixes and associated
566     # plugins
567     def commands
568       @commandmappers
569     end
570
571     # Tells the PluginManager that the next time it delegates an event, it
572     # should sort the modules by priority
573     def mark_priorities_dirty
574       @sorted_modules = nil
575     end
576
577     # Makes a string of error _err_ by adding text _str_
578     def report_error(str, err)
579       ([str, err.inspect] + err.backtrace).join("\n")
580     end
581
582     # This method is the one that actually loads a module from the
583     # file _fname_
584     #
585     # _desc_ is a simple description of what we are loading
586     # (plugin/botmodule/whatever) for error reporting
587     #
588     # It returns the Symbol :loaded on success, and an Exception
589     # on failure
590     #
591     def load_botmodule_file(fname, desc=nil)
592       # create a new, anonymous module to "house" the plugin
593       # the idea here is to prevent namespace pollution. perhaps there
594       # is another way?
595       plugin_module = Module.new
596       
597       # each plugin uses its own textdomain, we bind it automatically here
598       bindtextdomain_to(plugin_module, "rbot-#{File.basename(fname, '.rb')}")
599
600       desc = desc.to_s + " " if desc
601
602       begin
603         plugin_string = IO.read(fname)
604         debug "loading #{desc}#{fname}"
605         plugin_module.module_eval(plugin_string, fname)
606
607         return :loaded
608       rescue Exception => err
609         # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
610         error report_error("#{desc}#{fname} load failed", err)
611         bt = err.backtrace.select { |line|
612           line.match(/^(\(eval\)|#{fname}):\d+/)
613         }
614         bt.map! { |el|
615           el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
616             "#{fname}#{$1}#{$3}"
617           }
618         }
619         msg = err.to_s.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
620           "#{fname}#{$1}#{$3}"
621         }
622         msg.gsub!(fname, File.basename(fname))
623         begin
624           newerr = err.class.new(msg)
625         rescue ArgumentError => aerr_in_err
626           # Somebody should hang the ActiveSupport developers by their balls
627           # with barbed wire. Their MissingSourceFile extension to LoadError
628           # _expects_ a second argument, breaking the usual Exception interface
629           # (instead, the smart thing to do would have been to make the second
630           # parameter optional and run the code in the from_message method if
631           # it was missing).
632           # Anyway, we try to cope with this in the simplest possible way. On
633           # the upside, this new block can be extended to handle other similar
634           # idiotic approaches
635           if err.class.respond_to? :from_message
636             newerr = err.class.from_message(msg)
637           else
638             raise aerr_in_err
639           end
640         rescue NoMethodError => nmerr_in_err
641           # Another braindead extension to StandardError, OAuth2::Error,
642           # doesn't get a string as message, but a response
643           if err.respond_to? :response
644             newerr = err.class.new(err.response)
645           else
646             raise nmerr_in_err
647           end
648         end
649         newerr.set_backtrace(bt)
650         return newerr
651       end
652     end
653     private :load_botmodule_file
654
655     # add one or more directories to the list of directories to
656     # load core modules from
657     def add_core_module_dir(*dirlist)
658       @core_module_dirs += dirlist
659       debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
660     end
661
662     # add one or more directories to the list of directories to
663     # load plugins from
664     def add_plugin_dir(*dirlist)
665       @plugin_dirs += dirlist
666       debug "Plugin loading paths: #{@plugin_dirs.join(', ')}"
667     end
668
669     def clear_botmodule_dirs
670       @core_module_dirs.clear
671       @plugin_dirs.clear
672       debug "Core module and plugin loading paths cleared"
673     end
674
675     def scan_botmodules(opts={})
676       type = opts[:type]
677       processed = Hash.new
678
679       case type
680       when :core
681         dirs = @core_module_dirs
682       when :plugins
683         dirs = @plugin_dirs
684
685         @bot.config['plugins.blacklist'].each { |p|
686           pn = p + ".rb"
687           processed[pn.intern] = :blacklisted
688         }
689
690         whitelist = @bot.config['plugins.whitelist'].map { |p|
691           p + ".rb"
692         }
693       end
694
695       dirs.each do |dir|
696         next unless FileTest.directory?(dir)
697         d = Dir.new(dir)
698         d.sort.each do |file|
699           next unless file =~ /\.rb$/
700           next if file =~ /^\./
701
702           case type
703           when :plugins
704             if !whitelist.empty? && !whitelist.include?(file)
705               @ignored << {:name => file, :dir => dir, :reason => :"not whitelisted" }
706               next
707             elsif processed.has_key?(file.intern)
708               @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
709               next
710             end
711
712             if(file =~ /^(.+\.rb)\.disabled$/)
713               # GB: Do we want to do this? This means that a disabled plugin in a directory
714               #     will disable in all subsequent directories. This was probably meant
715               #     to be used before plugins.blacklist was implemented, so I think
716               #     we don't need this anymore
717               processed[$1.intern] = :disabled
718               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
719               next
720             end
721           end
722
723           begin
724             did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
725           rescue Exception => e
726             error e
727             did_it = e
728           end
729
730           case did_it
731           when Symbol
732             processed[file.intern] = did_it
733           when Exception
734             @failed << { :name => file, :dir => dir, :reason => did_it }
735           end
736         end
737       end
738     end
739
740     # load plugins from pre-assigned list of directories
741     def scan
742       @failed.clear
743       @ignored.clear
744       @delegate_list.clear
745
746       scan_botmodules(:type => :core)
747       scan_botmodules(:type => :plugins)
748
749       debug "finished loading plugins: #{status(true)}"
750       mark_priorities_dirty
751     end
752
753     # call the save method for each active plugin
754     #
755     # :botmodule ::
756     #   optional instance of a botmodule to save
757     def save(botmodule=nil)
758       if botmodule
759         botmodule.flush_registry
760         botmodule.save if botmodule.respond_to? 'save'
761       else
762         delegate 'flush_registry'
763         delegate 'save'
764       end
765     end
766
767     # call the cleanup method for each active plugin
768     #
769     # :botmodule ::
770     #   optional instance of a botmodule to cleanup
771     def cleanup(botmodule=nil)
772       if botmodule
773         botmodule.cleanup
774       else
775         delegate 'cleanup'
776       end
777       reset_botmodule_lists(botmodule)
778     end
779
780     # drops botmodules and rescan botmodules on disk
781     # calls save and cleanup for each botmodule before dropping them
782     # a optional _botmodule_ argument might specify a botmodule 
783     # instance that should be reloaded
784     #
785     # :botmodule ::
786     #   instance of the botmodule to rescan
787     def rescan(botmodule=nil)
788       save(botmodule)
789       cleanup(botmodule)
790       if botmodule
791         @failed.clear
792         @ignored.clear
793         filename = where_is(botmodule.class)
794         err = load_botmodule_file(filename, "plugin")
795         if err.is_a? Exception
796           @failed << { :name => botmodule.to_s,
797                        :dir => File.dirname(filename), :reason => err }
798         end
799       else
800         scan
801       end
802     end
803
804     def status(short=false)
805       output = []
806       if self.core_length > 0
807         if short
808           output << n_("%{count} core module loaded", "%{count} core modules loaded",
809                     self.core_length) % {:count => self.core_length}
810         else
811           output <<  n_("%{count} core module: %{list}",
812                      "%{count} core modules: %{list}", self.core_length) %
813                      { :count => self.core_length,
814                        :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
815         end
816       else
817         output << _("no core botmodules loaded")
818       end
819       # Active plugins first
820       if(self.length > 0)
821         if short
822           output << n_("%{count} plugin loaded", "%{count} plugins loaded",
823                        self.length) % {:count => self.length}
824         else
825           output << n_("%{count} plugin: %{list}",
826                        "%{count} plugins: %{list}", self.length) %
827                    { :count => self.length,
828                      :list => plugins.collect{ |p| p.name}.sort.join(", ") }
829         end
830       else
831         output << "no plugins active"
832       end
833       # Ignored plugins next
834       unless @ignored.empty? or @failures_shown
835         if short
836           output << n_("%{highlight}%{count} plugin ignored%{highlight}",
837                        "%{highlight}%{count} plugins ignored%{highlight}",
838                        @ignored.length) %
839                     { :count => @ignored.length, :highlight => Underline }
840         else
841           output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
842                        "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
843                        @ignored.length) %
844                     { :count => @ignored.length, :highlight => Underline,
845                       :bold => Bold, :command => "help ignored plugins"}
846         end
847       end
848       # Failed plugins next
849       unless @failed.empty? or @failures_shown
850         if short
851           output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
852                        "%{highlight}%{count} plugins failed to load%{highlight}",
853                        @failed.length) %
854                     { :count => @failed.length, :highlight => Reverse }
855         else
856           output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
857                        "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
858                        @failed.length) %
859                     { :count => @failed.length, :highlight => Reverse,
860                       :bold => Bold, :command => "help failed plugins"}
861         end
862       end
863       output.join '; '
864     end
865
866     # returns the last logged failure (if present) of a botmodule
867     #
868     # :name ::
869     #   name of the botmodule
870     def botmodule_failure(name)
871       failure = @failed.find { |f| f[:name] == name }
872       if failure
873         "%{exception}: %{reason}" % {
874           :exception => failure[:reason].class,
875           :reason => failure[:reason]
876         }
877       end
878     end
879
880     # return list of help topics (plugin names)
881     def helptopics
882       rv = status
883       @failures_shown = true
884       rv
885     end
886
887     def length
888       plugins.length
889     end
890
891     def core_length
892       core_modules.length
893     end
894
895     # return help for +topic+ (call associated plugin's help method)
896     def help(topic="")
897       case topic
898       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
899         # debug "Failures: #{@failed.inspect}"
900         return _("no plugins failed to load") if @failed.empty?
901         return @failed.collect { |p|
902           _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
903               :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
904               :exception => p[:reason].class, :reason => p[:reason],
905           } + if $1 && !p[:reason].backtrace.empty?
906                 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
907               else
908                 ''
909               end
910         }.join("\n")
911       when /ignored?\s*plugins?/
912         return _('no plugins were ignored') if @ignored.empty?
913
914         tmp = Hash.new
915         @ignored.each do |p|
916           reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
917           ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
918         end
919
920         return tmp.map do |dir, reasons|
921           # FIXME get rid of these string concatenations to make gettext easier
922           s = reasons.map { |r, list|
923             list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
924           }.join('; ')
925           "in #{dir}: #{s}"
926         end.join('; ')
927       when /^(\S+)\s*(.*)$/
928         key = $1
929         params = $2
930
931         # Let's see if we can match a plugin by the given name
932         (core_modules + plugins).each { |p|
933           next unless p.name == key
934           begin
935             return p.help(key, params)
936           rescue Exception => err
937             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
938             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
939           end
940         }
941
942         # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
943         k = key.to_sym
944         if commands.has_key?(k)
945           p = commands[k][:botmodule]
946           begin
947             return p.help(key, params)
948           rescue Exception => err
949             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
950             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
951           end
952         end
953       end
954       return false
955     end
956
957     def sort_modules
958       @sorted_modules = (core_modules + plugins).sort do |a, b|
959         a.priority <=> b.priority
960       end || []
961
962       @delegate_list.each_value do |list|
963         list.sort! {|a,b| a.priority <=> b.priority}
964       end
965     end
966
967     # delegate(method, [m,] opts={})
968     #
969     # see if each plugin handles _method_, and if so, call it, passing
970     # _m_ as a parameter (if present). BotModules are called in order of
971     # priority from lowest to highest.
972     #
973     # If the passed _m_ is a BasicUserMessage and is marked as #ignored?, it
974     # will only be delegated to plugins with negative priority. Conversely, if
975     # it's a fake message (see BotModule#fake_message), it will only be
976     # delegated to plugins with positive priority.
977     #
978     # Note that _m_ can also be an exploded Array, but in this case the last
979     # element of it cannot be a Hash, or it will be interpreted as the options
980     # Hash for delegate itself. The last element can be a subclass of a Hash, though.
981     # To be on the safe side, you can add an empty Hash as last parameter for delegate
982     # when calling it with an exploded Array:
983     #   @bot.plugins.delegate(method, *(args.push Hash.new))
984     #
985     # Currently supported options are the following:
986     # :above ::
987     #   if specified, the delegation will only consider plugins with a priority
988     #   higher than the specified value
989     # :below ::
990     #   if specified, the delegation will only consider plugins with a priority
991     #   lower than the specified value
992     #
993     def delegate(method, *args)
994       # if the priorities order of the delegate list is dirty,
995       # meaning some modules have been added or priorities have been
996       # changed, then the delegate list will need to be sorted before
997       # delegation.  This should always be true for the first delegation.
998       sort_modules unless @sorted_modules
999
1000       opts = {}
1001       opts.merge(args.pop) if args.last.class == Hash
1002
1003       m = args.first
1004       if BasicUserMessage === m
1005         # ignored messages should not be delegated
1006         # to plugins with positive priority
1007         opts[:below] ||= 0 if m.ignored?
1008         # fake messages should not be delegated
1009         # to plugins with negative priority
1010         opts[:above] ||= 0 if m.recurse_depth > 0
1011       end
1012
1013       above = opts[:above]
1014       below = opts[:below]
1015
1016       # debug "Delegating #{method.inspect}"
1017       ret = Array.new
1018       if method.match(DEFAULT_DELEGATE_PATTERNS)
1019         debug "fast-delegating #{method}"
1020         m = method.to_sym
1021         debug "no-one to delegate to" unless @delegate_list.has_key?(m)
1022         return [] unless @delegate_list.has_key?(m)
1023         @delegate_list[m].each { |p|
1024           begin
1025             prio = p.priority
1026             unless (above and above >= prio) or (below and below <= prio)
1027               ret.push p.send(method, *args)
1028             end
1029           rescue Exception => err
1030             raise if err.kind_of?(SystemExit)
1031             error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
1032           end
1033         }
1034       else
1035         debug "slow-delegating #{method}"
1036         @sorted_modules.each { |p|
1037           if(p.respond_to? method)
1038             begin
1039               # debug "#{p.botmodule_class} #{p.name} responds"
1040               prio = p.priority
1041               unless (above and above >= prio) or (below and below <= prio)
1042                 ret.push p.send(method, *args)
1043               end
1044             rescue Exception => err
1045               raise if err.kind_of?(SystemExit)
1046               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
1047             end
1048           end
1049         }
1050       end
1051       return ret
1052       # debug "Finished delegating #{method.inspect}"
1053     end
1054
1055     # see if we have a plugin that wants to handle this message, if so, pass
1056     # it to the plugin and return true, otherwise false
1057     def privmsg(m)
1058       debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
1059       return unless m.plugin
1060       k = m.plugin.to_sym
1061       if commands.has_key?(k)
1062         p = commands[k][:botmodule]
1063         a = commands[k][:auth]
1064         # We check here for things that don't check themselves
1065         # (e.g. mapped things)
1066         debug "Checking auth ..."
1067         if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
1068           debug "Checking response ..."
1069           if p.respond_to?("privmsg")
1070             begin
1071               debug "#{p.botmodule_class} #{p.name} responds"
1072               p.privmsg(m)
1073             rescue Exception => err
1074               raise if err.kind_of?(SystemExit)
1075               error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
1076             end
1077             debug "Successfully delegated #{m.inspect}"
1078             return true
1079           else
1080             debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
1081           end
1082         else
1083           debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
1084         end
1085       else
1086         debug "Command #{k} isn't handled"
1087       end
1088       return false
1089     end
1090
1091     # delegate IRC messages, by delegating 'listen' first, and the actual method
1092     # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message
1093     # as appropriate.
1094     def irc_delegate(method, m)
1095       delegate('listen', m)
1096       if method.to_sym == :privmsg
1097         delegate('ctcp_listen', m) if m.ctcp
1098         delegate('message', m)
1099         privmsg(m) if m.address? and not m.ignored?
1100         delegate('unreplied', m) unless m.replied
1101       else
1102         delegate(method, m)
1103       end
1104     end
1105   end
1106
1107   # Returns the only PluginManagerClass instance
1108   def Plugins.manager
1109     return PluginManagerClass.instance
1110   end
1111
1112 end
1113 end
1114 end