]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/core/remote.rb
56ba502de278fe838b0036b7398aead9d7dbfef6
[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 client ID and auth
13 #
14
15 require 'drb/drb'
16
17 module ::Irc
18
19   # A RemoteCommand is similar to a BaiscUserMessage
20   #
21   class RemoteMessage
22     # associated bot
23     attr_reader :bot
24
25     # when the message was received
26     attr_reader :time
27
28     # remote client that originated the message
29     attr_reader :source
30
31     # contents of the message
32     attr_accessor :message
33
34     def initialize(bot, source, message)
35       @bot = bot
36       @source = source
37       @message = message
38       @time = Time.now
39     end
40
41     # The target of a RemoteMessage
42     def target
43       @bot
44     end
45
46     # Remote messages are always 'private'
47     def private?
48       true
49     end
50   end
51
52   class RemoteDispatcher < MessageMapper
53
54     def initialize(bot)
55       super
56     end
57
58     # The map method for the RemoteDispatcher returns the index of the inserted
59     # template
60     #
61     def map(botmodule, *args)
62       super
63       return @templates.length - 1
64     end
65
66     # The map method for the RemoteDispatcher returns the index of the inserted
67     # template
68     #
69     def unmap(botmodule, handle)
70       tmpl = @templates[handle]
71       raise "Botmodule #{botmodule.name} tried to unmap #{tmpl.inspect} that was handled by #{tmpl.botmodule}" unless tmpl.botmodule == botmodule.name
72       debug "Unmapping #{tmpl.inspect}"
73       @templates[handle] = nil
74       @templates.clear unless @templates.nitems > 0
75     end
76
77     # We redefine the handle() method from MessageMapper, taking into account
78     # that @parent is a bot, and that we don't handle fallbacks
79     #
80     # TODO this same kind of mechanism could actually be used in MessageMapper
81     # itself to be able to handle the case of multiple plugins having the same
82     # 'first word' ...
83     #
84     def handle(m)
85       return false if @templates.empty?
86       failures = []
87       @templates.each do |tmpl|
88         # Skip this element if it was unmapped
89         next unless tmpl
90         botmodule = @parent.plugins[tmpl.botmodule]
91         options, failure = tmpl.recognize(m)
92         if options.nil?
93           failures << [tmpl, failure]
94         else
95           action = tmpl.options[:action]
96           unless botmodule.respond_to?(action)
97             failures << [tmpl, "#{botmodule} does not respond to action #{action}"]
98             next
99           end
100           # TODO
101           # auth = tmpl.options[:full_auth_path]
102           # debug "checking auth for #{auth}"
103           # if m.bot.auth.allow?(auth, m.source, m.replyto)
104             debug "template match found and auth'd: #{action.inspect} #{options.inspect}"
105             botmodule.send(action, m, options)
106             return true
107           # end
108           # debug "auth failed for #{auth}"
109           # # if it's just an auth failure but otherwise the match is good,
110           # # don't try any more handlers
111           # return false
112         end
113       end
114       failures.each {|f, r|
115         debug "#{f.inspect} => #{r}"
116       }
117       debug "no handler found"
118       return false
119     end
120
121   end
122
123   class IrcBot
124
125     # The Irc::IrcBot::RemoteObject class represents and object that will take care
126     # of interfacing with remote clients
127     #
128     class RemoteObject
129
130       # We don't want this object to be copied clientside, so we make it undumpable
131       include DRbUndumped
132
133       # Initialization is simple
134       def initialize(bot)
135         @bot = bot
136       end
137
138       # The delegate method. This is the main method used by remote clients to send
139       # commands to the bot. Most of the time, the method will be called with only
140       # two parameters (authorization code and a String), but we allow more parameters
141       # for future expansions
142       #
143       def delegate(auth, *pars)
144         warn "Ignoring extra parameters" if pars.length > 1
145         cmd = pars.first
146         # TODO implement auth <-> client conversion
147         # We need at least a RemoteBotUser class derived from Irc::Auth::BotUser
148         # and a way to associate the auth info to the appropriate RemoteBotUser class
149         client = auth
150         debug "Trying to dispatch command #{cmd.inspect} authorized by #{auth.inspect}"
151         m = RemoteMessage.new(@bot, client, cmd)
152         @bot.remote_dispatcher.handle(m)
153       end
154     end
155
156     # The bot also manages a single (for the moment) remote dispatcher. This method
157     # makes it accessible to the outside world, creating it if necessary.
158     #
159     def remote_dispatcher
160       if defined? @remote_dispatcher
161         @remote_dispatcher
162       else
163         @remote_dispatcher = RemoteDispatcher.new(self)
164       end
165     end
166
167     # The bot also manages a single (for the moment) remote object. This method
168     # makes it accessible to the outside world, creating it if necessary.
169     #
170     def remote_object
171       if defined? @remote_object
172         @remote_object
173       else
174         @remote_object = RemoteObject.new(self)
175       end
176     end
177
178   end
179
180   module Plugins
181
182     # We create a new Ruby module that can be included by BotModules that want to
183     # provide remote interfaces
184     #
185     module RemoteBotModule
186
187       # The remote_map acts just like the BotModule#map method, except that
188       # the map is registered to the @bot's remote_dispatcher. Also, the remote map handle
189       # is handled for the cleanup management
190       #
191       def remote_map(*args)
192         @remote_maps = Array.new unless defined? @remote_maps
193         @remote_maps << @bot.remote_dispatcher.map(self, *args)
194       end
195
196       # Unregister the remote maps.
197       #
198       def remote_cleanup
199         return unless defined? @remote_maps
200         @remote_maps.each { |h|
201           @bot.remote_dispatcher.unmap(self, h)
202         }
203         @remote_maps.clear
204       end
205
206       # Redefine the default cleanup method.
207       #
208       def cleanup
209         super
210         remote_cleanup
211       end
212     end
213
214     # And just because I like consistency:
215     #
216     module RemoteCoreBotModule
217       include RemoteBotModule
218     end
219
220     module RemotePlugin
221       include RemoteBotModule
222     end
223
224   end
225
226 end
227
228 class RemoteModule < CoreBotModule
229
230   include RemoteCoreBotModule
231
232   BotConfig.register BotConfigBooleanValue.new('remote.autostart',
233     :default => true,
234     :requires_rescan => true,
235     :desc => "Whether the remote service provider should be started automatically")
236
237   BotConfig.register BotConfigIntegerValue.new('remote.port',
238     :default => 7268, # that's 'rbot'
239     :requires_rescan => true,
240     :desc => "Port on which the remote interface will be presented")
241
242   BotConfig.register BotConfigStringValue.new('remote.host',
243     :default => '',
244     :requires_rescan => true,
245     :desc => "Port on which the remote interface will be presented")
246
247   def initialize
248     super
249     @port = @bot.config['remote.port']
250     @host = @bot.config['remote.host']
251     @drb = nil
252     begin
253       start_service if @bot.config['remote.autostart']
254     rescue => e
255       error "couldn't start remote service provider: #{e.inspect}"
256     end
257   end
258
259   def start_service
260     raise "Remote service provider already running" if @drb
261     @drb = DRb.start_service("druby://#{@host}:#{@port}", @bot.remote_object)
262   end
263
264   def stop_service
265     @drb.stop_service if @drb
266     @drb = nil
267   end
268
269   def cleanup
270     stop_service
271     super
272   end
273
274   def handle_start(m, params)
275     if @drb
276       rep = "remote service provider already running"
277       rep << " on port #{@port}" if m.private?
278     else
279       begin
280         start_service(@port)
281         rep = "remote service provider started"
282         rep << " on port #{@port}" if m.private?
283       rescue
284         rep = "couldn't start remote service provider"
285       end
286     end
287     m.reply rep
288   end
289
290   def remote_test(m, params)
291     @bot.say params[:channel], "This is a remote test"
292   end
293
294 end
295
296 remote = RemoteModule.new
297
298 remote.map "remote start",
299   :action => 'handle_start',
300   :auth_path => ':manage:'
301
302 remote.map "remote stop",
303   :action => 'handle_stop',
304   :auth_path => ':manage:'
305
306 remote.remote_map "remote test :channel",
307   :action => 'remote_test'
308
309 remote.default_auth('*', false)