typho-twitter 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+
23
+ test/oauth.yaml
24
+ .rvmrc
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 shock
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,61 @@
1
+ = Typho Twitter
2
+
3
+ == What
4
+
5
+ This is a RubyGem to simplify sending parallel batches of requests to the Twitter API in Ruby applications.
6
+
7
+ It is based on Typhoeus and OAuth.
8
+
9
+ It is currently a work in progress. Comments, suggestions, and feedback are welcome and encouraged.
10
+
11
+ == Why
12
+
13
+ Some applications need to send lots of individual requests to the Twitter API to do things such as retrieve details from a group of users, or get the recent statuses for a group of users.
14
+
15
+ For a sizable number of requests, doing this serially is extremely slow. TyphoTwitter lets you perform a batch of like requests in parallel, drastically reducing the amount of time it takes to perform the same number of requests.
16
+
17
+ == Installing
18
+
19
+ sudo gem install typho-twitter
20
+
21
+ The source code is hosted on GitHub: http://github.com/capitalthought/typho-twitter
22
+
23
+ == The basics
24
+
25
+
26
+ == Demonstration of usage
27
+
28
+ Create a TyphoTwitter instance. If you need to authorize:
29
+
30
+ @typho_twitter = TyphoTwitter.new(
31
+ :oauth=>{
32
+ :consumer_key=>'XXXXXXX',
33
+ :consumer_secret=>'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY',
34
+ :token=>'XXXXXXX-YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY',
35
+ :secret=>'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ',
36
+ :site=>'http://example.com'
37
+ }
38
+ )
39
+ screen_name_array = %w[02Blazer 080808news 0Amna0 100PercentTX 1043LaQueBuena 1049TheHorn 1070thefan 1070WINA 10jackrussel 10rWfe 10tonreverb 1337studios 141chars 1450whtc 1660THEFAN 16mthsapart 1968mike 1capplegate 1LUVMRWAY 1MattHopkins 1OneStone 1realestateteam 1stbassguitar 1stBrand 1ststepsmoney 1TeeTime 1weightliftin 2001MUgrad 203klender 20thCFlicks]
40
+ responses = @typho_twitter.get_users_show( screen_name_array )
41
+ responses.each do |response|
42
+ puts response.to_s
43
+ end
44
+
45
+ == More Information
46
+
47
+ * RDoc: http://rdoc.info/projects/capitalthought/typho-twitter/
48
+
49
+ == How to submit patches
50
+
51
+ The source code is hosted on the GitHub: http://github.com/capitalthought/typho-twitter
52
+
53
+ To submit a patch, please fork the typho-twitter project and create a patch with tests. Once you're happy with it send a pull request and post a message to the google group.
54
+
55
+ == License
56
+
57
+ This code is free to use under the terms of the MIT license.
58
+
59
+ == Contact
60
+
61
+ Comments are welcome. Send an email to me at typho-twitter@wdd.oib.com
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "typho-twitter"
8
+ gem.summary = %Q{Parallel twitter client using Typhoeus.}
9
+ gem.description = %Q{A Twitter client for performing a batch of Twitter calls in parallel.}
10
+ gem.email = "billdoughty@capitalthought.com"
11
+ gem.homepage = "http://github.com/capitalthought/typho-twitter"
12
+ gem.authors = ["shock"]
13
+ gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ gem.add_dependency "wdd-ruby-ext", ">= 0.3.1"
15
+ gem.add_dependency "oauth", ">=0.4.3"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'spec/rake/spectask'
24
+ Spec::Rake::SpecTask.new(:spec) do |spec|
25
+ spec.libs << 'lib' << 'spec'
26
+ spec.spec_files = FileList['spec/**/*_spec.rb']
27
+ end
28
+
29
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.pattern = 'spec/**/*_spec.rb'
32
+ spec.rcov = true
33
+ end
34
+
35
+ task :spec => :check_dependencies
36
+
37
+ task :default => :spec
38
+
39
+ require 'rake/rdoctask'
40
+ Rake::RDocTask.new do |rdoc|
41
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "typho-twitter #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.1
@@ -0,0 +1 @@
1
+ require 'typho-twitter/typho-twitter'
@@ -0,0 +1,474 @@
1
+ # Note the tests at the bottom. You can test this class by running it standalone in the interpreter.
2
+
3
+ require 'rubygems'
4
+ require 'wdd-ruby-ext'
5
+ require "cgi"
6
+ require "net/http"
7
+ require "uri"
8
+ require "time"
9
+ require "pp"
10
+ require 'json'
11
+ require 'base64'
12
+ require 'logger'
13
+ require "thread"
14
+ require "oauth"
15
+ require "oauth/request_proxy/typhoeus_request"
16
+
17
+ # Class to abstract access to Twitter's Web Traffic API.
18
+ # Makes use of the Typhoeus gem to enable concurrent API calls.
19
+
20
+ class TyphoTwitter
21
+
22
+ include WDD::Utils
23
+
24
+ private
25
+
26
+ def logger
27
+ @logger
28
+ end
29
+
30
+ def puts message
31
+ logger.debug message
32
+ end
33
+
34
+ public
35
+
36
+ class HTTPException < RuntimeError
37
+ attr :code
38
+ attr :body
39
+
40
+ def initialize( code, body )
41
+ @code = code
42
+ @body = body
43
+ super( "#{code} - #{body}" )
44
+ end
45
+ end
46
+
47
+ class TwitterException < RuntimeError
48
+ attr :code
49
+ attr :body
50
+
51
+ def initialize( code, body )
52
+ @code = code
53
+ @body = body
54
+ super( "#{code} - #{body}" )
55
+ end
56
+ end
57
+
58
+ attr :login
59
+ attr :password
60
+ attr :headers
61
+
62
+ # Constants
63
+ DEFAULT_REQUEST_TIMEOUT = 20000
64
+ DEFAULT_CONCURRENCY_LIMIT = 40
65
+ MAX_RETRIES = 10
66
+
67
+ # Create a TyphoTwitter instance.
68
+ # +options+ :
69
+ # :request_timeout Request timeout value in miliseconds
70
+ # :request_timeout Request timeout value in miliseconds
71
+ # :concurrency_limit Maximum number of concurrent Typhoeus requests
72
+ # :logger Logger to use - defaults to standard out with DEBUG level if not specified.
73
+ # :oauth Used to authorize with Twitter API.
74
+ # :consumer_key Consumer key for your application
75
+ # :consumer_secret Consumer secret for your appilcation
76
+ # :token Access token for the Twitter user to authenticate with
77
+ # :secret Access token secret
78
+ # :site The site URL for your application
79
+ def initialize options={}
80
+ if options[:oauth]
81
+ @oauth_options = {}
82
+ @oauth_options[:consumer] = OAuth::Consumer.new( options[:oauth][:consumer_key], options[:oauth][:consumer_secret], :site => options[:oauth][:site] )
83
+ @oauth_options[:access_token] = OAuth::AccessToken.from_hash( @oauth_options[:consumer], :oauth_token=>options[:oauth][:token], :oauth_token_secret=>options[:oauth][:secret] )
84
+ end
85
+ @headers = {}
86
+ @options = options
87
+ @request_timeout = options[:request_timeout] || DEFAULT_REQUEST_TIMEOUT
88
+ @concurrency_limit = options[:concurrency_limit] || DEFAULT_CONCURRENCY_LIMIT
89
+ @logger = options[:logger]
90
+ if @logger.nil?
91
+ $stdout.sync = true
92
+ @logger = Logger.new( $stdout )
93
+ @logger.level = Logger::DEBUG
94
+ end
95
+ end
96
+
97
+ # Executes a batch of Twitter calls. Automatically handles timeout_retries when possible on failures.
98
+ # Returns a hash where each +data_array+ element is a key mapping to either
99
+ # a hash containing the results of the twitter call or a TwitterException object of the twitter call that failed.
100
+ # If +data_array+ is bigger than the concurrency_limit set in the TyphoTwitter constructor, it is broken
101
+ # up into batches of requests by Typhoeus automatically.
102
+ # +data_array+ - An array of data inputs, one for each twitter call
103
+ # +&block+ - A block that accepts an element of +data_array+ and returns a Tyhpoeus::Request object.
104
+ def typho_twitter_batch data_array, &block
105
+
106
+ json_results = {}
107
+ timeout_retries = 0
108
+ time_gate = WDD::Utils::TimeGate.new
109
+ rate_limit_exceeded = false
110
+ while data_array.length > 0
111
+ puts "Waiting on rate limiting Time Gate"
112
+ time_gate.wait
113
+ puts "Time Gate released"
114
+ hydra = Typhoeus::Hydra.new(:max_concurrency => @concurrency_limit)
115
+ hydra.disable_memoization
116
+
117
+ retries = 0
118
+
119
+ timed_out_inputs = Queue.new
120
+ data_array.each do |data_input|
121
+ request = yield( data_input )
122
+ if @oauth_options
123
+ oauth_params = {:consumer => @oauth_options[:consumer], :token => @oauth_options[:access_token]}
124
+ oauth_helper = OAuth::Client::Helper.new(request, oauth_params.merge(:request_uri => request.url))
125
+ request.headers.merge!({"Authorization" => oauth_helper.header}) # Signs the request
126
+ end
127
+ # printvar :request, request
128
+ request.on_complete do |response|
129
+ puts "[#{response.code}] - #{request.url}"
130
+ case response.code
131
+ when 200:
132
+ begin
133
+ json_object = JSON.parse( response.body )
134
+ json_results[data_input] = json_object
135
+ retries = 0
136
+ rescue JSON::ParserError
137
+ if timeout_retries < MAX_RETRIES
138
+ timed_out_inputs.push data_input
139
+ else
140
+ json_results[data_input] = $!
141
+ logger.error "#{$!.inspect}"
142
+ logger.error "#{$!.backtrace.join("\n")}"
143
+ logger.error "response.body: •#{response.body}•"
144
+ end
145
+ end
146
+ when 0:
147
+ logger.debug "**** Twitter Timeout (#{response.code}) for #{data_input}."
148
+ logger.debug "Response body: #{response.body}"
149
+ if timeout_retries < MAX_RETRIES
150
+ timed_out_inputs.push data_input
151
+ else
152
+ json_results[data_input] = TwitterException.new(response.code, response.body)
153
+ end
154
+ when 400:
155
+ logger.debug "**** Twitter Rate Limit Exceeded (#{response.code}) for #{data_input}."
156
+ rate_limit_exceeded = true
157
+ json_results[data_input] = TwitterException.new(response.code, response.body)
158
+ when 401:
159
+ logger.debug "**** Twitter Authorization Failed (#{response.code}) for #{data_input}."
160
+ logger.debug "Request URL: #{request.url}"
161
+ json_results[data_input] = TwitterException.new(response.code, response.body)
162
+ when 404:
163
+ logger.debug "Unknown data_input: #{data_input}"
164
+ logger.debug "Request URL: #{request.url}"
165
+ json_results[data_input] = TwitterException.new(response.code, response.body)
166
+ when 502:
167
+ logger.debug "Twitter Over capacity (#{response.code}) for data_input: #{data_input}. Will retry."
168
+ logger.debug "Request URL: #{request.url}"
169
+ retries += 1
170
+ if retries < MAX_RETRIES
171
+ sleep_time = retries**2
172
+ logger.debug "Will retry after #{sleep_time} seconds."
173
+ sleep sleep_time
174
+ hydra.queue request
175
+ else
176
+ json_results[data_input] = TwitterException.new(response.code, response.body)
177
+ end
178
+ when 503:
179
+ logger.debug "Twitter Service Unavailable (#{response.code}) for data_input: #{data_input}. Will retry."
180
+ logger.debug "Request URL: #{request.url}"
181
+ retries += 1
182
+ if retries < MAX_RETRIES
183
+ sleep_time = retries**2
184
+ logger.debug "Will retry after #{sleep_time} seconds."
185
+ sleep sleep_time
186
+ hydra.queue request
187
+ else
188
+ json_results[data_input] = TwitterException.new(response.code, response.body)
189
+ end
190
+ when 500:
191
+ logger.debug "Twitter server error for data_input: #{data_input}. Will retry."
192
+ logger.debug "Request URL: #{request.url}"
193
+ retries += 1
194
+ if retries < MAX_RETRIES
195
+ sleep_time = retries**2
196
+ logger.debug "Will retry after #{sleep_time} seconds."
197
+ sleep sleep_time
198
+ hydra.queue request
199
+ else
200
+ json_results[data_input] = TwitterException.new(response.code, response.body)
201
+ end
202
+ else
203
+ logger.error "Unexpected HTTP result code: #{response.code}\n#{response.body}"
204
+ logger.error "Request URL: #{request.url}"
205
+ json_results[data_input] = TwitterException.new(response.code, response.body)
206
+ end
207
+ end
208
+ hydra.queue request
209
+ end
210
+ logger.debug "+++ Running Hydra."
211
+ hydra.run
212
+ logger.debug "--- Hydra run complete."
213
+ data_array = []
214
+ if timed_out_inputs.size > 0
215
+ logger.debug "#{timed_out_inputs.size} ERRORS encountered."
216
+ while !timed_out_inputs.empty?
217
+ failed_input = timed_out_inputs.pop
218
+ logger.debug "Reloading #{failed_input}"
219
+ data_array << failed_input
220
+ end
221
+ timeout_retries += 1
222
+ sleep_time = timeout_retries ** 2
223
+ logger.debug "Will retry after #{sleep_time} seconds."
224
+ sleep sleep_time
225
+ end
226
+ end
227
+ json_results
228
+ end
229
+
230
+
231
+ # Retrieves user profile data for a group of Twitter users.
232
+ # +twitter_id_array+ = An array twitter user ids, one for each user to get data for. Can be user_ids or screen_names.
233
+ # Returns a results Hash (see typho_twitter_batch)
234
+ def get_users_show twitter_id_array
235
+ typho_twitter_batch( twitter_id_array ) do |twitter_id|
236
+ if twitter_id.is_a? Fixnum
237
+ request = Typhoeus::Request.new("http://twitter.com/users/show.json?user_id=#{twitter_id}",
238
+ :headers => @headers,
239
+ :timeout => @request_timeout # miliseconds
240
+ )
241
+ else
242
+ request = Typhoeus::Request.new("http://twitter.com/users/show.json?screen_name=#{twitter_id}",
243
+ :timeout => @request_timeout, # miliseconds
244
+ :headers => @headers
245
+ )
246
+ end
247
+ request
248
+ end
249
+ end
250
+
251
+ # Retrieves the followers records for a group of Twitter users.
252
+ # +twitter_id_array+ = An array twitter user ids, one for each user to get data for
253
+ def get_statuses_followers twitter_id_array, limit=nil
254
+ master_results = {}
255
+ process_statuses_followers( twitter_id_array ) do |twitter_id, results|
256
+ master_results[twitter_id] ||= []
257
+ if results.is_a? TwitterException
258
+ master_results[twitter_id] = results
259
+ false
260
+ else
261
+ master_results[twitter_id] += results
262
+ if limit && master_results[twitter_id].length >= limit
263
+ master_results[twitter_id] = master_results[twitter_id].slice(0, limit)
264
+ continue = false
265
+ else
266
+ continue = true
267
+ end
268
+ logger.debug "#{twitter_id} - #{master_results[twitter_id].length} followers retrieved."
269
+ continue
270
+ end
271
+ end
272
+ master_results
273
+ end
274
+
275
+ # Retrieves the followers records for a group of twitter_ids from Twitter and feeds them to the supplied
276
+ # block one page at a time. The block passed is expected to return a true or false value. If it
277
+ # returns true, fetching of followers will continue for that twitter_id. If it returns false, fetching
278
+ # of followers will be aborted for that twitter_id only. This allows a batch of fetches to be started for
279
+ # multiple users. Fetching of individual user's followers may be aborted while continuing the others.
280
+ # +twitter_ids+ = An array twitter twitter_ids, one for each user to get data for.
281
+ #
282
+ # Returns nil.
283
+ #
284
+ # eg.
285
+ #
286
+ # process_statuses_followers( ['bdoughty', 'joshuabaer'] ) do |twitter_id, followers|
287
+ # puts "Twitter user #{twitter_id}"
288
+ # continue = true
289
+ # followers.each do |follower|
290
+ # continue = false if follower[:twitter_id] == 'needle'
291
+ # end
292
+ # continue
293
+ # end
294
+ def process_statuses_followers twitter_id_array, &block
295
+
296
+ raise "You must supply a block to this method." if !block_given?
297
+ # Track the proper Twitter API cursor for each twitter_id. Twitter requests an initial cursor of -1 (to begin paging)
298
+ cursor_tracker = {}
299
+ twitter_id_array.each do |twitter_id|
300
+ cursor_tracker[twitter_id] = -1
301
+ end
302
+
303
+ while( cursor_tracker.size > 0 )
304
+ twitter_results = typho_twitter_batch( cursor_tracker.keys ) do |twitter_id|
305
+ if twitter_id.is_a? Fixnum
306
+ request = Typhoeus::Request.new("http://twitter.com/statuses/followers.json?cursor=#{cursor_tracker[twitter_id]}&user_id=#{twitter_id}",
307
+ :headers => @headers,
308
+ :timeout => @request_timeout
309
+ )
310
+ else
311
+ request = Typhoeus::Request.new("http://twitter.com/statuses/followers.json?cursor=#{cursor_tracker[twitter_id]}&screen_name=#{twitter_id}",
312
+ :timeout => @request_timeout,
313
+ :headers => @headers
314
+ )
315
+ end
316
+ end
317
+ cursor_tracker = {}
318
+ twitter_results.each do |twitter_id, results|
319
+ next_cursor = 0
320
+ if results.is_a?( Hash ) && results['users'] && results['users'].length > 0
321
+ next_cursor = results["next_cursor"]
322
+ continue = yield( twitter_id, results['users'] )
323
+ else
324
+ continue = yield( twitter_id, results ) # return the exception
325
+ end
326
+ if next_cursor != 0 && continue
327
+ cursor_tracker[twitter_id] = next_cursor
328
+ else
329
+ cursor_tracker.delete( twitter_id ) # remove the twitter_id from processing
330
+ end
331
+ end
332
+ end
333
+
334
+ nil
335
+ end
336
+
337
+ # Retrieves the followers ids for a group of twitter_ids from Twitter and feeds them to the supplied
338
+ # block one page at a time. The block passed is expected to return a true or false value. If it
339
+ # returns true, fetching of follower ids will continue for that twitter_id. If it returns false, fetching
340
+ # of follower ids will be aborted for that twitter_id only. This allows a batch of fetches to be started for
341
+ # multiple users. Fetching of individual user's followers ids may be aborted while continuing the others.
342
+ # +twitter_ids+ = An array twitter twitter_ids, one for each user to get data for.
343
+ #
344
+ # Returns nil.
345
+ #
346
+ # eg.
347
+ #
348
+ # process_followers_ids( ['bdoughty', 'joshuabaer'] ) do |twitter_id, follower_ids|
349
+ # puts "Twitter user #{twitter_id}"
350
+ # continue = true
351
+ # follower_ids.each do |follower_id|
352
+ # continue = false if follower_id == SOME_TWITTER_USER_ID
353
+ # end
354
+ # continue
355
+ # end
356
+ def process_followers_ids twitter_id_array, &block
357
+
358
+ raise "You must supply a block to this method." if !block_given?
359
+ # Track the proper Twitter API cursor for each twitter_id. Twitter requests an initial cursor of -1 (to begin paging)
360
+ cursor_tracker = {}
361
+ twitter_id_array.each do |twitter_id|
362
+ cursor_tracker[twitter_id] = -1
363
+ end
364
+
365
+ while( cursor_tracker.size > 0 )
366
+ twitter_results = typho_twitter_batch( cursor_tracker.keys ) do |twitter_id|
367
+ if twitter_id.is_a? Fixnum
368
+ request = Typhoeus::Request.new("http://twitter.com/followers/ids.json?cursor=#{cursor_tracker[twitter_id]}&user_id=#{twitter_id}",
369
+ :headers => @headers,
370
+ :timeout => @request_timeout
371
+ )
372
+ else
373
+ request = Typhoeus::Request.new("http://twitter.com/followers/ids.json?cursor=#{cursor_tracker[twitter_id]}&screen_name=#{twitter_id}",
374
+ :headers => @headers,
375
+ :timeout => @request_timeout
376
+ )
377
+ end
378
+ end
379
+ cursor_tracker = {}
380
+ twitter_results.each do |twitter_id, results|
381
+ next_cursor = 0
382
+ if results.is_a?( Hash ) && results['ids'] && results['ids'].length > 0
383
+ next_cursor = results["next_cursor"]
384
+ continue = yield( twitter_id, results['ids'] )
385
+ else
386
+ continue = yield( twitter_id, results ) # return the exception
387
+ end
388
+ if next_cursor != 0 && continue
389
+ cursor_tracker[twitter_id] = next_cursor
390
+ else
391
+ cursor_tracker.delete( twitter_id ) # remove the twitter_id from processing
392
+ end
393
+ end
394
+ end
395
+
396
+ nil
397
+ end
398
+
399
+ # Retrieves all timeline updates for a group of twitter_ids from Twitter.
400
+ # This method calls process_statuses_user_timeline() with a block to aggregate the updates.
401
+ # +twitter_id_array+ = An array twitter user ids, one for each user to get data for
402
+ # Returns aggregated updates as a Hash with twitter_ids as keys, and arrays of updates as values.
403
+ # If an unresolvable exception occurred fetching a particular twitter_id, then the resulting TwitterException
404
+ # is returned for that screen name instead of an array of updates.
405
+ def get_statuses_user_timeline twitter_id_array
406
+ master_results = {}
407
+ process_statuses_user_timeline( twitter_id_array ) do |twitter_id, results|
408
+ master_results[twitter_id] ||= []
409
+ if results.is_a? TwitterException
410
+ master_results[twitter_id] = results
411
+ false
412
+ else
413
+ master_results[twitter_id] += results
414
+ true
415
+ end
416
+ end
417
+ master_results
418
+ end
419
+
420
+ # Retrieves the timeline updates for a group of twitter_ids from Twitter and feeds them to the supplied
421
+ # block one page at a time. The block passed is expected to return a true or false value. If it
422
+ # returns true, fetching of updates will continue for that twitter_id. If it returns false, fetching
423
+ # of updates will be aborted for that twitter_id only. This allows a batch of fetches to be started for
424
+ # multiple users. Fetching of individual user's updates may be aborted while continuing the others.
425
+ # +twitter_ids+ = An array twitter user ids, one for each user to get data for.
426
+ #
427
+ # Returns nil.
428
+ #
429
+ # eg.
430
+ #
431
+ # process_statuses_user_timeline( ['bdoughty', 'joshuabaer'] ) do |twitter_id, updates|
432
+ # puts "Twitter user #{twitter_id}"
433
+ # updates.each do |update|
434
+ # # do something with each status update
435
+ # end
436
+ # (twitter_id == 'bdoughty') # block return value - aborts 'joshuabaer' after the first page, continues 'bdoughty'
437
+ # end
438
+ def process_statuses_user_timeline twitter_ids, &block
439
+ page = 0
440
+ count = 200
441
+ while twitter_ids.length > 0
442
+ page += 1 # Twitter starts with page 1
443
+ logger.debug "Getting page #{page} for timelines."
444
+ twitter_results = typho_twitter_batch( twitter_ids ) do |twitter_id|
445
+ if twitter_id.is_a? Fixnum
446
+ request = Typhoeus::Request.new("http://twitter.com/statuses/user_timeline.json?user_id=#{twitter_id}&page=#{page}&count=#{count}",
447
+ :headers => @headers,
448
+ :timeout => @request_timeout
449
+ )
450
+ else
451
+ request = Typhoeus::Request.new("http://twitter.com/statuses/user_timeline.json?screen_name=#{twitter_id}&page=#{page}&count=#{count}",
452
+ :headers => @headers,
453
+ :timeout => @request_timeout
454
+ )
455
+ end
456
+ end
457
+
458
+ twitter_ids = []
459
+ twitter_results.each do |twitter_id, results|
460
+ if results && !( results.respond_to?( :length ) && results.length == 0 )
461
+ if block_given?
462
+ continue = yield( twitter_id, results )
463
+ else
464
+ raise "You must supply a block to this method."
465
+ end
466
+ # keep fetching for this twitter_id only if the block said to and there are more updates.
467
+ twitter_ids << twitter_id if continue && !results.is_a?( TwitterException ) && results.length != 0
468
+ end
469
+ end
470
+ end
471
+ nil
472
+ end
473
+
474
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'typho-twitter'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ module SpecLogger
8
+ def log message
9
+ puts message.to_s + "<br/>"
10
+ end
11
+ end
12
+
13
+ Spec::Runner.configure do |config|
14
+
15
+ end
@@ -0,0 +1,7 @@
1
+ ---
2
+ :oauth:
3
+ :consumer_key: CONSUMER_KEY
4
+ :consumer_secret: CONSUMER_SECRET
5
+ :secret: ACCESS_TOKEN_SECRET
6
+ :token: ACCESS_TOKEN
7
+ :site: http://example.com
@@ -0,0 +1,243 @@
1
+ # A brutally simple test suite to verify desired functionality for +my+ application.
2
+ # TODO: Generalize, and make unit tests.
3
+ # NOTE: These tests are meant to be run against a white-listed account.
4
+ # To authorize with Twitter, you need to copy the oauth.yaml.example to oauth.yaml
5
+ # and populate it with the OAuth credentials for a white-listed account.
6
+
7
+ BASE_DIR="#{File.dirname(__FILE__)}/../lib"
8
+ $: << BASE_DIR
9
+ require 'typho-twitter'
10
+ require 'test/unit'
11
+ include Test::Unit::Assertions
12
+ include WDD::Utils
13
+
14
+ UNKNOWN_ID = 'mixtercox'
15
+ TEST_USER_IDS = [
16
+ UNKNOWN_ID,
17
+ "bdoughty",
18
+ "joshuabaer",
19
+ "jotto",
20
+ "hoonpark",
21
+ "aplusk",
22
+ "barackobama",
23
+ "oprah",
24
+ "damon",
25
+ "TreyPlaysTunes",
26
+ "fiveredwoods",
27
+ "austinonrails",
28
+ "remlap42",
29
+ ]
30
+
31
+ # basic test that all is functioning and that we can lookup an array of screen_names successfully
32
+ def test_users_show_multi
33
+ screen_names = TEST_USER_IDS
34
+ screen_name_array = screen_names
35
+ # 10.times do |i|
36
+ # screen_name_array += screen_names.map{|c| c+i}
37
+ # end
38
+ twitter = TyphoTwitter.new( @oauth_options )
39
+ # responses = twitter.typho_twitter_batch screen_name_array
40
+ responses = twitter.get_users_show( screen_name_array )
41
+ responses.each do |key, value|
42
+ puts "#{key} => "
43
+ if key == UNKNOWN_ID
44
+ assert_instance_of( TyphoTwitter::TwitterException, value )
45
+ end
46
+ puts "#{value}"
47
+ end
48
+ puts "# Responses: #{responses.size}"
49
+ assert_equal( responses.size, screen_name_array.size )
50
+ end
51
+
52
+ # Verify that things still function correctly if we are only looking up one user.
53
+ def test_users_show_single
54
+ screen_names = [
55
+ "bdoughty"
56
+ ]
57
+ screen_name_array = screen_names
58
+ # screen_name_array += screen_names
59
+ twitter = TyphoTwitter.new( @oauth_options )
60
+ responses = twitter.get_users_show( screen_name_array )
61
+ responses.each do |key, value|
62
+ puts "#{key} => "
63
+ puts "#{value}"
64
+ end
65
+ puts "# Responses: #{responses.size}"
66
+ assert_equal( responses.size, screen_name_array.size )
67
+ end
68
+
69
+ # Test getting timelines passing a block to process_statuses_user_timeline.
70
+ def test_process_statuses_user_timeline
71
+ screen_names = TEST_USER_IDS
72
+ screen_name_array = screen_names
73
+ # screen_name_array += screen_names
74
+ twitter = TyphoTwitter.new( @oauth_options )
75
+ user_updates = {}
76
+ twitter.process_statuses_user_timeline( screen_name_array ) do |screen_name, updates|
77
+ if screen_name == UNKNOWN_ID
78
+ assert_instance_of( TyphoTwitter::TwitterException, updates )
79
+ puts "Verified that #{UNKNOWN_ID} is unknown."
80
+ false
81
+ else
82
+ user_updates[screen_name] ||= []
83
+ user_updates[screen_name] += updates
84
+ true
85
+ end
86
+ end
87
+ user_updates.each do |key, value|
88
+ puts "#{key} => "
89
+ puts value.length
90
+ end
91
+ # puts "# Responses: #{responses.size}"
92
+ assert_equal( user_updates.size, screen_name_array.size - 1)
93
+ end
94
+
95
+ # Test getting timelines from get_statuses_user_timeline
96
+ def test_get_statuses_user_timeline
97
+ screen_names = TEST_USER_IDS
98
+ screen_name_array = screen_names
99
+ # screen_name_array += screen_names
100
+ twitter = TyphoTwitter.new( @oauth_options )
101
+ user_updates = twitter.get_statuses_user_timeline( screen_name_array )
102
+ user_updates.each do |key, value|
103
+ puts "#{key} => "
104
+ if key == UNKNOWN_ID
105
+ assert_instance_of( TyphoTwitter::TwitterException, value )
106
+ puts "Verified that #{UNKNOWN_ID} is unknown."
107
+ else
108
+ case value.class.to_s
109
+ when 'TyphoTwitter::TwitterException'
110
+ puts value.inspect
111
+ else
112
+ puts value.length
113
+ end
114
+ end
115
+ # pp value
116
+ end
117
+ # puts "# Responses: #{responses.size}"
118
+ assert_equal( user_updates.size, screen_name_array.size )
119
+ end
120
+
121
+ # Test getting followers
122
+ def test_get_statuses_followers
123
+ # screen_names = ['hoonpark', 'bdoughty', 'jotto']
124
+ screen_names = ['bdoughty']
125
+ screen_name_array = screen_names
126
+ # screen_name_array += screen_names
127
+ twitter = TyphoTwitter.new( @oauth_options )
128
+ followers = twitter.get_statuses_followers( screen_name_array )
129
+ followers.each do |screen_name, results|
130
+ puts "#{screen_name}:"
131
+ puts "#{results.length} followers:"
132
+ results.each do |follower|
133
+ printvar :follower, follower
134
+ end
135
+ # pp value
136
+ end
137
+ assert_equal( followers.size, screen_name_array.size )
138
+
139
+ screen_names = ['basdkhasdf']
140
+ screen_name_array = screen_names
141
+ # screen_name_array += screen_names
142
+ twitter = TyphoTwitter.new( @oauth_options )
143
+ followers = twitter.get_statuses_followers( screen_name_array )
144
+ followers.each do |screen_name, results|
145
+ puts "#{screen_name}:"
146
+ assert_instance_of TyphoTwitter::TwitterException, results
147
+ end
148
+ end
149
+
150
+ # Test getting followers
151
+ def test_get_statuses_followers_with_limit
152
+ # screen_names = ['hoonpark', 'bdoughty', 'jotto']
153
+ screen_names = ['joshuabaer']
154
+ screen_name_array = screen_names
155
+ # screen_name_array += screen_names
156
+ twitter = TyphoTwitter.new( @oauth_options )
157
+ followers_data = twitter.get_statuses_followers( screen_name_array, 100 )
158
+ followers_data.each do |screen_name, followers|
159
+ puts "#{screen_name}:"
160
+ puts "#{followers.length} followers:"
161
+ followers.each do |follower|
162
+ printvar :follower, follower['screen_name']
163
+ end
164
+ assert_equal( followers.size <= 100, true )
165
+ # pp value
166
+ end
167
+ assert_equal( followers_data.size, screen_name_array.size )
168
+
169
+ screen_names = ['basdkhasdf']
170
+ screen_name_array = screen_names
171
+ # screen_name_array += screen_names
172
+ twitter = TyphoTwitter.new( @oauth_options )
173
+ followers = twitter.get_statuses_followers( screen_name_array )
174
+ followers.each do |screen_name, results|
175
+ puts "#{screen_name}:"
176
+ assert_instance_of TyphoTwitter::TwitterException, results
177
+ end
178
+ end
179
+
180
+ # Test getting timelines passing a block to process_statuses_user_timeline. Process the updates and abort
181
+ # on condition
182
+ def test_process_statuses_user_timeline_with_abort
183
+ screen_names = TEST_USER_IDS
184
+ screen_name_array = screen_names
185
+ # screen_name_array += screen_names
186
+ twitter = TyphoTwitter.new( @oauth_options )
187
+ user_updates = {}
188
+ twitter.process_statuses_user_timeline( screen_name_array ) do |screen_name, results|
189
+ puts "#{screen_name} =>"
190
+ continue = true
191
+ if results.is_a? TyphoTwitter::TwitterException
192
+ puts "Exception for #{screen_name}: #{results.inspect}"
193
+ continue = false
194
+ else
195
+ results.each do |update|
196
+ # puts update["text"]
197
+ if update["text"] =~ /otherinbox/i
198
+ puts "ABORTING #{screen_name} because of tweet:"
199
+ puts update["text"]
200
+ continue = false
201
+ break
202
+ end
203
+ end
204
+ user_updates[screen_name] ||= []
205
+ user_updates[screen_name] += results
206
+ end
207
+
208
+ continue
209
+ end
210
+ user_updates.each do |key, value|
211
+ puts "#{key} => "
212
+ puts value.length
213
+ # pp value
214
+ end
215
+ # puts "# Responses: #{responses.size}"
216
+ assert_equal( user_updates.size, screen_name_array.size - 1 )
217
+ end
218
+
219
+ def time_it method_id
220
+ et = WDD::Utils::elapsed_time do
221
+ self.send( method_id )
222
+ end
223
+ puts "========================================"
224
+ puts "#{method_id.to_s} - #{et} seconds"
225
+ puts
226
+ end
227
+
228
+ def run_tests
229
+ @oauth_options = YAML.load_file(File.dirname($0)+'/oauth.yaml')
230
+ if ARGV[0] && ARGV[0] != ""
231
+ eval ARGV[0]
232
+ else
233
+ time_it :test_users_show_multi
234
+ time_it :test_users_show_single
235
+ time_it :test_process_statuses_user_timeline
236
+ time_it :test_get_statuses_user_timeline
237
+ time_it :test_get_statuses_followers
238
+ time_it :test_get_statuses_followers_with_limit
239
+ time_it :test_process_statuses_user_timeline_with_abort
240
+ end
241
+ end
242
+
243
+ run_tests
@@ -0,0 +1,63 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{typho-twitter}
8
+ s.version = "0.2.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["shock"]
12
+ s.date = %q{2011-01-16}
13
+ s.description = %q{A Twitter client for performing a batch of Twitter calls in parallel.}
14
+ s.email = %q{billdoughty@capitalthought.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/typho-twitter.rb",
27
+ "lib/typho-twitter/typho-twitter.rb",
28
+ "spec/spec.opts",
29
+ "spec/spec_helper.rb",
30
+ "test/oauth.yaml.example",
31
+ "test/typho-twitter-test.rb",
32
+ "typho-twitter.gemspec"
33
+ ]
34
+ s.homepage = %q{http://github.com/capitalthought/typho-twitter}
35
+ s.rdoc_options = ["--charset=UTF-8"]
36
+ s.require_paths = ["lib"]
37
+ s.rubygems_version = %q{1.3.7}
38
+ s.summary = %q{Parallel twitter client using Typhoeus.}
39
+ s.test_files = [
40
+ "spec/spec_helper.rb",
41
+ "test/typho-twitter-test.rb"
42
+ ]
43
+
44
+ if s.respond_to? :specification_version then
45
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
46
+ s.specification_version = 3
47
+
48
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
49
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
50
+ s.add_runtime_dependency(%q<wdd-ruby-ext>, [">= 0.3.1"])
51
+ s.add_runtime_dependency(%q<oauth>, [">= 0.4.3"])
52
+ else
53
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
54
+ s.add_dependency(%q<wdd-ruby-ext>, [">= 0.3.1"])
55
+ s.add_dependency(%q<oauth>, [">= 0.4.3"])
56
+ end
57
+ else
58
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
59
+ s.add_dependency(%q<wdd-ruby-ext>, [">= 0.3.1"])
60
+ s.add_dependency(%q<oauth>, [">= 0.4.3"])
61
+ end
62
+ end
63
+
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: typho-twitter
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 1
10
+ version: 0.2.1
11
+ platform: ruby
12
+ authors:
13
+ - shock
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-16 00:00:00 -06:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 13
30
+ segments:
31
+ - 1
32
+ - 2
33
+ - 9
34
+ version: 1.2.9
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: wdd-ruby-ext
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 17
46
+ segments:
47
+ - 0
48
+ - 3
49
+ - 1
50
+ version: 0.3.1
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: oauth
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 9
62
+ segments:
63
+ - 0
64
+ - 4
65
+ - 3
66
+ version: 0.4.3
67
+ type: :runtime
68
+ version_requirements: *id003
69
+ description: A Twitter client for performing a batch of Twitter calls in parallel.
70
+ email: billdoughty@capitalthought.com
71
+ executables: []
72
+
73
+ extensions: []
74
+
75
+ extra_rdoc_files:
76
+ - LICENSE
77
+ - README.rdoc
78
+ files:
79
+ - .document
80
+ - .gitignore
81
+ - LICENSE
82
+ - README.rdoc
83
+ - Rakefile
84
+ - VERSION
85
+ - lib/typho-twitter.rb
86
+ - lib/typho-twitter/typho-twitter.rb
87
+ - spec/spec.opts
88
+ - spec/spec_helper.rb
89
+ - test/oauth.yaml.example
90
+ - test/typho-twitter-test.rb
91
+ - typho-twitter.gemspec
92
+ has_rdoc: true
93
+ homepage: http://github.com/capitalthought/typho-twitter
94
+ licenses: []
95
+
96
+ post_install_message:
97
+ rdoc_options:
98
+ - --charset=UTF-8
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ hash: 3
107
+ segments:
108
+ - 0
109
+ version: "0"
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ hash: 3
116
+ segments:
117
+ - 0
118
+ version: "0"
119
+ requirements: []
120
+
121
+ rubyforge_project:
122
+ rubygems_version: 1.3.7
123
+ signing_key:
124
+ specification_version: 3
125
+ summary: Parallel twitter client using Typhoeus.
126
+ test_files:
127
+ - spec/spec_helper.rb
128
+ - test/typho-twitter-test.rb