]> 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
index 250efa017c9859addc4f3f3e934b53eb0065768e..8621fe45341456e485894a4768c8d8c298ae8257 100644 (file)
@@ -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.
@@ -179,11 +183,12 @@ module Plugins
     def initialize
       @manager = Plugins::manager
       @bot = @manager.bot
+      @priority = nil
 
       @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')
@@ -197,7 +202,7 @@ module Plugins
       @priority ||= 1
     end
 
-    # Returns the symbol :BotModule 
+    # Returns the symbol :BotModule
     def botmodule_class
       :BotModule
     end
@@ -317,7 +322,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)
@@ -337,10 +342,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
@@ -348,6 +362,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.
@@ -382,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{^(?:
@@ -409,7 +440,8 @@ module Plugins
         h[k] = Array.new
       }
 
-      @dirs = []
+      @core_module_dirs = []
+      @plugin_dirs = []
 
       @failed = Array.new
       @ignored = Array.new
@@ -431,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
 
@@ -449,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)
@@ -490,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
 
@@ -523,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
@@ -533,13 +596,17 @@ 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
       rescue Exception => err
         # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
@@ -552,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
@@ -563,42 +656,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
@@ -612,47 +721,87 @@ module Plugins
               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
               next
             end
+          end
 
-            next unless(file =~ /\.rb$/)
-
+          begin
             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
+          rescue Exception => e
+            error e
+            did_it = e
+          end
 
-          }
+          case did_it
+          when Symbol
+            processed[file.intern] = did_it
+          when Exception
+            @failed << { :name => file, :dir => dir, :reason => did_it }
+          end
         end
-      }
+      end
+    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)
+
       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)
@@ -717,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
@@ -795,7 +958,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 || []
 
@@ -804,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
@@ -870,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
@@ -886,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
         }
@@ -916,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
@@ -940,7 +1099,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)