X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Fplugins.rb;h=9a41610f6e4961b9fb6de8431e80b8674d8d6cfd;hb=5079554263a9a2c8c661c1c8728b4ce1a0bbd05a;hp=96f61efc24a54d00dc068d8b205b6a59a05604b4;hpb=9dc851fd90cd6657a5f1c1f89e968a61f4c07a0f;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/plugins.rb b/lib/rbot/plugins.rb index 96f61efc..9a41610f 100644 --- a/lib/rbot/plugins.rb +++ b/lib/rbot/plugins.rb @@ -6,9 +6,13 @@ require 'singleton' module Irc - BotConfig.register BotConfigArrayValue.new('plugins.blacklist', +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' @@ -17,7 +21,7 @@ module Plugins functionality. Rather than subclassing BotModule, however, one should subclass either CoreBotModule (reserved for system modules) or Plugin (for user plugins). - + A BotModule interacts with Irc events by defining one or more of the following methods, which get called as appropriate when the corresponding Irc event happens. @@ -27,7 +31,7 @@ module Plugins map is the new, cleaner way to respond to specific message formats without littering your plugin code with regexps, and should be used instead of #register() and #privmsg() (see below) when possible. - + The difference between map and map! is that map! will not register the new command as an alternative name for the plugin. @@ -81,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 @@ -90,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 @@ -105,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) @@ -125,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: @@ -134,7 +167,7 @@ module Plugins # the rbot instance # @registry:: # the botmodule's registry, which can be used to store permanent data - # (see BotRegistryAccessor for additional documentation) + # (see Registry::Accessor for additional documentation) # # Other instance variables which are defined and should not be overwritten # byt the user, but aren't usually accessed directly, are: @@ -149,11 +182,12 @@ module Plugins def initialize @manager = Plugins::manager @bot = @manager.bot + @priority = nil @botmodule_triggers = Array.new @handler = MessageMapper.new(self) - @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, "")) + @registry = Registry::Accessor.new(@bot, self.class.to_s.gsub(/^.*::/, "")) @manager.add_botmodule(self) if self.respond_to?('set_language') @@ -161,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 @@ -194,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) @@ -203,15 +243,7 @@ module Plugins # responds to appropriately-formed messages on Irc. # def map(*args) - @handler.map(self, *args) - # register this map - name = @handler.last.items[0] - self.register name, :auth => nil - unless self.respond_to?('privmsg') - def self.privmsg(m) #:nodoc: - handle(m) - end - end + do_map(false, *args) end # call-seq: map!(template, options) @@ -220,10 +252,17 @@ module Plugins # as an alternative name for the plugin. # def map!(*args) + do_map(true, *args) + end + + # Auxiliary method called by #map and #map! + def do_map(silent, *args) @handler.map(self, *args) # register this map - name = @handler.last.items[0] - self.register name, :auth => nil, :hidden => true + 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) @@ -282,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) @@ -302,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. @@ -337,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. @@ -356,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 @@ -368,13 +445,28 @@ module Plugins bot_associate(nil) end + 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 + # Reset lists of botmodules def reset_botmodule_lists @botmodules[:CoreBotModule].clear @botmodules[:Plugin].clear @names_hash.clear @commandmappers.clear + @maps.clear @failures_shown = false + mark_priorities_dirty end # Associate with bot _bot_ @@ -400,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 @@ -413,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 @@ -431,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") @@ -449,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+/) @@ -468,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 @@ -479,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 @@ -528,26 +676,35 @@ module Plugins @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]} next end + end - next unless(file =~ /\.rb$/) + 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 - 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 + # 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) - } - 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 @@ -683,9 +840,9 @@ module Plugins key = $1 params = $2 - # Let's see if we can match a plugin by the given name + # Let's see if we can match a plugin by the given name (core_modules + plugins).each { |p| - next unless p.name == key + next unless p.name == key begin return p.help(key, params) rescue Exception => err @@ -694,7 +851,7 @@ module Plugins end } - # Nope, let's see if it's a command, and ask for help at the corresponding botmodule + # 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] @@ -709,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) @@ -721,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 } @@ -750,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) @@ -758,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 @@ -792,3 +1027,4 @@ module Plugins end end +end