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
22 # We extend the BotUser class to handle remote logins
26 # A rather simple method to handle remote logins. Nothing special, just a
29 def remote_login(password)
30 if password == @password
31 debug "remote login for #{self.inspect} succeeded"
39 # We extend the ManagerClass to handle remote logins
43 MAX_SESSION_ID = 2**128 - 1
45 # Creates a session id when the given password matches the given
48 def remote_login(botusername, pwd)
49 @remote_users = Hash.new unless defined? @remote_users
50 n = BotUser.sanitize_username(botusername)
52 raise "No such BotUser #{n}" unless include?(k)
54 if bu.remote_login(pwd)
55 raise "ran out of session ids!" if @remote_users.length == MAX_SESSION_ID
56 session_id = rand(MAX_SESSION_ID)
57 while @remote_users.has_key?(session_id)
58 session_id = rand(MAX_SESSION_ID)
60 @remote_users[session_id] = bu
66 # Returns the botuser associated with the given session id
67 def remote_user(session_id)
68 return everyone unless session_id
69 return nil unless defined? @remote_users
70 if @remote_users.has_key?(session_id)
71 return @remote_users[session_id]
81 # A RemoteMessage is similar to a BasicUserMessage
87 # when the message was received
90 # remote client that originated the message
93 # contents of the message
94 attr_accessor :message
96 def initialize(bot, source, message)
103 # The target of a RemoteMessage
108 # Remote messages are always 'private'
114 # The RemoteDispatcher is a kind of MessageMapper, tuned to handle
117 class RemoteDispatcher < MessageMapper
119 # It is initialized by passing it the bot instance
125 # The map method for the RemoteDispatcher returns the index of the inserted
128 def map(botmodule, *args)
130 return @templates.length - 1
133 # The unmap method for the RemoteDispatcher nils the template at the given index,
134 # therefore effectively removing the mapping
136 def unmap(botmodule, handle)
137 tmpl = @templates[handle]
138 raise "Botmodule #{botmodule.name} tried to unmap #{tmpl.inspect} that was handled by #{tmpl.botmodule}" unless tmpl.botmodule == botmodule.name
139 debug "Unmapping #{tmpl.inspect}"
140 @templates[handle] = nil
141 @templates.clear unless @templates.nitems > 0
144 # We redefine the handle() method from MessageMapper, taking into account
145 # that @parent is a bot, and that we don't handle fallbacks.
147 # On failure to dispatch anything, the method returns false. If dispatching
148 # is successfull, the method returns a Hash.
150 # Presently, the hash returned on success has only one key, :return, whose
151 # value is the actual return value of the successfull dispatch.
153 # TODO this same kind of mechanism could actually be used in MessageMapper
154 # itself to be able to handle the case of multiple plugins having the same
159 return false if @templates.empty?
161 @templates.each do |tmpl|
162 # Skip this element if it was unmapped
164 botmodule = @parent.plugins[tmpl.botmodule]
165 options, failure = tmpl.recognize(m)
167 failures << [tmpl, failure]
169 action = tmpl.options[:action]
170 unless botmodule.respond_to?(action)
171 failures << [tmpl, "#{botmodule} does not respond to action #{action}"]
174 auth = tmpl.options[:full_auth_path]
175 debug "checking auth for #{auth}"
176 # We check for private permission
177 if m.bot.auth.allow?(auth, m.source, '?')
178 debug "template match found and auth'd: #{action.inspect} #{options.inspect}"
179 return :return => botmodule.send(action, m, options)
181 debug "auth failed for #{auth}"
182 # if it's just an auth failure but otherwise the match is good,
183 # don't try any more handlers
187 failures.each {|f, r|
188 debug "#{f.inspect} => #{r}"
190 debug "no handler found"
196 # The Irc::Bot::RemoteObject class represents and object that will take care
197 # of interfacing with remote clients
199 # Example client session:
202 # rbot = DRbObject.new_with_uri('druby://localhost:7268')
203 # id = rbot.delegate(nil, 'remote login someuser somepass')[:return]
204 # rbot.delegate(id, 'some secret command')
206 # Of course, the remote login is only neede for commands which may not be available
211 # We don't want this object to be copied clientside, so we make it undumpable
214 # Initialization is simple
219 # The delegate method. This is the main method used by remote clients to send
220 # commands to the bot. Most of the time, the method will be called with only
221 # two parameters (session id and a String), but we allow more parameters
222 # for future expansions.
224 # The session_id can be nil, meaning that the remote client wants to work as
225 # an anoynomus botuser.
227 def delegate(session_id, *pars)
228 warn "Ignoring extra parameters" if pars.length > 1
230 client = @bot.auth.remote_user(session_id)
231 raise "No such session id #{session_id}" unless client
232 debug "Trying to dispatch command #{cmd.inspect} from #{client.inspect} authorized by #{session_id.inspect}"
233 m = RemoteMessage.new(@bot, client, cmd)
234 @bot.remote_dispatcher.handle(m)
237 private :instance_variables, :instance_variable_get, :instance_variable_set
240 # The bot also manages a single (for the moment) remote dispatcher. This method
241 # makes it accessible to the outside world, creating it if necessary.
243 def remote_dispatcher
244 if defined? @remote_dispatcher
247 @remote_dispatcher = RemoteDispatcher.new(self)
251 # The bot also manages a single (for the moment) remote object. This method
252 # makes it accessible to the outside world, creating it if necessary.
255 if defined? @remote_object
258 @remote_object = RemoteObject.new(self)
264 # We create a new Ruby module that can be included by BotModules that want to
265 # provide remote interfaces
267 module RemoteBotModule
269 # The remote_map acts just like the BotModule#map method, except that
270 # the map is registered to the @bot's remote_dispatcher. Also, the remote map handle
271 # is handled for the cleanup management
273 def remote_map(*args)
274 @remote_maps = Array.new unless defined? @remote_maps
275 @remote_maps << @bot.remote_dispatcher.map(self, *args)
278 # Unregister the remote maps.
281 return unless defined? @remote_maps
282 @remote_maps.each { |h|
283 @bot.remote_dispatcher.unmap(self, h)
288 # Redefine the default cleanup method.
296 # And just because I like consistency:
298 module RemoteCoreBotModule
299 include RemoteBotModule
303 include RemoteBotModule
311 class RemoteModule < CoreBotModule
313 include RemoteCoreBotModule
315 Config.register Config::BooleanValue.new('remote.autostart',
317 :requires_rescan => true,
318 :desc => "Whether the remote service provider should be started automatically")
320 Config.register Config::IntegerValue.new('remote.port',
321 :default => 7268, # that's 'rbot'
322 :requires_rescan => true,
323 :desc => "Port on which the remote interface will be presented")
325 Config.register Config::StringValue.new('remote.host',
326 :default => '127.0.0.1',
327 :requires_rescan => true,
328 :desc => "Host on which the remote interface will be presented")
332 @port = @bot.config['remote.port']
333 @host = @bot.config['remote.host']
336 start_service if @bot.config['remote.autostart']
338 error "couldn't start remote service provider: #{e.inspect}"
343 raise "Remote service provider already running" if @drb
344 @drb = DRb.start_service("druby://#{@host}:#{@port}", @bot.remote_object)
348 @drb.stop_service if @drb
357 def handle_start(m, params)
359 rep = "remote service provider already running"
360 rep << " on port #{@port}" if m.private?
364 rep = "remote service provider started"
365 rep << " on port #{@port}" if m.private?
367 rep = "couldn't start remote service provider"
373 def remote_test(m, params)
374 @bot.say params[:channel], "This is a remote test"
377 def remote_login(m, params)
378 id = @bot.auth.remote_login(params[:botuser], params[:password])
379 raise "login failed" unless id
385 remote = RemoteModule.new
387 remote.map "remote start",
388 :action => 'handle_start',
389 :auth_path => ':manage:'
391 remote.map "remote stop",
392 :action => 'handle_stop',
393 :auth_path => ':manage:'
395 remote.default_auth('*', false)
397 remote.remote_map "remote test :channel",
398 :action => 'remote_test'
400 remote.remote_map "remote login :botuser :password",
401 :action => 'remote_login'
403 remote.default_auth('login', true)