aim 0.1.1

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 (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
+