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.
- data/README.rdoc +38 -0
- data/Rakefile +57 -0
- data/VERSION.yml +4 -0
- data/lib/aim.rb +130 -0
- data/lib/aim/net_toc.rb +629 -0
- metadata +67 -0
data/README.rdoc
ADDED
|
@@ -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
|
data/Rakefile
ADDED
|
@@ -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
|
data/VERSION.yml
ADDED
data/lib/aim.rb
ADDED
|
@@ -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
|
data/lib/aim/net_toc.rb
ADDED
|
@@ -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
|
+
|