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

bot[R]
botmodules[R]
maps[R]

Public Class Methods

new() click to toggle 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

[](name) click to toggle source

Returns the botmodule with the given name

# File lib/rbot/plugins.rb, line 479
def [](name)
  @names_hash[name.to_sym]
end
add_botmodule(botmodule) click to toggle 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
add_core_module_dir(*dirlist) click to toggle source

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_plugin_dir(*dirlist) click to toggle source

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
bot_associate(bot) click to toggle source

Associate with bot bot

# File lib/rbot/plugins.rb, line 473
def bot_associate(bot)
  reset_botmodule_lists
  @bot = bot
end
cleanup() click to toggle source

call the cleanup method for each active plugin

# File lib/rbot/plugins.rb, line 716
def cleanup
  delegate 'cleanup'
  reset_botmodule_lists
end
clear_botmodule_dirs() click to toggle 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
commands() click to toggle source

Returns a hash of the registered message prefixes and associated plugins

# File lib/rbot/plugins.rb, line 536
def commands
  @commandmappers
end
core_length() click to toggle source
# File lib/rbot/plugins.rb, line 802
def core_length
  core_modules.length
end
core_modules() click to toggle source

Returns an array of the loaded plugins

# File lib/rbot/plugins.rb, line 525
def core_modules
  @botmodules[:CoreBotModule]
end
delegate</span><span class="method-args">(method, m, opts={})</span> click to toggle source
<span class="method-name">delegate</span><span class="method-args">(method, 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 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:

: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

# 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
help(topic="") click to toggle source

return help for topic (call associated plugin’s help method)

# File lib/rbot/plugins.rb, line 807
def help(topic="")
  case topic
  when %rfail(?: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 %rignored?\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(%r\.rb$/, '') }.join(', ') + " (#{r})"
      }.join('; ')
      "in #{dir}: #{s}"
    end.join('; ')
  when %r^(\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
helptopics() click to toggle source

return list of help topics (plugin names)

# File lib/rbot/plugins.rb, line 792
def helptopics
  rv = status
  @failures_shown = true
  rv
end
inspect() click to toggle 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
irc_delegate(method, m) click to toggle source

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
length() click to toggle source
# File lib/rbot/plugins.rb, line 798
def length
  plugins.length
end
mark_priorities_dirty() click to toggle source

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
plugins() click to toggle source

Returns an array of the loaded plugins

# File lib/rbot/plugins.rb, line 530
def plugins
  @botmodules[:Plugin]
end
privmsg(m) click to toggle source

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
register(botmodule, cmd, auth_path) click to toggle source

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
register_map(botmodule, map) click to toggle source

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

# 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
report_error(str, err) click to toggle source

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
rescan() click to toggle source

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_botmodule_lists() click to toggle source

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
save() click to toggle source

call the save method for each active plugin

# File lib/rbot/plugins.rb, line 710
def save
  delegate 'flush_registry'
  delegate 'save'
end
scan() click to toggle source

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
scan_botmodules(opts={}) click to toggle 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 =~ %r\.rb$/
      next if file =~ %r^\./

      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 =~ %r^(.+\.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
sort_modules() click to toggle 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
status(short=false) click to toggle 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
who_handles?(cmd) click to toggle source

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