4 # :title: Remote service provider for rbot
6 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
7 # Copyright:: Copyright (c) 2006 Giuseppe Bilotta
10 # From an idea by halorgium <rbot@spork.in>.
12 # TODO find a way to manage session id (logging out, manually and/or
21 # We extend the BotUser class to handle remote logins
25 # A rather simple method to handle remote logins. Nothing special, just a
28 def remote_login(password)
29 if password == @password
30 debug "remote login for #{self.inspect} succeeded"
38 # We extend the AuthManagerClass to handle remote logins
40 class AuthManagerClass
42 MAX_SESSION_ID = 2**128 - 1
44 # Creates a session id when the given password matches the given
47 def remote_login(botusername, pwd)
48 @remote_users = Hash.new unless defined? @remote_users
49 n = BotUser.sanitize_username(botusername)
51 raise "No such BotUser #{n}" unless include?(k)
53 if bu.remote_login(pwd)
54 raise "ran out of session ids!" if @remote_users.length == MAX_SESSION_ID
55 session_id = rand(MAX_SESSION_ID)
56 while @remote_users.has_key?(session_id)
57 session_id = rand(MAX_SESSION_ID)
59 @remote_users[session_id] = bu
65 # Returns the botuser associated with the given session id
66 def remote_user(session_id)
67 return everyone unless session_id
68 return nil unless defined? @remote_users
69 if @remote_users.has_key?(session_id)
70 return @remote_users[session_id]
80 # A RemoteMessage is similar to a BasicUserMessage
86 # when the message was received
89 # remote client that originated the message
92 # contents of the message
93 attr_accessor :message
95 def initialize(bot, source, message)
102 # The target of a RemoteMessage
107 # Remote messages are always 'private'
113 # The RemoteDispatcher is a kind of MessageMapper, tuned to handle
116 class RemoteDispatcher < MessageMapper
118 # It is initialized by passing it the bot instance
124 # The map method for the RemoteDispatcher returns the index of the inserted
127 def map(botmodule, *args)
129 return @templates.length - 1
132 # The unmap method for the RemoteDispatcher nils the template at the given index,
133 # therefore effectively removing the mapping
135 def unmap(botmodule, handle)
136 tmpl = @templates[handle]
137 raise "Botmodule #{botmodule.name} tried to unmap #{tmpl.inspect} that was handled by #{tmpl.botmodule}" unless tmpl.botmodule == botmodule.name
138 debug "Unmapping #{tmpl.inspect}"
139 @templates[handle] = nil
140 @templates.clear unless @templates.nitems > 0
143 # We redefine the handle() method from MessageMapper, taking into account
144 # that @parent is a bot, and that we don't handle fallbacks.
146 # On failure to dispatch anything, the method returns false. If dispatching
147 # is successfull, the method returns a Hash.
149 # Presently, the hash returned on success has only one key, :return, whose
150 # value is the actual return value of the successfull dispatch.
152 # TODO this same kind of mechanism could actually be used in MessageMapper
153 # itself to be able to handle the case of multiple plugins having the same
158 return false if @templates.empty?
160 @templates.each do |tmpl|
161 # Skip this element if it was unmapped
163 botmodule = @parent.plugins[tmpl.botmodule]
164 options, failure = tmpl.recognize(m)
166 failures << [tmpl, failure]
168 action = tmpl.options[:action]
169 unless botmodule.respond_to?(action)
170 failures << [tmpl, "#{botmodule} does not respond to action #{action}"]
173 auth = tmpl.options[:full_auth_path]
174 debug "checking auth for #{auth}"
175 # We check for private permission
176 if m.bot.auth.allow?(auth, m.source, '?')
177 debug "template match found and auth'd: #{action.inspect} #{options.inspect}"
178 return :return => botmodule.send(action, m, options)
180 debug "auth failed for #{auth}"
181 # if it's just an auth failure but otherwise the match is good,
182 # don't try any more handlers
186 failures.each {|f, r|
187 debug "#{f.inspect} => #{r}"
189 debug "no handler found"
197 # The Irc::Bot::RemoteObject class represents and object that will take care
198 # of interfacing with remote clients
200 # Example client session:
203 # rbot = DRbObject.new_with_uri('druby://localhost:7268')
204 # id = rbot.delegate(nil, 'remote login someuser somepass')[:return]
205 # rbot.delegate(id, 'some secret command')
207 # Of course, the remote login is only neede for commands which may not be available
212 # We don't want this object to be copied clientside, so we make it undumpable
215 # Initialization is simple
220 # The delegate method. This is the main method used by remote clients to send
221 # commands to the bot. Most of the time, the method will be called with only
222 # two parameters (session id and a String), but we allow more parameters
223 # for future expansions.
225 # The session_id can be nil, meaning that the remote client wants to work as
226 # an anoynomus botuser.
228 def delegate(session_id, *pars)
229 warn "Ignoring extra parameters" if pars.length > 1
231 client = @bot.auth.remote_user(session_id)
232 raise "No such session id #{session_id}" unless client
233 debug "Trying to dispatch command #{cmd.inspect} from #{client.inspect} authorized by #{session_id.inspect}"
234 m = RemoteMessage.new(@bot, client, cmd)
235 @bot.remote_dispatcher.handle(m)
238 private :instance_variables, :instance_variable_get, :instance_variable_set
241 # The bot also manages a single (for the moment) remote dispatcher. This method
242 # makes it accessible to the outside world, creating it if necessary.
244 def remote_dispatcher
245 if defined? @remote_dispatcher
248 @remote_dispatcher = RemoteDispatcher.new(self)
252 # The bot also manages a single (for the moment) remote object. This method
253 # makes it accessible to the outside world, creating it if necessary.
256 if defined? @remote_object
259 @remote_object = RemoteObject.new(self)
267 # We create a new Ruby module that can be included by BotModules that want to
268 # provide remote interfaces
270 module RemoteBotModule
272 # The remote_map acts just like the BotModule#map method, except that
273 # the map is registered to the @bot's remote_dispatcher. Also, the remote map handle
274 # is handled for the cleanup management
276 def remote_map(*args)
277 @remote_maps = Array.new unless defined? @remote_maps
278 @remote_maps << @bot.remote_dispatcher.map(self, *args)
281 # Unregister the remote maps.
284 return unless defined? @remote_maps
285 @remote_maps.each { |h|
286 @bot.remote_dispatcher.unmap(self, h)
291 # Redefine the default cleanup method.
299 # And just because I like consistency:
301 module RemoteCoreBotModule
302 include RemoteBotModule
306 include RemoteBotModule
313 class RemoteModule < CoreBotModule
315 include RemoteCoreBotModule
317 BotConfig.register BotConfigBooleanValue.new('remote.autostart',
319 :requires_rescan => true,
320 :desc => "Whether the remote service provider should be started automatically")
322 BotConfig.register BotConfigIntegerValue.new('remote.port',
323 :default => 7268, # that's 'rbot'
324 :requires_rescan => true,
325 :desc => "Port on which the remote interface will be presented")
327 BotConfig.register BotConfigStringValue.new('remote.host',
329 :requires_rescan => true,
330 :desc => "Port on which the remote interface will be presented")
334 @port = @bot.config['remote.port']
335 @host = @bot.config['remote.host']
338 start_service if @bot.config['remote.autostart']
340 error "couldn't start remote service provider: #{e.inspect}"
345 raise "Remote service provider already running" if @drb
346 @drb = DRb.start_service("druby://#{@host}:#{@port}", @bot.remote_object)
350 @drb.stop_service if @drb
359 def handle_start(m, params)
361 rep = "remote service provider already running"
362 rep << " on port #{@port}" if m.private?
366 rep = "remote service provider started"
367 rep << " on port #{@port}" if m.private?
369 rep = "couldn't start remote service provider"
375 def remote_test(m, params)
376 @bot.say params[:channel], "This is a remote test"
379 def remote_login(m, params)
380 id = @bot.auth.remote_login(params[:botuser], params[:password])
381 raise "login failed" unless id
387 remote = RemoteModule.new
389 remote.map "remote start",
390 :action => 'handle_start',
391 :auth_path => ':manage:'
393 remote.map "remote stop",
394 :action => 'handle_stop',
395 :auth_path => ':manage:'
397 remote.default_auth('*', false)
399 remote.remote_map "remote test :channel",
400 :action => 'remote_test'
402 remote.remote_map "remote login :botuser :password",
403 :action => 'remote_login'
405 remote.default_auth('login', true)