xh5-tweetstream 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +166 -0
- data/RELEASE_NOTES.rdoc +5 -0
- data/Rakefile +60 -0
- data/VERSION +1 -0
- data/examples/growl_daemon.rb +14 -0
- data/lib/tweetstream.rb +21 -0
- data/lib/tweetstream/client.rb +318 -0
- data/lib/tweetstream/daemon.rb +39 -0
- data/lib/tweetstream/hash.rb +17 -0
- data/lib/tweetstream/parsers/active_support.rb +11 -0
- data/lib/tweetstream/parsers/json_gem.rb +11 -0
- data/lib/tweetstream/parsers/json_pure.rb +11 -0
- data/lib/tweetstream/parsers/yajl.rb +11 -0
- data/lib/tweetstream/status.rb +12 -0
- data/lib/tweetstream/user.rb +7 -0
- data/spec/data/statuses.json +1 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/tweetstream/client_spec.rb +233 -0
- data/spec/tweetstream/hash_spec.rb +19 -0
- data/spec/tweetstream/parser_spec.rb +39 -0
- data/spec/tweetstream/status_spec.rb +15 -0
- data/spec/tweetstream_spec.rb +5 -0
- metadata +127 -0
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.
|
data/README.rdoc
ADDED
@@ -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.
|
data/RELEASE_NOTES.rdoc
ADDED
data/Rakefile
ADDED
@@ -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
|
data/lib/tweetstream.rb
ADDED
@@ -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,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 @@
|
|
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}
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -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
|
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
|