]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/plugins.rb
plugins.rb: use IO.read instead of readlins+join gimmicks
[user/henk/code/ruby/rbot.git] / lib / rbot / plugins.rb
index 81e29bd189bed04d925ff14e73a7cd52e848f2a8..4d51cfc5e845d380cef3b1e588755dfe33353ef5 100644 (file)
@@ -82,6 +82,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 +96,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 +118,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 +148,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 +179,7 @@ module Plugins
     def initialize
       @manager = Plugins::manager
       @bot = @manager.bot
+      @priority = nil
 
       @botmodule_triggers = Array.new
 
@@ -162,6 +192,12 @@ module Plugins
       end
     end
 
+    # 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
@@ -195,7 +231,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 +256,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)
@@ -303,6 +341,14 @@ module Plugins
       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
   end
 
   # A CoreBotModule is a BotModule that provides core functionality.
@@ -335,6 +381,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,6 +401,11 @@ 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
       }
@@ -385,7 +437,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 +465,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 +491,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 +510,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,11 +534,13 @@ 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
@@ -559,6 +635,7 @@ module Plugins
          @delegate_list[m.intern] << p
        }
       }
+      mark_priorities_dirty
     end
 
     # call the save method for each active plugin
@@ -720,9 +797,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</span><span class="method-args">(method, m, opts={})</span>
+    # <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 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,7 +866,10 @@ 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)
@@ -741,11 +878,14 @@ module Plugins
         }
       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)
@@ -761,7 +901,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 +909,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)
             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