X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Fplugins.rb;h=06cc09e7527b8a3ad14feaea6ac144b7215b33e8;hb=56e4713c5c0498838ed77a409e44fbc3251acde2;hp=5c5cdddc99416709ebabba65aff755625e2112e8;hpb=978ea255291ea27fef7d5d685a49eb0d52d4e9c9;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/plugins.rb b/lib/rbot/plugins.rb index 5c5cdddc..06cc09e7 100644 --- a/lib/rbot/plugins.rb +++ b/lib/rbot/plugins.rb @@ -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. @@ -157,6 +161,9 @@ module Plugins # the message map handler attr_reader :handler + # the directory in which the plugin is located + attr_reader :plugin_path + # Initialise your bot module. Always call super if you override this method, # as important variables are set up for you: # @@ -184,9 +191,10 @@ 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) + @plugin_path = @manager.next_plugin_path if self.respond_to?('set_language') self.set_language(@bot.lang.language) end @@ -211,6 +219,7 @@ module Plugins @registry.flush end + # Method called to cleanup before the plugin is unloaded. If you overload # this method to handle additional cleanup tasks, remember to call super() # so that the default cleanup actions are taken care of as well. @@ -338,6 +347,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 +415,10 @@ module Plugins attr_reader :botmodules attr_reader :maps + attr_reader :core_module_dirs + attr_reader :plugin_dirs + attr_reader :next_plugin_path + # This is the list of patterns commonly delegated to plugins. # A fast delegation lookup is enabled for them. DEFAULT_DELEGATE_PATTERNS = %r{^(?: @@ -447,13 +469,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 +505,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 +553,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 @@ -536,10 +588,15 @@ module Plugins ([str, err.inspect] + err.backtrace).join("\n") end + def get_plugin(name) + plugins.find { |plugin| plugin.name == name } + end + # 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 +606,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')}") @@ -557,7 +615,14 @@ module Plugins begin plugin_string = IO.read(fname) debug "loading #{desc}#{fname}" + + # set path of the plugin that will be loaded next (see BotModule#initialize) + @next_plugin_path = File.dirname fname + plugin_module.module_eval(plugin_string, fname) + + @next_plugin_path = nil + return :loaded rescue Exception => err # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err @@ -570,15 +635,44 @@ 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) + elsif ([:file, :line, :column, :offset, :problem, :context] & err.methods).length == 6 + # Another ‘brillian’ overload, this time from Psych::SyntaxError + # In this case we'll just leave the message as-is + newerr = err.dup + 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 end - private :load_botmodule_file # add one or more directories to the list of directories to # load core modules from @@ -600,28 +694,39 @@ module Plugins debug "Core module and plugin loading paths cleared" end - # load plugins from pre-assigned list of directories - def scan - @failed.clear - @ignored.clear - @delegate_list.clear - + 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 = @core_module_dirs + @plugin_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 @@ -635,47 +740,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) @@ -740,6 +885,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 @@ -827,8 +986,7 @@ module Plugins end end - # call-seq: delegate(method, m, opts={}) - # delegate(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 @@ -862,7 +1020,7 @@ module Plugins sort_modules unless @sorted_modules opts = {} - opts.merge(args.pop) if args.last.class == Hash + opts.merge!(args.pop) if args.last.class == Hash m = args.first if BasicUserMessage === m @@ -893,7 +1051,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 @@ -909,7 +1066,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 } @@ -939,7 +1095,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