]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/core/remote.rb
remove whitespace
[user/henk/code/ruby/rbot.git] / lib / rbot / core / remote.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Remote service provider for rbot
5 #
6 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
7 #
8 # From an idea by halorgium <rbot@spork.in>.
9 #
10 # TODO find a way to manage session id (logging out, manually and/or
11 # automatically)
12
13 require 'drb/drb'
14
15 module ::Irc
16 class Bot
17
18   module Auth
19
20     # We extend the BotUser class to handle remote logins
21     #
22     class BotUser
23
24       # A rather simple method to handle remote logins. Nothing special, just a
25       # password check.
26       #
27       def remote_login(password)
28         if password == @password
29           debug "remote login for #{self.inspect} succeeded"
30           return true
31         else
32           return false
33         end
34       end
35     end
36
37     # We extend the ManagerClass to handle remote logins
38     #
39     class ManagerClass
40
41       MAX_SESSION_ID = 2**128 - 1
42
43       # Creates a session id when the given password matches the given
44       # botusername
45       #
46       def remote_login(botusername, pwd)
47         @remote_users = Hash.new unless defined? @remote_users
48         n = BotUser.sanitize_username(botusername)
49         k = n.to_sym
50         raise "No such BotUser #{n}" unless include?(k)
51         bu = @allbotusers[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)
57           end
58           @remote_users[session_id] = bu
59           return session_id
60         end
61         return false
62       end
63
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]
70         else
71           return nil
72         end
73       end
74     end
75
76   end
77
78
79   # A RemoteMessage is similar to a BasicUserMessage
80   #
81   class RemoteMessage
82     # associated bot
83     attr_reader :bot
84
85     # when the message was received
86     attr_reader :time
87
88     # remote client that originated the message
89     attr_reader :source
90
91     # contents of the message
92     attr_accessor :message
93
94     def initialize(bot, source, message)
95       @bot = bot
96       @source = source
97       @message = message
98       @time = Time.now
99     end
100
101     # The target of a RemoteMessage
102     def target
103       @bot
104     end
105
106     # Remote messages are always 'private'
107     def private?
108       true
109     end
110   end
111
112   # The RemoteDispatcher is a kind of MessageMapper, tuned to handle
113   # RemoteMessages
114   #
115   class RemoteDispatcher < MessageMapper
116
117     # It is initialized by passing it the bot instance
118     #
119     def initialize(bot)
120       super
121     end
122
123     # The map method for the RemoteDispatcher returns the index of the inserted
124     # template
125     #
126     def map(botmodule, *args)
127       super
128       return @templates.length - 1
129     end
130
131     # The unmap method for the RemoteDispatcher nils the template at the given index,
132     # therefore effectively removing the mapping
133     #
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
140     end
141
142     # We redefine the handle() method from MessageMapper, taking into account
143     # that @parent is a bot, and that we don't handle fallbacks.
144     #
145     # On failure to dispatch anything, the method returns false. If dispatching
146     # is successfull, the method returns a Hash.
147     #
148     # Presently, the hash returned on success has only one key, :return, whose
149     # value is the actual return value of the successfull dispatch.
150     #
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
153     # 'first word' ...
154     #
155     #
156     def handle(m)
157       return false if @templates.empty?
158       failures = []
159       @templates.each do |tmpl|
160         # Skip this element if it was unmapped
161         next unless tmpl
162         botmodule = @parent.plugins[tmpl.botmodule]
163         options, failure = tmpl.recognize(m)
164         if options.nil?
165           failures << [tmpl, failure]
166         else
167           action = tmpl.options[:action]
168           unless botmodule.respond_to?(action)
169             failures << [tmpl, "#{botmodule} does not respond to action #{action}"]
170             next
171           end
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)
178           end
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
182           return false
183         end
184       end
185       failures.each {|f, r|
186         debug "#{f.inspect} => #{r}"
187       }
188       debug "no handler found"
189       return false
190     end
191
192   end
193
194     # The Irc::Bot::RemoteObject class represents and object that will take care
195     # of interfacing with remote clients
196     #
197     # Example client session:
198     #
199     #   require 'drb'
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')
203     #
204     # Of course, the remote login is only neede for commands which may not be available
205     # to everyone
206     #
207     class RemoteObject
208
209       # We don't want this object to be copied clientside, so we make it undumpable
210       include DRbUndumped
211
212       # Initialization is simple
213       def initialize(bot)
214         @bot = bot
215       end
216
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.
221       #
222       # The session_id can be nil, meaning that the remote client wants to work as
223       # an anoynomus botuser.
224       #
225       def delegate(session_id, *pars)
226         warn "Ignoring extra parameters" if pars.length > 1
227         cmd = pars.first
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)
233       end
234
235       private :instance_variables, :instance_variable_get, :instance_variable_set
236     end
237
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.
240     #
241     def remote_dispatcher
242       if defined? @remote_dispatcher
243         @remote_dispatcher
244       else
245         @remote_dispatcher = RemoteDispatcher.new(self)
246       end
247     end
248
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.
251     #
252     def remote_object
253       if defined? @remote_object
254         @remote_object
255       else
256         @remote_object = RemoteObject.new(self)
257       end
258     end
259
260   module Plugins
261
262     # We create a new Ruby module that can be included by BotModules that want to
263     # provide remote interfaces
264     #
265     module RemoteBotModule
266
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
270       #
271       def remote_map(*args)
272         @remote_maps = Array.new unless defined? @remote_maps
273         @remote_maps << @bot.remote_dispatcher.map(self, *args)
274       end
275
276       # Unregister the remote maps.
277       #
278       def remote_cleanup
279         return unless defined? @remote_maps
280         @remote_maps.each { |h|
281           @bot.remote_dispatcher.unmap(self, h)
282         }
283         @remote_maps.clear
284       end
285
286       # Redefine the default cleanup method.
287       #
288       def cleanup
289         super
290         remote_cleanup
291       end
292     end
293
294     # And just because I like consistency:
295     #
296     module RemoteCoreBotModule
297       include RemoteBotModule
298     end
299
300     module RemotePlugin
301       include RemoteBotModule
302     end
303
304   end
305
306 end
307 end
308
309 class RemoteModule < CoreBotModule
310
311   include RemoteCoreBotModule
312
313   Config.register Config::BooleanValue.new('remote.autostart',
314     :default => true,
315     :requires_rescan => true,
316     :desc => "Whether the remote service provider should be started automatically")
317
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")
322
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")
327
328   def initialize
329     super
330     @port = @bot.config['remote.port']
331     @host = @bot.config['remote.host']
332     @drb = nil
333     begin
334       start_service if @bot.config['remote.autostart']
335     rescue => e
336       error "couldn't start remote service provider: #{e.inspect}"
337     end
338   end
339
340   def start_service
341     raise "Remote service provider already running" if @drb
342     @drb = DRb.start_service("druby://#{@host}:#{@port}", @bot.remote_object)
343   end
344
345   def stop_service
346     @drb.stop_service if @drb
347     @drb = nil
348   end
349
350   def cleanup
351     stop_service
352     super
353   end
354
355   def handle_start(m, params)
356     if @drb
357       rep = "remote service provider already running"
358       rep << " on port #{@port}" if m.private?
359     else
360       begin
361         start_service(@port)
362         rep = "remote service provider started"
363         rep << " on port #{@port}" if m.private?
364       rescue
365         rep = "couldn't start remote service provider"
366       end
367     end
368     m.reply rep
369   end
370
371   def remote_test(m, params)
372     @bot.say params[:channel], "This is a remote test"
373   end
374
375   def remote_login(m, params)
376     id = @bot.auth.remote_login(params[:botuser], params[:password])
377     raise "login failed" unless id
378     return id
379   end
380
381 end
382
383 remote = RemoteModule.new
384
385 remote.map "remote start",
386   :action => 'handle_start',
387   :auth_path => ':manage:'
388
389 remote.map "remote stop",
390   :action => 'handle_stop',
391   :auth_path => ':manage:'
392
393 remote.default_auth('*', false)
394
395 remote.remote_map "remote test :channel",
396   :action => 'remote_test'
397
398 remote.remote_map "remote login :botuser :password",
399   :action => 'remote_login'
400
401 remote.default_auth('login', true)