xh5-tweetstream 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|