aim 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/README.rdoc +38 -0
  2. data/Rakefile +57 -0
  3. data/VERSION.yml +4 -0
  4. data/lib/aim.rb +130 -0
  5. data/lib/aim/net_toc.rb +629 -0
  6. metadata +67 -0
@@ -0,0 +1,38 @@
1
+ = AIM
2
+
3
+ AIM attempts to simplify the creation of AIM bots or do things log log all IMs to an AIM chat room, etc.
4
+
5
+ == Install
6
+
7
+ sudo gem install remi-aim
8
+
9
+ == Currently working Usage
10
+
11
+ require 'rubygems'
12
+ require 'aim'
13
+
14
+ AIM.login_as_user( 'screenname', 'password' ) do
15
+
16
+ im_user 'some user to IM', "hello! the time is #{ Time.now }"
17
+
18
+ log_chatroom 'the name of some chat room to log into', :output => 'file-to-save-chatroom-logs-to'
19
+
20
+ end
21
+
22
+ == NOTES
23
+
24
+ this app makes heavy use of Net::TOC, which I believe was
25
+ originally created by Ian Henderson: http://rubyforge.org/users/ianh/
26
+
27
+ http://rubyforge.org/projects/net-toc/ is released under
28
+ the BSD License: http://www.debian.org/misc/bsd.license
29
+
30
+ I am mentioning the name of the author to give him credit.
31
+
32
+ Per the BSD License, I will NOT use the author's name to
33
+ endorse or promote this product!
34
+
35
+ == TODO
36
+
37
+ 1. get a purty DSL working (using net/toc in the background)
38
+ 2. refactor net/toc --> AIM
@@ -0,0 +1,57 @@
1
+ require 'rake'
2
+ require 'rubygems'
3
+ require 'rake/rdoctask'
4
+ require 'spec/rake/spectask'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |s|
9
+ s.name = "aim"
10
+ s.summary = "Ruby gem for making AIM bots really easy to create"
11
+ s.email = "remi@remitaylor.com"
12
+ s.homepage = "http://github.com/remi/aim"
13
+ s.description = "Ruby gem for making AIM bots really easy to create"
14
+ s.authors = %w( remi )
15
+ s.files = FileList["[A-Z]*", "{lib,spec,examples,rails_generators}/**/*"]
16
+ # s.executables = "neato"
17
+ # s.add_dependency 'person-project'
18
+ end
19
+ rescue LoadError
20
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
21
+ end
22
+
23
+ Spec::Rake::SpecTask.new do |t|
24
+ t.spec_files = FileList['spec/**/*_spec.rb']
25
+ end
26
+
27
+ desc "Run all examples with RCov"
28
+ Spec::Rake::SpecTask.new('rcov') do |t|
29
+ t.spec_files = FileList['spec/**/*_spec.rb']
30
+ t.rcov = true
31
+ end
32
+
33
+ Rake::RDocTask.new do |rdoc|
34
+ rdoc.rdoc_dir = 'rdoc'
35
+ rdoc.title = 'aim'
36
+ rdoc.options << '--line-numbers' << '--inline-source'
37
+ rdoc.rdoc_files.include('README.rdoc')
38
+ rdoc.rdoc_files.include('lib/**/*.rb')
39
+ end
40
+
41
+ desc 'Confirm that gemspec is $SAFE'
42
+ task :safe do
43
+ require 'yaml'
44
+ require 'rubygems/specification'
45
+ data = File.read('aim.gemspec')
46
+ spec = nil
47
+ if data !~ %r{!ruby/object:Gem::Specification}
48
+ Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
49
+ else
50
+ spec = YAML.load(data)
51
+ end
52
+ spec.validate
53
+ puts spec
54
+ puts "OK"
55
+ end
56
+
57
+ task :default => :spec
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 0
4
+ :minor: 1
@@ -0,0 +1,130 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'rubygems'
4
+ require 'aim/net_toc' # <--- using in the background for now
5
+ # but i'll be refacting it into AIM
6
+
7
+ # TODO extract classes to separate files!
8
+
9
+ # Easily connect to AIM!
10
+ class AIM
11
+
12
+ class << self
13
+
14
+ #
15
+ # AIM.login_as_user( 'username', 'password' ) do
16
+ #
17
+ # im_user :bob, 'hello bob!'
18
+ #
19
+ # log_chatroom :some_chatroom, :output => 'some_chatroom.log'
20
+ #
21
+ # when :im do |im|
22
+ # im_user im.user, "thanks for the IM, #{ im.user.name }"
23
+ # end
24
+ #
25
+ # end
26
+ #
27
+ # when a block is passed, we'll call everything passed in
28
+ # on the AIM::User created by #login_as_user, and we'll user.wait!
29
+ #
30
+ # without a block, we just return the AIM::User and you
31
+ # have to manually call user.wait! (which actually just waits)
32
+ #
33
+ # this auto logs in too
34
+ #
35
+ def login_as_user username, password, &block
36
+ @block = block if block
37
+ @user = get_user username, password
38
+ @user.login!
39
+
40
+ reload! # take the @user, clear all of the user's event subscriptions, and reload the block
41
+
42
+ @user.wait!
43
+ @user
44
+ end
45
+
46
+ def reload!
47
+ @user.clear_events!
48
+ @user.instance_eval &@block
49
+ end
50
+
51
+ # returns an AIM::User, but does not login
52
+ def get_user username, password
53
+ AIM::User.new username, password
54
+ end
55
+
56
+ end
57
+
58
+ # represents an AIM User, initialized with a username and password
59
+ class User
60
+ attr_accessor :username, :password
61
+
62
+ def initialize username, password
63
+ @username, @password = username, password
64
+ end
65
+
66
+ def login!
67
+ connection.connect
68
+ end
69
+
70
+ # undoes all event subscriptions
71
+ def clear_events!
72
+ connection.clear_callbacks!
73
+ end
74
+
75
+ # do something on an event
76
+ #
77
+ # user.when :im do |message, buddy|
78
+ # puts "message '#{message}' received from #{ buddy }"
79
+ # end
80
+ #
81
+ # user.when :error do |error|
82
+ # ...
83
+ # end
84
+ #
85
+ # user.when :chat do |message, buddy, room|
86
+ # ...
87
+ # end
88
+ #
89
+ def when event_name, &block
90
+ connection.send "on_#{ event_name }", &block
91
+ end
92
+
93
+ # user.im_user 'bob', "hi bob!"
94
+ def im_user screenname, message
95
+ connection.buddy_list.buddy_named(screenname).send_im(message)
96
+ end
97
+
98
+ # helper, will save all messages in a chatroom to a file, or in memory, or wherever
99
+ #
100
+ # right now, usage is simply:
101
+ #
102
+ # user.log_chatroom 'roomname', :output => 'filename.log'
103
+ #
104
+ def log_chatroom room_name, options = { }
105
+ options[:output] ||= "#{ room_name }.log"
106
+ self.when :chat do |message, buddy, room|
107
+ File.open(options[:output], 'a'){|f| f << "#{ buddy.screen_name }: #{ message }\n" }
108
+ end
109
+
110
+ join_chatroom room_name
111
+ end
112
+
113
+ # tell the user to just chill! hang out and wait for events
114
+ def wait!
115
+ connection.wait
116
+ end
117
+
118
+ def join_chatroom room_name
119
+ connection.join_chat room_name
120
+ end
121
+
122
+ private
123
+
124
+ # the Net::TOC backend
125
+ def connection
126
+ @connection ||= Net::TOC.new @username, @password
127
+ end
128
+ end
129
+
130
+ end
@@ -0,0 +1,629 @@
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
+ require 'socket'
11
+
12
+ module Net
13
+
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
+ puts ([command] + args).join(" ").inspect
240
+ send(([command] + args).join(" "))
241
+ end
242
+ end
243
+
244
+ class Buddy
245
+ include TOC
246
+ include Comparable
247
+
248
+ attr_reader :screen_name, :status, :warning_level, :last_signon, :idle_time
249
+
250
+ def initialize(screen_name, conn) # :nodoc:
251
+ @screen_name = screen_name
252
+ @conn = conn
253
+ @status = :offline
254
+ @warning_level = 0
255
+ @on_status = {}
256
+ @last_signon = :never
257
+ @idle_time = 0
258
+ end
259
+
260
+ def <=>(other) # :nodoc:
261
+ format_screen_name(@screen_name) <=> format_screen_name(other.screen_name)
262
+ end
263
+
264
+ # Pass a block to be called when status changes to any of +statuses+. This replaces any previously set on_status block for these statuses.
265
+ def on_status(*statuses, &callback) #:yields:
266
+ statuses.each { | status | @on_status[status] = callback }
267
+ end
268
+
269
+ # Returns +true+ unless status == :offline.
270
+ def online?
271
+ status != :offline
272
+ end
273
+
274
+ # Returns +true+ if status == :available.
275
+ def available?
276
+ status == :available
277
+ end
278
+
279
+ # Returns +true+ if status == :away.
280
+ def away?
281
+ status == :away
282
+ end
283
+
284
+ # Returns +true+ if buddy is idle.
285
+ def idle?
286
+ @idle_time > 0
287
+ end
288
+
289
+ # Sends the instant message +message+ to the buddy. If +auto_response+ is true, the message is marked as an automated response.
290
+ def send_im(message, auto_response=false)
291
+ puts "send_im: #{ message }" # remi
292
+ args = [format_screen_name(@screen_name), "\"" + format_message(message) + "\""]
293
+ args << "auto" if auto_response
294
+ puts "@conn.toc_send_im #{args.inspect}" # remi
295
+ @conn.toc_send_im *args
296
+ end
297
+
298
+ # Warns the buddy. If the argument is :anonymous, the buddy is warned anonymously. Otherwise, your name is sent with the warning.
299
+ # You may only warn buddies who have recently IMed you.
300
+ def warn(anon=:named)
301
+ @conn.toc_evil(format_screen_name(@screen_name), anon == :anonymous ? "anon" : "norm")
302
+ end
303
+
304
+ # The string representation of a buddy; equivalent to Buddy#screen_name.
305
+ def to_s
306
+ screen_name
307
+ end
308
+
309
+ def raw_update(val) # :nodoc:
310
+ # TODO: Support user types properly.
311
+ name, online, warning, signon_time, idle, user_type = *val.split(":")
312
+ @warning_level = warning.to_i
313
+ @last_signon = Time.at(signon_time.to_i)
314
+ @idle_time = idle.to_i
315
+ if online == "F"
316
+ update_status :offline
317
+ elsif user_type[2...3] and user_type[2...3] == "U"
318
+ update_status :away
319
+ elsif @idle_time > 0
320
+ update_status :idle
321
+ else
322
+ update_status :available
323
+ end
324
+ end
325
+
326
+ private
327
+
328
+ def update_status(status)
329
+ if @on_status[status] and status != @status
330
+ @status = status
331
+ @on_status[status].call
332
+ else
333
+ @status = status
334
+ end
335
+ end
336
+ end
337
+
338
+ # Manages groups and buddies. Don't create one yourself - get one using Client#buddy_list.
339
+ class BuddyList
340
+ include TOC
341
+
342
+ def initialize(conn) # :nodoc:
343
+ @conn = conn
344
+ @buddies = {}
345
+ @groups = {}
346
+ @group_order = []
347
+ end
348
+
349
+ # Constructs a printable string representation of the buddy list.
350
+ def to_s
351
+ s = ""
352
+ each_group do | group, buddies |
353
+ s << "== #{group} ==\n"
354
+ buddies.each do | buddy |
355
+ s << " * #{buddy}\n"
356
+ end
357
+ end
358
+ s
359
+ end
360
+
361
+ # Calls the passed block once for each group, passing the group name and the list of buddies as parameters.
362
+ def each_group
363
+ @group_order.each do | group |
364
+ buddies = @groups[group]
365
+ yield group, buddies
366
+ end
367
+ end
368
+
369
+ # Adds a new group named +group_name+.
370
+ # Setting +sync+ to :dont_sync will prevent this change from being sent to the server.
371
+ def add_group(group_name, sync=:sync)
372
+ if @groups[group_name].nil?
373
+ @groups[group_name] = []
374
+ @group_order << group_name
375
+ @conn.toc2_new_group group_name if sync == :sync
376
+ end
377
+ end
378
+
379
+ # Adds the buddy named +buddy_name+ to the group named +group+. If this group does not exist, it is created.
380
+ # Setting +sync+ to :dont_sync will prevent this change from being sent to the server.
381
+ def add_buddy(group, buddy_name, sync=:sync)
382
+ add_group(group, sync) if @groups[group].nil?
383
+ @groups[group] << buddy_named(buddy_name)
384
+ @conn.toc2_new_buddies("{g:#{group}\nb:#{format_screen_name(buddy_name)}\n}") if sync == :sync
385
+ end
386
+
387
+ # Removes the buddy named +buddy_name+ from the group named +group+.
388
+ # Setting +sync+ to :dont_sync will prevent this change from being sent to the server.
389
+ def remove_buddy(group, buddy_name, sync=:sync)
390
+ unless @groups[group].nil?
391
+ buddy = buddy_named(buddy_name)
392
+ @groups[group].reject! { | b | b == buddy }
393
+ @conn.toc2_remove_buddy(format_screen_name(buddy_name), group) if sync == :sync
394
+ end
395
+ end
396
+
397
+ # Returns the buddy named +name+. If the buddy does not exist, it is created. +name+ is not case- or whitespace-sensitive.
398
+ def buddy_named(name)
399
+ formatted_name = format_screen_name(name)
400
+ buddy = @buddies[formatted_name]
401
+ if buddy.nil?
402
+ buddy = Buddy.new(name, @conn)
403
+ @buddies[formatted_name] = buddy
404
+ end
405
+ buddy
406
+ end
407
+
408
+ # Decodes the buddy list from raw CONFIG data.
409
+ def decode_toc(val) # :nodoc:
410
+ current_group = nil
411
+ val.each_line do | line |
412
+ letter, name = *line.split(":")
413
+ name = name.chomp
414
+ case letter
415
+ when "g"
416
+ add_group(name, :dont_sync)
417
+ current_group = name
418
+ when "b"
419
+ add_buddy(current_group, name, :dont_sync)
420
+ end
421
+ end
422
+ end
423
+ end
424
+
425
+ # A high-level interface to TOC. It supports asynchronous message handling through the use of threads, and maintains a list of buddies.
426
+ class Client
427
+ include TOC
428
+
429
+ attr_reader :buddy_list, :screen_name
430
+
431
+ # You must initialize the client with your screen name and password.
432
+ # If a block is given, Client#listen will be invoked with the block after initialization.
433
+ def initialize(screen_name, password, &optional_block) # :yields: message, buddy, auto_response, client
434
+ @conn = Connection.new(screen_name)
435
+ @screen_name = format_screen_name(screen_name)
436
+ @password = password
437
+ @callbacks = {}
438
+ @buddy_list = BuddyList.new(@conn)
439
+ add_callback(:config, :config2) { |v| @buddy_list.decode_toc v }
440
+ add_callback(:update_buddy, :update_buddy2) { |v| update_buddy v }
441
+ on_error do | error |
442
+ $stderr.puts "Error: #{error}"
443
+ end
444
+ listen(&optional_block) if block_given?
445
+ end
446
+
447
+ # Connects to the server and starts an event-handling thread.
448
+ def connect(server="toc.oscar.aol.com", port=9898, oscar_server="login.oscar.aol.com", oscar_port=5190)
449
+ @conn.open(server, port)
450
+ code = 7696 * @screen_name[0] * @password[0]
451
+ @conn.toc2_signon(oscar_server, oscar_port, @screen_name, roasted_pass, "english", "\"TIC:toc.rb\"", 160, code)
452
+
453
+ @conn.recv do |msg, val|
454
+ if msg == :sign_on
455
+ @conn.toc_add_buddy(@screen_name)
456
+ @conn.toc_init_done
457
+ capabilities.each do |capability|
458
+ @conn.toc_set_caps(capability)
459
+ end
460
+ end
461
+ end
462
+ @thread.kill unless @thread.nil? # ha
463
+ @thread = Thread.new { loop { event_loop } }
464
+ end
465
+
466
+ # Disconnects and kills the event-handling thread. You may still add callbacks while disconnected.
467
+ def disconnect
468
+ @thread.kill unless @thread.nil?
469
+ @thread = nil
470
+ @conn.close
471
+ end
472
+
473
+ # Connects to the server and forwards received IMs to the given block. See Client#connect for the arguments.
474
+ def listen(*args) # :yields: message, buddy, auto_response, client
475
+ on_im do | message, buddy, auto_response |
476
+ yield message, buddy, auto_response, self
477
+ end
478
+ connect(*args)
479
+ wait
480
+ end
481
+
482
+ # Pass a block to be called every time an IM is received. This will replace any previous on_im handler.
483
+ def on_im
484
+ raise ArgumentException, "on_im requires a block argument" unless block_given?
485
+ add_callback(:im_in, :im_in2) do |val|
486
+ screen_name, auto, f2, *message = *val.split(":")
487
+ message = message.join(":")
488
+ buddy = @buddy_list.buddy_named(screen_name)
489
+ auto_response = auto == "T"
490
+ yield message, buddy, auto_response
491
+ end
492
+ end
493
+ # received event: :im_in2 => "remitaylor:F:F:hi"
494
+
495
+ # remi
496
+ def keep_track_of_rooms_joined
497
+ @keeping_track_of_rooms_joined = true
498
+ add_callback(:chat_join) do |val|
499
+ room_id, room_name = *val.split(":")
500
+ puts "joined chat room #{ room_name } [#{ room_id }]"
501
+ @rooms ||= { }
502
+ @rooms[room_id] = room_name # not an object for now, just strings!
503
+ end
504
+ end
505
+ def keeping_track_of_rooms_joined?
506
+ @keeping_track_of_rooms_joined
507
+ end
508
+
509
+ # JOIN & SEND should be on a Room object ... maybe
510
+
511
+ # remi
512
+ def join_chat room_name
513
+ @conn.toc_chat_join 4, room_name if room_name
514
+ end
515
+
516
+ # remi
517
+ def send_chat room_name, message
518
+ room = @rooms.find {|id,name| name == room_name } # end up with nil or [ '1234', 'the_name' ]
519
+ room_id = room.first || room_name
520
+ puts "i wanna send #{ message } to room with name #{ room_name } and therefore, id #{ room_id }"
521
+ message = "\"" + format_message(message) + "\""
522
+ @conn.toc_chat_send room_id, message
523
+ end
524
+
525
+ # remi
526
+ # Pass a block to be called every time an IM is received. This will replace any previous on_im handler.
527
+ def on_chat
528
+ raise ArgumentException, "on_chat requires a block argument" unless block_given?
529
+ keep_track_of_rooms_joined unless keeping_track_of_rooms_joined?
530
+ add_callback(:chat_in) do |val|
531
+ puts "chat_in val => #{ val.inspect }"
532
+ room_id, screen_name, auto, *message = *val.split(":")
533
+ message = message.join(":")
534
+ message = message.gsub('<br>',"\n") # ... before getting rid of html
535
+ message = message.chomp.gsub(/<[^>]+>/,"") # get rid of html
536
+ message = message.gsub("\n",'<br />') # ... turn newlines back into br's
537
+ buddy = @buddy_list.buddy_named(screen_name)
538
+ room = @rooms[room_id] || room_id
539
+ auto_response = auto == "T"
540
+ yield message, buddy, room, auto_response
541
+ end
542
+ end
543
+ # received event: :chat_in => "820142221:remitaylor:F:<HTML>w00t</HTML>"
544
+
545
+ # 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.
546
+ def on_error
547
+ raise ArgumentException, "on_error requires a block argument" unless block_given?
548
+ add_callback(:error) do |val|
549
+ code, param = *val.split(":")
550
+ error = ErrorCode[code.to_i]
551
+ error = "An unknown error occurred." if error.nil?
552
+ error.gsub!("<param>", param) unless param.nil?
553
+ yield error
554
+ end
555
+ end
556
+
557
+ # Sets your status to away and +away_message+ as your away message.
558
+ def go_away(away_message)
559
+ @conn.toc_set_away "\"#{away_message.gsub("\"","\\\"")}\""
560
+ end
561
+
562
+ # Sets your status to available.
563
+ def come_back
564
+ @conn.toc_set_away
565
+ end
566
+
567
+ # Sets your idle time in seconds. You only need to set this once; afterwards, the server will keep track itself.
568
+ # Set to 0 to stop being idle.
569
+ def idle_time=(seconds)
570
+ @conn.toc_set_idle seconds
571
+ end
572
+
573
+ def clear_callbacks!
574
+ @callbacks = { }
575
+ end
576
+
577
+ # 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.
578
+ # For example, the following script will exit right after connecting:
579
+ # client = Net::TOC.new("screenname", "p455w0rd")
580
+ # client.connect
581
+ # To prevent this, use wait:
582
+ # client = Net::TOC.new("screenname", "p455w0rd")
583
+ # client.connect
584
+ # client.wait
585
+ # Now the program will wait until the client has disconnected before exiting.
586
+ def wait(limit=nil)
587
+ @thread.join limit
588
+ end
589
+
590
+ # Returns a list of this client's capabilities. Not yet implemented.
591
+ def capabilities
592
+ [] # TODO
593
+ end
594
+
595
+ private
596
+
597
+ # Returns an "encrypted" version of the password to be sent across the internet.
598
+ # Decrypting it is trivial, though.
599
+ def roasted_pass
600
+ tictoc = "Tic/Toc".unpack "c*"
601
+ pass = @password.unpack "c*"
602
+ roasted = "0x"
603
+ pass.each_index do |i|
604
+ roasted << sprintf("%02x", pass[i] ^ tictoc[i % tictoc.length])
605
+ end
606
+ roasted
607
+ end
608
+
609
+ def update_buddy(val)
610
+ screen_name = val.split(":").first.chomp
611
+ buddy = @buddy_list.buddy_named(screen_name)
612
+ buddy.raw_update(val)
613
+ end
614
+
615
+ def add_callback(*callbacks, &block)
616
+ callbacks.each do |callback|
617
+ @callbacks[callback] = block;
618
+ end
619
+ end
620
+
621
+ def event_loop
622
+ @conn.recv do |msg, val|
623
+ puts "received event: #{ msg.inspect } => #{ val.inspect }" # remi
624
+ @callbacks[msg].call(val) unless @callbacks[msg].nil?
625
+ end
626
+ end
627
+ end
628
+ end
629
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aim
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 1
9
+ version: 0.1.1
10
+ platform: ruby
11
+ authors:
12
+ - remi
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2009-02-24 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Ruby gem for making AIM bots really easy to create
22
+ email: remi@remitaylor.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - Rakefile
31
+ - VERSION.yml
32
+ - README.rdoc
33
+ - lib/aim.rb
34
+ - lib/aim/net_toc.rb
35
+ has_rdoc: true
36
+ homepage: http://github.com/remi/aim
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options:
41
+ - --inline-source
42
+ - --charset=UTF-8
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ segments:
50
+ - 0
51
+ version: "0"
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.3.6
63
+ signing_key:
64
+ specification_version: 2
65
+ summary: Ruby gem for making AIM bots really easy to create
66
+ test_files: []
67
+