X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;ds=inline;f=lib%2Frbot%2Fplugins.rb;h=9a41610f6e4961b9fb6de8431e80b8674d8d6cfd;hb=16c921257d38522fd2930b1ee8d52675f0d4f1e0;hp=81e29bd189bed04d925ff14e73a7cd52e848f2a8;hpb=afeb2476d6a25220b20d27030a61bf49335e2e33;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git
diff --git a/lib/rbot/plugins.rb b/lib/rbot/plugins.rb
index 81e29bd1..9a41610f 100644
--- a/lib/rbot/plugins.rb
+++ b/lib/rbot/plugins.rb
@@ -10,6 +10,9 @@ 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'
@@ -82,6 +85,11 @@ module Plugins
use message.ctcp_reply, which sends a private NOTICE
to the sender.
+ message(PrivMessage)::
+ Called for all PRIVMSG. Hook on this method if you
+ need to handle PRIVMSGs regardless of whether they are
+ addressed to the bot or not, and regardless of
+
privmsg(PrivMessage)::
Called for a PRIVMSG if the first word matches one
the plugin #register()ed for. Use m.plugin to get
@@ -91,10 +99,17 @@ module Plugins
unreplied(PrivMessage)::
Called for a PRIVMSG which has not been replied to.
+ notice(NoticeMessage)::
+ Called for all Notices. Please notice that in general
+ should not be replied to.
+
kick(KickMessage)::
Called when a user (or the bot) is kicked from a
channel the bot is in.
+ invite(InviteMessage)::
+ Called when the bot is invited to a channel.
+
join(JoinMessage)::
Called when a user (or the bot) joins a channel
@@ -106,10 +121,20 @@ module Plugins
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
+ welcome(WelcomeMessage)::
+ Called when the welcome message is received on
+ joining a server succesfully.
+
+ motd(MotdMessage)::
+ Called when the Message Of The Day is fully
+ recevied from the server.
+
connect:: Called when a server is joined successfully, but
before autojoin channels are joined (no params)
@@ -126,7 +151,14 @@ module Plugins
=end
class BotModule
- attr_reader :bot # the associated bot
+ # the associated bot
+ attr_reader :bot
+
+ # the plugin registry
+ attr_reader :registry
+
+ # the message map handler
+ attr_reader :handler
# Initialise your bot module. Always call super if you override this method,
# as important variables are set up for you:
@@ -150,6 +182,7 @@ module Plugins
def initialize
@manager = Plugins::manager
@bot = @manager.bot
+ @priority = nil
@botmodule_triggers = Array.new
@@ -162,7 +195,13 @@ module Plugins
end
end
- # Returns the symbol :BotModule
+ # Changing the value of @priority directly will cause problems,
+ # Please use priority=.
+ def priority
+ @priority ||= 1
+ end
+
+ # Returns the symbol :BotModule
def botmodule_class
:BotModule
end
@@ -195,7 +234,7 @@ module Plugins
# 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)
@@ -220,8 +259,10 @@ module Plugins
def do_map(silent, *args)
@handler.map(self, *args)
# register this map
- name = @handler.last.items[0]
+ map = @handler.last
+ name = map.items[0]
self.register name, :auth => nil, :hidden => silent
+ @manager.register_map(self, map)
unless self.respond_to?('privmsg')
def self.privmsg(m) #:nodoc:
handle(m)
@@ -280,7 +321,7 @@ module Plugins
#
# 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)
@@ -300,9 +341,40 @@ 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
+ # 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
+ @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.
@@ -335,6 +407,7 @@ module Plugins
include Singleton
attr_reader :bot
attr_reader :botmodules
+ attr_reader :maps
# This is the list of patterns commonly delegated to plugins.
# A fast delegation lookup is enabled for them.
@@ -354,11 +427,17 @@ module Plugins
@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
}
- @dirs = []
+ @core_module_dirs = []
+ @plugin_dirs = []
@failed = Array.new
@ignored = Array.new
@@ -385,7 +464,9 @@ module Plugins
@botmodules[:Plugin].clear
@names_hash.clear
@commandmappers.clear
+ @maps.clear
@failures_shown = false
+ mark_priorities_dirty
end
# Associate with bot _bot_
@@ -411,6 +492,19 @@ module Plugins
@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:
+ #
+ # 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
+ #
+ #
+ 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
+
def add_botmodule(botmodule)
raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
kl = botmodule.botmodule_class
@@ -424,6 +518,7 @@ module Plugins
end
@botmodules[kl] << botmodule
@names_hash[botmodule.to_sym] = botmodule
+ mark_priorities_dirty
end
# Returns an array of the loaded plugins
@@ -442,6 +537,12 @@ module Plugins
@commandmappers
end
+ # Tells the PluginManager that the next time it delegates an event, it
+ # should sort the modules by priority
+ def mark_priorities_dirty
+ @sorted_modules = nil
+ end
+
# Makes a string of error _err_ by adding text _str_
def report_error(str, err)
([str, err.inspect] + err.backtrace).join("\n")
@@ -460,16 +561,19 @@ 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')}")
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
+ raise if err.kind_of? DBFatal
error report_error("#{desc}#{fname} load failed", err)
bt = err.backtrace.select { |line|
line.match(/^(\(eval\)|#{fname}):\d+/)
@@ -479,10 +583,27 @@ 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)
+ 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
@@ -490,42 +611,58 @@ module Plugins
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
@@ -539,26 +676,35 @@ module Plugins
@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
- next unless(file =~ /\.rb$/)
+ # load plugins from pre-assigned list of directories
+ def scan
+ @failed.clear
+ @ignored.clear
+ @delegate_list.clear
- 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
+ scan_botmodules(:type => :core)
+ scan_botmodules(:type => :plugins)
- }
- end
- }
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
@@ -720,9 +866,66 @@ module Plugins
return false
end
- # see if each plugin handles +method+, and if so, call it, passing
- # +message+ as a parameter
+ 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
+
+ # call-seq: delegate(method, m, opts={})
+ # delegate(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 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
+ #
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)
@@ -732,24 +935,30 @@ module Plugins
return [] unless @delegate_list.has_key?(m)
@delegate_list[m].each { |p|
begin
- ret.push p.send(method, *args)
+ 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)
+ raise if err.kind_of?(DBFatal)
end
}
else
debug "slow-delegating #{method}"
- (core_modules + plugins).each { |p|
+ @sorted_modules.each { |p|
if(p.respond_to? method)
begin
# debug "#{p.botmodule_class} #{p.name} responds"
- ret.push p.send(method, *args)
+ 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)
+ raise if err.kind_of?(DBFatal)
end
end
}
@@ -761,7 +970,7 @@ module Plugins
# see if we have a plugin that wants to handle this message, if so, pass
# it to the plugin and return true, otherwise false
def privmsg(m)
- # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
+ debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
return unless m.plugin
k = m.plugin.to_sym
if commands.has_key?(k)
@@ -769,30 +978,45 @@ module Plugins
a = commands[k][:auth]
# We check here for things that don't check themselves
# (e.g. mapped things)
- # debug "Checking auth ..."
+ debug "Checking auth ..."
if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
- # debug "Checking response ..."
+ debug "Checking response ..."
if p.respond_to?("privmsg")
begin
- # debug "#{p.botmodule_class} #{p.name} responds"
+ 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)
+ raise if err.kind_of?(DBFatal)
end
- # debug "Successfully delegated #{m.message}"
+ debug "Successfully delegated #{m.inspect}"
return true
else
- # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
+ 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}"
+ 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
- # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
return false
- # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
+ end
+
+ # delegate IRC messages, by delegating 'listen' first, and the actual method
+ # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message
+ # as appropriate.
+ 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
end