]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/plugins.rb
Survive active_support idiocy
[user/henk/code/ruby/rbot.git] / lib / rbot / plugins.rb
index 9dccf9124f7996ee5b429dc39a06d105884d4a45..55733ceed5417b481a285175321eaa11a3f44fe0 100644 (file)
@@ -10,6 +10,9 @@ class Bot
     Config.register Config::ArrayValue.new('plugins.blacklist',
       :default => [], :wizard => false, :requires_rescan => true,
       :desc => "Plugins that should not be loaded")
+    Config.register Config::ArrayValue.new('plugins.whitelist',
+      :default => [], :wizard => false, :requires_rescan => true,
+      :desc => "Only whitelisted plugins will be loaded unless the list is empty")
 module Plugins
   require 'rbot/messagemapper'
 
@@ -96,6 +99,10 @@ module Plugins
   unreplied(PrivMessage)::
                          Called for a PRIVMSG which has not been replied to.
 
+  notice(NoticeMessage)::
+                         Called for all Notices. Please notice that in general
+                         should not be replied to.
+
   kick(KickMessage)::
                          Called when a user (or the bot) is kicked from a
                          channel the bot is in.
@@ -114,10 +121,20 @@ module Plugins
 
   nick(NickMessage)::
                          Called when a user (or the bot) changes Nick
+  modechange(ModeChangeMessage)::
+                         Called when a User or Channel mode is changed
   topic(TopicMessage)::
                          Called when a user (or the bot) changes a channel
                          topic
 
+  welcome(WelcomeMessage)::
+                         Called when the welcome message is received on
+                         joining a server succesfully.
+
+  motd(MotdMessage)::
+                         Called when the Message Of The Day is fully
+                         recevied from the server.
+
   connect::              Called when a server is joined successfully, but
                          before autojoin channels are joined (no params)
 
@@ -165,6 +182,7 @@ module Plugins
     def initialize
       @manager = Plugins::manager
       @bot = @manager.bot
+      @priority = nil
 
       @botmodule_triggers = Array.new
 
@@ -183,7 +201,7 @@ module Plugins
       @priority ||= 1
     end
 
-    # Returns the symbol :BotModule 
+    # Returns the symbol :BotModule
     def botmodule_class
       :BotModule
     end
@@ -216,7 +234,7 @@ module Plugins
     # Signal to other BotModules that an even happened.
     #
     def call_event(ev, *args)
-      @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *args)
+      @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *(args.push Hash.new))
     end
 
     # call-seq: map(template, options)
@@ -303,7 +321,7 @@ module Plugins
     #
     # This command is now superceded by the #map() command, which should be used
     # instead whenever possible.
-    # 
+    #
     def register(cmd, opts={})
       raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
       who = @manager.who_handles?(cmd)
@@ -323,10 +341,19 @@ module Plugins
     # MessageMapper uses 'usage' as its default fallback method.
     #
     def usage(m, params = {})
+      if params[:failures].respond_to? :find
+        friendly = params[:failures].find do |f|
+          f.kind_of? MessageMapper::FriendlyFailure
+        end
+        if friendly
+          m.reply friendly.friendly
+          return
+        end
+      end
       m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
     end
 
-    # Define the priority of the module.  During event delegation, lower 
+    # Define the priority of the module.  During event delegation, lower
     # priority modules will be called first.  Default priority is 1
     def priority=(prio)
       if @priority != prio
@@ -334,6 +361,20 @@ module Plugins
         @bot.plugins.mark_priorities_dirty
       end
     end
+
+    # Directory name to be joined to the botclass to access data files. By
+    # default this is the plugin name itself, but may be overridden, for
+    # example by plugins that share their datafiles or for backwards
+    # compatibilty
+    def dirname
+      name
+    end
+
+    # Filename for a datafile built joining the botclass, plugin dirname and
+    # actual file name
+    def datafile(*fname)
+      @bot.path dirname, *fname
+    end
   end
 
   # A CoreBotModule is a BotModule that provides core functionality.
@@ -395,7 +436,8 @@ module Plugins
         h[k] = Array.new
       }
 
-      @dirs = []
+      @core_module_dirs = []
+      @plugin_dirs = []
 
       @failed = Array.new
       @ignored = Array.new
@@ -519,11 +561,13 @@ module Plugins
       # the idea here is to prevent namespace pollution. perhaps there
       # is another way?
       plugin_module = Module.new
+      # each plugin uses its own textdomain, we bind it automatically here
+      bindtextdomain_to(plugin_module, "rbot-#{File.basename(fname, '.rb')}")
 
       desc = desc.to_s + " " if desc
 
       begin
-        plugin_string = IO.readlines(fname).join("")
+        plugin_string = IO.read(fname)
         debug "loading #{desc}#{fname}"
         plugin_module.module_eval(plugin_string, fname)
         return :loaded
@@ -538,10 +582,27 @@ module Plugins
             "#{fname}#{$1}#{$3}"
           }
         }
-        msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
+        msg = err.to_s.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
           "#{fname}#{$1}#{$3}"
         }
-        newerr = err.class.new(msg)
+        begin
+          newerr = err.class.new(msg)
+        rescue ArgumentError => err_in_err
+          # Somebody should hang the ActiveSupport developers by their balls
+          # with barbed wire. Their MissingSourceFile extension to LoadError
+          # _expects_ a second argument, breaking the usual Exception interface
+          # (instead, the smart thing to do would have been to make the second
+          # parameter optional and run the code in the from_message method if
+          # it was missing).
+          # Anyway, we try to cope with this in the simplest possible way. On
+          # the upside, this new block can be extended to handle other similar
+          # idiotic approaches
+          if err.class.respond_to? :from_message
+            newerr = err.class.from_message(msg)
+          else
+            raise err_in_err
+          end
+        end
         newerr.set_backtrace(bt)
         return newerr
       end
@@ -549,42 +610,58 @@ module Plugins
     private :load_botmodule_file
 
     # add one or more directories to the list of directories to
-    # load botmodules from
-    #
-    # TODO find a way to specify necessary plugins which _must_ be loaded
-    #
-    def add_botmodule_dir(*dirlist)
-      @dirs += dirlist
-      debug "Botmodule loading path: #{@dirs.join(', ')}"
+    # load core modules from
+    def add_core_module_dir(*dirlist)
+      @core_module_dirs += dirlist
+      debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
     end
 
-    def clear_botmodule_dirs
-      @dirs.clear
-      debug "Botmodule loading path cleared"
+    # add one or more directories to the list of directories to
+    # load plugins from
+    def add_plugin_dir(*dirlist)
+      @plugin_dirs += dirlist
+      debug "Plugin loading paths: #{@plugin_dirs.join(', ')}"
     end
 
-    # load plugins from pre-assigned list of directories
-    def scan
-      @failed.clear
-      @ignored.clear
-      @delegate_list.clear
+    def clear_botmodule_dirs
+      @core_module_dirs.clear
+      @plugin_dirs.clear
+      debug "Core module and plugin loading paths cleared"
+    end
 
+    def scan_botmodules(opts={})
+      type = opts[:type]
       processed = Hash.new
 
-      @bot.config['plugins.blacklist'].each { |p|
-        pn = p + ".rb"
-        processed[pn.intern] = :blacklisted
-      }
+      case type
+      when :core
+        dirs = @core_module_dirs
+      when :plugins
+        dirs = @plugin_dirs
 
-      dirs = @dirs
-      dirs.each {|dir|
-        if(FileTest.directory?(dir))
-          d = Dir.new(dir)
-          d.sort.each {|file|
+        @bot.config['plugins.blacklist'].each { |p|
+          pn = p + ".rb"
+          processed[pn.intern] = :blacklisted
+        }
 
-            next if(file =~ /^\./)
+        whitelist = @bot.config['plugins.whitelist'].map { |p|
+          p + ".rb"
+        }
+      end
 
-            if processed.has_key?(file.intern)
+      dirs.each do |dir|
+        next unless FileTest.directory?(dir)
+        d = Dir.new(dir)
+        d.sort.each do |file|
+          next unless file =~ /\.rb$/
+          next if file =~ /^\./
+
+          case type
+          when :plugins
+            if !whitelist.empty? && !whitelist.include?(file)
+              @ignored << {:name => file, :dir => dir, :reason => :"not whitelisted" }
+              next
+            elsif processed.has_key?(file.intern)
               @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
               next
             end
@@ -598,20 +675,28 @@ module Plugins
               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
               next
             end
+          end
 
-            next unless(file =~ /\.rb$/)
+          did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
+          case did_it
+          when Symbol
+            processed[file.intern] = did_it
+          when Exception
+            @failed << { :name => file, :dir => dir, :reason => did_it }
+          end
+        end
+      end
+    end
 
-            did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
-            case did_it
-            when Symbol
-              processed[file.intern] = did_it
-            when Exception
-              @failed <<  { :name => file, :dir => dir, :reason => did_it }
-            end
+    # load plugins from pre-assigned list of directories
+    def scan
+      @failed.clear
+      @ignored.clear
+      @delegate_list.clear
+
+      scan_botmodules(:type => :core)
+      scan_botmodules(:type => :plugins)
 
-          }
-        end
-      }
       debug "finished loading plugins: #{status(true)}"
       (core_modules + plugins).each { |p|
        p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
@@ -781,7 +866,7 @@ module Plugins
     end
 
     def sort_modules
-      @sorted_modules = (core_modules + plugins).sort do |a, b| 
+      @sorted_modules = (core_modules + plugins).sort do |a, b|
         a.priority <=> b.priority
       end || []
 
@@ -790,32 +875,55 @@ module Plugins
       end
     end
 
-    # see if each plugin handles +method+, and if so, call it, passing
-    # +message+ as a parameter.  botmodules are called in order of priority
-    # from lowest to highest.  +DEPRECATED+ please use delegate_event.
+    # call-seq: delegate</span><span class="method-args">(method, m, opts={})</span>
+    # <span class="method-name">delegate</span><span class="method-args">(method, opts={})
+    #
+    # see if each plugin handles _method_, and if so, call it, passing
+    # _m_ as a parameter (if present). BotModules are called in order of
+    # priority from lowest to highest.
+    #
+    # If the passed _m_ is a BasicUserMessage and is marked as #ignored?, it
+    # will only be delegated to plugins with negative priority. Conversely, if
+    # it's a fake message (see BotModule#fake_message), it will only be
+    # delegated to plugins with positive priority.
+    #
+    # Note that _m_ can also be an exploded Array, but in this case the last
+    # element of it cannot be a Hash, or it will be interpreted as the options
+    # Hash for delegate itself. The last element can be a subclass of a Hash, though.
+    # To be on the safe side, you can add an empty Hash as last parameter for delegate
+    # when calling it with an exploded Array:
+    #   @bot.plugins.delegate(method, *(args.push Hash.new))
+    #
+    # Currently supported options are the following:
+    # :above ::
+    #   if specified, the delegation will only consider plugins with a priority
+    #   higher than the specified value
+    # :below ::
+    #   if specified, the delegation will only consider plugins with a priority
+    #   lower than the specified value
+    #
     def delegate(method, *args)
-      delegate_event(method, :args => args)
-    end
-
-    # see if each plugin handles +method+, and if so, call it, passing
-    # +opts[:args]+ as a parameter.  +opts[:above]+ and +opts[:below]+
-    # are used for a threshold of botmodule priorities that will be called.
-    # If :above is defined, only botmodules with a priority above the value
-    # will be called, for example.  botmodules are called in order of
-    # priority from lowest to hightest.
-    def delegate_event(method, o={})
       # if the priorities order of the delegate list is dirty,
       # meaning some modules have been added or priorities have been
       # changed, then the delegate list will need to be sorted before
       # delegation.  This should always be true for the first delegation.
       sort_modules unless @sorted_modules
 
-      # set defaults
-      opts = {:args => []}.merge(o)
+      opts = {}
+      opts.merge(args.pop) if args.last.class == Hash
+
+      m = args.first
+      if BasicUserMessage === m
+        # ignored messages should not be delegated
+        # to plugins with positive priority
+        opts[:below] ||= 0 if m.ignored?
+        # fake messages should not be delegated
+        # to plugins with negative priority
+        opts[:above] ||= 0 if m.recurse_depth > 0
+      end
 
       above = opts[:above]
       below = opts[:below]
-      args = opts[:args]
 
       # debug "Delegating #{method.inspect}"
       ret = Array.new
@@ -828,7 +936,7 @@ module Plugins
           begin
             prio = p.priority
             unless (above and above >= prio) or (below and below <= prio)
-              ret.push p.send(method, *(args||[]))
+              ret.push p.send(method, *args)
             end
           rescue Exception => err
             raise if err.kind_of?(SystemExit)
@@ -844,7 +952,7 @@ module Plugins
               # debug "#{p.botmodule_class} #{p.name} responds"
               prio = p.priority
               unless (above and above >= prio) or (below and below <= prio)
-                ret.push p.send(method, *(args||[]))
+                ret.push p.send(method, *args)
               end
             rescue Exception => err
               raise if err.kind_of?(SystemExit)
@@ -903,7 +1011,7 @@ module Plugins
       if method.to_sym == :privmsg
         delegate('ctcp_listen', m) if m.ctcp
         delegate('message', m)
-        privmsg(m) if m.address?
+        privmsg(m) if m.address? and not m.ignored?
         delegate('unreplied', m) unless m.replied
       else
         delegate(method, m)