Singleton to manage multiple plugins and delegate messages to them for handling
This is the list of patterns commonly delegated to plugins. A fast delegation lookup is enabled for them.
# File lib/rbot/plugins.rb, line 422 def initialize @botmodules = { :CoreBotModule => [], :Plugin => [] } @names_hash = Hash.new @commandmappers = Hash.new @maps = Hash.new # modules will be sorted on first delegate call @sorted_modules = nil @delegate_list = Hash.new { |h, k| h[k] = Array.new } @core_module_dirs = [] @plugin_dirs = [] @failed = Array.new @ignored = Array.new bot_associate(nil) end
Returns the botmodule with the given name
# File lib/rbot/plugins.rb, line 479 def [](name) @names_hash[name.to_sym] end
# File lib/rbot/plugins.rb, line 508 def add_botmodule(botmodule) raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule) kl = botmodule.botmodule_class if @names_hash.has_key?(botmodule.to_sym) case self[botmodule].botmodule_class when kl raise "#{kl} #{botmodule} already registered!" else raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}" end end @botmodules[kl] << botmodule @names_hash[botmodule.to_sym] = botmodule mark_priorities_dirty end
add one or more directories to the list of directories to load core modules from
# File lib/rbot/plugins.rb, line 614 def add_core_module_dir(*dirlist) @core_module_dirs += dirlist debug "Core module loading paths: #{@core_module_dirs.join(', ')}" end
add one or more directories to the list of directories to load plugins from
# File lib/rbot/plugins.rb, line 621 def add_plugin_dir(*dirlist) @plugin_dirs += dirlist debug "Plugin loading paths: #{@plugin_dirs.join(', ')}" end
Associate with bot bot
# File lib/rbot/plugins.rb, line 473 def bot_associate(bot) reset_botmodule_lists @bot = bot end
call the cleanup method for each active plugin
# File lib/rbot/plugins.rb, line 716 def cleanup delegate 'cleanup' reset_botmodule_lists end
# File lib/rbot/plugins.rb, line 626 def clear_botmodule_dirs @core_module_dirs.clear @plugin_dirs.clear debug "Core module and plugin loading paths cleared" end
Returns a hash of the registered message prefixes and associated plugins
# File lib/rbot/plugins.rb, line 536 def commands @commandmappers end
# File lib/rbot/plugins.rb, line 802 def core_length core_modules.length end
Returns an array of the loaded plugins
# File lib/rbot/plugins.rb, line 525 def core_modules @botmodules[:CoreBotModule] end
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 Irc::Bot::Plugins::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:
if specified, the delegation will only consider plugins with a priority higher than the specified value
if specified, the delegation will only consider plugins with a priority lower than the specified value
# File lib/rbot/plugins.rb, line 905 def delegate(method, *args) # 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 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] # debug "Delegating #{method.inspect}" ret = Array.new if method.match(DEFAULT_DELEGATE_PATTERNS) debug "fast-delegating #{method}" m = method.to_sym debug "no-one to delegate to" unless @delegate_list.has_key?(m) return [] unless @delegate_list.has_key?(m) @delegate_list[m].each { |p| begin prio = p.priority unless (above and above >= prio) or (below and below <= prio) ret.push p.send(method, *args) end 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 debug "slow-delegating #{method}" @sorted_modules.each { |p| if(p.respond_to? method) begin # 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) end 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 } end return ret # debug "Finished delegating #{method.inspect}" end
return help for topic
(call associated plugin's help
method)
# File lib/rbot/plugins.rb, line 807 def help(topic="") case topic when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/ # debug "Failures: #{@failed.inspect}" return _("no plugins failed to load") if @failed.empty? return @failed.collect { |p| _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % { :highlight => Bold, :plugin => p[:name], :dir => p[:dir], :exception => p[:reason].class, :reason => p[:reason], } + if $1 && !p[:reason].backtrace.empty? _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')} else '' end }.join("\n") when /ignored?\s*plugins?/ return _('no plugins were ignored') if @ignored.empty? tmp = Hash.new @ignored.each do |p| reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s) ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name]) end return tmp.map do |dir, reasons| # FIXME get rid of these string concatenations to make gettext easier s = reasons.map { |r, list| list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})" }.join('; ') "in #{dir}: #{s}" end.join('; ') when /^(\S+)\s*(.*)$/ key = $1 params = $2 # Let's see if we can match a plugin by the given name (core_modules + plugins).each { |p| next unless p.name == key begin return p.help(key, params) rescue Exception => err #rescue TimeoutError, StandardError, NameError, SyntaxError => err error report_error("#{p.botmodule_class} #{p.name} help() failed:", err) end } # Nope, let's see if it's a command, and ask for help at the corresponding botmodule k = key.to_sym if commands.has_key?(k) p = commands[k][:botmodule] begin return p.help(key, params) rescue Exception => err #rescue TimeoutError, StandardError, NameError, SyntaxError => err error report_error("#{p.botmodule_class} #{p.name} help() failed:", err) end end end return false end
return list of help topics (plugin names)
# File lib/rbot/plugins.rb, line 792 def helptopics rv = status @failures_shown = true rv end
# File lib/rbot/plugins.rb, line 448 def inspect ret = self.to_s[0..-2] ret << ' corebotmodules=' ret << @botmodules[:CoreBotModule].map { |m| m.name }.inspect ret << ' plugins=' ret << @botmodules[:Plugin].map { |m| m.name }.inspect ret << ">" end
delegate IRC messages, by delegating 'listen' first, and the actual method afterwards. Delegating 'privmsg' also delegates ctcp_listen and message as appropriate.
# File lib/rbot/plugins.rb, line 1009 def irc_delegate(method, m) delegate('listen', m) if method.to_sym == :privmsg delegate('ctcp_listen', m) if m.ctcp delegate('message', m) privmsg(m) if m.address? and not m.ignored? delegate('unreplied', m) unless m.replied else delegate(method, m) end end
# File lib/rbot/plugins.rb, line 798 def length plugins.length end
Tells the PluginManager that the next time it delegates an event, it should sort the modules by priority
# File lib/rbot/plugins.rb, line 542 def mark_priorities_dirty @sorted_modules = nil end
Returns an array of the loaded plugins
# File lib/rbot/plugins.rb, line 530 def plugins @botmodules[:Plugin] end
see if we have a plugin that wants to handle this message, if so, pass it to the plugin and return true, otherwise false
# File lib/rbot/plugins.rb, line 971 def privmsg(m) debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}" return unless m.plugin k = m.plugin.to_sym if commands.has_key?(k) p = commands[k][:botmodule] a = commands[k][:auth] # We check here for things that don't check themselves # (e.g. mapped things) debug "Checking auth ..." if a.nil? || @bot.auth.allow?(a, m.source, m.replyto) debug "Checking response ..." if p.respond_to?("privmsg") begin debug "#{p.botmodule_class} #{p.name} responds" p.privmsg(m) 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 else debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()" end else debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}" end else debug "Command #{k} isn't handled" end return false end
Registers botmodule botmodule with command cmd and command path auth_path
# File lib/rbot/plugins.rb, line 490 def register(botmodule, cmd, auth_path) raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule) @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path} end
Registers botmodule botmodule with map map. This adds the map to the maps hash which has three keys:
the associated botmodule
an array of auth keys checked by the map; the first is the full_auth_path of the map
the actual MessageTemplate object
# File lib/rbot/plugins.rb, line 503 def register_map(botmodule, map) raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule) @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map } end
Makes a string of error err by adding text str
# File lib/rbot/plugins.rb, line 547 def report_error(str, err) ([str, err.inspect] + err.backtrace).join("\n") end
drop all plugins and rescan plugins on disk calls save and cleanup for each plugin before dropping them
# File lib/rbot/plugins.rb, line 723 def rescan save cleanup scan end
Reset lists of botmodules
# File lib/rbot/plugins.rb, line 462 def reset_botmodule_lists @botmodules[:CoreBotModule].clear @botmodules[:Plugin].clear @names_hash.clear @commandmappers.clear @maps.clear @failures_shown = false mark_priorities_dirty end
call the save method for each active plugin
# File lib/rbot/plugins.rb, line 710 def save delegate 'flush_registry' delegate 'save' end
load plugins from pre-assigned list of directories
# File lib/rbot/plugins.rb, line 692 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
# File lib/rbot/plugins.rb, line 632 def scan_botmodules(opts={}) type = opts[:type] processed = Hash.new case type when :core dirs = @core_module_dirs when :plugins dirs = @plugin_dirs @bot.config['plugins.blacklist'].each { |p| pn = p + ".rb" processed[pn.intern] = :blacklisted } whitelist = @bot.config['plugins.whitelist'].map { |p| p + ".rb" } end 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 if(file =~ /^(.+\.rb)\.disabled$/) # GB: Do we want to do this? This means that a disabled plugin in a directory # will disable in all subsequent directories. This was probably meant # to be used before plugins.blacklist was implemented, so I think # we don't need this anymore processed[$1.intern] = :disabled @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]} next 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 end end end
# File lib/rbot/plugins.rb, line 868 def sort_modules @sorted_modules = (core_modules + plugins).sort do |a, b| a.priority <=> b.priority end || [] @delegate_list.each_value do |list| list.sort! {|a,b| a.priority <=> b.priority} end end
# File lib/rbot/plugins.rb, line 729 def status(short=false) output = [] if self.core_length > 0 if short output << n_("%{count} core module loaded", "%{count} core modules loaded", self.core_length) % {:count => self.core_length} else output << n_("%{count} core module: %{list}", "%{count} core modules: %{list}", self.core_length) % { :count => self.core_length, :list => core_modules.collect{ |p| p.name}.sort.join(", ") } end else output << _("no core botmodules loaded") end # Active plugins first if(self.length > 0) if short output << n_("%{count} plugin loaded", "%{count} plugins loaded", self.length) % {:count => self.length} else output << n_("%{count} plugin: %{list}", "%{count} plugins: %{list}", self.length) % { :count => self.length, :list => plugins.collect{ |p| p.name}.sort.join(", ") } end else output << "no plugins active" end # Ignored plugins next unless @ignored.empty? or @failures_shown if short output << n_("%{highlight}%{count} plugin ignored%{highlight}", "%{highlight}%{count} plugins ignored%{highlight}", @ignored.length) % { :count => @ignored.length, :highlight => Underline } else output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why", "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why", @ignored.length) % { :count => @ignored.length, :highlight => Underline, :bold => Bold, :command => "help ignored plugins"} end end # Failed plugins next unless @failed.empty? or @failures_shown if short output << n_("%{highlight}%{count} plugin failed to load%{highlight}", "%{highlight}%{count} plugins failed to load%{highlight}", @failed.length) % { :count => @failed.length, :highlight => Reverse } else output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why", "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why", @failed.length) % { :count => @failed.length, :highlight => Reverse, :bold => Bold, :command => "help failed plugins"} end end output.join '; ' end
Returns true
if cmd has already been registered as a
command
# File lib/rbot/plugins.rb, line 484 def who_handles?(cmd) return nil unless @commandmappers.has_key?(cmd.to_sym) return @commandmappers[cmd.to_sym][:botmodule] 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)
It returns the Symbol :loaded on success, and an Exception on failure
# File lib/rbot/plugins.rb, line 559 def load_botmodule_file(fname, desc=nil) # create a new, anonymous module to "house" the plugin # 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.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 error report_error("#{desc}#{fname} load failed", err) bt = err.backtrace.select { |line| line.match(/^(\(eval\)|#{fname}):\d+/) } bt.map! { |el| el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m| "#{fname}#{$1}#{$3}" } } msg = err.to_s.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m| "#{fname}#{$1}#{$3}" } 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 end