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