X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Fcore%2Fremote.rb;h=7ffb62e1bb6e5a2e7613e67b22456c486e5ff85e;hb=2ecf2f58c843895ce4ad143d0a05283c4b7e37e8;hp=8604a2249c198728a583002d205902f410ab3f2a;hpb=fd1355077d816f51a34a6f08029d58aa345632c2;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/core/remote.rb b/lib/rbot/core/remote.rb index 8604a224..7ffb62e1 100644 --- a/lib/rbot/core/remote.rb +++ b/lib/rbot/core/remote.rb @@ -9,19 +9,76 @@ # # From an idea by halorgium . # -# 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)