class Irc::Bot::Plugins::PluginManagerClass
Singleton
to manage multiple plugins and delegate messages to them for handling
Constants
- DEFAULT_DELEGATE_PATTERNS
-
This is the list of patterns commonly delegated to plugins. A fast delegation lookup is enabled for them.
Attributes
Public Class Methods
Source
# 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
Public Instance Methods
Source
# File lib/rbot/plugins.rb, line 479 def [](name) @names_hash[name.to_sym] end
Returns the botmodule with the given name
Source
# 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
Source
# 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 core modules from
Source
# File lib/rbot/plugins.rb, line 621 def add_plugin_dir(*dirlist) @plugin_dirs += dirlist debug "Plugin loading paths: #{@plugin_dirs.join(', ')}" end
add one or more directories to the list of directories to load plugins from
Source
# File lib/rbot/plugins.rb, line 473 def bot_associate(bot) reset_botmodule_lists @bot = bot end
Associate with bot bot
Source
# File lib/rbot/plugins.rb, line 716 def cleanup delegate 'cleanup' reset_botmodule_lists end
call the cleanup method for each active plugin
Source
# 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
Source
# File lib/rbot/plugins.rb, line 536 def commands @commandmappers end
Returns a hash of the registered message prefixes and associated plugins
Source
# File lib/rbot/plugins.rb, line 525 def core_modules @botmodules[:CoreBotModule] end
Returns an array of the loaded plugins
Source
# 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
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
Source
# 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 help for topic
(call associated plugin’s help method)
Source
# File lib/rbot/plugins.rb, line 792 def helptopics rv = status @failures_shown = true rv end
return list of help topics (plugin names)
Source
# 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
Source
# 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
delegate IRC messages, by delegating ‘listen’ first, and the actual method afterwards. Delegating ‘privmsg’ also delegates ctcp_listen and message as appropriate.
Source
# File lib/rbot/plugins.rb, line 542 def mark_priorities_dirty @sorted_modules = nil end
Tells the PluginManager that the next time it delegates an event, it should sort the modules by priority
Source
# File lib/rbot/plugins.rb, line 530 def plugins @botmodules[:Plugin] end
Returns an array of the loaded plugins
Source
# 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
see if we have a plugin that wants to handle this message, if so, pass it to the plugin and return true, otherwise false
Source
# 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 command cmd and command path auth_path
Source
# 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
Registers botmodule botmodule with map map. This adds the map to the maps
hash which has three keys:
- botmodule
-
the associated botmodule
- auth
-
an array of auth keys checked by the map; the first is the full_auth_path of the map
- map
-
the actual
MessageTemplate
object
Source
# File lib/rbot/plugins.rb, line 547 def report_error(str, err) ([str, err.inspect] + err.backtrace).join("\n") end
Makes a string of error err by adding text str
Source
# File lib/rbot/plugins.rb, line 723 def rescan save cleanup scan end
drop all plugins and rescan plugins on disk calls save and cleanup for each plugin before dropping them
Source
# 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
Reset lists of botmodules
Source
# File lib/rbot/plugins.rb, line 710 def save delegate 'flush_registry' delegate 'save' end
call the save method for each active plugin
Source
# 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
load plugins from pre-assigned list of directories
Source
# 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
Source
# 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
Source
# 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
Source
# 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
Returns true
if cmd has already been registered as a command
Private Instance Methods
Source
# 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
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