#
# 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
- # 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 AuthManagerClass to handle remote logins
+ #
+ class AuthManagerClass
+
+ 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
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?
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,
end
- class IrcBot
+ class Bot
- # 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
# 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.
#
- def delegate(auth, *pars)
+ # The session_id can be nil, meaning that the remote client wants to work as
+ # an anoynomus botuser.
+ #
+ 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
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
class RemoteModule < CoreBotModule
+ include RemoteCoreBotModule
+
+ BotConfig.register BotConfigBooleanValue.new('remote.autostart',
+ :default => true,
+ :requires_rescan => true,
+ :desc => "Whether the remote service provider should be started automatically")
+
BotConfig.register BotConfigIntegerValue.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',
: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
@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
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
: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)