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