typho-twitter 0.2.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/.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