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