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 AuthManagerClass to handle remote logins
41 class AuthManagerClass
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"
198 # The Irc::IrcBot::RemoteObject class represents and object that will take care
199 # of interfacing with remote clients
201 # Example client session:
204 # rbot = DRbObject.new_with_uri('druby://localhost:7268')
205 # id = rbot.delegate(nil, 'remote login someuser somepass')[:return]
206 # rbot.delegate(id, 'some secret command')
208 # Of course, the remote login is only neede for commands which may not be available
213 # We don't want this object to be copied clientside, so we make it undumpable
216 # Initialization is simple
221 # The delegate method. This is the main method used by remote clients to send
222 # commands to the bot. Most of the time, the method will be called with only
223 # two parameters (session id and a String), but we allow more parameters
224 # for future expansions.
226 # The session_id can be nil, meaning that the remote client wants to work as
227 # an anoynomus botuser.
229 def delegate(session_id, *pars)
230 warn "Ignoring extra parameters" if pars.length > 1
232 client = @bot.auth.remote_user(session_id)
233 raise "No such session id #{session_id}" unless client
234 debug "Trying to dispatch command #{cmd.inspect} from #{client.inspect} authorized by #{session_id.inspect}"
235 m = RemoteMessage.new(@bot, client, cmd)
236 @bot.remote_dispatcher.handle(m)
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)
266 # We create a new Ruby module that can be included by BotModules that want to
267 # provide remote interfaces
269 module RemoteBotModule
271 # The remote_map acts just like the BotModule#map method, except that
272 # the map is registered to the @bot's remote_dispatcher. Also, the remote map handle
273 # is handled for the cleanup management
275 def remote_map(*args)
276 @remote_maps = Array.new unless defined? @remote_maps
277 @remote_maps << @bot.remote_dispatcher.map(self, *args)
280 # Unregister the remote maps.
283 return unless defined? @remote_maps
284 @remote_maps.each { |h|
285 @bot.remote_dispatcher.unmap(self, h)
290 # Redefine the default cleanup method.
298 # And just because I like consistency:
300 module RemoteCoreBotModule
301 include RemoteBotModule
305 include RemoteBotModule
312 class RemoteModule < CoreBotModule
314 include RemoteCoreBotModule
316 BotConfig.register BotConfigBooleanValue.new('remote.autostart',
318 :requires_rescan => true,
319 :desc => "Whether the remote service provider should be started automatically")
321 BotConfig.register BotConfigIntegerValue.new('remote.port',
322 :default => 7268, # that's 'rbot'
323 :requires_rescan => true,
324 :desc => "Port on which the remote interface will be presented")
326 BotConfig.register BotConfigStringValue.new('remote.host',
328 :requires_rescan => true,
329 :desc => "Port on which the remote interface will be presented")
333 @port = @bot.config['remote.port']
334 @host = @bot.config['remote.host']
337 start_service if @bot.config['remote.autostart']
339 error "couldn't start remote service provider: #{e.inspect}"
344 raise "Remote service provider already running" if @drb
345 @drb = DRb.start_service("druby://#{@host}:#{@port}", @bot.remote_object)
349 @drb.stop_service if @drb
358 def handle_start(m, params)
360 rep = "remote service provider already running"
361 rep << " on port #{@port}" if m.private?
365 rep = "remote service provider started"
366 rep << " on port #{@port}" if m.private?
368 rep = "couldn't start remote service provider"
374 def remote_test(m, params)
375 @bot.say params[:channel], "This is a remote test"
378 def remote_login(m, params)
379 id = @bot.auth.remote_login(params[:botuser], params[:password])
380 raise "login failed" unless id
386 remote = RemoteModule.new
388 remote.map "remote start",
389 :action => 'handle_start',
390 :auth_path => ':manage:'
392 remote.map "remote stop",
393 :action => 'handle_stop',
394 :auth_path => ':manage:'
396 remote.default_auth('*', false)
398 remote.remote_map "remote test :channel",
399 :action => 'remote_test'
401 remote.remote_map "remote login :botuser :password",
402 :action => 'remote_login'
404 remote.default_auth('login', true)