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 client ID and auth
19 # A RemoteCommand is similar to a BaiscUserMessage
25 # when the message was received
28 # remote client that originated the message
31 # contents of the message
32 attr_accessor :message
34 def initialize(bot, source, message)
41 # The target of a RemoteMessage
46 # Remote messages are always 'private'
52 class RemoteDispatcher < MessageMapper
58 # The map method for the RemoteDispatcher returns the index of the inserted
61 def map(botmodule, *args)
63 return @templates.length - 1
66 # The map method for the RemoteDispatcher returns the index of the inserted
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
77 # We redefine the handle() method from MessageMapper, taking into account
78 # that @parent is a bot, and that we don't handle fallbacks
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
85 return false if @templates.empty?
87 @templates.each do |tmpl|
88 # Skip this element if it was unmapped
90 botmodule = @parent.plugins[tmpl.botmodule]
91 options, failure = tmpl.recognize(m)
93 failures << [tmpl, failure]
95 action = tmpl.options[:action]
96 unless botmodule.respond_to?(action)
97 failures << [tmpl, "#{botmodule} does not respond to action #{action}"]
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)
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
114 failures.each {|f, r|
115 debug "#{f.inspect} => #{r}"
117 debug "no handler found"
125 # The Irc::IrcBot::RemoteObject class represents and object that will take care
126 # of interfacing with remote clients
130 # We don't want this object to be copied clientside, so we make it undumpable
133 # Initialization is simple
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
143 def delegate(auth, *pars)
144 warn "Ignoring extra parameters" if pars.length > 1
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
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)
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.
159 def remote_dispatcher
160 if defined? @remote_dispatcher
163 @remote_dispatcher = RemoteDispatcher.new(self)
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.
171 if defined? @remote_object
174 @remote_object = RemoteObject.new(self)
182 # We create a new Ruby module that can be included by BotModules that want to
183 # provide remote interfaces
185 module RemoteBotModule
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
191 def remote_map(*args)
192 @remote_maps = Array.new unless defined? @remote_maps
193 @remote_maps << @bot.remote_dispatcher.map(self, *args)
196 # Unregister the remote maps.
199 return unless defined? @remote_maps
200 @remote_maps.each { |h|
201 @bot.remote_dispatcher.unmap(self, h)
206 # Redefine the default cleanup method.
214 # And just because I like consistency:
216 module RemoteCoreBotModule
217 include RemoteBotModule
221 include RemoteBotModule
228 class RemoteModule < CoreBotModule
230 include RemoteCoreBotModule
232 BotConfig.register BotConfigBooleanValue.new('remote.autostart',
234 :requires_rescan => true,
235 :desc => "Whether the remote service provider should be started automatically")
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")
242 BotConfig.register BotConfigStringValue.new('remote.host',
244 :requires_rescan => true,
245 :desc => "Port on which the remote interface will be presented")
249 @port = @bot.config['remote.port']
250 @host = @bot.config['remote.host']
253 start_service if @bot.config['remote.autostart']
255 error "couldn't start remote service provider: #{e.inspect}"
260 raise "Remote service provider already running" if @drb
261 @drb = DRb.start_service("druby://#{@host}:#{@port}", @bot.remote_object)
265 @drb.stop_service if @drb
274 def handle_start(m, params)
276 rep = "remote service provider already running"
277 rep << " on port #{@port}" if m.private?
281 rep = "remote service provider started"
282 rep << " on port #{@port}" if m.private?
284 rep = "couldn't start remote service provider"
290 def remote_test(m, params)
291 @bot.say params[:channel], "This is a remote test"
296 remote = RemoteModule.new
298 remote.map "remote start",
299 :action => 'handle_start',
300 :auth_path => ':manage:'
302 remote.map "remote stop",
303 :action => 'handle_stop',
304 :auth_path => ':manage:'
306 remote.remote_map "remote test :channel",
307 :action => 'remote_test'
309 remote.default_auth('*', false)