]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/plugins.rb
Previous attempt at cleaning up the prefix matcher were too restrictive, try using...
[user/henk/code/ruby/rbot.git] / lib / rbot / plugins.rb
index bb4c744a6c238c9a821db33a44b3c4db8642ec4a..e101e627772d0cf977689f9b5173216abacd84b7 100644 (file)
@@ -2,7 +2,7 @@ require 'singleton'
 
 module Irc
     BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
 
 module Irc
     BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
-      :default => [], :wizard => false, :requires_restart => true,
+      :default => [], :wizard => false, :requires_rescan => true,
       :desc => "Plugins that should not be loaded")
 module Plugins
   require 'rbot/messagemapper'
       :desc => "Plugins that should not be loaded")
 module Plugins
   require 'rbot/messagemapper'
@@ -61,11 +61,14 @@ module Plugins
                          etc.
 
   privmsg(PrivMessage)::
                          etc.
 
   privmsg(PrivMessage)::
-                         called for a PRIVMSG if the first word matches one
+                         Called for a PRIVMSG if the first word matches one
                          the plugin register()d for. Use m.plugin to get
                          that word and m.params for the rest of the message,
                          if applicable.
 
                          the plugin register()d for. Use m.plugin to get
                          that word and m.params for the rest of the message,
                          if applicable.
 
+  unreplied(PrivMessage)::
+                         Called for a PRIVMSG which has not been replied to.
+
   kick(KickMessage)::
                          Called when a user (or the bot) is kicked from a
                          channel the bot is in.
   kick(KickMessage)::
                          Called when a user (or the bot) is kicked from a
                          channel the bot is in.
@@ -88,6 +91,10 @@ module Plugins
   connect()::            Called when a server is joined successfully, but
                          before autojoin channels are joined (no params)
 
   connect()::            Called when a server is joined successfully, but
                          before autojoin channels are joined (no params)
 
+  set_language(String)::
+                         Called when the user sets a new language
+                         whose name is the given String
+
   save::                 Called when you are required to save your plugin's
                          state, if you maintain data between sessions
 
   save::                 Called when you are required to save your plugin's
                          state, if you maintain data between sessions
 
@@ -98,21 +105,26 @@ module Plugins
 
   class BotModule
     attr_reader :bot   # the associated bot
 
   class BotModule
     attr_reader :bot   # the associated bot
-    attr_reader :botmodule_class # the botmodule class (:coremodule or :plugin)
 
     # initialise your bot module. Always call super if you override this method,
     # as important variables are set up for you
 
     # initialise your bot module. Always call super if you override this method,
     # as important variables are set up for you
-    def initialize(kl)
+    def initialize
       @manager = Plugins::pluginmanager
       @bot = @manager.bot
 
       @manager = Plugins::pluginmanager
       @bot = @manager.bot
 
-      @botmodule_class = kl.to_sym
       @botmodule_triggers = Array.new
 
       @handler = MessageMapper.new(self)
       @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
 
       @botmodule_triggers = Array.new
 
       @handler = MessageMapper.new(self)
       @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
 
-      @manager.add_botmodule(kl, self)
+      @manager.add_botmodule(self)
+      if self.respond_to?('set_language')
+        self.set_language(@bot.lang.language)
+      end
+    end
+
+    def botmodule_class
+      :BotModule
     end
 
     def flush_registry
     end
 
     def flush_registry
@@ -130,10 +142,10 @@ module Plugins
     end
 
     def map(*args)
     end
 
     def map(*args)
-      @handler.map(*args)
+      @handler.map(self, *args)
       # register this map
       name = @handler.last.items[0]
       # register this map
       name = @handler.last.items[0]
-      self.register name
+      self.register name, :auth => nil
       unless self.respond_to?('privmsg')
         def self.privmsg(m)
           handle(m)
       unless self.respond_to?('privmsg')
         def self.privmsg(m)
           handle(m)
@@ -142,10 +154,10 @@ module Plugins
     end
 
     def map!(*args)
     end
 
     def map!(*args)
-      @handler.map(*args)
+      @handler.map(self, *args)
       # register this map
       name = @handler.last.items[0]
       # register this map
       name = @handler.last.items[0]
-      self.register name, {:hidden => true}
+      self.register name, :auth => nil, :hidden => true
       unless self.respond_to?('privmsg')
         def self.privmsg(m)
           handle(m)
       unless self.respond_to?('privmsg')
         def self.privmsg(m)
           handle(m)
@@ -153,10 +165,29 @@ module Plugins
       end
     end
 
       end
     end
 
+    # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
+    # usually _chan_ is either "*" for everywhere, public and private (in which
+    # case it can be omitted) or "?" for private communications
+    #
+    def default_auth(cmd, val, chan="*")
+      case cmd
+      when "*", ""
+        c = nil
+      else
+        c = cmd
+      end
+      Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
+    end
+
+    # Gets the default command path which would be given to command _cmd_
+    def propose_default_path(cmd)
+      [name, cmd].compact.join("::")
+    end
+
     # return an identifier for this plugin, defaults to a list of the message
     # prefixes handled (used for error messages etc)
     def name
     # return an identifier for this plugin, defaults to a list of the message
     # prefixes handled (used for error messages etc)
     def name
-      self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin)?$/,"")
+      self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
     end
 
     # just calls name
     end
 
     # just calls name
@@ -164,6 +195,11 @@ module Plugins
       name
     end
 
       name
     end
 
+    # intern the name
+    def to_sym
+      self.name.to_sym
+    end
+
     # return a help string for your module. for complex modules, you may wish
     # to break your help into topics, and return a list of available topics if
     # +topic+ is nil. +plugin+ is passed containing the matching prefix for
     # return a help string for your module. for complex modules, you may wish
     # to break your help into topics, and return a list of available topics if
     # +topic+ is nil. +plugin+ is passed containing the matching prefix for
@@ -176,11 +212,19 @@ module Plugins
     # register the plugin as a handler for messages prefixed +name+
     # this can be called multiple times for a plugin to handle multiple
     # message prefixes
     # register the plugin as a handler for messages prefixed +name+
     # this can be called multiple times for a plugin to handle multiple
     # message prefixes
-    def register(name, opts={})
+    def register(cmd, opts={})
       raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
       raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
-      return if @manager.knows?(name, @botmodule_class)
-      @manager.register(name, @botmodule_class, self)
-      @botmodule_triggers << name unless opts.fetch(:hidden, false)
+      who = @manager.who_handles?(cmd)
+      if who
+        raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
+        return
+      end
+      if opts.has_key?(:auth)
+        @manager.register(self, cmd, opts[:auth])
+      else
+        @manager.register(self, cmd, propose_default_path(cmd))
+      end
+      @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
     end
 
     # default usage method provided as a utility for simple plugins. The
     end
 
     # default usage method provided as a utility for simple plugins. The
@@ -192,14 +236,14 @@ module Plugins
   end
 
   class CoreBotModule < BotModule
   end
 
   class CoreBotModule < BotModule
-    def initialize
-      super(:coremodule)
+    def botmodule_class
+      :CoreBotModule
     end
   end
 
   class Plugin < BotModule
     end
   end
 
   class Plugin < BotModule
-    def initialize
-      super(:plugin)
+    def botmodule_class
+      :Plugin
     end
   end
 
     end
   end
 
@@ -211,23 +255,28 @@ module Plugins
     attr_reader :botmodules
 
     def initialize
     attr_reader :botmodules
 
     def initialize
-      bot_associate(nil)
+      @botmodules = {
+        :CoreBotModule => [],
+        :Plugin => []
+      }
+
+      @names_hash = Hash.new
+      @commandmappers = Hash.new
 
       @dirs = []
 
       @dirs = []
+
+      @failed = Array.new
+      @ignored = Array.new
+
+      bot_associate(nil)
     end
 
     # Reset lists of botmodules
     def reset_botmodule_lists
     end
 
     # Reset lists of botmodules
     def reset_botmodule_lists
-      @botmodules = {
-        :coremodule => [],
-        :plugin => []
-      }
-
-      @commandmappers = {
-        :coremodule => {},
-        :plugin => {}
-      }
-
+      @botmodules[:CoreBotModule].clear
+      @botmodules[:Plugin].clear
+      @names_hash.clear
+      @commandmappers.clear
     end
 
     # Associate with bot _bot_
     end
 
     # Associate with bot _bot_
@@ -236,43 +285,52 @@ module Plugins
       @bot = bot
     end
 
       @bot = bot
     end
 
-    # Returns +true+ if _name_ is a known botmodule of class kl
-    def knows?(name, kl)
-      return @commandmappers[kl.to_sym].has_key?(name.to_sym)
+    # Returns the botmodule with the given _name_
+    def [](name)
+      @names_hash[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)
+      return @commandmappers[cmd.to_sym][:botmodule]
     end
 
     end
 
-    # Returns +true+ if _name_ is a known botmodule of class kl
-    def register(name, kl, botmodule)
-      raise TypeError, "Third argument #{botmodule.inspect} is not of class BotModule" unless botmodule.class <= BotModule
-      @commandmappers[kl.to_sym][name.to_sym] = botmodule
+    # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
+    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
 
     end
 
-    def add_botmodule(kl, botmodule)
-      raise TypeError, "Second argument #{botmodule.inspect} is not of class BotModule" unless botmodule.class <= BotModule
-      raise "#{kl.to_s} #{botmodule.name} already registered!" if @botmodules[kl.to_sym].include?(botmodule)
-      @botmodules[kl.to_sym] << botmodule
+    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
     end
 
     # Returns an array of the loaded plugins
     def core_modules
     end
 
     # Returns an array of the loaded plugins
     def core_modules
-      @botmodules[:coremodule]
+      @botmodules[:CoreBotModule]
     end
 
     # Returns an array of the loaded plugins
     def plugins
     end
 
     # Returns an array of the loaded plugins
     def plugins
-      @botmodules[:plugin]
+      @botmodules[:Plugin]
     end
 
     # Returns a hash of the registered message prefixes and associated
     # plugins
     end
 
     # Returns a hash of the registered message prefixes and associated
     # plugins
-    def plugin_commands
-      @commandmappers[:plugin]
-    end
-
-    # Returns a hash of the registered message prefixes and associated
-    # core modules
-    def core_commands
-      @commandmappers[:coremodule]
+    def commands
+      @commandmappers
     end
 
     # Makes a string of error _err_ by adding text _str_
     end
 
     # Makes a string of error _err_ by adding text _str_
@@ -325,6 +383,8 @@ module Plugins
     # add one or more directories to the list of directories to
     # load botmodules from
     #
     # 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(', ')}"
     def add_botmodule_dir(*dirlist)
       @dirs += dirlist
       debug "Botmodule loading path: #{@dirs.join(', ')}"
@@ -332,8 +392,8 @@ module Plugins
 
     # load plugins from pre-assigned list of directories
     def scan
 
     # load plugins from pre-assigned list of directories
     def scan
-      @failed = Array.new
-      @ignored = Array.new
+      @failed.clear
+      @ignored.clear
       processed = Hash.new
 
       @bot.config['plugins.blacklist'].each { |p|
       processed = Hash.new
 
       @bot.config['plugins.blacklist'].each { |p|
@@ -438,7 +498,7 @@ module Plugins
 
     # return list of help topics (plugin names)
     def helptopics
 
     # return list of help topics (plugin names)
     def helptopics
-      return " [#{status}]"
+      return status
     end
 
     def length
     end
 
     def length
@@ -455,15 +515,15 @@ module Plugins
       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
         # debug "Failures: #{@failed.inspect}"
         return "no plugins failed to load" if @failed.empty?
       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
         # debug "Failures: #{@failed.inspect}"
         return "no plugins failed to load" if @failed.empty?
-        return (@failed.inject(Array.new) { |list, p|
+        return @failed.inject(Array.new) { |list, p|
           list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
           list << "with error #{p[:reason].class}: #{p[:reason]}"
           list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
           list
           list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
           list << "with error #{p[:reason].class}: #{p[:reason]}"
           list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
           list
-        }).join("\n")
+        }.join("\n")
       when /ignored?\s*plugins?/
         return "no plugins were ignored" if @ignored.empty?
       when /ignored?\s*plugins?/
         return "no plugins were ignored" if @ignored.empty?
-        return (@ignored.inject(Array.new) { |list, p|
+        return @ignored.inject(Array.new) { |list, p|
           case p[:reason]
           when :loaded
             list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
           case p[:reason]
           when :loaded
             list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
@@ -471,91 +531,93 @@ module Plugins
             list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
           end
           list
             list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
           end
           list
-        }).join(", ")
+        }.join(", ")
       when /^(\S+)\s*(.*)$/
         key = $1
         params = $2
       when /^(\S+)\s*(.*)$/
         key = $1
         params = $2
-        [core_commands, plugin_commands].each { |pl|
-          if(pl.has_key?(key))
-            begin
-              return pl[key].help(key, params)
-            rescue Exception => err
-              #rescue TimeoutError, StandardError, NameError, SyntaxError => err
-              error report_error("#{p.botmodule_class} #{plugins[key].name} help() failed:", err)
-            end
-          else
-            return false
+
+       # 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
         }
           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
       end
+      return false
     end
 
     # see if each plugin handles +method+, and if so, call it, passing
     # +message+ as a parameter
     def delegate(method, *args)
     end
 
     # see if each plugin handles +method+, and if so, call it, passing
     # +message+ as a parameter
     def delegate(method, *args)
-      debug "Delegating #{method.inspect}"
+      debug "Delegating #{method.inspect}"
       [core_modules, plugins].each { |pl|
         pl.each {|p|
           if(p.respond_to? method)
             begin
       [core_modules, plugins].each { |pl|
         pl.each {|p|
           if(p.respond_to? method)
             begin
-              debug "#{p.botmodule_class} #{p.name} responds"
+              debug "#{p.botmodule_class} #{p.name} responds"
               p.send method, *args
             rescue Exception => err
               p.send method, *args
             rescue Exception => err
+              raise if err.kind_of?(SystemExit)
               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
-              raise if err.class <= BDB::Fatal
+              raise if err.kind_of?(BDB::Fatal)
             end
           end
         }
       }
             end
           end
         }
       }
-      debug "Finished delegating #{method.inspect}"
+      debug "Finished delegating #{method.inspect}"
     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
     def privmsg(m)
     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
     def privmsg(m)
-      debug "Delegating privmsg with key #{m.plugin}"
+      # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
       return unless m.plugin
       return unless m.plugin
-      begin
-        [core_commands, plugin_commands].each { |pl|
-          # We do it this way to skip creating spurious keys
-          # FIXME use fetch?
-          k = m.plugin.to_sym
-          if pl.has_key?(k)
-            p = pl[k]
-          else
-            p = nil
-          end
-          if p
-            # TODO This should probably be checked elsewhere
-            debug "Checking auth ..."
-            if @bot.auth.allow?(m.plugin, 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
-                  error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
-                  raise if err.class <= BDB::Fatal
-                end
-                debug "Successfully delegated privmsg with key #{m.plugin}"
-                return true
-              else
-                debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsgs"
-              end
-            else
-              debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to use #{m.plugin} on #{m.replyto}"
+      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
             end
+            # debug "Successfully delegated #{m.message}"
+            return true
           else
           else
-            debug "No #{pl.values.first.botmodule_class} registered #{m.plugin}" unless pl.empty?
+            # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
           end
           end
-          debug "Finished delegating privmsg with key #{m.plugin}" + ( pl.empty? ? "" : " to #{pl.values.first.botmodule_class}s" )
-        }
-        return false
-      rescue Exception => e
-        error report_error("couldn't delegate #{m}", e)
+        else
+          # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
+        end
       end
       end
-      debug "Finished delegating privmsg with key #{m.plugin}"
+      # 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
   end
 
     end
   end