]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/plugins.rb
Merge pull request #4 from ahpook/rename_karma
[user/henk/code/ruby/rbot.git] / lib / rbot / plugins.rb
old mode 100755 (executable)
new mode 100644 (file)
index 00fc423..8621fe4
@@ -4,12 +4,16 @@
 # :title: rbot plugin management
 
 require 'singleton'
+require_relative './core/utils/where_is.rb'
 
 module Irc
 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'
 
@@ -34,36 +38,36 @@ module Plugins
 
      Examples:
 
-       plugin.map 'karmastats', :action => 'karma_stats'
+       plugin.map 'pointstats', :action => 'point_stats'
 
        # while in the plugin...
-       def karma_stats(m, params)
+       def point_stats(m, params)
          m.reply "..."
        end
 
        # the default action is the first component
-       plugin.map 'karma'
+       plugin.map 'points'
 
        # attributes can be pulled out of the match string
-       plugin.map 'karma for :key'
-       plugin.map 'karma :key'
+       plugin.map 'points for :key'
+       plugin.map 'points :key'
 
        # while in the plugin...
-       def karma(m, params)
+       def points(m, params)
          item = params[:key]
-         m.reply 'karma for #{item}'
+         m.reply 'points for #{item}'
        end
 
        # you can setup defaults, to make parameters optional
-       plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
+       plugin.map 'points :key', :defaults => {:key => 'defaultvalue'}
 
        # the default auth check is also against the first component
        # but that can be changed
-       plugin.map 'karmastats', :auth => 'karma'
+       plugin.map 'pointstats', :auth => 'points'
 
        # maps can be restricted to public or private message:
-       plugin.map 'karmastats', :private => false
-       plugin.map 'karmastats', :public => false
+       plugin.map 'pointstats', :private => false
+       plugin.map 'pointstats', :public => false
 
      See MessageMapper#map for more information on the template format and the
      allowed options.
@@ -184,7 +188,7 @@ module Plugins
       @botmodule_triggers = Array.new
 
       @handler = MessageMapper.new(self)
-      @registry = Registry::Accessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
+      @registry = @bot.registry_factory.create(@bot.path, self.class.to_s.gsub(/^.*::/, ''))
 
       @manager.add_botmodule(self)
       if self.respond_to?('set_language')
@@ -338,6 +342,15 @@ 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
 
@@ -397,6 +410,9 @@ module Plugins
     attr_reader :botmodules
     attr_reader :maps
 
+    attr_reader :core_module_dirs
+    attr_reader :plugin_dirs
+
     # This is the list of patterns commonly delegated to plugins.
     # A fast delegation lookup is enabled for them.
     DEFAULT_DELEGATE_PATTERNS = %r{^(?:
@@ -447,13 +463,31 @@ module Plugins
     end
 
     # Reset lists of botmodules
-    def reset_botmodule_lists
-      @botmodules[:CoreBotModule].clear
-      @botmodules[:Plugin].clear
-      @names_hash.clear
-      @commandmappers.clear
-      @maps.clear
-      @failures_shown = false
+    #
+    # :botmodule ::
+    #   optional instance of a botmodule to remove from the lists
+    def reset_botmodule_lists(botmodule=nil)
+      if botmodule
+        # deletes only references of the botmodule
+        @botmodules[:CoreBotModule].delete botmodule
+        @botmodules[:Plugin].delete botmodule
+        @names_hash.delete_if {|key, value| value == botmodule}
+        @commandmappers.delete_if {|key, value| value[:botmodule] == botmodule }
+        @delegate_list.each_pair { |cmd, list|
+          list.delete botmodule
+        }
+        @delegate_list.delete_if {|key, value| value.empty?}
+        @maps.delete_if {|key, value| value[:botmodule] == botmodule }
+        @failures_shown = false
+      else
+        @botmodules[:CoreBotModule].clear
+        @botmodules[:Plugin].clear
+        @names_hash.clear
+        @commandmappers.clear
+        @delegate_list.clear
+        @maps.clear
+        @failures_shown = false
+      end
       mark_priorities_dirty
     end
 
@@ -465,9 +499,16 @@ module Plugins
 
     # Returns the botmodule with the given _name_
     def [](name)
+      return if not name
       @names_hash[name.to_sym]
     end
 
+    # Returns +true+ if a botmodule named _name_ exists.
+    def has_key?(name)
+      return if not name
+      @names_hash.has_key?(name.to_sym)
+    end
+
     # Returns +true+ if _cmd_ has already been registered as a command
     def who_handles?(cmd)
       return nil unless @commandmappers.has_key?(cmd.to_sym)
@@ -506,6 +547,11 @@ module Plugins
       end
       @botmodules[kl] << botmodule
       @names_hash[botmodule.to_sym] = botmodule
+      # add itself to the delegate list for the fast-delegation
+      # of methods like cleanup or privmsg, etc..
+      botmodule.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
+        @delegate_list[m.intern] << botmodule
+      }
       mark_priorities_dirty
     end
 
@@ -539,7 +585,8 @@ module Plugins
     # This method is the one that actually loads a module from the
     # file _fname_
     #
-    # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
+    # _desc_ is a simple description of what we are loading
+    # (plugin/botmodule/whatever) for error reporting
     #
     # It returns the Symbol :loaded on success, and an Exception
     # on failure
@@ -549,6 +596,7 @@ 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')}")
 
@@ -558,6 +606,7 @@ module Plugins
         plugin_string = IO.read(fname)
         debug "loading #{desc}#{fname}"
         plugin_module.module_eval(plugin_string, fname)
+
         return :loaded
       rescue Exception => err
         # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
@@ -570,10 +619,36 @@ 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)
+        msg.gsub!(fname, File.basename(fname))
+        begin
+          newerr = err.class.new(msg)
+        rescue ArgumentError => aerr_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 aerr_in_err
+          end
+        rescue NoMethodError => nmerr_in_err
+          # Another braindead extension to StandardError, OAuth2::Error,
+          # doesn't get a string as message, but a response
+          if err.respond_to? :response
+            newerr = err.class.new(err.response)
+          else
+            raise nmerr_in_err
+          end
+        end
         newerr.set_backtrace(bt)
         return newerr
       end
@@ -614,6 +689,10 @@ module Plugins
           pn = p + ".rb"
           processed[pn.intern] = :blacklisted
         }
+
+        whitelist = @bot.config['plugins.whitelist'].map { |p|
+          p + ".rb"
+        }
       end
 
       dirs.each do |dir|
@@ -625,7 +704,10 @@ module Plugins
 
           case type
           when :plugins
-            if processed.has_key?(file.intern)
+            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
@@ -641,7 +723,13 @@ module Plugins
             end
           end
 
-          did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
+          begin
+            did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
+          rescue Exception => e
+            error e
+            did_it = e
+          end
+
           case did_it
           when Symbol
             processed[file.intern] = did_it
@@ -662,32 +750,58 @@ module Plugins
       scan_botmodules(:type => :plugins)
 
       debug "finished loading plugins: #{status(true)}"
-      (core_modules + plugins).each { |p|
-       p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
-         @delegate_list[m.intern] << p
-       }
-      }
       mark_priorities_dirty
     end
 
     # call the save method for each active plugin
-    def save
-      delegate 'flush_registry'
-      delegate 'save'
+    #
+    # :botmodule ::
+    #   optional instance of a botmodule to save
+    def save(botmodule=nil)
+      if botmodule
+        botmodule.flush_registry
+        botmodule.save if botmodule.respond_to? 'save'
+      else
+        delegate 'flush_registry'
+        delegate 'save'
+      end
     end
 
     # call the cleanup method for each active plugin
-    def cleanup
-      delegate 'cleanup'
-      reset_botmodule_lists
+    #
+    # :botmodule ::
+    #   optional instance of a botmodule to cleanup
+    def cleanup(botmodule=nil)
+      if botmodule
+        botmodule.cleanup
+      else
+        delegate 'cleanup'
+      end
+      reset_botmodule_lists(botmodule)
     end
 
-    # drop all plugins and rescan plugins on disk
-    # calls save and cleanup for each plugin before dropping them
-    def rescan
-      save
-      cleanup
-      scan
+    # drops botmodules and rescan botmodules on disk
+    # calls save and cleanup for each botmodule before dropping them
+    # a optional _botmodule_ argument might specify a botmodule 
+    # instance that should be reloaded
+    #
+    # :botmodule ::
+    #   instance of the botmodule to rescan
+    def rescan(botmodule=nil)
+      save(botmodule)
+      cleanup(botmodule)
+      if botmodule
+        @failed.clear
+        @ignored.clear
+        filename = where_is(botmodule.class)
+        err = load_botmodule_file(filename, "plugin")
+        if err.is_a? Exception
+          @failed << { :name => botmodule.to_s,
+                       :dir => File.dirname(filename), :reason => err }
+        end
+      else
+        scan
+      end
     end
 
     def status(short=false)
@@ -752,6 +866,20 @@ module Plugins
       output.join '; '
     end
 
+    # returns the last logged failure (if present) of a botmodule
+    #
+    # :name ::
+    #   name of the botmodule
+    def botmodule_failure(name)
+      failure = @failed.find { |f| f[:name] == name }
+      if failure
+        "%{exception}: %{reason}" % {
+          :exception => failure[:reason].class,
+          :reason => failure[:reason]
+        }
+      end
+    end
+
     # return list of help topics (plugin names)
     def helptopics
       rv = status
@@ -839,8 +967,7 @@ module Plugins
       end
     end
 
-    # call-seq: delegate</span><span class="method-args">(method, m, opts={})</span>
-    # <span class="method-name">delegate</span><span class="method-args">(method, opts={})
+    # delegate(method, [m,] 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
@@ -905,7 +1032,6 @@ module Plugins
           rescue Exception => err
             raise if err.kind_of?(SystemExit)
             error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
-            raise if err.kind_of?(BDB::Fatal)
           end
         }
       else
@@ -921,7 +1047,6 @@ module Plugins
             rescue Exception => err
               raise if err.kind_of?(SystemExit)
               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
-              raise if err.kind_of?(BDB::Fatal)
             end
           end
         }
@@ -951,7 +1076,6 @@ module Plugins
             rescue Exception => err
               raise if err.kind_of?(SystemExit)
               error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
-              raise if err.kind_of?(BDB::Fatal)
             end
             debug "Successfully delegated #{m.inspect}"
             return true