]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/core/remote.rb
Initial work on a DRb-based remote service for rbot. Thanks to halorgium for the...
[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 # TODO Irc::Plugins::RemotePlugin module to be included by plugins that want to
14 # provide a remote interface. Such module would define a remote_map() method
15 # that would register the plugin to received mapped commands from remote clients.
16 # FIXME how should be handle cleanups/rescans? Probably just clear() the
17 # RemoteDispatcher template list. Provide a cleanup() method for
18 # RemoteDispatcher and think about this.
19
20 require 'drb/drb'
21
22 module ::Irc
23
24   # A RemoteCommand is similar to a BaiscUserMessage
25   #
26   class RemoteMessage
27     # associated bot
28     attr_reader :bot
29
30     # when the message was received
31     attr_reader :time
32
33     # remote client that originated the message
34     attr_reader :source
35
36     # contents of the message
37     attr_accessor :message
38
39     def initialize(bot, source, message)
40       @bot = bot
41       @source = source
42       @message = message
43       @time = Time.now
44     end
45
46     # The target of a RemoteMessage
47     def target
48       @bot
49     end
50
51     # Remote messages are always 'private'
52     def private?
53       true
54     end
55   end
56
57   class RemoteDispatcher < MessageMapper
58
59     def initialize(bot)
60       super(bot)
61     end
62
63     # We redefine the handle() method from MessageMapper, taking into account
64     # that @parent is a bot, and that we don't handle fallbacks
65     #
66     # TODO this same kind of mechanism could actually be used in MessageMapper
67     # itself to be able to handle the case of multiple plugins having the same
68     # 'first word' ...
69     #
70     def handle(m)
71       return false if @templates.empty?
72       failures = []
73       @templates.each do |tmpl|
74         botmodule = @parent.plugins[tmpl.botmodule]
75         options, failure = tmpl.recognize(m)
76         if options.nil?
77           failures << [tmpl, failure]
78         else
79           action = tmpl.options[:action]
80           unless botmodule.respond_to?(action)
81             failures << [tmpl, "#{botmodule} does not respond to action #{action}"]
82             next
83           end
84           auth = tmpl.options[:full_auth_path]
85           debug "checking auth for #{auth}"
86           if m.bot.auth.allow?(auth, m.source, m.replyto)
87             debug "template match found and auth'd: #{action.inspect} #{options.inspect}"
88             @parent.send(action, m, options)
89             return true
90           end
91           debug "auth failed for #{auth}"
92           # if it's just an auth failure but otherwise the match is good,
93           # don't try any more handlers
94           return false
95         end
96       end
97       failures.each {|f, r|
98         debug "#{f.inspect} => #{r}"
99       }
100       debug "no handler found"
101       return false
102     end
103
104   end
105
106   class IrcBot
107
108     # The Irc::IrcBot::RemoteObject class represents and object that will take care
109     # of interfacing with remote clients
110     #
111     class RemoteObject
112
113       # We don't want this object to be copied clientside, so we make it undumpable
114       include DRbUndumped
115
116       # Initialization is simple
117       def initialize(bot)
118         @bot = bot
119         @dispatcher = RemoteDispatcher.new(@bot)
120       end
121
122       # The delegate method. This is the main method used by remote clients to send
123       # commands to the bot. Most of the time, the method will be called with only
124       # two parameters (authorization code and a String), but we allow more parameters
125       # for future expansions
126       #
127       def delegate(auth, *pars)
128         warn "Ignoring extra parameters" if pars.length > 1
129         cmd = pars.first
130         # TODO implement auth <-> client conversion
131         # We need at least a RemoteBotUser class derived from Irc::Auth::BotUser
132         # and a way to associate the auth info to the appropriate RemoteBotUser class
133         client = auth
134         debug "Trying to dispatch command #{cmd.inspect} authorized by #{auth.inspect}"
135         m = RemoteMessage.new(@bot, client, cmd)
136         @dispatcher.handle(m)
137       end
138     end
139
140     # The bot also manages a single (for the moment) remote object. This method
141     # makes it accessible to the outside world, creating it if necessary.
142     #
143     def remote_object
144       if defined? @remote_object
145         @remote_object
146       else
147         @remote_object = RemoteObject.new(self)
148       end
149     end
150
151   end
152
153 end
154
155 class RemoteModule < CoreBotModule
156
157   BotConfig.register BotConfigIntegerValue.new('remote.port',
158     :default => 7268, # that's 'rbot'
159     :on_change => Proc.new { |bot, v|
160       stop_service
161       @port = v
162       start_service
163     },
164     :requires_restart => true,
165     :desc => "Port on which the remote interface will be presented")
166
167   BotConfig.register BotConfigStringValue.new('remote.host',
168     :default => '',
169     :on_change => Proc.new { |bot, v|
170       stop_service
171       @host = v
172       start_service
173     },
174     :requires_restart => true,
175     :desc => "Port on which the remote interface will be presented")
176
177   def initialize
178     super
179     @port = @bot.config['remote.port']
180     @host = @bot.config['remote.host']
181     @drb = nil
182     start_service
183   end
184
185   def start_service
186     raise "Remote service provider already running" if @drb
187     @drb = DRb.start_service("druby://#{@host}:#{@port}", @bot.remote_object)
188   end
189
190   def stop_service
191     @drb.stop_service if @drb
192     @drb = nil
193   end
194
195   def cleanup
196     stop_service
197     super
198   end
199
200   def handle_start(m, params)
201     if @drb
202       rep = "remote service provider already running"
203       rep << " on port #{@port}" if m.private?
204     else
205       begin
206         start_service(@port)
207         rep = "remote service provider started"
208         rep << " on port #{@port}" if m.private?
209       rescue
210         rep = "couldn't start remote service provider"
211       end
212     end
213     m.reply rep
214   end
215
216 end
217
218 remote = RemoteModule.new
219
220 remote.map "remote start",
221   :action => 'handle_start',
222   :auth_path => ':manage:'
223
224 remote.map "remote stop",
225   :action => 'handle_stop',
226   :auth_path => ':manage:'
227
228 remote.default_auth('*', false)