# :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'
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.
nick(NickMessage)::
Called when a user (or the bot) changes Nick
+ modechange(ModeChangeMessage)::
+ Called when a User or Channel mode is changed
topic(TopicMessage)::
Called when a user (or the bot) changes a channel
topic
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')
@priority ||= 1
end
- # Returns the symbol :BotModule
+ # Returns the symbol :BotModule
def botmodule_class
:BotModule
end
# Signal to other BotModules that an even happened.
#
def call_event(ev, *args)
- @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *args)
+ @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *(args.push Hash.new))
end
# call-seq: map(template, options)
#
# 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)
# 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
@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.
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{^(?:
h[k] = Array.new
}
- @dirs = []
+ @core_module_dirs = []
+ @plugin_dirs = []
@failed = Array.new
@ignored = Array.new
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
# 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)
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
# 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
# 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
"#{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
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
@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)
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
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 || []
end
end
- # see if each plugin handles +method+, and if so, call it, passing
- # +message+ as a parameter. botmodules are called in order of priority
- # from lowest to highest.
+ # delegate(method, [m,] opts={})
#
- # If the passed +message+ 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
+ # 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.
#
- # For delegation with more extensive options, see delegate_event
+ # 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
#
def delegate(method, *args)
- opts = {:args => 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?
+ 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
+ opts[:above] ||= 0 if m.recurse_depth > 0
end
- delegate_event(method, opts)
- end
-
- # see if each plugin handles +method+, and if so, call it, passing
- # +opts[:args]+ as a parameter. +opts[:above]+ and +opts[:below]+
- # are used for a threshold of botmodule priorities that will be called.
- # If :above is defined, only botmodules with a priority above the value
- # will be called, for example. botmodules are called in order of
- # priority from lowest to hightest.
- def delegate_event(method, o={})
- # 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
-
- # set defaults
- opts = {:args => []}.merge(o)
above = opts[:above]
below = opts[:below]
- args = opts[:args]
# debug "Delegating #{method.inspect}"
ret = Array.new
begin
prio = p.priority
unless (above and above >= prio) or (below and below <= prio)
- ret.push p.send(method, *(args||[]))
+ 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 "#{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||[]))
+ 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
}
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
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)