net-toc 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []