xh5-tweetstream 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Intridea, Inc.
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.
@@ -0,0 +1,166 @@
1
+ = TweetStream
2
+
3
+ TweetStream provides simple Ruby access to Twitter's Streaming API
4
+ (http://apiwiki.twitter.com/Streaming-API-Documentation).
5
+
6
+ This fork has been modified to support the site_streams API currently in Beta.
7
+
8
+ We have also added support for location-based queries to the streaming API.
9
+
10
+ == Installation
11
+
12
+ To install from Gemcutter:
13
+
14
+ gem install tweetstream
15
+
16
+ == Usage
17
+
18
+ Using TweetStream is quite simple:
19
+
20
+ require 'rubygems'
21
+ require 'tweetstream'
22
+
23
+ # This will pull a sample of all tweets based on
24
+ # your Twitter account's Streaming API role.
25
+ TweetStream::Client.new('username','password').sample do |status|
26
+ # The status object is a special Hash with
27
+ # method access to its keys.
28
+ puts "#{status.text}"
29
+ end
30
+
31
+ You can also use it to track keywords or follow a given set of
32
+ user ids:
33
+
34
+ # Use 'track' to track a list of single-word keywords
35
+ TweetStream::Client.new('username','password').track('term1', 'term2') do |status|
36
+ puts "#{status.text}"
37
+ end
38
+
39
+ # Use 'follow' to follow a group of user ids (integers, not screen names)
40
+ TweetStream::Client.new('username','password').follow(14252, 53235) do |status|
41
+ puts "#{status.text}"
42
+ end
43
+
44
+ The methods available to TweetStream::Client will be kept in parity
45
+ with the methods available on the Streaming API wiki page.
46
+
47
+ == Swappable JSON Parsing
48
+
49
+ As of version 1.0, TweetStream supports swappable JSON backends for
50
+ parsing the Tweets. These are specified when you initialize the
51
+ client or daemon by passing it in as the last argument:
52
+
53
+ # Parse tweets using Yajl-Ruby
54
+ TweetStream::Client.new('abc','def',:yajl) # ...
55
+
56
+ Available options are <tt>:yajl</tt>, <tt>:json_gem</tt> (default),
57
+ <tt>:json_pure</tt>, and <tt>:active_support</tt>.
58
+
59
+ == Handling Deletes and Rate Limitations
60
+
61
+ Sometimes the Streaming API will send messages other than statuses.
62
+ Specifically, it does so when a status is deleted or rate limitations
63
+ have caused some tweets not to appear in the stream. To handle these,
64
+ you can use the on_delete and on_limit methods. Example:
65
+
66
+ @client = TweetStream::Client.new('user','pass')
67
+
68
+ @client.on_delete do |status_id, user_id|
69
+ Tweet.delete(status_id)
70
+ end
71
+
72
+ @client.on_limit do |skip_count|
73
+ # do something
74
+ end
75
+
76
+ @client.track('intridea')
77
+
78
+ The on_delete and on_limit methods can also be chained, like so:
79
+
80
+ TweetStream::Client.new('user','pass').on_delete{ |status_id, user_id|
81
+ Tweet.delete(status_id)
82
+ }.on_limit { |skip_count|
83
+ # do something
84
+ }.track('intridea') do |status|
85
+ # do something with the status like normal
86
+ end
87
+
88
+ You can also provide <tt>:delete</tt> and/or <tt>:limit</tt>
89
+ options when you make your method call:
90
+
91
+ TweetStream::Client.new('user','pass').track('intridea',
92
+ :delete => Proc.new{ |status_id, user_id| # do something },
93
+ :limit => Proc.new{ |skip_count| # do something }
94
+ ) do |status|
95
+ # do something with the status like normal
96
+ end
97
+
98
+ Twitter recommends honoring deletions as quickly as possible, and
99
+ you would likely be wise to integrate this functionality into your
100
+ application.
101
+
102
+ == Errors and Reconnecting
103
+
104
+ TweetStream uses EventMachine to connect to the Twitter Streaming
105
+ API, and attempts to honor Twitter's guidelines in terms of automatic
106
+ reconnection. When Twitter becomes unavailable, the block specified
107
+ by you in <tt>on_error</tt> will be called. Note that this does not
108
+ indicate something is actually wrong, just that Twitter is momentarily
109
+ down. It could be for routine maintenance, etc.
110
+
111
+ TweetStream::Client.new('abc','def').on_error do |message|
112
+ # Log your error message somewhere
113
+ end.track('term') do |status|
114
+ # Do things when nothing's wrong
115
+ end
116
+
117
+ However, if the maximum number of reconnect attempts has been reached,
118
+ TweetStream will raise a <tt>TweetStream::ReconnectError</tt> with
119
+ information about the timeout and number of retries attempted.
120
+
121
+ == Terminating a TweetStream
122
+
123
+ It is often the case that you will need to change the parameters of your
124
+ track or follow tweet streams. In the case that you need to terminate
125
+ a stream, you may add a second argument to your block that will yield
126
+ the client itself:
127
+
128
+ # Stop after collecting 10 statuses
129
+ @statuses = []
130
+ TweetStream::Client.new('username','password').sample do |status, client|
131
+ @statuses << status
132
+ client.stop if @statuses.size >= 10
133
+ end
134
+
135
+ When <tt>stop</tt> is called, TweetStream will return from the block
136
+ the last successfully yielded status, allowing you to make note of
137
+ it in your application as necessary.
138
+
139
+ == Daemonizing
140
+
141
+ It is also possible to create a daemonized script quite easily
142
+ using the TweetStream library:
143
+
144
+ # The third argument is an optional process name
145
+ TweetStream::Daemon.new('username','password', 'tracker').track('term1', 'term2') do |status|
146
+ # do something in the background
147
+ end
148
+
149
+ If you put the above into a script and run the script with <tt>ruby scriptname.rb</tt>, you will see a list of daemonization commands such
150
+ as start, stop, and run.
151
+
152
+ == Note on Patches/Pull Requests
153
+
154
+ * Fork the project.
155
+ * Make your feature addition or bug fix.
156
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
157
+ * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
158
+ * Send me a pull request. Bonus points for topic branches.
159
+
160
+ == Contributors
161
+
162
+ * Michael Bleigh (initial gem)
163
+
164
+ == Copyright
165
+
166
+ Copyright (c) 2009 Intridea, Inc. (http://www.intridea.com/). See LICENSE for details.
@@ -0,0 +1,5 @@
1
+ == Version 1.0.0
2
+
3
+ * Swappable JSON backend support
4
+ * Switches to use EventMachine instead of Yajl for the HTTP Stream
5
+ * Support reconnect and on_error
@@ -0,0 +1,60 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "xh5-tweetstream"
8
+ gem.summary = %Q{TweetStream is a simple wrapper for consuming the Twitter Streaming API.}
9
+ gem.description = %Q{TweetStream allows you to easily consume the Twitter Streaming API utilizing the YAJL Ruby gem.}
10
+ gem.email = "eric@xhfive.com"
11
+ gem.homepage = "http://github.com/erichurst/tweetstream"
12
+ gem.authors = ["Michael Bleigh"]
13
+ gem.files = FileList["[A-Z]*", "{lib,spec,examples}/**/*"] - FileList["**/*.log"]
14
+ gem.add_development_dependency "rspec"
15
+ gem.add_dependency 'xh5-twitter-stream'
16
+ gem.add_dependency 'daemons'
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
22
+ end
23
+
24
+ namespace :release do
25
+ %w(patch minor major).each do |level|
26
+ desc "Tag a #{level} version and push it to Gemcutter."
27
+ task level.to_sym => %w(version:bump:patch release gemcutter:release)
28
+ end
29
+ end
30
+
31
+ require 'spec/rake/spectask'
32
+ Spec::Rake::SpecTask.new(:spec) do |spec|
33
+ spec.libs << 'lib' << 'spec'
34
+ spec.spec_files = FileList['spec/**/*_spec.rb']
35
+ end
36
+
37
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
38
+ spec.libs << 'lib' << 'spec'
39
+ spec.pattern = 'spec/**/*_spec.rb'
40
+ spec.rcov = true
41
+ spec.rcov_opts = %w{--exclude "spec\/*,gems\/*"}
42
+ end
43
+
44
+ task :spec => :check_dependencies
45
+
46
+ task :default => :spec
47
+
48
+ require 'rake/rdoctask'
49
+ Rake::RDocTask.new do |rdoc|
50
+ if File.exist?('VERSION')
51
+ version = File.read('VERSION')
52
+ else
53
+ version = ""
54
+ end
55
+
56
+ rdoc.rdoc_dir = 'rdoc'
57
+ rdoc.title = "tweetstream #{version}"
58
+ rdoc.rdoc_files.include('README*')
59
+ rdoc.rdoc_files.include('lib/**/*.rb')
60
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.1.0
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'tweetstream'
3
+ require 'ruby-growl'
4
+
5
+ if args_start = ARGV.index('--')
6
+ username, password = ARGV[args_start + 1].split(':')
7
+ tracks = ARGV[args_start + 2 .. -1]
8
+ puts "Starting a GrowlTweet to track: #{tracks.inspect}"
9
+ end
10
+
11
+ TweetStream::Daemon.new(username,password).track(*tracks) do |status|
12
+ g = Growl.new 'localhost', 'growltweet', ['tweet']
13
+ g.notify 'tweet', status.user.screen_name, status.text
14
+ end
@@ -0,0 +1,21 @@
1
+ require 'tweetstream/client'
2
+ require 'tweetstream/hash'
3
+ require 'tweetstream/status'
4
+ require 'tweetstream/user'
5
+ require 'tweetstream/daemon'
6
+
7
+ module TweetStream
8
+ class Terminated < ::StandardError; end
9
+ class Error < ::StandardError; end
10
+ class ConnectionError < TweetStream::Error; end
11
+ # A ReconnectError is raised when the maximum number of retries has
12
+ # failed to re-establish a connection.
13
+ class ReconnectError < StandardError
14
+ attr_accessor :timeout, :retries
15
+ def initialize(timeout, retries)
16
+ self.timeout = timeout
17
+ self.retries = retries
18
+ super("Failed to reconnect after #{retries} tries.")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,318 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ require 'eventmachine'
4
+ require 'twitter_stream/json_stream'
5
+ require 'json'
6
+
7
+ module TweetStream
8
+ # Provides simple access to the Twitter Streaming API (http://apiwiki.twitter.com/Streaming-API-Documentation)
9
+ # for Ruby scripts that need to create a long connection to
10
+ # Twitter for tracking and other purposes.
11
+ #
12
+ # Basic usage of the library is to call one of the provided
13
+ # methods and provide a block that will perform actions on
14
+ # a yielded TweetStream::Status. For example:
15
+ #
16
+ # TweetStream::Client.new('user','pass').track('fail') do |status|
17
+ # puts "[#{status.user.screen_name}] #{status.text}"
18
+ # end
19
+ #
20
+ # For information about a daemonized TweetStream client,
21
+ # view the TweetStream::Daemon class.
22
+ class Client
23
+ attr_accessor :consumer_key, :consumer_secret, :access_key, :access_secret
24
+ attr_reader :parser
25
+
26
+ # Set the JSON Parser for this client. Acceptable options are:
27
+ #
28
+ # <tt>:json_gem</tt>:: Parse using the JSON gem.
29
+ # <tt>:json_pure</tt>:: Parse using the pure-ruby implementation of the JSON gem.
30
+ # <tt>:active_support</tt>:: Parse using ActiveSupport::JSON.decode
31
+ # <tt>:yajl</tt>:: Parse using <tt>yajl-ruby</tt>.
32
+ #
33
+ # You may also pass a class that will return a hash with symbolized
34
+ # keys when <tt>YourClass.parse</tt> is called with a JSON string.
35
+ def parser=(parser)
36
+ @parser = parser_from(parser)
37
+ end
38
+
39
+ # Create a new client with the Twitter credentials
40
+ # of the account you want to be using its API quota.
41
+ # You may also set the JSON parsing library as specified
42
+ # in the #parser= setter.
43
+ def initialize(consumer_key, consumer_secret, access_key, access_secret, parser = :json_gem)
44
+ self.consumer_key = consumer_key
45
+ self.consumer_secret = consumer_secret
46
+ self.access_key = access_key
47
+ self.access_secret = access_secret
48
+ self.parser = parser
49
+ end
50
+
51
+ # Returns all public statuses. The Firehose is not a generally
52
+ # available resource. Few applications require this level of access.
53
+ # Creative use of a combination of other resources and various access
54
+ # levels can satisfy nearly every application use case.
55
+ def firehose(query_parameters = {}, &block)
56
+ start('statuses/firehose', query_parameters, &block)
57
+ end
58
+
59
+ # Returns all retweets. The retweet stream is not a generally available
60
+ # resource. Few applications require this level of access. Creative
61
+ # use of a combination of other resources and various access levels
62
+ # can satisfy nearly every application use case. As of 9/11/2009,
63
+ # the site-wide retweet feature has not yet launched,
64
+ # so there are currently few, if any, retweets on this stream.
65
+ def retweet(query_parameters = {}, &block)
66
+ start('statuses/retweet', query_parameters, &block)
67
+ end
68
+
69
+ # Returns a random sample of all public statuses. The default access level
70
+ # provides a small proportion of the Firehose. The "Gardenhose" access
71
+ # level provides a proportion more suitable for data mining and
72
+ # research applications that desire a larger proportion to be statistically
73
+ # significant sample.
74
+ def sample(query_parameters = {}, &block)
75
+ start('statuses/sample', query_parameters, &block)
76
+ end
77
+
78
+ # Specify keywords to track. Queries are subject to Track Limitations,
79
+ # described in Track Limiting and subject to access roles, described in
80
+ # the statuses/filter method. Track keywords are case-insensitive logical
81
+ # ORs. Terms are exact-matched, and also exact-matched ignoring
82
+ # punctuation. Phrases, keywords with spaces, are not supported.
83
+ # Keywords containing punctuation will only exact match tokens.
84
+ # Query parameters may be passed as the last argument.
85
+ def track(*keywords, &block)
86
+ query_params = keywords.pop if keywords.last.is_a?(::Hash)
87
+ query_params ||= {}
88
+ filter(query_params.merge(:track => keywords), &block)
89
+ end
90
+
91
+ # Returns public statuses from or in reply to a set of users. Mentions
92
+ # ("Hello @user!") and implicit replies ("@user Hello!" created without
93
+ # pressing the reply "swoosh") are not matched. Requires integer user
94
+ # IDs, not screen names. Query parameters may be passed as the last argument.
95
+ def follow(*user_ids, &block)
96
+ query_params = user_ids.pop if user_ids.last.is_a?(::Hash)
97
+ query_params ||= {}
98
+ filter(query_params.merge(:follow => user_ids), &block)
99
+ end
100
+
101
+ def locations(coords, &block)
102
+ filter(query_params.merge(:locations => coords), &block)
103
+ end
104
+
105
+ # Make a call to the statuses/filter method of the Streaming API,
106
+ # you may provide <tt>:follow</tt>, <tt>:track</tt> or both as options
107
+ # to follow the tweets of specified users or track keywords. This
108
+ # method is provided separately for cases when it would conserve the
109
+ # number of HTTP connections to combine track and follow.
110
+ def filter(query_params = {}, &block)
111
+ [:follow, :track, :locations].each do |param|
112
+ if query_params[param].is_a?(Array)
113
+ query_params[param] = query_params[param].collect{|q| q.to_s}.join(',')
114
+ elsif query_params[param]
115
+ query_params[param] = query_params[param].to_s
116
+ end
117
+ end
118
+ start('statuses/filter', query_params.merge(:method => :post), &block)
119
+ end
120
+
121
+ def site_follow(*user_ids, &block)
122
+ query_params ||= {:follow => user_ids}
123
+
124
+ if query_params[:follow].is_a?(Array)
125
+ query_params[:follow] = query_params[:follow].collect{|q| q.to_s}.join(',')
126
+ elsif query_params[:follow]
127
+ query_params[:follow] = query_params[:follow].to_s
128
+ end
129
+ query_params[:site_streams] = query_params[:follow]
130
+ query_params[:track] = ["foo"]
131
+
132
+ start('site', query_params.merge(:method => :post, :host => 'betastream.twitter.com', :version => '2b'), &block)
133
+ end
134
+
135
+ # Set a Proc to be run when a deletion notice is received
136
+ # from the Twitter stream. For example:
137
+ #
138
+ # @client = TweetStream::Client.new('user','pass')
139
+ # @client.on_delete do |status_id, user_id|
140
+ # Tweet.delete(status_id)
141
+ # end
142
+ #
143
+ # Block must take two arguments: the status id and the user id.
144
+ # If no block is given, it will return the currently set
145
+ # deletion proc. When a block is given, the TweetStream::Client
146
+ # object is returned to allow for chaining.
147
+ def on_delete(&block)
148
+ if block_given?
149
+ @on_delete = block
150
+ self
151
+ else
152
+ @on_delete
153
+ end
154
+ end
155
+
156
+ # Set a Proc to be run when a rate limit notice is received
157
+ # from the Twitter stream. For example:
158
+ #
159
+ # @client = TweetStream::Client.new('user','pass')
160
+ # @client.on_limit do |discarded_count|
161
+ # # Make note of discarded count
162
+ # end
163
+ #
164
+ # Block must take one argument: the number of discarded tweets.
165
+ # If no block is given, it will return the currently set
166
+ # limit proc. When a block is given, the TweetStream::Client
167
+ # object is returned to allow for chaining.
168
+ def on_limit(&block)
169
+ if block_given?
170
+ @on_limit = block
171
+ self
172
+ else
173
+ @on_limit
174
+ end
175
+ end
176
+
177
+ # Set a Proc to be run when an HTTP error is encountered in the
178
+ # processing of the stream. Note that TweetStream will automatically
179
+ # try to reconnect, this is for reference only. Don't panic!
180
+ #
181
+ # @client = TweetStream::Client.new('user','pass')
182
+ # @client.on_error do |message|
183
+ # # Make note of error message
184
+ # end
185
+ #
186
+ # Block must take one argument: the error message.
187
+ # If no block is given, it will return the currently set
188
+ # error proc. When a block is given, the TweetStream::Client
189
+ # object is returned to allow for chaining.
190
+ def on_error(&block)
191
+ if block_given?
192
+ @on_error = block
193
+ self
194
+ else
195
+ @on_error
196
+ end
197
+ end
198
+
199
+ def start(path, query_parameters = {}, &block) #:nodoc:
200
+
201
+ host = query_parameters.delete(:host) || 'stream.twitter.com'
202
+ version = query_parameters.delete(:version) || '1'
203
+
204
+ method = query_parameters.delete(:method) || :get
205
+ delete_proc = query_parameters.delete(:delete) || self.on_delete
206
+ limit_proc = query_parameters.delete(:limit) || self.on_limit
207
+ error_proc = query_parameters.delete(:error) || self.on_error
208
+
209
+ uri = method == :get ? build_uri(path, version, query_parameters) : build_uri(path, version)
210
+
211
+ oauth = {
212
+ :consumer_key => self.consumer_key,
213
+ :consumer_secret => self.consumer_secret,
214
+ :access_key => self.access_key,
215
+ :access_secret => self.access_secret
216
+ }
217
+
218
+ EventMachine::run {
219
+ @stream = TwitterStream::JSONStream.connect(
220
+ :path => uri,
221
+ :host => host,
222
+ :oauth => oauth,
223
+ :filters => query_parameters[:track],
224
+ :follow => query_parameters[:follow],
225
+ :locations => query_parameters[:locations],
226
+ :site_streams => query_parameters[:site_streams],
227
+ :method => method.to_s.upcase,
228
+ :content => (method == :post ? build_post_body(query_parameters) : ''),
229
+ :user_agent => 'TweetStream'
230
+ )
231
+
232
+ @stream.each_item do |item|
233
+ raw_hash = @parser.decode(item)
234
+
235
+ unless raw_hash.is_a?(::Hash)
236
+ error_proc.call("Unexpected JSON object in stream: #{item}")
237
+ next
238
+ end
239
+
240
+ hash = TweetStream::Hash.new(raw_hash) # @parser.parse(item)
241
+
242
+ if hash[:delete] && hash[:delete][:status]
243
+ delete_proc.call(hash[:delete][:status][:id], hash[:delete][:status][:user_id]) if delete_proc.is_a?(Proc)
244
+ elsif hash[:limit] && hash[:limit][:track]
245
+ limit_proc.call(hash[:limit][:track]) if limit_proc.is_a?(Proc)
246
+ elsif hash[:text] && hash[:user]
247
+ @last_status = TweetStream::Status.new(hash)
248
+
249
+ # Give the block the option to receive either one
250
+ # or two arguments, depending on its arity.
251
+ case block.arity
252
+ when 1
253
+ yield @last_status
254
+ when 2
255
+ yield @last_status, self
256
+ end
257
+ elsif hash[:for_user]
258
+
259
+ @last_status = TweetStream::Status.new(hash[:messages] || hash[:message])
260
+
261
+ # Give the block the option to receive either one
262
+ # or two arguments, depending on its arity.
263
+ case block.arity
264
+ when 2
265
+ yield @last_status, hash[:for_user]
266
+ when 3
267
+ yield @last_status, hash[:for_user], self
268
+ end
269
+ end
270
+ end
271
+
272
+ @stream.on_error do |message|
273
+ error_proc.call(message) if error_proc.is_a?(Proc)
274
+ end
275
+
276
+ @stream.on_max_reconnects do |timeout, retries|
277
+ raise TweetStream::ReconnectError.new(timeout, retries)
278
+ end
279
+ }
280
+ end
281
+
282
+ # Terminate the currently running TweetStream.
283
+ def stop
284
+ EventMachine.stop_event_loop
285
+ @last_status
286
+ end
287
+
288
+ protected
289
+
290
+ def parser_from(parser)
291
+ case parser
292
+ when Class
293
+ parser
294
+ when Symbol
295
+ require "tweetstream/parsers/#{parser.to_s}"
296
+ eval("TweetStream::Parsers::#{parser.to_s.split('_').map{|s| s.capitalize}.join('')}")
297
+ end
298
+ end
299
+
300
+ def build_uri(path, version = '1', query_parameters = {}) #:nodoc:
301
+ URI.parse("/#{version}/#{path}.json#{build_query_parameters(query_parameters)}")
302
+ end
303
+
304
+ def build_query_parameters(query)
305
+ query.size > 0 ? "?#{build_post_body(query)}" : ''
306
+ end
307
+
308
+ def build_post_body(query) #:nodoc:
309
+ return '' unless query && query.is_a?(::Hash) && query.size > 0
310
+ pairs = []
311
+
312
+ query.each_pair do |k,v|
313
+ pairs << "#{k.to_s}=#{CGI.escape(v.to_s)}"
314
+ end
315
+ pairs.join('&')
316
+ end
317
+ end
318
+ end
@@ -0,0 +1,39 @@
1
+ require 'daemons'
2
+
3
+ # A daemonized TweetStream client that will allow you to
4
+ # create backgroundable scripts for application specific
5
+ # processes. For instance, if you create a script called
6
+ # <tt>tracker.rb</tt> and fill it with this:
7
+ #
8
+ # require 'rubygems'
9
+ # require 'tweetstream'
10
+ #
11
+ # TweetStream::Daemon.new('user','pass', 'tracker').track('intridea') do |status|
12
+ # # do something here
13
+ # end
14
+ #
15
+ # And then you call this from the shell:
16
+ #
17
+ # ruby tracker.rb start
18
+ #
19
+ # A daemon process will spawn that will automatically
20
+ # run the code in the passed block whenever a new tweet
21
+ # matching your search term ('intridea' in this case)
22
+ # is posted.
23
+ #
24
+ class TweetStream::Daemon < TweetStream::Client
25
+ # Initialize a Daemon with the credentials of the
26
+ # Twitter account you wish to use. The daemon has
27
+ # an optional process name for use when querying
28
+ # running processes.
29
+ def initialize(consumer_key, consumer_secret, access_key, access_secret, app_name=nil, parser=:json_gem)
30
+ @app_name = app_name
31
+ super(consumer_key, consumer_secret, access_key, access_secret, parser)
32
+ end
33
+
34
+ def start(path, query_parameters = {}, &block) #:nodoc:
35
+ Daemons.run_proc(@app_name || 'tweetstream', :multiple => true) do
36
+ super(path, query_parameters, &block)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ class TweetStream::Hash < ::Hash #:nodoc: all
2
+ def initialize(other_hash = {})
3
+ other_hash.keys.each do |key|
4
+ value = other_hash[key]
5
+ value = TweetStream::Hash.new(value) if value.is_a?(::Hash)
6
+ self[key.to_sym] = value
7
+ end
8
+ end
9
+
10
+ def method_missing(method_name, *args)
11
+ if key?(method_name.to_sym)
12
+ self[method_name.to_sym]
13
+ else
14
+ super
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ require 'active_support/json' unless defined?(::ActiveSupport::JSON)
2
+
3
+ module TweetStream
4
+ module Parsers
5
+ class ActiveSupport
6
+ def self.decode(string)
7
+ ::ActiveSupport::JSON.decode(string)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'json' unless defined?(JSON)
2
+
3
+ module TweetStream
4
+ module Parsers
5
+ class JsonGem
6
+ def self.decode(string)
7
+ ::JSON.parse(string)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'json/pure' unless defined?(::JSON)
2
+
3
+ module TweetStream
4
+ module Parsers
5
+ class JsonPure
6
+ def self.decode(string)
7
+ ::JSON.parse(string)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'yajl' unless defined?(Yajl)
2
+
3
+ module TweetStream
4
+ module Parsers
5
+ class Yajl
6
+ def self.decode(string)
7
+ ::Yajl::Parser.new(:symbolize_keys => true).parse(string)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # A simple Hash wrapper that gives you method-based
2
+ # access to the properties of a Twitter status.
3
+ class TweetStream::Status < TweetStream::Hash
4
+ def initialize(hash)
5
+ super
6
+ self[:user] = TweetStream::User.new(self[:user]) if self[:user]
7
+ end
8
+
9
+ def id
10
+ self[:id] || super
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ # A simple Hash wrapper that gives you method-based
2
+ # access to user properties returned by the streamer.
3
+ class TweetStream::User < TweetStream::Hash
4
+ def id
5
+ self[:id] || super
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ {"favorited":false,"text":"listening to Where U Headed by Universal Playaz. http://iLike.com/s/9zpOZ #musicmonday something for the ladies","in_reply_to_user_id":null,"in_reply_to_screen_name":null,"source":"<a href=\"http://www.iLike.com\" rel=\"nofollow\">iLike</a>","truncated":false,"created_at":"Tue Sep 22 01:29:13 +0000 2009","user":{"statuses_count":378,"favourites_count":1,"profile_text_color":"666666","location":"Atlanta, Ga","profile_background_image_url":"http://a3.twimg.com/profile_background_images/36516125/Universal_Playaz.jpg","profile_link_color":"2FC2EF","description":"Paper Chaser","following":null,"verified":false,"notifications":null,"profile_sidebar_fill_color":"252429","profile_image_url":"http://a1.twimg.com/profile_images/413331530/DIESELSTATScopy_normal.jpg","url":"http://www.myspace.com/DieselDtheg","profile_sidebar_border_color":"181A1E","screen_name":"DieselD2143","profile_background_tile":true,"followers_count":75,"protected":false,"time_zone":"Eastern Time (US & Canada)","created_at":"Thu Jun 18 15:56:32 +0000 2009","name":"Diesel D","friends_count":119,"profile_background_color":"1A1B1F","id":48392351,"utc_offset":-18000},"in_reply_to_status_id":null,"id":4161231023} {"favorited":false,"text":"David Bowie and Nine Inch Nails perform \"Hurt\" http://bit.ly/AOaWG #musicmonday #nineinchnails #nin","in_reply_to_user_id":null,"in_reply_to_screen_name":null,"source":"web","truncated":false,"created_at":"Tue Sep 22 01:29:16 +0000 2009","user":{"statuses_count":668,"favourites_count":25,"profile_text_color":"445d85","location":"S\u00e3o Paulo, Brazil","profile_background_image_url":"http://a3.twimg.com/profile_background_images/38174991/GeorgeRomero-oil-400.jpg","profile_link_color":"555757","description":"You think I ain't worth a dollar, but I feel like a millionaire","following":null,"verified":false,"notifications":null,"profile_sidebar_fill_color":"a3a7ad","profile_image_url":"http://a1.twimg.com/profile_images/96034368/n1076431955_30001395_7912_normal.jpg","url":null,"profile_sidebar_border_color":"c7d1ed","screen_name":"RenatonMiranda","profile_background_tile":true,"followers_count":111,"protected":false,"time_zone":"Santiago","created_at":"Sat Mar 14 15:03:59 +0000 2009","name":"Renato Miranda","friends_count":143,"profile_background_color":"287356","id":24379310,"utc_offset":-14400},"in_reply_to_status_id":null,"id":4161232008} {"favorited":false,"text":"#musicmonday ,time to download some songs today!! :)","in_reply_to_user_id":null,"in_reply_to_screen_name":null,"source":"web","truncated":false,"created_at":"Tue Sep 22 01:29:19 +0000 2009","user":{"statuses_count":188,"favourites_count":0,"profile_text_color":"3D1957","location":"under the water","profile_background_image_url":"http://s.twimg.com/a/1253562286/images/themes/theme10/bg.gif","profile_link_color":"FF0000","description":"ask me ","following":null,"verified":false,"notifications":null,"profile_sidebar_fill_color":"7AC3EE","profile_image_url":"http://a1.twimg.com/profile_images/421281292/twit_pic_normal.jpg","url":"http://www.exploretalent.com/contest_video.php?talentnum=2053105&cm_id=3398","profile_sidebar_border_color":"65B0DA","screen_name":"julieanne11343","profile_background_tile":true,"followers_count":9,"protected":false,"time_zone":"Pacific Time (US & Canada)","created_at":"Mon Jul 20 21:08:22 +0000 2009","name":"Julieanne","friends_count":17,"profile_background_color":"642D8B","id":58591151,"utc_offset":-28800},"in_reply_to_status_id":null,"id":4161233120} {"text":"#Musicmonday \"Dont be tardy f0r the party\"","truncated":false,"source":"<a href=\"http://twitterhelp.blogspot.com/2008/05/twitter-via-mobile-web-mtwittercom.html\" rel=\"nofollow\">mobile web</a>","in_reply_to_status_id":null,"favorited":false,"created_at":"Tue Sep 22 01:29:19 +0000 2009","user":{"verified":false,"notifications":null,"profile_sidebar_fill_color":"e0ff92","location":"Dope Girl Island","profile_sidebar_border_color":"87bc44","description":"","following":null,"profile_background_tile":false,"followers_count":29,"profile_image_url":"http://a3.twimg.com/profile_images/217487577/badbad_normal.jpg","time_zone":"Eastern Time (US & Canada)","url":null,"friends_count":65,"profile_background_color":"9ae4e8","screen_name":"SwagGirlOnDeck","protected":false,"statuses_count":847,"favourites_count":0,"created_at":"Fri May 01 16:59:15 +0000 2009","profile_text_color":"000000","name":"Mariah Reta","id":36987168,"profile_background_image_url":"http://s.twimg.com/a/1253301564/images/themes/theme1/bg.png","utc_offset":-18000,"profile_link_color":"0000ff"},"in_reply_to_user_id":null,"id":4161233317,"in_reply_to_screen_name":null}
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format progress
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'rubygems'
5
+ require 'tweetstream'
6
+ require 'spec'
7
+ require 'spec/autorun'
8
+ require 'yajl'
9
+ require 'json'
10
+
11
+ def sample_tweets
12
+ if @tweets
13
+ @tweets
14
+ else
15
+ @tweets = []
16
+ Yajl::Parser.parse(File.open(File.dirname(__FILE__) + '/data/statuses.json', 'r'), :symbolize_keys => true) do |hash|
17
+ @tweets << hash
18
+ end
19
+ @tweets
20
+ end
21
+ end
22
+
23
+ Spec::Runner.configure do |config|
24
+
25
+ end
@@ -0,0 +1,233 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe TweetStream::Client do
4
+ #it 'should set the username and password from the initializers' do
5
+ #@client = TweetStream::Client.new('abc','def','hij','klm')
6
+ #@client.username.should == 'abc'
7
+ #@client.password.should == 'def'
8
+ #end
9
+
10
+ describe '#build_uri' do
11
+ before do
12
+ @client = TweetStream::Client.new('abc','def','hij','klm')
13
+ end
14
+
15
+ it 'should return a URI' do
16
+ @client.send(:build_uri, '').is_a?(URI).should be_true
17
+ end
18
+
19
+ it 'should have the specified path with the version prefix and a json extension' do
20
+ @client.send(:build_uri, 'awesome').path.should == '/1/awesome.json'
21
+ end
22
+
23
+ it 'should add on a query string if such parameters are specified' do
24
+ @client.send(:build_uri, 'awesome', 1, :q => 'abc').query.should == 'q=abc'
25
+ end
26
+ end
27
+
28
+ describe '#build_post_body' do
29
+ before do
30
+ @client = TweetStream::Client.new('abc','def','hij','klm')
31
+ end
32
+
33
+ it 'should return a blank string if passed a nil value' do
34
+ @client.send(:build_post_body, nil).should == ''
35
+ end
36
+
37
+ it 'should return a blank string if passed an empty hash' do
38
+ @client.send(:build_post_body, {}).should == ''
39
+ end
40
+
41
+ it 'should add a query parameter for a key' do
42
+ @client.send(:build_post_body, {:query => 'abc'}).should == 'query=abc'
43
+ end
44
+
45
+ it 'should escape characters in the value' do
46
+ @client.send(:build_post_body, {:query => 'awesome guy'}).should == 'query=awesome+guy'
47
+ end
48
+
49
+ it 'should join multiple pairs together' do
50
+ ['a=b&c=d','c=d&a=b'].include?(@client.send(:build_post_body, {:a => 'b', :c => 'd'})).should be_true
51
+ end
52
+ end
53
+
54
+ describe '#start' do
55
+ before do
56
+ @stream = stub("TwitterStream::JSONStream",
57
+ :connect => true,
58
+ :unbind => true,
59
+ :each_item => true,
60
+ :on_error => true,
61
+ :on_max_reconnects => true,
62
+ :connection_completed => true
63
+ )
64
+ EM.stub!(:run).and_yield
65
+ TwitterStream::JSONStream.stub!(:connect).and_return(@stream)
66
+ @client = TweetStream::Client.new('abc','def','hij','klm')
67
+ end
68
+
69
+ it 'should try to connect via a JSON stream' do
70
+ #TwitterStream::JSONStream.should_receive(:connect).with(
71
+ # :oauth => {:access_key=>"hij", :access_secret=>"klm", :consumer_key=>"abc", :consumer_secret=>"def"},
72
+ # :content => 'track=monday',
73
+ # :path => URI.parse('/1/statuses/filter.json'),
74
+ # :method => 'POST',
75
+ # :user_agent => 'TweetStream'
76
+ #).and_return(@stream)
77
+
78
+ @client.track('monday')
79
+ end
80
+
81
+ describe '#each_item' do
82
+ it 'should call the appropriate parser' do
83
+ @client = TweetStream::Client.new('abc','def','hij','klm',:active_support)
84
+ TweetStream::Parsers::ActiveSupport.should_receive(:decode).and_return({})
85
+ @stream.should_receive(:each_item).and_yield(sample_tweets[0].to_json)
86
+ @client.track('abc','def')
87
+ end
88
+
89
+ it 'should yield a TweetStream::Status' do
90
+ @stream.should_receive(:each_item).and_yield(sample_tweets[0].to_json)
91
+ @client.track('abc'){|s| s.should be_kind_of(TweetStream::Status)}
92
+ end
93
+
94
+ it 'should also yield the client if a block with arity 2 is given' do
95
+ @stream.should_receive(:each_item).and_yield(sample_tweets[0].to_json)
96
+ @client.track('abc'){|s,c| c.should == @client}
97
+ end
98
+
99
+ it 'should include the proper values' do
100
+ tweet = sample_tweets[0]
101
+ tweet[:id] = 123
102
+ tweet[:user][:screen_name] = 'monkey'
103
+ tweet[:text] = "Oo oo aa aa"
104
+ @stream.should_receive(:each_item).and_yield(tweet.to_json)
105
+ @client.track('abc') do |s|
106
+ s[:id].should == 123
107
+ s.user.screen_name.should == 'monkey'
108
+ s.text.should == 'Oo oo aa aa'
109
+ end
110
+ end
111
+
112
+ it 'should call the on_delete if specified' do
113
+ delete = '{ "delete": { "status": { "id": 1234, "user_id": 3 } } }'
114
+ @stream.should_receive(:each_item).and_yield(delete)
115
+ @client.on_delete do |id, user_id|
116
+ id.should == 1234
117
+ user_id.should == 3
118
+ end.track('abc')
119
+ end
120
+
121
+ it 'should call the on_limit if specified' do
122
+ limit = '{ "limit": { "track": 1234 } }'
123
+ @stream.should_receive(:each_item).and_yield(limit)
124
+ @client.on_limit do |track|
125
+ track.should == 1234
126
+ end.track('abc')
127
+ end
128
+
129
+ it 'should call on_error if a non-hash response is received' do
130
+ @stream.should_receive(:each_item).and_yield('["favorited"]')
131
+ @client.on_error do |message|
132
+ message.should == 'Unexpected JSON object in stream: ["favorited"]'
133
+ end.track('abc')
134
+ end
135
+ end
136
+
137
+ describe '#on_error' do
138
+ it 'should pass the message on to the error block' do
139
+ @stream.should_receive(:on_error).and_yield('Uh oh')
140
+ @client.on_error do |m|
141
+ m.should == 'Uh oh'
142
+ end.track('abc')
143
+ end
144
+ end
145
+
146
+ describe '#on_max_reconnects' do
147
+ it 'should raise a ReconnectError' do
148
+ @stream.should_receive(:on_max_reconnects).and_yield(30, 20)
149
+ lambda{@client.track('abc')}.should raise_error(TweetStream::ReconnectError) do |e|
150
+ e.timeout.should == 30
151
+ e.retries.should == 20
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ describe ' API methods' do
158
+ before do
159
+ @client = TweetStream::Client.new('abc','def','hij','klm')
160
+ end
161
+
162
+ %w(firehose retweet sample).each do |method|
163
+ it "##{method} should make a call to start with \"statuses/#{method}\"" do
164
+ @client.should_receive(:start).once.with('statuses/' + method, {})
165
+ @client.send(method)
166
+ end
167
+ end
168
+
169
+ it '#track should make a call to start with "statuses/filter" and a track query parameter' do
170
+ @client.should_receive(:start).once.with('statuses/filter', :track => 'test', :method => :post)
171
+ @client.track('test')
172
+ end
173
+
174
+ it '#track should comma-join multiple arguments' do
175
+ @client.should_receive(:start).once.with('statuses/filter', :track => 'foo,bar,baz', :method => :post)
176
+ @client.track('foo', 'bar', 'baz')
177
+ end
178
+
179
+ it '#follow should make a call to start with "statuses/filter" and a follow query parameter' do
180
+ @client.should_receive(:start).once.with('statuses/filter', :follow => '123', :method => :post)
181
+ @client.follow(123)
182
+ end
183
+
184
+ it '#follow should comma-join multiple arguments' do
185
+ @client.should_receive(:start).once.with('statuses/filter', :follow => '123,456', :method => :post)
186
+ @client.follow(123, 456)
187
+ end
188
+
189
+ it '#filter should make a call to "statuses/filter" with the query params provided' do
190
+ @client.should_receive(:start).once.with('statuses/filter', :follow => '123', :method => :post)
191
+ @client.filter(:follow => 123)
192
+ end
193
+ end
194
+
195
+ %w(on_delete on_limit).each do |proc_setter|
196
+ describe "##{proc_setter}" do
197
+ before do
198
+ @client = TweetStream::Client.new('abc','def','hij','klm')
199
+ end
200
+
201
+ it 'should set when a block is given' do
202
+ proc = Proc.new{|a,b| puts a }
203
+ @client.send(proc_setter, &proc)
204
+ @client.send(proc_setter).should == proc
205
+ end
206
+ end
207
+ end
208
+
209
+ describe '#track' do
210
+ before do
211
+ @client = TweetStream::Client.new('abc','def','hij','klm')
212
+ end
213
+
214
+ it 'should call #start with "statuses/filter" and the provided queries' do
215
+ @client.should_receive(:start).once.with('statuses/filter', :track => 'rock', :method => :post)
216
+ @client.track('rock')
217
+ end
218
+ end
219
+
220
+ describe 'instance .stop' do
221
+ it 'should call EventMachine::stop_event_loop' do
222
+ EventMachine.should_receive :stop_event_loop
223
+ TweetStream::Client.new('abc','def','hij','klm').stop.should be_nil
224
+ end
225
+
226
+ it 'should return the last status yielded' do
227
+ EventMachine.should_receive :stop_event_loop
228
+ client = TweetStream::Client.new('abc','def','hij','klm')
229
+ client.send(:instance_variable_set, :@last_status, {})
230
+ client.stop.should == {}
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,19 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe TweetStream::Hash do
4
+ it 'should be initialized by passing in an existing hash' do
5
+ TweetStream::Hash.new(:abc => 123)[:abc].should == 123
6
+ end
7
+
8
+ it 'should symbolize incoming keys' do
9
+ TweetStream::Hash.new('abc' => 123)[:abc].should == 123
10
+ end
11
+
12
+ it 'should allow access via method calls' do
13
+ TweetStream::Hash.new(:abc => 123).abc.should == 123
14
+ end
15
+
16
+ it 'should still throw NoMethod for non-existent keys' do
17
+ lambda{TweetStream::Hash.new({}).akabi}.should raise_error(NoMethodError)
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe 'TweetStream JSON Parsers' do
4
+ it 'should default to the JSON Gem' do
5
+ TweetStream::Client.new('abc','def','hij','klm').parser.should == TweetStream::Parsers::JsonGem
6
+ end
7
+
8
+ [:json_gem, :yajl, :active_support, :json_pure].each do |engine|
9
+ describe "#{engine} parsing" do
10
+ before do
11
+ @client = TweetStream::Client.new('abc','def','hij','klm',engine)
12
+ @class_name = "TweetStream::Parsers::#{engine.to_s.split('_').map{|s| s.capitalize}.join('')}"
13
+ end
14
+
15
+ it 'should set the parser to the appropriate class' do
16
+ @client.parser.to_s == @class_name
17
+ end
18
+
19
+ it 'should be settable via client.parser=' do
20
+ @client.parser = nil
21
+ @client.parser.should be_nil
22
+ @client.parser = engine
23
+ @client.parser.to_s.should == @class_name
24
+ end
25
+ end
26
+ end
27
+
28
+ class FakeParser
29
+ def self.decode(text)
30
+ {}
31
+ end
32
+ end
33
+
34
+ it 'should be settable to a class' do
35
+ @client = TweetStream::Client.new('abc','def','hij','klm')
36
+ @client.parser = FakeParser
37
+ @client.parser.should == FakeParser
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe TweetStream::Status do
4
+ it 'should modify the :user key into a TweetStream::User object' do
5
+ @status = TweetStream::Status.new(:user => {:screen_name => 'bob'})
6
+ @status.user.is_a?(TweetStream::User).should be_true
7
+ @status.user.screen_name.should == 'bob'
8
+ end
9
+
10
+ it 'should override the #id method for itself and the user' do
11
+ @status = TweetStream::Status.new(:id => 123, :user => {:id => 345})
12
+ @status.id.should == 123
13
+ @status.user.id.should == 345
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe TweetStream do
4
+
5
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xh5-tweetstream
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 1
8
+ - 0
9
+ version: 1.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Michael Bleigh
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-10-12 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: xh5-twitter-stream
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ type: :runtime
43
+ version_requirements: *id002
44
+ - !ruby/object:Gem::Dependency
45
+ name: daemons
46
+ prerelease: false
47
+ requirement: &id003 !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ segments:
52
+ - 0
53
+ version: "0"
54
+ type: :runtime
55
+ version_requirements: *id003
56
+ description: TweetStream allows you to easily consume the Twitter Streaming API utilizing the YAJL Ruby gem.
57
+ email: eric@xhfive.com
58
+ executables: []
59
+
60
+ extensions: []
61
+
62
+ extra_rdoc_files:
63
+ - LICENSE
64
+ - README.rdoc
65
+ files:
66
+ - LICENSE
67
+ - README.rdoc
68
+ - RELEASE_NOTES.rdoc
69
+ - Rakefile
70
+ - VERSION
71
+ - examples/growl_daemon.rb
72
+ - lib/tweetstream.rb
73
+ - lib/tweetstream/client.rb
74
+ - lib/tweetstream/daemon.rb
75
+ - lib/tweetstream/hash.rb
76
+ - lib/tweetstream/parsers/active_support.rb
77
+ - lib/tweetstream/parsers/json_gem.rb
78
+ - lib/tweetstream/parsers/json_pure.rb
79
+ - lib/tweetstream/parsers/yajl.rb
80
+ - lib/tweetstream/status.rb
81
+ - lib/tweetstream/user.rb
82
+ - spec/data/statuses.json
83
+ - spec/spec.opts
84
+ - spec/spec_helper.rb
85
+ - spec/tweetstream/client_spec.rb
86
+ - spec/tweetstream/hash_spec.rb
87
+ - spec/tweetstream/parser_spec.rb
88
+ - spec/tweetstream/status_spec.rb
89
+ - spec/tweetstream_spec.rb
90
+ has_rdoc: true
91
+ homepage: http://github.com/erichurst/tweetstream
92
+ licenses: []
93
+
94
+ post_install_message:
95
+ rdoc_options:
96
+ - --charset=UTF-8
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ segments:
104
+ - 0
105
+ version: "0"
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ requirements: []
114
+
115
+ rubyforge_project:
116
+ rubygems_version: 1.3.6
117
+ signing_key:
118
+ specification_version: 3
119
+ summary: TweetStream is a simple wrapper for consuming the Twitter Streaming API.
120
+ test_files:
121
+ - spec/spec_helper.rb
122
+ - spec/tweetstream/client_spec.rb
123
+ - spec/tweetstream/hash_spec.rb
124
+ - spec/tweetstream/parser_spec.rb
125
+ - spec/tweetstream/status_spec.rb
126
+ - spec/tweetstream_spec.rb
127
+ - examples/growl_daemon.rb