]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/core/remote.rb
config core module: default command auth fixes
[user/henk/code/ruby/rbot.git] / lib / rbot / core / remote.rb
index 8604a2249c198728a583002d205902f410ab3f2a..7ffb62e1bb6e5a2e7613e67b22456c486e5ff85e 100644 (file)
@@ -9,19 +9,76 @@
 #
 # From an idea by halorgium <rbot@spork.in>.
 #
-# TODO client ID and auth
-# TODO Irc::Plugins::RemotePlugin module to be included by plugins that want to
-# provide a remote interface. Such module would define a remote_map() method
-# that would register the plugin to received mapped commands from remote clients.
-# FIXME how should be handle cleanups/rescans? Probably just clear() the
-# RemoteDispatcher template list. Provide a cleanup() method for
-# RemoteDispatcher and think about this.
+# TODO find a way to manage session id (logging out, manually and/or
+# automatically)
 
 require 'drb/drb'
 
 module ::Irc
+class Bot
 
-  # A RemoteCommand is similar to a BaiscUserMessage
+  module Auth
+
+    # We extend the BotUser class to handle remote logins
+    #
+    class BotUser
+
+      # A rather simple method to handle remote logins. Nothing special, just a
+      # password check.
+      #
+      def remote_login(password)
+        if password == @password
+          debug "remote login for #{self.inspect} succeeded"
+          return true
+        else
+          return false
+        end
+      end
+    end
+
+    # We extend the ManagerClass to handle remote logins
+    #
+    class ManagerClass
+
+      MAX_SESSION_ID = 2**128 - 1
+
+      # Creates a session id when the given password matches the given
+      # botusername
+      #
+      def remote_login(botusername, pwd)
+        @remote_users = Hash.new unless defined? @remote_users
+        n = BotUser.sanitize_username(botusername)
+        k = n.to_sym
+        raise "No such BotUser #{n}" unless include?(k)
+        bu = @allbotusers[k]
+        if bu.remote_login(pwd)
+          raise "ran out of session ids!" if @remote_users.length == MAX_SESSION_ID
+          session_id = rand(MAX_SESSION_ID)
+          while @remote_users.has_key?(session_id)
+            session_id = rand(MAX_SESSION_ID)
+          end
+          @remote_users[session_id] = bu
+          return session_id
+        end
+        return false
+      end
+
+      # Returns the botuser associated with the given session id
+      def remote_user(session_id)
+        return everyone unless session_id
+        return nil unless defined? @remote_users
+        if @remote_users.has_key?(session_id)
+          return @remote_users[session_id]
+        else
+          return nil
+        end
+      end
+    end
+
+  end
+
+
+  # A RemoteMessage is similar to a BasicUserMessage
   #
   class RemoteMessage
     # associated bot
@@ -54,23 +111,56 @@ module ::Irc
     end
   end
 
+  # The RemoteDispatcher is a kind of MessageMapper, tuned to handle
+  # RemoteMessages
+  #
   class RemoteDispatcher < MessageMapper
 
+    # It is initialized by passing it the bot instance
+    #
     def initialize(bot)
-      super(bot)
+      super
+    end
+
+    # The map method for the RemoteDispatcher returns the index of the inserted
+    # template
+    #
+    def map(botmodule, *args)
+      super
+      return @templates.length - 1
+    end
+
+    # The unmap method for the RemoteDispatcher nils the template at the given index,
+    # therefore effectively removing the mapping
+    #
+    def unmap(botmodule, handle)
+      tmpl = @templates[handle]
+      raise "Botmodule #{botmodule.name} tried to unmap #{tmpl.inspect} that was handled by #{tmpl.botmodule}" unless tmpl.botmodule == botmodule.name
+      debug "Unmapping #{tmpl.inspect}"
+      @templates[handle] = nil
+      @templates.clear unless @templates.nitems > 0
     end
 
     # We redefine the handle() method from MessageMapper, taking into account
-    # that @parent is a bot, and that we don't handle fallbacks
+    # that @parent is a bot, and that we don't handle fallbacks.
+    #
+    # On failure to dispatch anything, the method returns false. If dispatching
+    # is successfull, the method returns a Hash.
     #
+    # Presently, the hash returned on success has only one key, :return, whose
+    # value is the actual return value of the successfull dispatch.
+    # 
     # TODO this same kind of mechanism could actually be used in MessageMapper
     # itself to be able to handle the case of multiple plugins having the same
     # 'first word' ...
     #
+    #
     def handle(m)
       return false if @templates.empty?
       failures = []
       @templates.each do |tmpl|
+        # Skip this element if it was unmapped
+        next unless tmpl
         botmodule = @parent.plugins[tmpl.botmodule]
         options, failure = tmpl.recognize(m)
         if options.nil?
@@ -83,10 +173,10 @@ module ::Irc
           end
           auth = tmpl.options[:full_auth_path]
           debug "checking auth for #{auth}"
-          if m.bot.auth.allow?(auth, m.source, m.replyto)
+          # We check for private permission
+          if m.bot.auth.allow?(auth, m.source, '?')
             debug "template match found and auth'd: #{action.inspect} #{options.inspect}"
-            @parent.send(action, m, options)
-            return true
+            return :return => botmodule.send(action, m, options)
           end
           debug "auth failed for #{auth}"
           # if it's just an auth failure but otherwise the match is good,
@@ -103,11 +193,19 @@ module ::Irc
 
   end
 
-  class IrcBot
-
-    # The Irc::IrcBot::RemoteObject class represents and object that will take care
+    # The Irc::Bot::RemoteObject class represents and object that will take care
     # of interfacing with remote clients
     #
+    # Example client session:
+    #
+    #   require 'drb'
+    #   rbot = DRbObject.new_with_uri('druby://localhost:7268')
+    #   id = rbot.delegate(nil, 'remote login someuser somepass')[:return]
+    #   rbot.delegate(id, 'some secret command')
+    #
+    # Of course, the remote login is only neede for commands which may not be available
+    # to everyone
+    #
     class RemoteObject
 
       # We don't want this object to be copied clientside, so we make it undumpable
@@ -116,24 +214,37 @@ module ::Irc
       # Initialization is simple
       def initialize(bot)
         @bot = bot
-        @dispatcher = RemoteDispatcher.new(@bot)
       end
 
       # The delegate method. This is the main method used by remote clients to send
       # commands to the bot. Most of the time, the method will be called with only
-      # two parameters (authorization code and a String), but we allow more parameters
-      # for future expansions
+      # two parameters (session id and a String), but we allow more parameters
+      # for future expansions.
+      #
+      # The session_id can be nil, meaning that the remote client wants to work as
+      # an anoynomus botuser.
       #
-      def delegate(auth, *pars)
+      def delegate(session_id, *pars)
         warn "Ignoring extra parameters" if pars.length > 1
         cmd = pars.first
-        # TODO implement auth <-> client conversion
-        # We need at least a RemoteBotUser class derived from Irc::Auth::BotUser
-        # and a way to associate the auth info to the appropriate RemoteBotUser class
-        client = auth
-        debug "Trying to dispatch command #{cmd.inspect} authorized by #{auth.inspect}"
+        client = @bot.auth.remote_user(session_id)
+        raise "No such session id #{session_id}" unless client
+        debug "Trying to dispatch command #{cmd.inspect} from #{client.inspect} authorized by #{session_id.inspect}"
         m = RemoteMessage.new(@bot, client, cmd)
-        @dispatcher.handle(m)
+        @bot.remote_dispatcher.handle(m)
+      end
+
+      private :instance_variables, :instance_variable_get, :instance_variable_set
+    end
+
+    # The bot also manages a single (for the moment) remote dispatcher. This method
+    # makes it accessible to the outside world, creating it if necessary.
+    #
+    def remote_dispatcher
+      if defined? @remote_dispatcher
+        @remote_dispatcher
+      else
+        @remote_dispatcher = RemoteDispatcher.new(self)
       end
     end
 
@@ -148,30 +259,72 @@ module ::Irc
       end
     end
 
+  module Plugins
+
+    # We create a new Ruby module that can be included by BotModules that want to
+    # provide remote interfaces
+    #
+    module RemoteBotModule
+
+      # The remote_map acts just like the BotModule#map method, except that
+      # the map is registered to the @bot's remote_dispatcher. Also, the remote map handle
+      # is handled for the cleanup management
+      #
+      def remote_map(*args)
+        @remote_maps = Array.new unless defined? @remote_maps
+        @remote_maps << @bot.remote_dispatcher.map(self, *args)
+      end
+
+      # Unregister the remote maps.
+      #
+      def remote_cleanup
+        return unless defined? @remote_maps
+        @remote_maps.each { |h|
+          @bot.remote_dispatcher.unmap(self, h)
+        }
+        @remote_maps.clear
+      end
+
+      # Redefine the default cleanup method.
+      #
+      def cleanup
+        super
+        remote_cleanup
+      end
+    end
+
+    # And just because I like consistency:
+    #
+    module RemoteCoreBotModule
+      include RemoteBotModule
+    end
+
+    module RemotePlugin
+      include RemoteBotModule
+    end
+
   end
 
 end
+end
 
 class RemoteModule < CoreBotModule
 
-  BotConfig.register BotConfigIntegerValue.new('remote.port',
+  include RemoteCoreBotModule
+
+  Config.register Config::BooleanValue.new('remote.autostart',
+    :default => true,
+    :requires_rescan => true,
+    :desc => "Whether the remote service provider should be started automatically")
+
+  Config.register Config::IntegerValue.new('remote.port',
     :default => 7268, # that's 'rbot'
-    :on_change => Proc.new { |bot, v|
-      stop_service
-      @port = v
-      start_service
-    },
-    :requires_restart => true,
+    :requires_rescan => true,
     :desc => "Port on which the remote interface will be presented")
 
-  BotConfig.register BotConfigStringValue.new('remote.host',
+  Config.register Config::StringValue.new('remote.host',
     :default => '',
-    :on_change => Proc.new { |bot, v|
-      stop_service
-      @host = v
-      start_service
-    },
-    :requires_restart => true,
+    :requires_rescan => true,
     :desc => "Port on which the remote interface will be presented")
 
   def initialize
@@ -179,7 +332,11 @@ class RemoteModule < CoreBotModule
     @port = @bot.config['remote.port']
     @host = @bot.config['remote.host']
     @drb = nil
-    start_service
+    begin
+      start_service if @bot.config['remote.autostart']
+    rescue => e
+      error "couldn't start remote service provider: #{e.inspect}"
+    end
   end
 
   def start_service
@@ -213,6 +370,16 @@ class RemoteModule < CoreBotModule
     m.reply rep
   end
 
+  def remote_test(m, params)
+    @bot.say params[:channel], "This is a remote test"
+  end
+
+  def remote_login(m, params)
+    id = @bot.auth.remote_login(params[:botuser], params[:password])
+    raise "login failed" unless id
+    return id
+  end
+
 end
 
 remote = RemoteModule.new
@@ -226,3 +393,11 @@ remote.map "remote stop",
   :auth_path => ':manage:'
 
 remote.default_auth('*', false)
+
+remote.remote_map "remote test :channel",
+  :action => 'remote_test'
+
+remote.remote_map "remote login :botuser :password",
+  :action => 'remote_login'
+
+remote.default_auth('login', true)