twitter-userstream 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +10 -0
- data/Rakefile +20 -0
- data/Readme.rdoc +53 -0
- data/VERSION +1 -0
- data/bin/chirp_growl +59 -0
- data/image/bird.png +0 -0
- data/lib/userstream.rb +153 -0
- data/lib/userstream/connect.rb +60 -0
- data/lib/userstream/event.rb +5 -0
- data/lib/userstream/event/delete.rb +7 -0
- data/lib/userstream/event/direct_message.rb +24 -0
- data/lib/userstream/event/favorite.rb +8 -0
- data/lib/userstream/event/follow.rb +7 -0
- data/lib/userstream/event/retweet.rb +8 -0
- data/lib/userstream/event/unfavorite.rb +8 -0
- data/lib/userstream/friend.rb +4 -0
- data/lib/userstream/tweet.rb +17 -0
- data/lib/userstream/twitter_object.rb +135 -0
- data/lib/userstream/user.rb +46 -0
- metadata +286 -0
data/Gemfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Generated from /Users/joshua/Development/userstream/userstream.gemspec
|
2
|
+
source :gemcutter
|
3
|
+
gem "eventmachine", ">= 0.12.10"
|
4
|
+
gem "em-http-request", ">= 0.2.7"
|
5
|
+
gem "yajl-ruby", ">= 0.7.5"
|
6
|
+
gem "load_path_find", ">= 0.0.5"
|
7
|
+
gem "oauth", ">= 0.4.0"
|
8
|
+
gem 'rainbow'
|
9
|
+
gem 'growl'
|
10
|
+
gem 'json'
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |s|
|
4
|
+
s.name = "twitter-userstream"
|
5
|
+
s.description = s.summary = "Eventmachine-based Twitter Userstream client"
|
6
|
+
s.email = "noah@involver.com"
|
7
|
+
s.homepage = "https://github.com/nhorton/twitter-userstream"
|
8
|
+
s.authors = ["Joshua Hull", "Fabien Penso", "Noah Horton"]
|
9
|
+
s.files = FileList["[A-Z]*", "{lib}/**/*", "{bin}/**/*", "{image}/**/*"]
|
10
|
+
s.add_dependency 'eventmachine', ">= 0.12.10"
|
11
|
+
s.add_dependency 'em-http-request', ">= 0.2.7"
|
12
|
+
s.add_dependency 'yajl-ruby', ">= 0.7.5"
|
13
|
+
s.add_dependency 'load_path_find', ">= 0.0.5"
|
14
|
+
s.add_dependency 'oauth', ">= 0.4.0"
|
15
|
+
s.executables = ['chirp_growl']
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
20
|
+
end
|
data/Readme.rdoc
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
= Userstream
|
2
|
+
|
3
|
+
== About
|
4
|
+
|
5
|
+
This is an experimental EventMachine-based client for http://userstream.twitter.com.
|
6
|
+
|
7
|
+
== Fun Things You Can Do At Home
|
8
|
+
|
9
|
+
Fire up the growl client!
|
10
|
+
|
11
|
+
sudo gem install userstream
|
12
|
+
chirp_growl
|
13
|
+
|
14
|
+
Wow! It's growling my tweets! How meta.
|
15
|
+
|
16
|
+
== Usage
|
17
|
+
|
18
|
+
require 'rubygems'
|
19
|
+
require 'userstream'
|
20
|
+
require 'rainbow'
|
21
|
+
|
22
|
+
chirp = Userstream.new
|
23
|
+
chirp.on_reconnect{ puts "reconnecting..." }
|
24
|
+
chirp.on_tweet {|t, u| puts "#{t.text} (from #{t.user.name.foreground(:red)} (#{('@' + t.user.screen_name).foreground(:green)}))" }
|
25
|
+
chirp.on_follow {|t, u| puts "#{t.source.screen_name.foreground(:green)} following #{t.target.screen_name.foreground(:green)}" }
|
26
|
+
chirp.on_favorite {|t, u| puts "#{t.source.screen_name.foreground(:green)} <3 -> #{t.target_object.text}" }
|
27
|
+
chirp.on_retweet {|t, u| puts "#{t.source.screen_name.foreground(:green)} RT -> #{t.target_object.text}" }
|
28
|
+
chirp.on_direct_message {|t, u| puts "DM : #{t.text} (from #{t.sender.name.foreground(:red)} (#{('@' + t.sender.screen_name).foreground(:green)}))" }
|
29
|
+
chirp.connect(Userstream::Connect::User.basic('joshbuddy', 'xxxxxxxx'))
|
30
|
+
|
31
|
+
== Oauth Usage
|
32
|
+
|
33
|
+
chirp = Userstream.oauth(consumer_token, consumer_secret)
|
34
|
+
chirp.on_reconnect{ puts "reconnecting..." }
|
35
|
+
chirp.on_tweet {|t, u| puts "#{t.text} (from #{t.user.name.foreground(:red)} (#{('@' + t.user.screen_name).foreground(:green)}))" }
|
36
|
+
chirp.on_follow {|t, u| puts "#{t.source.screen_name.foreground(:green)} following #{t.target.screen_name.foreground(:green)}" }
|
37
|
+
chirp.on_favorite {|t, u| puts "#{t.source.screen_name.foreground(:green)} <3 -> #{t.target_object.text}" }
|
38
|
+
chirp.on_retweet {|t, u| puts "#{t.source.screen_name.foreground(:green)} RT -> #{t.target_object.text}" }
|
39
|
+
chirp.on_direct_message {|t, u| puts "DM : #{t.text} (from #{t.sender.name.foreground(:red)} (#{('@' + t.sender.screen_name).foreground(:green)}))" }
|
40
|
+
chirp.connect(Userstream::Connect::User.oauth(access_token, access_secret))
|
41
|
+
|
42
|
+
== Multi User Usage
|
43
|
+
|
44
|
+
chirp = Userstream.new(consumer_token, consumer_secret)
|
45
|
+
|
46
|
+
chirp.on_reconnect{ puts "reconnecting..." }
|
47
|
+
chirp.on_tweet {|t, u| puts "#{t.text} (from #{t.user.name.foreground(:red)} (#{('@' + t.user.screen_name).foreground(:green)}))" }
|
48
|
+
chirp.on_follow {|t, u| puts "#{t.source.screen_name.foreground(:green)} following #{t.target.screen_name.foreground(:green)}" }
|
49
|
+
chirp.on_favorite {|t, u| puts "#{t.source.screen_name.foreground(:green)} <3 -> #{t.target_object.text}" }
|
50
|
+
chirp.on_retweet {|t, u| puts "#{t.source.screen_name.foreground(:green)} RT -> #{t.target_object.text}" }
|
51
|
+
chirp.on_direct_message {|t, u| puts "DM : #{t.text} (from #{t.sender.name.foreground(:red)} (#{('@' + t.sender.screen_name).foreground(:green)}))" }
|
52
|
+
|
53
|
+
chirp.connect(Userstream::Connect::User.oauth(access_token1, access_secret1), Userstream::Connect::User.oauth(access_token2, access_secret2))
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/chirp_growl
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby -rubygems
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'growl'
|
5
|
+
rescue LoadError
|
6
|
+
puts "You'll need to install growl\nsudo gem install growl"
|
7
|
+
exit(1)
|
8
|
+
end
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'json'
|
12
|
+
rescue LoadError
|
13
|
+
puts "You'll need to install json\nsudo gem install json"
|
14
|
+
exit(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'userstream')
|
18
|
+
require 'etc'
|
19
|
+
require 'fileutils'
|
20
|
+
|
21
|
+
FileUtils.mkdir_p(File.join(Etc.getpwuid.dir, '.userstream'))
|
22
|
+
FileUtils.mkdir_p(File.join(Etc.getpwuid.dir, '.userstream', 'cache'))
|
23
|
+
|
24
|
+
json_file = File.join(Etc.getpwuid.dir, '.userstream', 'user.json')
|
25
|
+
cache_dir = File.join(Etc.getpwuid.dir, '.userstream', 'cache')
|
26
|
+
|
27
|
+
if File.exist?(json_file)
|
28
|
+
data = JSON.parse(File.read(json_file))
|
29
|
+
@username = data['username']
|
30
|
+
@password = data['password']
|
31
|
+
else
|
32
|
+
puts "Enter your username:"
|
33
|
+
@username = gets.strip
|
34
|
+
puts "Enter your password:"
|
35
|
+
@password = gets.strip
|
36
|
+
File.open(json_file, 'w') {|f| f << {:username => @username, :password => @password}.to_json}
|
37
|
+
end
|
38
|
+
|
39
|
+
chirp = Userstream.new
|
40
|
+
|
41
|
+
Growl.notify {
|
42
|
+
self.message = 'Starting chirp_growl...'
|
43
|
+
self.title = 'ChirpStreeeeem!'
|
44
|
+
self.image = File.join(File.dirname(__FILE__), '..', 'image', 'bird.png')
|
45
|
+
}
|
46
|
+
|
47
|
+
chirp.on_tweet { |t, u|
|
48
|
+
t.user.with_profile_image(u, cache_dir) do |image_path|
|
49
|
+
Growl.notify {
|
50
|
+
self.message = t.text
|
51
|
+
self.title = "@#{t.user.screen_name}"
|
52
|
+
self.image = image_path if image_path
|
53
|
+
}
|
54
|
+
end
|
55
|
+
}
|
56
|
+
|
57
|
+
chirp.connect(Userstream::Connect::User.basic(@username, @password))
|
58
|
+
|
59
|
+
|
data/image/bird.png
ADDED
Binary file
|
data/lib/userstream.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'em-http'
|
3
|
+
require 'yajl'
|
4
|
+
require 'pp'
|
5
|
+
require 'load_path_find'
|
6
|
+
|
7
|
+
require 'oauth'
|
8
|
+
require 'oauth/client/em_http'
|
9
|
+
|
10
|
+
$LOAD_PATH.add_current
|
11
|
+
|
12
|
+
require 'userstream/twitter_object'
|
13
|
+
require 'userstream/event'
|
14
|
+
require 'userstream/event/follow'
|
15
|
+
require 'userstream/event/direct_message'
|
16
|
+
require 'userstream/event/retweet'
|
17
|
+
require 'userstream/event/favorite'
|
18
|
+
require 'userstream/event/unfavorite'
|
19
|
+
require 'userstream/event/delete'
|
20
|
+
require 'userstream/user'
|
21
|
+
require 'userstream/friend'
|
22
|
+
require 'userstream/tweet'
|
23
|
+
require 'userstream/connect'
|
24
|
+
|
25
|
+
class Userstream
|
26
|
+
|
27
|
+
include Connect
|
28
|
+
|
29
|
+
attr_reader :handlers
|
30
|
+
|
31
|
+
EventHandlerTypes = [:friend, :tweet, :follow, :unfollow, :favorite, :unfavorite, :retweet, :delete, :direct_message, :block, :unblock, :list_member_removed, :list_member_added, :list_user_subscribed, :list_user_unsubscribed]
|
32
|
+
ConnectionHandlerTypes = [:disconnect, :connect]
|
33
|
+
HandlerTypes = ConnectionHandlerTypes + EventHandlerTypes
|
34
|
+
|
35
|
+
Handlers = Struct.new(*HandlerTypes)
|
36
|
+
|
37
|
+
attr_reader :consumer_token, :consumer_secret, :fill_in
|
38
|
+
|
39
|
+
def initialize(options = nil)
|
40
|
+
@consumer_token = options && options[:consumer_token]
|
41
|
+
@consumer_secret = options && options[:consumer_secret]
|
42
|
+
@fill_in = options && options[:fill_in]
|
43
|
+
@connect_url = "https://userstream.twitter.com/2/user.json"
|
44
|
+
@handlers = Handlers.new(*HandlerTypes.map{|h| []})
|
45
|
+
@on_connect_called = {}
|
46
|
+
@users = []
|
47
|
+
end
|
48
|
+
|
49
|
+
HandlerTypes.each do |h|
|
50
|
+
module_eval "
|
51
|
+
def on_#{h}(&block)
|
52
|
+
@handlers.#{h} << block
|
53
|
+
end
|
54
|
+
"
|
55
|
+
end
|
56
|
+
|
57
|
+
EventHandlerTypes.each do |h|
|
58
|
+
const_name = h.to_s.split("_").map{|p| p.capitalize}.join
|
59
|
+
module_eval "
|
60
|
+
def dispatch_#{h}(user, data)
|
61
|
+
unless @handlers.#{h}.empty?
|
62
|
+
obj = #{const_name}.new(self, data)
|
63
|
+
@fill_in ? obj.load_all(user) { |nobj|
|
64
|
+
@handlers.#{h}.each{|h| h.call(nobj, user)}
|
65
|
+
} : @handlers.#{h}.each{|h| h.call(obj, user)}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
"
|
69
|
+
end
|
70
|
+
|
71
|
+
def dispatch_friend(user, data)
|
72
|
+
unless @handlers.friend.empty?
|
73
|
+
data['friends'].each_slice(100) do |friend_ids|
|
74
|
+
parser = Yajl::Parser.new
|
75
|
+
parser.on_parse_complete = proc { |friends|
|
76
|
+
friends.each do |friend|
|
77
|
+
@handlers.friend.each{|h| h.call(friend)}
|
78
|
+
end
|
79
|
+
}
|
80
|
+
friend_http = get_connection(user, "http://api.twitter.com/1/users/lookup.json", :post, :body => {'user_id' => friend_ids.join(',')})
|
81
|
+
friend_http.stream { |chunk|
|
82
|
+
parser << chunk
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def dispatch_disconnect(user)
|
89
|
+
return if @handlers.disconnect.empty?
|
90
|
+
@handlers.disconnect.each{|h| h.call(user)}
|
91
|
+
end
|
92
|
+
|
93
|
+
def dispatch_connect(user)
|
94
|
+
return if @on_connect_called[user.name]
|
95
|
+
@handlers.connect.each{|h| h.call(user)}
|
96
|
+
@on_connect_called[user.name] = true
|
97
|
+
end
|
98
|
+
|
99
|
+
def connect(*users)
|
100
|
+
unless EM.reactor_running?
|
101
|
+
EM.run { connect(*users) }
|
102
|
+
else
|
103
|
+
@users.concat(users)
|
104
|
+
@user_adder ||= EM.add_periodic_timer(1) {
|
105
|
+
users_to_connect = @users.slice!(0, 10)
|
106
|
+
if users_to_connect && !users_to_connect.empty?
|
107
|
+
users_to_connect.each do |user|
|
108
|
+
connect_single(user)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def connect_single(user)
|
116
|
+
parser = Yajl::Parser.new
|
117
|
+
parser.on_parse_complete = Proc.new{|parsed_data|
|
118
|
+
if parsed_data['direct_message']
|
119
|
+
dispatch_direct_message(user, parsed_data)
|
120
|
+
elsif parsed_data['friends']
|
121
|
+
dispatch_friend(user, parsed_data)
|
122
|
+
elsif parsed_data['text']
|
123
|
+
dispatch_tweet(user, parsed_data)
|
124
|
+
elsif parsed_data['event']
|
125
|
+
method_sym = "dispatch_#{parsed_data['event']}".to_sym
|
126
|
+
if respond_to?(method_sym)
|
127
|
+
send("dispatch_#{parsed_data['event']}".to_sym, user, parsed_data)
|
128
|
+
else
|
129
|
+
puts "no handler for #{parsed_data['event']}"
|
130
|
+
end
|
131
|
+
elsif parsed_data['delete']
|
132
|
+
dispatch_delete(user, parsed_data)
|
133
|
+
else
|
134
|
+
puts "i didn't know what to do with this!"
|
135
|
+
pp parsed_data
|
136
|
+
end
|
137
|
+
}
|
138
|
+
|
139
|
+
http = get_connection(user, @connect_url, :get)
|
140
|
+
http.errback { |e, err|
|
141
|
+
dispatch_disconnect(user)
|
142
|
+
}
|
143
|
+
http.stream { |chunk|
|
144
|
+
dispatch_connect(user)
|
145
|
+
begin
|
146
|
+
parser << chunk
|
147
|
+
rescue Yajl::ParseError
|
148
|
+
p $!
|
149
|
+
puts "bad chunk: #{chunk.inspect}"
|
150
|
+
end
|
151
|
+
}
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class Userstream
|
2
|
+
module Connect
|
3
|
+
class User
|
4
|
+
|
5
|
+
class Oauth
|
6
|
+
attr_reader :name, :access_token, :access_secret
|
7
|
+
def initialize(access_token, access_secret, name = nil)
|
8
|
+
@name = name
|
9
|
+
@access_token = access_token
|
10
|
+
@access_secret = access_secret
|
11
|
+
end
|
12
|
+
|
13
|
+
def oauth?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Basic
|
19
|
+
attr_reader :name, :password
|
20
|
+
def initialize(name, password)
|
21
|
+
@name = name
|
22
|
+
@password = password
|
23
|
+
end
|
24
|
+
|
25
|
+
def oauth?
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.oauth(access_token, access_secret, name = nil)
|
31
|
+
Oauth.new(access_token, access_secret, name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.basic(name, password)
|
35
|
+
Basic.new(name, password)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_connection(user, url, method, extras = nil)
|
40
|
+
options = extras ? extras.merge(:timeout => 0) : {:timeout => 0}
|
41
|
+
if user.oauth?
|
42
|
+
request = EM::HttpRequest.new(url)
|
43
|
+
request.send(method, options) do |client|
|
44
|
+
twitter_oauth_consumer.sign!(client, twitter_oauth_access_token(user.access_token, user.access_secret))
|
45
|
+
end
|
46
|
+
else
|
47
|
+
http = EM::HttpRequest.new(url).send(method, options.merge(:head => {'authorization' => [user.name, user.password]}))
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def twitter_oauth_consumer
|
53
|
+
OAuth::Consumer.new(consumer_token, consumer_secret, :site => "http://twitter.com")
|
54
|
+
end
|
55
|
+
|
56
|
+
def twitter_oauth_access_token(token=nil,secret=nil)
|
57
|
+
OAuth::AccessToken.new(twitter_oauth_consumer, token, secret)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Userstream
|
2
|
+
class DirectMessage < TwitterObject
|
3
|
+
|
4
|
+
ATTRS = [:coordinates, :recipient, :sender, :sender_screen_name, :text, :id, :recipient_id, :sender_id]
|
5
|
+
|
6
|
+
attr_accessor *ATTRS
|
7
|
+
user_writer :sender
|
8
|
+
user_writer :recipient
|
9
|
+
|
10
|
+
def initialize(base, data = nil)
|
11
|
+
@base = base
|
12
|
+
from_json(data['direct_message']) if data and data['direct_message']
|
13
|
+
yield self if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def tweet_loadable_id
|
17
|
+
id
|
18
|
+
end
|
19
|
+
|
20
|
+
def loaded?
|
21
|
+
!text.nil?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Userstream
|
2
|
+
class Tweet < TwitterObject
|
3
|
+
|
4
|
+
ATTRS = [:coordinates, :favorited, :created_at, :truncated, :contributors, :text, :id, :geo, :in_reply_to_user_id, :place, :source, :in_reply_to_screen_name, :in_reply_to_status_id, :user]
|
5
|
+
|
6
|
+
attr_accessor *ATTRS
|
7
|
+
user_writer :user
|
8
|
+
|
9
|
+
def tweet_loadable_id
|
10
|
+
id
|
11
|
+
end
|
12
|
+
|
13
|
+
def loaded?
|
14
|
+
!text.nil?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
class TwitterObject
|
2
|
+
|
3
|
+
SINGLE_USER_URL = "http://api.twitter.com/1/users/show/%s.json"
|
4
|
+
SINGLE_TWEET_URL = "http://api.twitter.com/1/statuses/show/%s.json"
|
5
|
+
|
6
|
+
attr_reader :base, :data
|
7
|
+
|
8
|
+
def initialize(base, data = nil)
|
9
|
+
@base, @data = base, data
|
10
|
+
from_json(data) if data
|
11
|
+
yield self if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.user_writer(*attrs)
|
15
|
+
attrs.each do |attr|
|
16
|
+
module_eval "
|
17
|
+
def #{attr}=(#{attr})
|
18
|
+
@#{attr} = if #{attr}.is_a?(Hash)
|
19
|
+
Userstream::User.new(base, #{attr})
|
20
|
+
else
|
21
|
+
Userstream::User.new(base) {|u| u.id = #{attr}}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.tweet_writer(*attrs)
|
29
|
+
attrs.each do |attr|
|
30
|
+
module_eval "
|
31
|
+
def #{attr}=(#{attr})
|
32
|
+
@#{attr} = if #{attr}.is_a?(Hash)
|
33
|
+
Userstream::Tweet.new(base, #{attr})
|
34
|
+
else
|
35
|
+
Userstream::Tweet.new(base) {|t| t.id = #{attr}}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def from_json(data)
|
43
|
+
self.class.attrs.each { |a| self.send(:"#{a}=", data[a.to_s]) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.attrs
|
47
|
+
const_get(:ATTRS)
|
48
|
+
end
|
49
|
+
|
50
|
+
def load_all(loading_user, &block)
|
51
|
+
attrs = self.class.attrs
|
52
|
+
if respond_to?(:loaded?) && !loaded?
|
53
|
+
if respond_to?(:user_loadable_id)
|
54
|
+
from_json(get_user_data(loading_user, user_loadable_id)[user_loadable_id])
|
55
|
+
load_all(&block)
|
56
|
+
elsif respond_to?(:tweet_loadable_id)
|
57
|
+
from_json(get_tweet_data(loading_user, tweet_loadable_id)[tweet_loadable_id])
|
58
|
+
load_all(&block)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
tweet_ids = {}
|
62
|
+
user_ids = {}
|
63
|
+
attrs.each do |a|
|
64
|
+
obj = send(a)
|
65
|
+
if obj.respond_to?(:loaded?) && !obj.loaded?
|
66
|
+
if obj.respond_to?(:user_loadable_id)
|
67
|
+
user_ids[a] = obj.user_loadable_id
|
68
|
+
elsif obj.respond_to?(:tweet_loadable_id)
|
69
|
+
tweet_ids[a] = obj.tweet_loadable_id
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
get_tweet_data(loading_user, tweet_ids.values) { |tweet_data|
|
74
|
+
tweet_ids.each{|k,v| self.send(:"#{k}=", tweet_data[v])}
|
75
|
+
user_data = get_user_data(loading_user, user_ids.values) { |user_data|
|
76
|
+
user_ids.each{|k,v| self.send(:"#{k}=", user_data[v])}
|
77
|
+
yield self
|
78
|
+
}
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_tweet_data(loading_user, ids)
|
85
|
+
ids = Array(ids).uniq
|
86
|
+
data = {}
|
87
|
+
if ids.empty?
|
88
|
+
yield data
|
89
|
+
else
|
90
|
+
load_tweet_data(loading_user, ids, data) { yield data }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def load_tweet_data(loading_user, ids, data, &block)
|
95
|
+
id = ids.shift
|
96
|
+
if (id)
|
97
|
+
parser = Yajl::Parser.new
|
98
|
+
parser.on_parse_complete = proc { |parsed|
|
99
|
+
data[id] = parsed
|
100
|
+
load_tweet_data(ids, data, &block)
|
101
|
+
}
|
102
|
+
http = base.get_connection(loading_user, "http://api.twitter.com/1/statuses/show/%s.json" % id, :get)
|
103
|
+
http.stream { |chunk|
|
104
|
+
parser << chunk
|
105
|
+
}
|
106
|
+
else
|
107
|
+
yield
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_user_data(loading_user, ids)
|
112
|
+
ids = Array(ids).uniq
|
113
|
+
data = {}
|
114
|
+
if ids.empty?
|
115
|
+
yield data
|
116
|
+
else
|
117
|
+
load_user_data(loading_user, ids, data) { yield data }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def load_user_data(loading_user, ids, data)
|
122
|
+
parser = Yajl::Parser.new
|
123
|
+
parser.on_parse_complete = proc { |parsed|
|
124
|
+
parsed.each do |user|
|
125
|
+
data[user["id"]] = user
|
126
|
+
end
|
127
|
+
yield
|
128
|
+
}
|
129
|
+
http = base.get_connection(loading_user, "http://api.twitter.com/1/users/lookup.json", :post, :body => {'user_id' => ids.join(',')})
|
130
|
+
http.stream { |chunk|
|
131
|
+
parser << chunk
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'em-http'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
class Userstream
|
5
|
+
class User < TwitterObject
|
6
|
+
|
7
|
+
ATTRS = [
|
8
|
+
:profile_background_tile, :name, :profile_sidebar_border_color, :profile_sidebar_fill_color, :profile_image_url, :location, :created_at,
|
9
|
+
:profile_link_color, :url, :contributors_enabled, :favourites_count, :utc_offset, :id, :followers_count, :protected, :lang,
|
10
|
+
:profile_text_color, :geo_enabled, :profile_background_color, :time_zone, :notifications, :description, :verified, :profile_background_image_url,
|
11
|
+
:statuses_count, :friends_count, :screen_name, :following
|
12
|
+
]
|
13
|
+
|
14
|
+
attr_accessor *ATTRS
|
15
|
+
|
16
|
+
def loaded?
|
17
|
+
name
|
18
|
+
end
|
19
|
+
|
20
|
+
def user_loadable_id
|
21
|
+
id
|
22
|
+
end
|
23
|
+
|
24
|
+
def with_profile_image(loading_user, cache_dir)
|
25
|
+
raise unless loaded?
|
26
|
+
|
27
|
+
cached_file = File.join(cache_dir, "#{Digest::MD5.hexdigest(profile_image_url)}#{File.extname(profile_image_url)}")
|
28
|
+
|
29
|
+
if File.exist?(cached_file)
|
30
|
+
yield cached_file
|
31
|
+
else
|
32
|
+
http = base.get_connection(loading_user, profile_image_url, :get)
|
33
|
+
http.callback do
|
34
|
+
if http.response_header.status == 200
|
35
|
+
File.open(cached_file, 'w') {|f| f << http.response}
|
36
|
+
yield cached_file
|
37
|
+
else
|
38
|
+
yield nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,286 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: twitter-userstream
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Joshua Hull
|
14
|
+
- Fabien Penso
|
15
|
+
- Noah Horton
|
16
|
+
autorequire:
|
17
|
+
bindir: bin
|
18
|
+
cert_chain: []
|
19
|
+
|
20
|
+
date: 2011-05-14 00:00:00 Z
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: eventmachine
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 59
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
- 12
|
34
|
+
- 10
|
35
|
+
version: 0.12.10
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id001
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: em-http-request
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
hash: 25
|
47
|
+
segments:
|
48
|
+
- 0
|
49
|
+
- 2
|
50
|
+
- 7
|
51
|
+
version: 0.2.7
|
52
|
+
type: :runtime
|
53
|
+
version_requirements: *id002
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: yajl-ruby
|
56
|
+
prerelease: false
|
57
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
hash: 9
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
- 7
|
66
|
+
- 5
|
67
|
+
version: 0.7.5
|
68
|
+
type: :runtime
|
69
|
+
version_requirements: *id003
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: load_path_find
|
72
|
+
prerelease: false
|
73
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
hash: 21
|
79
|
+
segments:
|
80
|
+
- 0
|
81
|
+
- 0
|
82
|
+
- 5
|
83
|
+
version: 0.0.5
|
84
|
+
type: :runtime
|
85
|
+
version_requirements: *id004
|
86
|
+
- !ruby/object:Gem::Dependency
|
87
|
+
name: oauth
|
88
|
+
prerelease: false
|
89
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
hash: 15
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
- 4
|
98
|
+
- 0
|
99
|
+
version: 0.4.0
|
100
|
+
type: :runtime
|
101
|
+
version_requirements: *id005
|
102
|
+
- !ruby/object:Gem::Dependency
|
103
|
+
name: rainbow
|
104
|
+
prerelease: false
|
105
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
hash: 3
|
111
|
+
segments:
|
112
|
+
- 0
|
113
|
+
version: "0"
|
114
|
+
type: :runtime
|
115
|
+
version_requirements: *id006
|
116
|
+
- !ruby/object:Gem::Dependency
|
117
|
+
name: growl
|
118
|
+
prerelease: false
|
119
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
hash: 3
|
125
|
+
segments:
|
126
|
+
- 0
|
127
|
+
version: "0"
|
128
|
+
type: :runtime
|
129
|
+
version_requirements: *id007
|
130
|
+
- !ruby/object:Gem::Dependency
|
131
|
+
name: json
|
132
|
+
prerelease: false
|
133
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
hash: 3
|
139
|
+
segments:
|
140
|
+
- 0
|
141
|
+
version: "0"
|
142
|
+
type: :runtime
|
143
|
+
version_requirements: *id008
|
144
|
+
- !ruby/object:Gem::Dependency
|
145
|
+
name: eventmachine
|
146
|
+
prerelease: false
|
147
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
148
|
+
none: false
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
hash: 59
|
153
|
+
segments:
|
154
|
+
- 0
|
155
|
+
- 12
|
156
|
+
- 10
|
157
|
+
version: 0.12.10
|
158
|
+
type: :runtime
|
159
|
+
version_requirements: *id009
|
160
|
+
- !ruby/object:Gem::Dependency
|
161
|
+
name: em-http-request
|
162
|
+
prerelease: false
|
163
|
+
requirement: &id010 !ruby/object:Gem::Requirement
|
164
|
+
none: false
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
hash: 25
|
169
|
+
segments:
|
170
|
+
- 0
|
171
|
+
- 2
|
172
|
+
- 7
|
173
|
+
version: 0.2.7
|
174
|
+
type: :runtime
|
175
|
+
version_requirements: *id010
|
176
|
+
- !ruby/object:Gem::Dependency
|
177
|
+
name: yajl-ruby
|
178
|
+
prerelease: false
|
179
|
+
requirement: &id011 !ruby/object:Gem::Requirement
|
180
|
+
none: false
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
hash: 9
|
185
|
+
segments:
|
186
|
+
- 0
|
187
|
+
- 7
|
188
|
+
- 5
|
189
|
+
version: 0.7.5
|
190
|
+
type: :runtime
|
191
|
+
version_requirements: *id011
|
192
|
+
- !ruby/object:Gem::Dependency
|
193
|
+
name: load_path_find
|
194
|
+
prerelease: false
|
195
|
+
requirement: &id012 !ruby/object:Gem::Requirement
|
196
|
+
none: false
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
hash: 21
|
201
|
+
segments:
|
202
|
+
- 0
|
203
|
+
- 0
|
204
|
+
- 5
|
205
|
+
version: 0.0.5
|
206
|
+
type: :runtime
|
207
|
+
version_requirements: *id012
|
208
|
+
- !ruby/object:Gem::Dependency
|
209
|
+
name: oauth
|
210
|
+
prerelease: false
|
211
|
+
requirement: &id013 !ruby/object:Gem::Requirement
|
212
|
+
none: false
|
213
|
+
requirements:
|
214
|
+
- - ">="
|
215
|
+
- !ruby/object:Gem::Version
|
216
|
+
hash: 15
|
217
|
+
segments:
|
218
|
+
- 0
|
219
|
+
- 4
|
220
|
+
- 0
|
221
|
+
version: 0.4.0
|
222
|
+
type: :runtime
|
223
|
+
version_requirements: *id013
|
224
|
+
description: Eventmachine-based Twitter Userstream client
|
225
|
+
email: noah@involver.com
|
226
|
+
executables:
|
227
|
+
- chirp_growl
|
228
|
+
extensions: []
|
229
|
+
|
230
|
+
extra_rdoc_files: []
|
231
|
+
|
232
|
+
files:
|
233
|
+
- Gemfile
|
234
|
+
- Rakefile
|
235
|
+
- Readme.rdoc
|
236
|
+
- VERSION
|
237
|
+
- bin/chirp_growl
|
238
|
+
- image/bird.png
|
239
|
+
- lib/userstream.rb
|
240
|
+
- lib/userstream/connect.rb
|
241
|
+
- lib/userstream/event.rb
|
242
|
+
- lib/userstream/event/delete.rb
|
243
|
+
- lib/userstream/event/direct_message.rb
|
244
|
+
- lib/userstream/event/favorite.rb
|
245
|
+
- lib/userstream/event/follow.rb
|
246
|
+
- lib/userstream/event/retweet.rb
|
247
|
+
- lib/userstream/event/unfavorite.rb
|
248
|
+
- lib/userstream/friend.rb
|
249
|
+
- lib/userstream/tweet.rb
|
250
|
+
- lib/userstream/twitter_object.rb
|
251
|
+
- lib/userstream/user.rb
|
252
|
+
homepage: https://github.com/nhorton/twitter-userstream
|
253
|
+
licenses: []
|
254
|
+
|
255
|
+
post_install_message:
|
256
|
+
rdoc_options: []
|
257
|
+
|
258
|
+
require_paths:
|
259
|
+
- lib
|
260
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
261
|
+
none: false
|
262
|
+
requirements:
|
263
|
+
- - ">="
|
264
|
+
- !ruby/object:Gem::Version
|
265
|
+
hash: 3
|
266
|
+
segments:
|
267
|
+
- 0
|
268
|
+
version: "0"
|
269
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
270
|
+
none: false
|
271
|
+
requirements:
|
272
|
+
- - ">="
|
273
|
+
- !ruby/object:Gem::Version
|
274
|
+
hash: 3
|
275
|
+
segments:
|
276
|
+
- 0
|
277
|
+
version: "0"
|
278
|
+
requirements: []
|
279
|
+
|
280
|
+
rubyforge_project:
|
281
|
+
rubygems_version: 1.8.1
|
282
|
+
signing_key:
|
283
|
+
specification_version: 3
|
284
|
+
summary: Eventmachine-based Twitter Userstream client
|
285
|
+
test_files: []
|
286
|
+
|