4 # :title: Remote service provider for rbot
6 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
8 # From an idea by halorgium <rbot@spork.in>.
10 # TODO find a way to manage session id (logging out, manually and/or
20 # We extend the BotUser class to handle remote logins
24 # A rather simple method to handle remote logins. Nothing special, just a
27 def remote_login(password)
28 if password == @password
29 debug "remote login for #{self.inspect} succeeded"
37 # We extend the ManagerClass to handle remote logins
41 MAX_SESSION_ID = 2**128 - 1
43 # Creates a session id when the given password matches the given
46 def remote_login(botusername, pwd)
47 @remote_users = Hash.new unless defined? @remote_users
48 n = BotUser.sanitize_username(botusername)
50 raise "No such BotUser #{n}" unless include?(k)
52 if bu.remote_login(pwd)
53 raise "ran out of session ids!" if @remote_users.length == MAX_SESSION_ID
54 session_id = rand(MAX_SESSION_ID)
55 while @remote_users.has_key?(session_id)
56 session_id = rand(MAX_SESSION_ID)
58 @remote_users[session_id] = bu
64 # Returns the botuser associated with the given session id
65 def remote_user(session_id)
66 return everyone unless session_id
67 return nil unless defined? @remote_users
68 if @remote_users.has_key?(session_id)
69 return @remote_users[session_id]
79 # A RemoteMessage is similar to a BasicUserMessage
85 # when the message was received
88 # remote client that originated the message
91 # contents of the message
92 attr_accessor :message
94 def initialize(bot, source, message)
101 # The target of a RemoteMessage
106 # Remote messages are always 'private'
112 # The RemoteDispatcher is a kind of MessageMapper, tuned to handle
115 class RemoteDispatcher < MessageMapper
117 # It is initialized by passing it the bot instance
123 # The map method for the RemoteDispatcher returns the index of the inserted
126 def map(botmodule, *args)
128 return @templates.length - 1
131 # The unmap method for the RemoteDispatcher nils the template at the given index,
132 # therefore effectively removing the mapping
134 def unmap(botmodule, handle)
135 tmpl = @templates[handle]
136 raise "Botmodule #{botmodule.name} tried to unmap #{tmpl.inspect} that was handled by #{tmpl.botmodule}" unless tmpl.botmodule == botmodule.name
137 debug "Unmapping #{tmpl.inspect}"
138 @templates[handle] = nil
139 @templates.clear unless @templates.nitems > 0
142 # We redefine the handle() method from MessageMapper, taking into account
143 # that @parent is a bot, and that we don't handle fallbacks.
145 # On failure to dispatch anything, the method returns false. If dispatching
146 # is successfull, the method returns a Hash.
148 # Presently, the hash returned on success has only one key, :return, whose
149 # value is the actual return value of the successfull dispatch.
151 # TODO this same kind of mechanism could actually be used in MessageMapper
152 # itself to be able to handle the case of multiple plugins having the same
157 return false if @templates.empty?
159 @templates.each do |tmpl|
160 # Skip this element if it was unmapped
162 botmodule = @parent.plugins[tmpl.botmodule]
163 options, failure = tmpl.recognize(m)
165 failures << [tmpl, failure]
167 action = tmpl.options[:action]
168 unless botmodule.respond_to?(action)
169 failures << [tmpl, "#{botmodule} does not respond to action #{action}"]
172 auth = tmpl.options[:full_auth_path]
173 debug "checking auth for #{auth}"
174 # We check for private permission
175 if m.bot.auth.allow?(auth, m.source, '?')
176 debug "template match found and auth'd: #{action.inspect} #{options.inspect}"
177 return :return => botmodule.send(action, m, options)
179 debug "auth failed for #{auth}"
180 # if it's just an auth failure but otherwise the match is good,
181 # don't try any more handlers
185 failures.each {|f, r|
186 debug "#{f.inspect} => #{r}"
188 debug "no handler found"
194 # The Irc::Bot::RemoteObject class represents and object that will take care
195 # of interfacing with remote clients
197 # Example client session:
200 # rbot = DRbObject.new_with_uri('druby://localhost:7268')
201 # id = rbot.delegate(nil, 'remote login someuser somepass')[:return]
202 # rbot.delegate(id, 'some secret command')
204 # Of course, the remote login is only neede for commands which may not be available
209 # We don't want this object to be copied clientside, so we make it undumpable
212 # Initialization is simple
217 # The delegate method. This is the main method used by remote clients to send
218 # commands to the bot. Most of the time, the method will be called with only
219 # two parameters (session id and a String), but we allow more parameters
220 # for future expansions.
222 # The session_id can be nil, meaning that the remote client wants to work as
223 # an anoynomus botuser.
225 def delegate(session_id, *pars)
226 warn "Ignoring extra parameters" if pars.length > 1
228 client = @bot.auth.remote_user(session_id)
229 raise "No such session id #{session_id}" unless client
230 debug "Trying to dispatch command #{cmd.inspect} from #{client.inspect} authorized by #{session_id.inspect}"
231 m = RemoteMessage.new(@bot, client, cmd)
232 @bot.remote_dispatcher.handle(m)
235 private :instance_variables, :instance_variable_get, :instance_variable_set
238 # The bot also manages a single (for the moment) remote dispatcher. This method
239 # makes it accessible to the outside world, creating it if necessary.
241 def remote_dispatcher
242 if defined? @remote_dispatcher
245 @remote_dispatcher = RemoteDispatcher.new(self)
249 # The bot also manages a single (for the moment) remote object. This method
250 # makes it accessible to the outside world, creating it if necessary.
253 if defined? @remote_object
256 @remote_object = RemoteObject.new(self)
262 # We create a new Ruby module that can be included by BotModules that want to
263 # provide remote interfaces
265 module RemoteBotModule
267 # The remote_map acts just like the BotModule#map method, except that
268 # the map is registered to the @bot's remote_dispatcher. Also, the remote map handle
269 # is handled for the cleanup management
271 def remote_map(*args)
272 @remote_maps = Array.new unless defined? @remote_maps
273 @remote_maps << @bot.remote_dispatcher.map(self, *args)
276 # Unregister the remote maps.
279 return unless defined? @remote_maps
280 @remote_maps.each { |h|
281 @bot.remote_dispatcher.unmap(self, h)
286 # Redefine the default cleanup method.
294 # And just because I like consistency:
296 module RemoteCoreBotModule
297 include RemoteBotModule
301 include RemoteBotModule
309 class RemoteModule < CoreBotModule
311 include RemoteCoreBotModule
313 Config.register Config::BooleanValue.new('remote.autostart',
315 :requires_rescan => true,
316 :desc => "Whether the remote service provider should be started automatically")
318 Config.register Config::IntegerValue.new('remote.port',
319 :default => 7268, # that's 'rbot'
320 :requires_rescan => true,
321 :desc => "Port on which the remote interface will be presented")
323 Config.register Config::StringValue.new('remote.host',
324 :default => '127.0.0.1',
325 :requires_rescan => true,
326 :desc => "Host on which the remote interface will be presented")
330 @port = @bot.config['remote.port']
331 @host = @bot.config['remote.host']
334 start_service if @bot.config['remote.autostart']
336 error "couldn't start remote service provider: #{e.inspect}"
341 raise "Remote service provider already running" if @drb
342 @drb = DRb.start_service("druby://#{@host}:#{@port}", @bot.remote_object)
346 @drb.stop_service if @drb
355 def handle_start(m, params)
357 rep = "remote service provider already running"
358 rep << " on port #{@port}" if m.private?
362 rep = "remote service provider started"
363 rep << " on port #{@port}" if m.private?
365 rep = "couldn't start remote service provider"
371 def remote_test(m, params)
372 @bot.say params[:channel], "This is a remote test"
375 def remote_login(m, params)
376 id = @bot.auth.remote_login(params[:botuser], params[:password])
377 raise "login failed" unless id
383 remote = RemoteModule.new
385 remote.map "remote start",
386 :action => 'handle_start',
387 :auth_path => ':manage:'
389 remote.map "remote stop",
390 :action => 'handle_stop',
391 :auth_path => ':manage:'
393 remote.default_auth('*', false)
395 remote.remote_map "remote test :channel",
396 :action => 'remote_test'
398 remote.remote_map "remote login :botuser :password",
399 :action => 'remote_login'
401 remote.default_auth('login', true)