net-toc 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/net/toc.rb +570 -0
  2. metadata +38 -0
@@ -0,0 +1,570 @@
1
+ # A small library that connects to AOL Instant Messenger using the TOC v2.0 protocol.
2
+ #
3
+ # Author:: Ian Henderson (mailto:ian@ianhenderson.org)
4
+ # Copyright:: Copyright (c) 2006 Ian Henderson
5
+ # License:: revised BSD license (http://www.opensource.org/licenses/bsd-license.php)
6
+ # Version:: 0.2
7
+ #
8
+ # See Net::TOC for documentation.
9
+
10
+
11
+ require 'socket'
12
+
13
+ module Net
14
+ # == Overview
15
+ # === Opening a Connection
16
+ # Pass Net::Toc.new your screenname and password to create a new connection.
17
+ # It will return a Client object, which is used to communicate with the server.
18
+ #
19
+ # client = Net::TOC.new("screenname", "p455w0rd")
20
+ #
21
+ # To actually connect, use Client#connect.
22
+ #
23
+ # client.connect
24
+ #
25
+ # If your program uses an input loop (e.g., reading from stdin), you can start it here.
26
+ # Otherwise, you must use Client#wait to prevent the program from exiting immediately.
27
+ #
28
+ # client.wait
29
+ #
30
+ # === Opening a Connection - The Shortcut
31
+ # If your program only sends IMs in response to received IMs, you can save yourself some code.
32
+ # Net::TOC.new takes an optional block argument, to be called each time a message arrives (it is passed to Client#on_im).
33
+ # Client#connect and Client#wait are automatically called.
34
+ #
35
+ # Net::TOC.new("screenname", "p455w0rd") do | message, buddy |
36
+ # # handle the im
37
+ # end
38
+ #
39
+ # === Receiving Events
40
+ # Client supports two kinds of event handlers: Client#on_im and Client#on_error.
41
+ #
42
+ # The given block will be called every time the event occurs.
43
+ # client.on_im do | message, buddy |
44
+ # puts "#{buddy.screen_name}: #{message}"
45
+ # end
46
+ # client.on_error do | error |
47
+ # puts "!! #{error}"
48
+ # end
49
+ #
50
+ # You can also receive events using Buddy#on_status.
51
+ # Pass it any number of statuses (e.g., :away, :offline, :available, :idle) and a block;
52
+ # the block will be called each time the buddy's status changes to one of the statuses.
53
+ #
54
+ # friend = client.buddy_list.buddy_named("friend")
55
+ # friend.on_status(:available) do
56
+ # friend.send_im "Hi!"
57
+ # end
58
+ # friend.on_status(:idle, :away) do
59
+ # friend.send_im "Bye!"
60
+ # end
61
+ #
62
+ # === Sending IMs
63
+ # To send an instant message, call Buddy#send_im.
64
+ #
65
+ # friend.send_im "Hello, #{friend.screen_name}!"
66
+ #
67
+ # === Status Changes
68
+ # You can modify your state using these Client methods: Client#go_away, Client#come_back, and Client#idle_time=.
69
+ #
70
+ # client.go_away "Away"
71
+ # client.idle_time = 600 # ten minutes
72
+ # client.come_back
73
+ # client.idle_time = 0 # stop being idle
74
+ #
75
+ # It is not necessary to call Client#idle_time= continuously; the server will automatically keep track.
76
+ #
77
+ # == Examples
78
+ # === Simple Bot
79
+ # This bot lets you run ruby commands remotely, but only if your screenname is in the authorized list.
80
+ #
81
+ # require 'net/toc'
82
+ # authorized = ["admin_screenname"]
83
+ # Net::TOC.new("screenname", "p455w0rd") do | message, buddy |
84
+ # if authorized.member? buddy.screen_name
85
+ # begin
86
+ # result = eval(message.chomp.gsub(/<[^>]+>/,"")) # remove html formatting
87
+ # buddy.send_im result.to_s if result.respond_to? :to_s
88
+ # rescue Exception => e
89
+ # buddy.send_im "#{e.class}: #{e}"
90
+ # end
91
+ # end
92
+ # end
93
+ # === (Slightly) More Complicated and Contrived Bot
94
+ # If you message this bot when you're available, you get a greeting and the date you logged in.
95
+ # If you message it when you're away, you get scolded, and then pestered each time you become available.
96
+ #
97
+ # require 'net/toc'
98
+ # client = Net::TOC.new("screenname", "p455w0rd")
99
+ # client.on_error do | error |
100
+ # admin = client.buddy_list.buddy_named("admin_screenname")
101
+ # admin.send_im("Error: #{error}")
102
+ # end
103
+ # client.on_im do | message, buddy, auto_response |
104
+ # return if auto_response
105
+ # if buddy.available?
106
+ # buddy.send_im("Hello, #{buddy.screen_name}. You have been logged in since #{buddy.last_signon}.")
107
+ # else
108
+ # buddy.send_im("Liar!")
109
+ # buddy.on_status(:available) { buddy.send_im("Welcome back, liar.") }
110
+ # end
111
+ # end
112
+ # client.connect
113
+ # client.wait
114
+ # === Simple Interactive Client
115
+ # Use screenname<<message to send message.
116
+ # <<message sends message to the last buddy you messaged.
117
+ # When somebody sends you a message, it is displayed as screenname>>message.
118
+ #
119
+ # require 'net/toc'
120
+ # print "screen name: "
121
+ # screen_name = gets.chomp
122
+ # print "password: "
123
+ # password = gets.chomp
124
+ #
125
+ # client = Net::TOC.new(screen_name, password)
126
+ #
127
+ # client.on_im do | message, buddy |
128
+ # puts "#{buddy}>>#{message}"
129
+ # end
130
+ #
131
+ # client.connect
132
+ #
133
+ # puts "connected"
134
+ #
135
+ # last_buddy = ""
136
+ # loop do
137
+ # buddy_name, message = *gets.chomp.split("<<",2)
138
+ #
139
+ # buddy_name = last_buddy if buddy_name == ""
140
+ #
141
+ # unless buddy_name.nil? or message.nil?
142
+ # last_buddy = buddy_name
143
+ # client.buddy_list.buddy_named(buddy_name).send_im(message)
144
+ # end
145
+ # end
146
+ module TOC
147
+ class CommunicationError < RuntimeError # :nodoc:
148
+ end
149
+
150
+ # Converts a screen name into its canonical form - lowercase, with no spaces.
151
+ def format_screen_name(screen_name)
152
+ screen_name.downcase.gsub(/\s+/, '')
153
+ end
154
+
155
+ # Escapes a message so it doesn't confuse the server. You should never have to call this directly.
156
+ def format_message(message) # :nodoc:
157
+ msg = message.gsub(/(\r|\n|\r\n)/, '<br>')
158
+ msg.gsub(/[{}\\"]/, "\\\\\\0") # oh dear
159
+ end
160
+
161
+ # Creates a new Client. See the Client.new method for details.
162
+ def self.new(screen_name, password, &optional_block) # :yields: message, buddy, auto_response, client
163
+ Client.new(screen_name, password, &optional_block)
164
+ end
165
+
166
+ Debug = false # :nodoc:
167
+
168
+ ErrorCode = {
169
+ 901 => "<param> is not available.",
170
+ 902 => "Warning <param> is not allowed.",
171
+ 903 => "Message dropped; you are exceeding the server speed limit",
172
+ 980 => "Incorrect screen name or password.",
173
+ 981 => "The service is temporarily unavailable.",
174
+ 982 => "Your warning level is too high to sign on.",
175
+ 983 => "You have been connecting and disconnecting too frequently. Wait 10 minutes and try again.",
176
+ 989 => "An unknown error has occurred in the signon process."
177
+ }
178
+
179
+ # The Connection class handles low-level communication using the TOC protocol. You shouldn't use it directly.
180
+ class Connection # :nodoc:
181
+ include TOC
182
+
183
+ def initialize(screen_name)
184
+ @user = format_screen_name screen_name
185
+ @msgseq = rand(100000)
186
+ end
187
+
188
+ def open(server="toc.oscar.aol.com", port=9898)
189
+ close
190
+ @sock = TCPSocket.new(server, port)
191
+
192
+ @sock.send "FLAPON\r\n\r\n", 0
193
+
194
+ toc_version = *recv.unpack("N")
195
+
196
+ send [1, 1, @user.length, @user].pack("Nnna*"), :sign_on
197
+ end
198
+
199
+ def close
200
+ @sock.close unless @sock.nil?
201
+ end
202
+
203
+ FrameType = {
204
+ :sign_on => 1,
205
+ :data => 2
206
+ }
207
+
208
+ def send(message, type=:data)
209
+ message << "\0"
210
+ puts " send: #{message}" if Debug
211
+ @msgseq = @msgseq.next
212
+ header = ['*', FrameType[type], @msgseq, message.length].pack("aCnn")
213
+ packet = header + message
214
+ @sock.send packet, 0
215
+ end
216
+
217
+ def recv
218
+ header = @sock.recv 6
219
+ raise CommunicationError, "Server didn't send full header." if header.length < 6
220
+
221
+ asterisk, type, serverseq, length = header.unpack "aCnn"
222
+
223
+ response = @sock.recv length
224
+ puts " recv: #{response}" if Debug
225
+ unless type == FrameType[:sign_on]
226
+ message, value = response.split(":", 2)
227
+ unless message.nil? or value.nil?
228
+ msg_sym = message.downcase.to_sym
229
+ yield msg_sym, value if block_given?
230
+ end
231
+ end
232
+ response
233
+ end
234
+
235
+ private
236
+
237
+ # Any unknown methods are assumed to be messages for the server.
238
+ def method_missing(command, *args)
239
+ send(([command] + args).join(" "))
240
+ end
241
+ end
242
+
243
+ class Buddy
244
+ include TOC
245
+ include Comparable
246
+
247
+ attr_reader :screen_name, :status, :warning_level, :last_signon, :idle_time
248
+
249
+ def initialize(screen_name, conn) # :nodoc:
250
+ @screen_name = screen_name
251
+ @conn = conn
252
+ @status = :offline
253
+ @warning_level = 0
254
+ @on_status = {}
255
+ @last_signon = :never
256
+ @idle_time = 0
257
+ end
258
+
259
+ def <=>(other) # :nodoc:
260
+ format_screen_name(@screen_name) <=> format_screen_name(other.screen_name)
261
+ end
262
+
263
+ # Pass a block to be called when status changes to any of +statuses+. This replaces any previously set on_status block for these statuses.
264
+ def on_status(*statuses, &callback) #:yields:
265
+ statuses.each { | status | @on_status[status] = callback }
266
+ end
267
+
268
+ # Returns +true+ unless status == :offline.
269
+ def online?
270
+ status != :offline
271
+ end
272
+
273
+ # Returns +true+ if status == :available.
274
+ def available?
275
+ status == :available
276
+ end
277
+
278
+ # Returns +true+ if status == :away.
279
+ def away?
280
+ status == :away
281
+ end
282
+
283
+ # Returns +true+ if buddy is idle.
284
+ def idle?
285
+ @idle_time > 0
286
+ end
287
+
288
+ # Sends the instant message +message+ to the buddy. If +auto_response+ is true, the message is marked as an automated response.
289
+ def send_im(message, auto_response=false)
290
+ args = [format_screen_name(@screen_name), "\"" + format_message(message) + "\""]
291
+ args << "auto" if auto_response
292
+ @conn.toc_send_im *args
293
+ end
294
+
295
+ # Warns the buddy. If the argument is :anonymous, the buddy is warned anonymously. Otherwise, your name is sent with the warning.
296
+ # You may only warn buddies who have recently IMed you.
297
+ def warn(anon=:named)
298
+ @conn.toc_evil(format_screen_name(@screen_name), anon == :anonymous ? "anon" : "norm")
299
+ end
300
+
301
+ # The string representation of a buddy; equivalent to Buddy#screen_name.
302
+ def to_s
303
+ screen_name
304
+ end
305
+
306
+ def raw_update(val) # :nodoc:
307
+ # TODO: Support user types properly.
308
+ name, online, warning, signon_time, idle, user_type = *val.split(":")
309
+ @warning_level = warning.to_i
310
+ @last_signon = Time.at(signon_time.to_i)
311
+ @idle_time = idle.to_i
312
+ if online == "F"
313
+ update_status :offline
314
+ elsif user_type[2...3] and user_type[2...3] == "U"
315
+ update_status :away
316
+ elsif @idle_time > 0
317
+ update_status :idle
318
+ else
319
+ update_status :available
320
+ end
321
+ end
322
+
323
+ private
324
+
325
+ def update_status(status)
326
+ if @on_status[status] and status != @status
327
+ @status = status
328
+ @on_status[status].call
329
+ else
330
+ @status = status
331
+ end
332
+ end
333
+ end
334
+
335
+ # Manages groups and buddies. Don't create one yourself - get one using Client#buddy_list.
336
+ class BuddyList
337
+ include TOC
338
+
339
+ def initialize(conn) # :nodoc:
340
+ @conn = conn
341
+ @buddies = {}
342
+ @groups = {}
343
+ @group_order = []
344
+ end
345
+
346
+ # Constructs a printable string representation of the buddy list.
347
+ def to_s
348
+ s = ""
349
+ each_group do | group, buddies |
350
+ s << "== #{group} ==\n"
351
+ buddies.each do | buddy |
352
+ s << " * #{buddy}\n"
353
+ end
354
+ end
355
+ s
356
+ end
357
+
358
+ # Calls the passed block once for each group, passing the group name and the list of buddies as parameters.
359
+ def each_group
360
+ @group_order.each do | group |
361
+ buddies = @groups[group]
362
+ yield group, buddies
363
+ end
364
+ end
365
+
366
+ # Adds a new group named +group_name+.
367
+ # Setting +sync+ to :dont_sync will prevent this change from being sent to the server.
368
+ def add_group(group_name, sync=:sync)
369
+ if @groups[group_name].nil?
370
+ @groups[group_name] = []
371
+ @group_order << group_name
372
+ @conn.toc2_new_group group_name if sync == :sync
373
+ end
374
+ end
375
+
376
+ # Adds the buddy named +buddy_name+ to the group named +group+. If this group does not exist, it is created.
377
+ # Setting +sync+ to :dont_sync will prevent this change from being sent to the server.
378
+ def add_buddy(group, buddy_name, sync=:sync)
379
+ add_group(group, sync) if @groups[group].nil?
380
+ @groups[group] << buddy_named(buddy_name)
381
+ @conn.toc2_new_buddies("{g:#{group}\nb:#{format_screen_name(buddy_name)}\n}") if sync == :sync
382
+ end
383
+
384
+ # Removes the buddy named +buddy_name+ from the group named +group+.
385
+ # Setting +sync+ to :dont_sync will prevent this change from being sent to the server.
386
+ def remove_buddy(group, buddy_name, sync=:sync)
387
+ unless @groups[group].nil?
388
+ buddy = buddy_named(buddy_name)
389
+ @groups[group].reject! { | b | b == buddy }
390
+ @conn.toc2_remove_buddy(format_screen_name(buddy_name), group) if sync == :sync
391
+ end
392
+ end
393
+
394
+ # Returns the buddy named +name+. If the buddy does not exist, it is created. +name+ is not case- or whitespace-sensitive.
395
+ def buddy_named(name)
396
+ formatted_name = format_screen_name(name)
397
+ buddy = @buddies[formatted_name]
398
+ if buddy.nil?
399
+ buddy = Buddy.new(name, @conn)
400
+ @buddies[formatted_name] = buddy
401
+ end
402
+ buddy
403
+ end
404
+
405
+ # Decodes the buddy list from raw CONFIG data.
406
+ def decode_toc(val) # :nodoc:
407
+ current_group = nil
408
+ val.each_line do | line |
409
+ letter, name = *line.split(":")
410
+ name = name.chomp
411
+ case letter
412
+ when "g"
413
+ add_group(name, :dont_sync)
414
+ current_group = name
415
+ when "b"
416
+ add_buddy(current_group, name, :dont_sync)
417
+ end
418
+ end
419
+ end
420
+ end
421
+
422
+ # A high-level interface to TOC. It supports asynchronous message handling through the use of threads, and maintains a list of buddies.
423
+ class Client
424
+ include TOC
425
+
426
+ attr_reader :buddy_list, :screen_name
427
+
428
+ # You must initialize the client with your screen name and password.
429
+ # If a block is given, Client#listen will be invoked with the block after initialization.
430
+ def initialize(screen_name, password, &optional_block) # :yields: message, buddy, auto_response, client
431
+ @conn = Connection.new(screen_name)
432
+ @screen_name = format_screen_name(screen_name)
433
+ @password = password
434
+ @callbacks = {}
435
+ @buddy_list = BuddyList.new(@conn)
436
+ add_callback(:config, :config2) { |v| @buddy_list.decode_toc v }
437
+ add_callback(:update_buddy, :update_buddy2) { |v| update_buddy v }
438
+ on_error do | error |
439
+ $stderr.puts "Error: #{error}"
440
+ end
441
+ listen(&optional_block) if block_given?
442
+ end
443
+
444
+ # Connects to the server and starts an event-handling thread.
445
+ def connect(server="toc.oscar.aol.com", port=9898, oscar_server="login.oscar.aol.com", oscar_port=5190)
446
+ @conn.open(server, port)
447
+ code = 7696 * @screen_name[0] * @password[0]
448
+ @conn.toc2_signon(oscar_server, oscar_port, @screen_name, roasted_pass, "english", "\"TIC:toc.rb\"", 160, code)
449
+
450
+ @conn.recv do |msg, val|
451
+ if msg == :sign_on
452
+ @conn.toc_add_buddy(@screen_name)
453
+ @conn.toc_init_done
454
+ capabilities.each do |capability|
455
+ @conn.toc_set_caps(capability)
456
+ end
457
+ end
458
+ end
459
+ @thread.kill unless @thread.nil? # ha
460
+ @thread = Thread.new { loop { event_loop } }
461
+ end
462
+
463
+ # Disconnects and kills the event-handling thread. You may still add callbacks while disconnected.
464
+ def disconnect
465
+ @thread.kill unless @thread.nil?
466
+ @thread = nil
467
+ @conn.close
468
+ end
469
+
470
+ # Connects to the server and forwards received IMs to the given block. See Client#connect for the arguments.
471
+ def listen(*args) # :yields: message, buddy, auto_response, client
472
+ on_im do | message, buddy, auto_response |
473
+ yield message, buddy, auto_response, self
474
+ end
475
+ connect(*args)
476
+ wait
477
+ end
478
+
479
+ # Pass a block to be called every time an IM is received. This will replace any previous on_im handler.
480
+ def on_im
481
+ raise ArgumentException, "on_im requires a block argument" unless block_given?
482
+ add_callback(:im_in, :im_in2) do |val|
483
+ screen_name, auto, f2, *message = *val.split(":")
484
+ message = message.join(":")
485
+ buddy = @buddy_list.buddy_named(screen_name)
486
+ auto_response = auto == "T"
487
+ yield message, buddy, auto_response
488
+ end
489
+ end
490
+
491
+ # Pass a block to be called every time an error occurs. This will replace any previous on_error handler, including the default exception-raising behavior.
492
+ def on_error
493
+ raise ArgumentException, "on_error requires a block argument" unless block_given?
494
+ add_callback(:error) do |val|
495
+ code, param = *val.split(":")
496
+ error = ErrorCode[code.to_i]
497
+ error = "An unknown error occurred." if error.nil?
498
+ error.gsub!("<param>", param) unless param.nil?
499
+ yield error
500
+ end
501
+ end
502
+
503
+ # Sets your status to away and +away_message+ as your away message.
504
+ def go_away(away_message)
505
+ @conn.toc_set_away "\"#{away_message.gsub("\"","\\\"")}\""
506
+ end
507
+
508
+ # Sets your status to available.
509
+ def come_back
510
+ @conn.toc_set_away
511
+ end
512
+
513
+ # Sets your idle time in seconds. You only need to set this once; afterwards, the server will keep track itself.
514
+ # Set to 0 to stop being idle.
515
+ def idle_time=(seconds)
516
+ @conn.toc_set_idle seconds
517
+ end
518
+
519
+ # Waits for the event-handling thread for +limit+ seconds, or indefinitely if no argument is given. Use this to prevent your program from exiting prematurely.
520
+ # For example, the following script will exit right after connecting:
521
+ # client = Net::TOC.new("screenname", "p455w0rd")
522
+ # client.connect
523
+ # To prevent this, use wait:
524
+ # client = Net::TOC.new("screenname", "p455w0rd")
525
+ # client.connect
526
+ # client.wait
527
+ # Now the program will wait until the client has disconnected before exiting.
528
+ def wait(limit=nil)
529
+ @thread.join limit
530
+ end
531
+
532
+ # Returns a list of this client's capabilities. Not yet implemented.
533
+ def capabilities
534
+ [] # TODO
535
+ end
536
+
537
+ private
538
+
539
+ # Returns an "encrypted" version of the password to be sent across the internet.
540
+ # Decrypting it is trivial, though.
541
+ def roasted_pass
542
+ tictoc = "Tic/Toc".unpack "c*"
543
+ pass = @password.unpack "c*"
544
+ roasted = "0x"
545
+ pass.each_index do |i|
546
+ roasted << sprintf("%02x", pass[i] ^ tictoc[i % tictoc.length])
547
+ end
548
+ roasted
549
+ end
550
+
551
+ def update_buddy(val)
552
+ screen_name = val.split(":").first.chomp
553
+ buddy = @buddy_list.buddy_named(screen_name)
554
+ buddy.raw_update(val)
555
+ end
556
+
557
+ def add_callback(*callbacks, &block)
558
+ callbacks.each do |callback|
559
+ @callbacks[callback] = block;
560
+ end
561
+ end
562
+
563
+ def event_loop
564
+ @conn.recv do |msg, val|
565
+ @callbacks[msg].call(val) unless @callbacks[msg].nil?
566
+ end
567
+ end
568
+ end
569
+ end
570
+ end
metadata ADDED
@@ -0,0 +1,38 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.10
3
+ specification_version: 1
4
+ name: net-toc
5
+ version: !ruby/object:Gem::Version
6
+ version: "0.2"
7
+ date: 2006-08-02
8
+ summary: "A ruby library which uses the TOC protocol to connect to AOL's instant messaging
9
+ network."
10
+ require_paths:
11
+ - "."
12
+ email: ian@ianhenderson.org
13
+ homepage: http://net-toc.rubyforge.org
14
+ rubyforge_project: net-toc
15
+ description:
16
+ autorequire:
17
+ default_executable:
18
+ bindir: bin
19
+ has_rdoc: true
20
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
21
+ requirements:
22
+ -
23
+ - ">"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.0.0
26
+ version:
27
+ platform: ruby
28
+ authors:
29
+ - Ian Henderson
30
+ files:
31
+ - net/toc.rb
32
+ test_files: []
33
+ rdoc_options: []
34
+ extra_rdoc_files: []
35
+ executables: []
36
+ extensions: []
37
+ requirements: []
38
+ dependencies: []